@fml-inc/fml 0.0.27 → 0.0.29
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-ND2YSPQ6.js → chunk-RJOFOTNH.js} +9 -3
- package/dist/{chunk-ND2YSPQ6.js.map → chunk-RJOFOTNH.js.map} +1 -1
- package/dist/{chunk-FS55W24B.js → chunk-TXQRKU2Y.js} +35 -5
- package/dist/chunk-TXQRKU2Y.js.map +1 -0
- package/dist/cli.js +192 -37
- package/dist/cli.js.map +1 -1
- package/dist/mcp/server.js +2 -2
- package/dist/{token-store-KQSMR6XA.js → token-store-NE7AM2PH.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-FS55W24B.js.map +0 -1
- /package/dist/{token-store-KQSMR6XA.js.map → token-store-NE7AM2PH.js.map} +0 -0
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
CONVEX_URL,
|
|
6
6
|
getSelectedOrg,
|
|
7
7
|
getValidToken
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-TXQRKU2Y.js";
|
|
9
9
|
|
|
10
10
|
// src/native-check.ts
|
|
11
11
|
import { createRequire } from "module";
|
|
@@ -213,13 +213,19 @@ function createApiClient(token) {
|
|
|
213
213
|
async callBackend(toolName, args, opts) {
|
|
214
214
|
try {
|
|
215
215
|
if (isServiceToken) {
|
|
216
|
+
const { readTokens } = await import("./token-store-NE7AM2PH.js");
|
|
217
|
+
const userExternalId = process.env.FML_USER_EXTERNAL_ID ?? readTokens()?.user.id;
|
|
216
218
|
const res = await fetch(`${getSiteUrl()}/api/tools/execute`, {
|
|
217
219
|
method: "POST",
|
|
218
220
|
headers: {
|
|
219
221
|
"Content-Type": "application/json",
|
|
220
222
|
Authorization: `Bearer ${token}`
|
|
221
223
|
},
|
|
222
|
-
body: JSON.stringify({
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
toolName,
|
|
226
|
+
args,
|
|
227
|
+
...userExternalId && { userExternalId }
|
|
228
|
+
})
|
|
223
229
|
});
|
|
224
230
|
const text = await res.text();
|
|
225
231
|
let data;
|
|
@@ -310,4 +316,4 @@ export {
|
|
|
310
316
|
createApiClient,
|
|
311
317
|
getAuthenticatedClient
|
|
312
318
|
};
|
|
313
|
-
//# sourceMappingURL=chunk-
|
|
319
|
+
//# sourceMappingURL=chunk-RJOFOTNH.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/native-check.ts","../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/repo.ts","../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/workspaces/superset.ts","../src/convex-client.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport { execSync } from \"node:child_process\";\nimport { rmSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * Verify that better-sqlite3's native addon matches the running Node version.\n * If mismatched (e.g. user upgraded Node), attempt an automatic rebuild.\n * Call this before any code that imports better-sqlite3.\n */\nexport function ensureNativeModules(): void {\n const require = createRequire(import.meta.url);\n try {\n require(\"better-sqlite3\");\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"\";\n if (!msg.includes(\"NODE_MODULE_VERSION\")) {\n throw err;\n }\n\n console.error(\n \"[fml] better-sqlite3 was compiled for a different Node.js version, rebuilding...\",\n );\n\n const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), \"..\");\n\n // Remove stale build directory to avoid EEXIST errors from node-gyp\n const sqlitePath = dirname(require.resolve(\"better-sqlite3/package.json\"));\n rmSync(join(sqlitePath, \"build\"), { recursive: true, force: true });\n\n try {\n execSync(\"npm rebuild better-sqlite3\", {\n cwd: pkgRoot,\n stdio: \"inherit\",\n timeout: 60_000,\n });\n console.error(\"[fml] Rebuild complete.\");\n } catch {\n console.error(\n \"[fml] Automatic rebuild failed. Please run manually:\\n\" +\n ` cd ${pkgRoot} && rm -rf node_modules/better-sqlite3/build && npm rebuild better-sqlite3`,\n );\n process.exit(1);\n }\n }\n}\n","import { execFileSync } from \"node:child_process\";\nimport { SupersetProvider } from \"./workspaces/superset.js\";\nimport type { WorkspaceProvider } from \"./workspaces/types.js\";\n\nexport interface RepoInfo {\n repo: string;\n branch?: string | null;\n}\n\n// Cache: cwd → RepoInfo | null\nconst repoCache = new Map<string, RepoInfo | null>();\n\n// Registered workspace providers — checked in order when git fails.\nconst providers: WorkspaceProvider[] = [new SupersetProvider()];\n\n/** Resolve \"org/repo\" from a directory's git remote origin URL. */\nfunction resolveGitRemote(dir: string): string | null {\n try {\n const url = execFileSync(\n \"git\",\n [\"-C\", dir, \"remote\", \"get-url\", \"origin\"],\n {\n encoding: \"utf-8\",\n timeout: 5000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n },\n ).trim();\n\n // SSH: git@github.com:org/repo.git\n const sshMatch = url.match(/git@[^:]+:([^/]+\\/[^/]+?)(?:\\.git)?$/);\n if (sshMatch) return sshMatch[1];\n\n // HTTPS: https://github.com/org/repo.git\n const httpsMatch = url.match(/\\/([^/]+\\/[^/]+?)(?:\\.git)?$/);\n if (httpsMatch) return httpsMatch[1];\n } catch {\n // Not a git repo, no remote, etc.\n }\n return null;\n}\n\n/** Resolve the current git branch for a directory. */\nfunction resolveGitBranch(dir: string): string | null {\n try {\n return (\n execFileSync(\"git\", [\"-C\", dir, \"rev-parse\", \"--abbrev-ref\", \"HEAD\"], {\n encoding: \"utf-8\",\n timeout: 5000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim() || null\n );\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve the GitHub \"org/repo\" and branch for a working directory.\n * Results are cached for the lifetime of the process.\n *\n * 1. Try git directly on the CWD\n * 2. On failure, ask registered workspace providers (e.g. Superset)\n * for an alternative repo directory to resolve against\n */\nexport function resolveRepoFromCwd(cwd: string): RepoInfo | null {\n if (repoCache.has(cwd)) return repoCache.get(cwd)!;\n\n let result: RepoInfo | null = null;\n\n const repo = resolveGitRemote(cwd);\n if (repo) {\n result = { repo, branch: resolveGitBranch(cwd) };\n } else {\n // Ask workspace providers for a fallback\n for (const provider of providers) {\n if (!provider.canResolve(cwd)) continue;\n const resolved = provider.resolve(cwd);\n if (!resolved) continue;\n\n // Provider returned repo name directly (no git needed)\n if (resolved.repo) {\n result = { repo: resolved.repo, branch: resolved.branch };\n break;\n }\n\n // Provider returned a directory to resolve via git\n if (resolved.repoDir) {\n const fallbackRepo = resolveGitRemote(resolved.repoDir);\n if (fallbackRepo) {\n result = { repo: fallbackRepo, branch: resolved.branch };\n break;\n }\n }\n }\n }\n\n repoCache.set(cwd, result);\n return result;\n}\n\n// Cache: cwd → git identity\nconst gitIdentityCache = new Map<\n string,\n { name: string | null; email: string | null }\n>();\n\n/** Resolve git user.name / user.email for a directory. Cached per-process. */\nexport function resolveGitIdentity(cwd: string): {\n name: string | null;\n email: string | null;\n} {\n const cached = gitIdentityCache.get(cwd);\n if (cached) return cached;\n\n const result = { name: null as string | null, email: null as string | null };\n try {\n result.name =\n execFileSync(\"git\", [\"-C\", cwd, \"config\", \"user.name\"], {\n encoding: \"utf-8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim() || null;\n } catch {\n // no user.name configured\n }\n try {\n result.email =\n execFileSync(\"git\", [\"-C\", cwd, \"config\", \"user.email\"], {\n encoding: \"utf-8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim() || null;\n } catch {\n // no user.email configured\n }\n gitIdentityCache.set(cwd, result);\n return result;\n}\n\n/** Reset caches (for testing). */\nexport function _resetRepoCache(): void {\n repoCache.clear();\n gitIdentityCache.clear();\n for (const p of providers) p.close?.();\n}\n","import os from \"node:os\";\nimport path from \"node:path\";\nimport { Database } from \"../db/driver.js\";\nimport type { WorkspaceProvider } from \"./types.js\";\n\nconst SUPERSET_MARKER = `${path.sep}.superset${path.sep}`;\nconst SUPERSET_DB_PATH = path.join(os.homedir(), \".superset\", \"local.db\");\n\ninterface WorktreeRow {\n main_repo_path: string;\n branch: string | null;\n github_owner: string | null;\n project_name: string;\n}\n\nexport class SupersetProvider implements WorkspaceProvider {\n readonly id = \"superset\";\n\n private db: Database | null = null;\n private dbFailed = false;\n\n canResolve(cwd: string): boolean {\n return cwd.includes(SUPERSET_MARKER);\n }\n\n resolve(\n cwd: string,\n ): { repo?: string; repoDir?: string; branch?: string | null } | null {\n const db = this.getDb();\n if (!db) return null;\n\n try {\n // Try worktrees table first (for ~/.superset/worktrees/ paths)\n const wt = db\n .prepare(\n `SELECT p.main_repo_path, w.branch, p.github_owner, p.name as project_name\n FROM worktrees w\n JOIN projects p ON w.project_id = p.id\n WHERE ? LIKE w.path || '%'\n ORDER BY LENGTH(w.path) DESC\n LIMIT 1`,\n )\n .get(cwd) as WorktreeRow | undefined;\n if (wt) return this.buildResult(wt);\n\n // Fall back to projects table (for ~/.superset/projects/ paths)\n const proj = db\n .prepare(\n `SELECT main_repo_path, default_branch AS branch, github_owner, name as project_name\n FROM projects\n WHERE ? LIKE main_repo_path || '%'\n ORDER BY LENGTH(main_repo_path) DESC\n LIMIT 1`,\n )\n .get(cwd) as WorktreeRow | undefined;\n if (proj) return this.buildResult(proj);\n } catch {\n // Schema mismatch, corrupt DB, etc.\n }\n\n return null;\n }\n\n close(): void {\n if (this.db) {\n try {\n this.db.close();\n } catch {}\n this.db = null;\n }\n this.dbFailed = false;\n }\n\n private buildResult(row: WorktreeRow): {\n repo?: string;\n repoDir?: string;\n branch?: string | null;\n } {\n // Prefer deriving repo name directly from Superset DB (no git needed)\n if (row.github_owner) {\n return {\n repo: `${row.github_owner}/${row.project_name}`,\n branch: row.branch,\n };\n }\n // Fall back to git resolution on the main repo path\n return { repoDir: row.main_repo_path, branch: row.branch };\n }\n\n private getDb(): Database | null {\n if (this.dbFailed) return null;\n if (this.db) return this.db;\n try {\n this.db = new Database(SUPERSET_DB_PATH, {\n readonly: true,\n fileMustExist: true,\n });\n return this.db;\n } catch {\n this.dbFailed = true;\n return null;\n }\n }\n}\n","import type { FunctionReference } from \"convex/server\";\nimport { 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 interface ToolResult {\n ok: boolean;\n result?: unknown;\n error?: string;\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\nfunction ref<T extends \"query\" | \"mutation\" | \"action\">(\n path: string,\n): FunctionReference<T> {\n return path as unknown as FunctionReference<T>;\n}\n\n// ── API client factory ──────────────────────────────────────────────────────\n\nexport function createApiClient(token: string) {\n const isServiceToken = token.startsWith(\"fml_st_\");\n\n let clientPromise: Promise<{\n query: <T>(\n ref: FunctionReference<\"query\">,\n args: Record<string, unknown>,\n ) => Promise<T>;\n mutation: <T>(\n ref: FunctionReference<\"mutation\">,\n args: Record<string, unknown>,\n ) => Promise<T>;\n action: <T>(\n ref: FunctionReference<\"action\">,\n args: Record<string, unknown>,\n ) => Promise<T>;\n }> | null = null;\n\n function getClient() {\n if (!clientPromise) {\n clientPromise = import(\"convex/browser\").then(({ ConvexHttpClient }) => {\n const client = new ConvexHttpClient(CONVEX_URL);\n client.setAuth(token);\n return client;\n });\n }\n return clientPromise;\n }\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 return {\n // ── Orgs ──────────────────────────────────────────────────────────────\n\n async queryOrgs(): Promise<OrgInfo[]> {\n if (isServiceToken) {\n // Service tokens can't call Convex actions directly — org is embedded in the token\n return [];\n }\n const client = await getClient();\n return await client.query(ref(\"user/plugin:getMyOrgsAndRepos\"), {});\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 if (isServiceToken) {\n // Service token path: POST to HTTP endpoint (Convex can't validate non-JWT tokens)\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({ toolName, args }),\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 data;\n }\n\n // JWT path: standard Convex action\n const client = await getClient();\n const actionArgs: Record<string, unknown> = { toolName, args };\n // Explicit org > stored org selection > repo-based inference\n const org = opts?.org ?? getSelectedOrg();\n if (org) actionArgs.org = org;\n const repo = resolveRepoFromCwd(process.cwd());\n if (repo) actionArgs.repo = repo.repo;\n\n const result = await client.action(\n ref<\"action\">(\"user/tool_gateway:executeTool\"),\n actionArgs,\n );\n return { ok: true, result };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes(\"Unauthorized\") || msg.includes(\"not authenticated\")) {\n return {\n ok: false,\n error:\n \"Authentication expired. Run `fml login` to sign in again, then restart Claude Code.\",\n };\n }\n return { ok: false, error: msg };\n }\n },\n\n // ── Repo resolution ──────────────────────────────────────────────────\n\n async resolveRepo(\n orgSlug: string,\n repoFullName: string,\n ): Promise<ResolvedRepo | null> {\n const client = await getClient();\n return await client.query(ref(\"user/cli:resolveRepo\"), {\n orgSlug,\n repoFullName,\n });\n },\n\n // ── Config snapshots ─────────────────────────────────────────────────\n\n async listUserConfigSnapshots(\n orgSlug: string,\n ): Promise<UserConfigSnapshotSummary[]> {\n const client = await getClient();\n const result = await client.query<UserConfigSnapshotSummary[] | null>(\n ref(\"user/config_snapshots:listUserSnapshots\"),\n { orgSlug },\n );\n return result ?? [];\n },\n\n async getUserConfigDetail(\n orgSlug: string,\n githubUsername: string,\n ): Promise<UserConfigSnapshotDetail | null> {\n const client = await getClient();\n return await client.query(ref(\"user/config_snapshots:getUserDetail\"), {\n orgSlug,\n githubUsername,\n });\n },\n\n async listRepoConfigSnapshots(\n orgSlug: string,\n repository?: string,\n ): Promise<RepoConfigSnapshotSummary[]> {\n const client = await getClient();\n const result = await client.query<RepoConfigSnapshotSummary[] | null>(\n ref(\"user/config_snapshots:listRepoSnapshots\"),\n { orgSlug, repository },\n );\n return result ?? [];\n },\n\n async getRepoConfigDetail(\n orgSlug: string,\n repository: string,\n ): Promise<RepoConfigSnapshotDetail | null> {\n const client = await getClient();\n return await client.query(ref(\"user/config_snapshots:getRepoDetail\"), {\n orgSlug,\n repository,\n });\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 createApiClient(token);\n}\n"],"mappings":"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","names":["require","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/native-check.ts","../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/repo.ts","../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/workspaces/superset.ts","../src/convex-client.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport { execSync } from \"node:child_process\";\nimport { rmSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * Verify that better-sqlite3's native addon matches the running Node version.\n * If mismatched (e.g. user upgraded Node), attempt an automatic rebuild.\n * Call this before any code that imports better-sqlite3.\n */\nexport function ensureNativeModules(): void {\n const require = createRequire(import.meta.url);\n try {\n require(\"better-sqlite3\");\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"\";\n if (!msg.includes(\"NODE_MODULE_VERSION\")) {\n throw err;\n }\n\n console.error(\n \"[fml] better-sqlite3 was compiled for a different Node.js version, rebuilding...\",\n );\n\n const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), \"..\");\n\n // Remove stale build directory to avoid EEXIST errors from node-gyp\n const sqlitePath = dirname(require.resolve(\"better-sqlite3/package.json\"));\n rmSync(join(sqlitePath, \"build\"), { recursive: true, force: true });\n\n try {\n execSync(\"npm rebuild better-sqlite3\", {\n cwd: pkgRoot,\n stdio: \"inherit\",\n timeout: 60_000,\n });\n console.error(\"[fml] Rebuild complete.\");\n } catch {\n console.error(\n \"[fml] Automatic rebuild failed. Please run manually:\\n\" +\n ` cd ${pkgRoot} && rm -rf node_modules/better-sqlite3/build && npm rebuild better-sqlite3`,\n );\n process.exit(1);\n }\n }\n}\n","import { execFileSync } from \"node:child_process\";\nimport { SupersetProvider } from \"./workspaces/superset.js\";\nimport type { WorkspaceProvider } from \"./workspaces/types.js\";\n\nexport interface RepoInfo {\n repo: string;\n branch?: string | null;\n}\n\n// Cache: cwd → RepoInfo | null\nconst repoCache = new Map<string, RepoInfo | null>();\n\n// Registered workspace providers — checked in order when git fails.\nconst providers: WorkspaceProvider[] = [new SupersetProvider()];\n\n/** Resolve \"org/repo\" from a directory's git remote origin URL. */\nfunction resolveGitRemote(dir: string): string | null {\n try {\n const url = execFileSync(\n \"git\",\n [\"-C\", dir, \"remote\", \"get-url\", \"origin\"],\n {\n encoding: \"utf-8\",\n timeout: 5000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n },\n ).trim();\n\n // SSH: git@github.com:org/repo.git\n const sshMatch = url.match(/git@[^:]+:([^/]+\\/[^/]+?)(?:\\.git)?$/);\n if (sshMatch) return sshMatch[1];\n\n // HTTPS: https://github.com/org/repo.git\n const httpsMatch = url.match(/\\/([^/]+\\/[^/]+?)(?:\\.git)?$/);\n if (httpsMatch) return httpsMatch[1];\n } catch {\n // Not a git repo, no remote, etc.\n }\n return null;\n}\n\n/** Resolve the current git branch for a directory. */\nfunction resolveGitBranch(dir: string): string | null {\n try {\n return (\n execFileSync(\"git\", [\"-C\", dir, \"rev-parse\", \"--abbrev-ref\", \"HEAD\"], {\n encoding: \"utf-8\",\n timeout: 5000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim() || null\n );\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve the GitHub \"org/repo\" and branch for a working directory.\n * Results are cached for the lifetime of the process.\n *\n * 1. Try git directly on the CWD\n * 2. On failure, ask registered workspace providers (e.g. Superset)\n * for an alternative repo directory to resolve against\n */\nexport function resolveRepoFromCwd(cwd: string): RepoInfo | null {\n if (repoCache.has(cwd)) return repoCache.get(cwd)!;\n\n let result: RepoInfo | null = null;\n\n const repo = resolveGitRemote(cwd);\n if (repo) {\n result = { repo, branch: resolveGitBranch(cwd) };\n } else {\n // Ask workspace providers for a fallback\n for (const provider of providers) {\n if (!provider.canResolve(cwd)) continue;\n const resolved = provider.resolve(cwd);\n if (!resolved) continue;\n\n // Provider returned repo name directly (no git needed)\n if (resolved.repo) {\n result = { repo: resolved.repo, branch: resolved.branch };\n break;\n }\n\n // Provider returned a directory to resolve via git\n if (resolved.repoDir) {\n const fallbackRepo = resolveGitRemote(resolved.repoDir);\n if (fallbackRepo) {\n result = { repo: fallbackRepo, branch: resolved.branch };\n break;\n }\n }\n }\n }\n\n repoCache.set(cwd, result);\n return result;\n}\n\n// Cache: cwd → git identity\nconst gitIdentityCache = new Map<\n string,\n { name: string | null; email: string | null }\n>();\n\n/** Resolve git user.name / user.email for a directory. Cached per-process. */\nexport function resolveGitIdentity(cwd: string): {\n name: string | null;\n email: string | null;\n} {\n const cached = gitIdentityCache.get(cwd);\n if (cached) return cached;\n\n const result = { name: null as string | null, email: null as string | null };\n try {\n result.name =\n execFileSync(\"git\", [\"-C\", cwd, \"config\", \"user.name\"], {\n encoding: \"utf-8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim() || null;\n } catch {\n // no user.name configured\n }\n try {\n result.email =\n execFileSync(\"git\", [\"-C\", cwd, \"config\", \"user.email\"], {\n encoding: \"utf-8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim() || null;\n } catch {\n // no user.email configured\n }\n gitIdentityCache.set(cwd, result);\n return result;\n}\n\n/** Reset caches (for testing). */\nexport function _resetRepoCache(): void {\n repoCache.clear();\n gitIdentityCache.clear();\n for (const p of providers) p.close?.();\n}\n","import os from \"node:os\";\nimport path from \"node:path\";\nimport { Database } from \"../db/driver.js\";\nimport type { WorkspaceProvider } from \"./types.js\";\n\nconst SUPERSET_MARKER = `${path.sep}.superset${path.sep}`;\nconst SUPERSET_DB_PATH = path.join(os.homedir(), \".superset\", \"local.db\");\n\ninterface WorktreeRow {\n main_repo_path: string;\n branch: string | null;\n github_owner: string | null;\n project_name: string;\n}\n\nexport class SupersetProvider implements WorkspaceProvider {\n readonly id = \"superset\";\n\n private db: Database | null = null;\n private dbFailed = false;\n\n canResolve(cwd: string): boolean {\n return cwd.includes(SUPERSET_MARKER);\n }\n\n resolve(\n cwd: string,\n ): { repo?: string; repoDir?: string; branch?: string | null } | null {\n const db = this.getDb();\n if (!db) return null;\n\n try {\n // Try worktrees table first (for ~/.superset/worktrees/ paths)\n const wt = db\n .prepare(\n `SELECT p.main_repo_path, w.branch, p.github_owner, p.name as project_name\n FROM worktrees w\n JOIN projects p ON w.project_id = p.id\n WHERE ? LIKE w.path || '%'\n ORDER BY LENGTH(w.path) DESC\n LIMIT 1`,\n )\n .get(cwd) as WorktreeRow | undefined;\n if (wt) return this.buildResult(wt);\n\n // Fall back to projects table (for ~/.superset/projects/ paths)\n const proj = db\n .prepare(\n `SELECT main_repo_path, default_branch AS branch, github_owner, name as project_name\n FROM projects\n WHERE ? LIKE main_repo_path || '%'\n ORDER BY LENGTH(main_repo_path) DESC\n LIMIT 1`,\n )\n .get(cwd) as WorktreeRow | undefined;\n if (proj) return this.buildResult(proj);\n } catch {\n // Schema mismatch, corrupt DB, etc.\n }\n\n return null;\n }\n\n close(): void {\n if (this.db) {\n try {\n this.db.close();\n } catch {}\n this.db = null;\n }\n this.dbFailed = false;\n }\n\n private buildResult(row: WorktreeRow): {\n repo?: string;\n repoDir?: string;\n branch?: string | null;\n } {\n // Prefer deriving repo name directly from Superset DB (no git needed)\n if (row.github_owner) {\n return {\n repo: `${row.github_owner}/${row.project_name}`,\n branch: row.branch,\n };\n }\n // Fall back to git resolution on the main repo path\n return { repoDir: row.main_repo_path, branch: row.branch };\n }\n\n private getDb(): Database | null {\n if (this.dbFailed) return null;\n if (this.db) return this.db;\n try {\n this.db = new Database(SUPERSET_DB_PATH, {\n readonly: true,\n fileMustExist: true,\n });\n return this.db;\n } catch {\n this.dbFailed = true;\n return null;\n }\n }\n}\n","import type { FunctionReference } from \"convex/server\";\nimport { 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 interface ToolResult {\n ok: boolean;\n result?: unknown;\n error?: string;\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\nfunction ref<T extends \"query\" | \"mutation\" | \"action\">(\n path: string,\n): FunctionReference<T> {\n return path as unknown as FunctionReference<T>;\n}\n\n// ── API client factory ──────────────────────────────────────────────────────\n\nexport function createApiClient(token: string) {\n const isServiceToken = token.startsWith(\"fml_st_\");\n\n let clientPromise: Promise<{\n query: <T>(\n ref: FunctionReference<\"query\">,\n args: Record<string, unknown>,\n ) => Promise<T>;\n mutation: <T>(\n ref: FunctionReference<\"mutation\">,\n args: Record<string, unknown>,\n ) => Promise<T>;\n action: <T>(\n ref: FunctionReference<\"action\">,\n args: Record<string, unknown>,\n ) => Promise<T>;\n }> | null = null;\n\n function getClient() {\n if (!clientPromise) {\n clientPromise = import(\"convex/browser\").then(({ ConvexHttpClient }) => {\n const client = new ConvexHttpClient(CONVEX_URL);\n client.setAuth(token);\n return client;\n });\n }\n return clientPromise;\n }\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 return {\n // ── Orgs ──────────────────────────────────────────────────────────────\n\n async queryOrgs(): Promise<OrgInfo[]> {\n if (isServiceToken) {\n // Service tokens can't call Convex actions directly — org is embedded in the token\n return [];\n }\n const client = await getClient();\n return await client.query(ref(\"user/plugin:getMyOrgsAndRepos\"), {});\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 if (isServiceToken) {\n // Service token path: POST to HTTP endpoint (Convex can't validate non-JWT tokens)\n // Thread user identity: sandbox agents set FML_USER_EXTERNAL_ID;\n // device-flow logins fall back to the id stashed on the stored token\n // so tools can execute with the real user context even though the\n // token's actAsExternalId is the namespaced system:cli:* string.\n const { readTokens } = await import(\"./auth/token-store.js\");\n const userExternalId =\n process.env.FML_USER_EXTERNAL_ID ?? readTokens()?.user.id;\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({\n toolName,\n args,\n ...(userExternalId && { userExternalId }),\n }),\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 data;\n }\n\n // JWT path: standard Convex action\n const client = await getClient();\n const actionArgs: Record<string, unknown> = { toolName, args };\n // Explicit org > stored org selection > repo-based inference\n const org = opts?.org ?? getSelectedOrg();\n if (org) actionArgs.org = org;\n const repo = resolveRepoFromCwd(process.cwd());\n if (repo) actionArgs.repo = repo.repo;\n\n const result = await client.action(\n ref<\"action\">(\"user/tool_gateway:executeTool\"),\n actionArgs,\n );\n return { ok: true, result };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes(\"Unauthorized\") || msg.includes(\"not authenticated\")) {\n return {\n ok: false,\n error:\n \"Authentication expired. Run `fml login` to sign in again, then restart Claude Code.\",\n };\n }\n return { ok: false, error: msg };\n }\n },\n\n // ── Repo resolution ──────────────────────────────────────────────────\n\n async resolveRepo(\n orgSlug: string,\n repoFullName: string,\n ): Promise<ResolvedRepo | null> {\n const client = await getClient();\n return await client.query(ref(\"user/cli:resolveRepo\"), {\n orgSlug,\n repoFullName,\n });\n },\n\n // ── Config snapshots ─────────────────────────────────────────────────\n\n async listUserConfigSnapshots(\n orgSlug: string,\n ): Promise<UserConfigSnapshotSummary[]> {\n const client = await getClient();\n const result = await client.query<UserConfigSnapshotSummary[] | null>(\n ref(\"user/config_snapshots:listUserSnapshots\"),\n { orgSlug },\n );\n return result ?? [];\n },\n\n async getUserConfigDetail(\n orgSlug: string,\n githubUsername: string,\n ): Promise<UserConfigSnapshotDetail | null> {\n const client = await getClient();\n return await client.query(ref(\"user/config_snapshots:getUserDetail\"), {\n orgSlug,\n githubUsername,\n });\n },\n\n async listRepoConfigSnapshots(\n orgSlug: string,\n repository?: string,\n ): Promise<RepoConfigSnapshotSummary[]> {\n const client = await getClient();\n const result = await client.query<RepoConfigSnapshotSummary[] | null>(\n ref(\"user/config_snapshots:listRepoSnapshots\"),\n { orgSlug, repository },\n );\n return result ?? [];\n },\n\n async getRepoConfigDetail(\n orgSlug: string,\n repository: string,\n ): Promise<RepoConfigSnapshotDetail | null> {\n const client = await getClient();\n return await client.query(ref(\"user/config_snapshots:getRepoDetail\"), {\n orgSlug,\n repository,\n });\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 createApiClient(token);\n}\n"],"mappings":";;;;;;;;;;AAAA,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB;AACzB,SAAS,cAAc;AACvB,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAOvB,SAAS,sBAA4B;AAC1C,QAAMA,WAAU,cAAc,YAAY,GAAG;AAC7C,MAAI;AACF,IAAAA,SAAQ,gBAAgB;AAAA,EAC1B,SAAS,KAAK;AACZ,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU;AACjD,QAAI,CAAC,IAAI,SAAS,qBAAqB,GAAG;AACxC,YAAM;AAAA,IACR;AAEA,YAAQ;AAAA,MACN;AAAA,IACF;AAEA,UAAM,UAAU,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,IAAI;AAGlE,UAAM,aAAa,QAAQA,SAAQ,QAAQ,6BAA6B,CAAC;AACzE,WAAO,KAAK,YAAY,OAAO,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAElE,QAAI;AACF,eAAS,8BAA8B;AAAA,QACrC,KAAK;AAAA,QACL,OAAO;AAAA,QACP,SAAS;AAAA,MACX,CAAC;AACD,cAAQ,MAAM,yBAAyB;AAAA,IACzC,QAAQ;AACN,cAAQ;AAAA,QACN;AAAA,OACU,OAAO;AAAA,MACnB;AACA,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF;AACF;;;AC9CA,SAAS,oBAAoB;ACA7B,OAAO,QAAQ;AACf,OAAO,UAAU;AAIjB,IAAM,kBAAkB,GAAG,KAAK,GAAG,YAAY,KAAK,GAAG;AACvD,IAAM,mBAAmB,KAAK,KAAK,GAAG,QAAQ,GAAG,aAAa,UAAU;AASjE,IAAM,mBAAN,MAAoD;EAChD,KAAK;EAEN,KAAsB;EACtB,WAAW;EAEnB,WAAW,KAAsB;AAC/B,WAAO,IAAI,SAAS,eAAe;EACrC;EAEA,QACE,KACoE;AACpE,UAAM,KAAK,KAAK,MAAM;AACtB,QAAI,CAAC,GAAI,QAAO;AAEhB,QAAI;AAEF,YAAM,KAAK,GACR;QACC;;;;;;MAMF,EACC,IAAI,GAAG;AACV,UAAI,GAAI,QAAO,KAAK,YAAY,EAAE;AAGlC,YAAM,OAAO,GACV;QACC;;;;;MAKF,EACC,IAAI,GAAG;AACV,UAAI,KAAM,QAAO,KAAK,YAAY,IAAI;IACxC,QAAQ;IAER;AAEA,WAAO;EACT;EAEA,QAAc;AACZ,QAAI,KAAK,IAAI;AACX,UAAI;AACF,aAAK,GAAG,MAAM;MAChB,QAAQ;MAAC;AACT,WAAK,KAAK;IACZ;AACA,SAAK,WAAW;EAClB;EAEQ,YAAY,KAIlB;AAEA,QAAI,IAAI,cAAc;AACpB,aAAO;QACL,MAAM,GAAG,IAAI,YAAY,IAAI,IAAI,YAAY;QAC7C,QAAQ,IAAI;MACd;IACF;AAEA,WAAO,EAAE,SAAS,IAAI,gBAAgB,QAAQ,IAAI,OAAO;EAC3D;EAEQ,QAAyB;AAC/B,QAAI,KAAK,SAAU,QAAO;AAC1B,QAAI,KAAK,GAAI,QAAO,KAAK;AACzB,QAAI;AACF,WAAK,KAAK,IAAI,SAAS,kBAAkB;QACvC,UAAU;QACV,eAAe;MACjB,CAAC;AACD,aAAO,KAAK;IACd,QAAQ;AACN,WAAK,WAAW;AAChB,aAAO;IACT;EACF;AACF;AD7FA,IAAM,YAAY,oBAAI,IAA6B;AAGnD,IAAM,YAAiC,CAAC,IAAI,iBAAiB,CAAC;AAG9D,SAAS,iBAAiB,KAA4B;AACpD,MAAI;AACF,UAAM,MAAM;MACV;MACA,CAAC,MAAM,KAAK,UAAU,WAAW,QAAQ;MACzC;QACE,UAAU;QACV,SAAS;QACT,OAAO,CAAC,UAAU,QAAQ,QAAQ;MACpC;IACF,EAAE,KAAK;AAGP,UAAM,WAAW,IAAI,MAAM,sCAAsC;AACjE,QAAI,SAAU,QAAO,SAAS,CAAC;AAG/B,UAAM,aAAa,IAAI,MAAM,8BAA8B;AAC3D,QAAI,WAAY,QAAO,WAAW,CAAC;EACrC,QAAQ;EAER;AACA,SAAO;AACT;AAGA,SAAS,iBAAiB,KAA4B;AACpD,MAAI;AACF,WACE,aAAa,OAAO,CAAC,MAAM,KAAK,aAAa,gBAAgB,MAAM,GAAG;MACpE,UAAU;MACV,SAAS;MACT,OAAO,CAAC,UAAU,QAAQ,QAAQ;IACpC,CAAC,EAAE,KAAK,KAAK;EAEjB,QAAQ;AACN,WAAO;EACT;AACF;AAUO,SAAS,mBAAmB,KAA8B;AAC/D,MAAI,UAAU,IAAI,GAAG,EAAG,QAAO,UAAU,IAAI,GAAG;AAEhD,MAAI,SAA0B;AAE9B,QAAM,OAAO,iBAAiB,GAAG;AACjC,MAAI,MAAM;AACR,aAAS,EAAE,MAAM,QAAQ,iBAAiB,GAAG,EAAE;EACjD,OAAO;AAEL,eAAW,YAAY,WAAW;AAChC,UAAI,CAAC,SAAS,WAAW,GAAG,EAAG;AAC/B,YAAM,WAAW,SAAS,QAAQ,GAAG;AACrC,UAAI,CAAC,SAAU;AAGf,UAAI,SAAS,MAAM;AACjB,iBAAS,EAAE,MAAM,SAAS,MAAM,QAAQ,SAAS,OAAO;AACxD;MACF;AAGA,UAAI,SAAS,SAAS;AACpB,cAAM,eAAe,iBAAiB,SAAS,OAAO;AACtD,YAAI,cAAc;AAChB,mBAAS,EAAE,MAAM,cAAc,QAAQ,SAAS,OAAO;AACvD;QACF;MACF;IACF;EACF;AAEA,YAAU,IAAI,KAAK,MAAM;AACzB,SAAO;AACT;;;AEjEA,SAAS,IACPC,OACsB;AACtB,SAAOA;AACT;AAIO,SAAS,gBAAgB,OAAe;AAC7C,QAAM,iBAAiB,MAAM,WAAW,SAAS;AAEjD,MAAI,gBAaQ;AAEZ,WAAS,YAAY;AACnB,QAAI,CAAC,eAAe;AAClB,sBAAgB,OAAO,gBAAgB,EAAE,KAAK,CAAC,EAAE,iBAAiB,MAAM;AACtE,cAAM,SAAS,IAAI,iBAAiB,UAAU;AAC9C,eAAO,QAAQ,KAAK;AACpB,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAOA,WAAS,aAAqB;AAC5B,WAAO,WAAW,QAAQ,iBAAiB,cAAc,EAAE;AAAA,MACzD;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA;AAAA,IAGL,MAAM,YAAgC;AACpC,UAAI,gBAAgB;AAElB,eAAO,CAAC;AAAA,MACV;AACA,YAAM,SAAS,MAAM,UAAU;AAC/B,aAAO,MAAM,OAAO,MAAM,IAAI,+BAA+B,GAAG,CAAC,CAAC;AAAA,IACpE;AAAA;AAAA,IAIA,MAAM,YACJ,UACA,MACA,MACqB;AACrB,UAAI;AACF,YAAI,gBAAgB;AAMlB,gBAAM,EAAE,WAAW,IAAI,MAAM,OAAO,2BAAuB;AAC3D,gBAAM,iBACJ,QAAQ,IAAI,wBAAwB,WAAW,GAAG,KAAK;AACzD,gBAAM,MAAM,MAAM,MAAM,GAAG,WAAW,CAAC,sBAAsB;AAAA,YAC3D,QAAQ;AAAA,YACR,SAAS;AAAA,cACP,gBAAgB;AAAA,cAChB,eAAe,UAAU,KAAK;AAAA,YAChC;AAAA,YACA,MAAM,KAAK,UAAU;AAAA,cACnB;AAAA,cACA;AAAA,cACA,GAAI,kBAAkB,EAAE,eAAe;AAAA,YACzC,CAAC;AAAA,UACH,CAAC;AACD,gBAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,cAAI;AACJ,cAAI;AACF,mBAAO,KAAK,MAAM,IAAI;AAAA,UACxB,QAAQ;AACN,mBAAO;AAAA,cACL,IAAI;AAAA,cACJ,OAAO,QAAQ,IAAI,MAAM,KAAK,KAAK,MAAM,GAAG,GAAG,CAAC;AAAA,YAClD;AAAA,UACF;AACA,cAAI,CAAC,IAAI,MAAM,CAAC,KAAK,OAAO;AAC1B,mBAAO,EAAE,IAAI,OAAO,OAAO,QAAQ,IAAI,MAAM,GAAG;AAAA,UAClD;AACA,iBAAO;AAAA,QACT;AAGA,cAAM,SAAS,MAAM,UAAU;AAC/B,cAAM,aAAsC,EAAE,UAAU,KAAK;AAE7D,cAAM,MAAM,MAAM,OAAO,eAAe;AACxC,YAAI,IAAK,YAAW,MAAM;AAC1B,cAAM,OAAO,mBAAmB,QAAQ,IAAI,CAAC;AAC7C,YAAI,KAAM,YAAW,OAAO,KAAK;AAEjC,cAAM,SAAS,MAAM,OAAO;AAAA,UAC1B,IAAc,+BAA+B;AAAA,UAC7C;AAAA,QACF;AACA,eAAO,EAAE,IAAI,MAAM,OAAO;AAAA,MAC5B,SAAS,KAAK;AACZ,cAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,YAAI,IAAI,SAAS,cAAc,KAAK,IAAI,SAAS,mBAAmB,GAAG;AACrE,iBAAO;AAAA,YACL,IAAI;AAAA,YACJ,OACE;AAAA,UACJ;AAAA,QACF;AACA,eAAO,EAAE,IAAI,OAAO,OAAO,IAAI;AAAA,MACjC;AAAA,IACF;AAAA;AAAA,IAIA,MAAM,YACJ,SACA,cAC8B;AAC9B,YAAM,SAAS,MAAM,UAAU;AAC/B,aAAO,MAAM,OAAO,MAAM,IAAI,sBAAsB,GAAG;AAAA,QACrD;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA;AAAA,IAIA,MAAM,wBACJ,SACsC;AACtC,YAAM,SAAS,MAAM,UAAU;AAC/B,YAAM,SAAS,MAAM,OAAO;AAAA,QAC1B,IAAI,yCAAyC;AAAA,QAC7C,EAAE,QAAQ;AAAA,MACZ;AACA,aAAO,UAAU,CAAC;AAAA,IACpB;AAAA,IAEA,MAAM,oBACJ,SACA,gBAC0C;AAC1C,YAAM,SAAS,MAAM,UAAU;AAC/B,aAAO,MAAM,OAAO,MAAM,IAAI,qCAAqC,GAAG;AAAA,QACpE;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IAEA,MAAM,wBACJ,SACA,YACsC;AACtC,YAAM,SAAS,MAAM,UAAU;AAC/B,YAAM,SAAS,MAAM,OAAO;AAAA,QAC1B,IAAI,yCAAyC;AAAA,QAC7C,EAAE,SAAS,WAAW;AAAA,MACxB;AACA,aAAO,UAAU,CAAC;AAAA,IACpB;AAAA,IAEA,MAAM,oBACJ,SACA,YAC0C;AAC1C,YAAM,SAAS,MAAM,UAAU;AAC/B,aAAO,MAAM,OAAO,MAAM,IAAI,qCAAqC,GAAG;AAAA,QACpE;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAQA,eAAsB,yBAAyB;AAC7C,QAAM,QAAQ,MAAM,cAAc;AAClC,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,gBAAgB,KAAK;AAC9B;","names":["require","path"]}
|
|
@@ -11,6 +11,10 @@ import fs from "fs";
|
|
|
11
11
|
import os from "os";
|
|
12
12
|
import path from "path";
|
|
13
13
|
var DEFAULT_PROD_URL = "https://trustworthy-chihuahua-382.convex.cloud";
|
|
14
|
+
var DEFAULT_SYNC_URL = DEFAULT_PROD_URL.replace(
|
|
15
|
+
".convex.cloud",
|
|
16
|
+
".convex.site"
|
|
17
|
+
);
|
|
14
18
|
var DEFAULT_TARGET_NAME = "fml";
|
|
15
19
|
var ENV_CONFIG_PATH = path.join(FML_DATA_DIR, "env.json");
|
|
16
20
|
function readEnvConfig() {
|
|
@@ -56,10 +60,16 @@ function getActiveEnv() {
|
|
|
56
60
|
return { name, convexUrl: null };
|
|
57
61
|
}
|
|
58
62
|
var env = getActiveEnv();
|
|
59
|
-
var CONVEX_URL = (process.env.FML_CONVEX_URL ?? env.convexUrl ??
|
|
63
|
+
var CONVEX_URL = (process.env.FML_CONVEX_URL ?? env.convexUrl ?? DEFAULT_PROD_URL).replace(/\/$/, "");
|
|
60
64
|
var WORKOS_API_URL = "https://api.workos.com";
|
|
61
65
|
var WORKOS_AUTH_URL = "https://auth.fml.inc";
|
|
62
66
|
var AUTH_STORE_PATH = path.join(FML_DATA_DIR, `auth.${env.name}.json`);
|
|
67
|
+
function getSiteUrl() {
|
|
68
|
+
return CONVEX_URL.replace(".convex.cloud", ".convex.site").replace(
|
|
69
|
+
/:\d+$/,
|
|
70
|
+
""
|
|
71
|
+
);
|
|
72
|
+
}
|
|
63
73
|
|
|
64
74
|
// src/sentry.ts
|
|
65
75
|
var initialized = false;
|
|
@@ -132,14 +142,14 @@ function setSelectedOrg(orgSlug) {
|
|
|
132
142
|
var serviceTokenCache = null;
|
|
133
143
|
var serviceRefreshPromise = null;
|
|
134
144
|
var SERVICE_ACCESS_TOKEN_PATH = path2.join(FML_DATA_DIR, "access_token");
|
|
135
|
-
function
|
|
145
|
+
function getSiteUrl2() {
|
|
136
146
|
const explicit = process.env.CONVEX_SITE_URL;
|
|
137
147
|
if (explicit) return explicit.replace(/\/$/, "");
|
|
138
148
|
return CONVEX_URL.replace(".convex.cloud", ".convex.site");
|
|
139
149
|
}
|
|
140
150
|
async function refreshServiceToken(refreshToken2) {
|
|
141
151
|
try {
|
|
142
|
-
const response = await fetch(`${
|
|
152
|
+
const response = await fetch(`${getSiteUrl2()}/api/tokens/refresh`, {
|
|
143
153
|
method: "POST",
|
|
144
154
|
headers: {
|
|
145
155
|
"Content-Type": "application/json",
|
|
@@ -202,6 +212,25 @@ async function getValidToken() {
|
|
|
202
212
|
if (stored.expiresAt > Date.now() + 6e4) {
|
|
203
213
|
return stored.accessToken;
|
|
204
214
|
}
|
|
215
|
+
if (stored.tokenType === "service") {
|
|
216
|
+
if (serviceRefreshPromise) return serviceRefreshPromise;
|
|
217
|
+
serviceRefreshPromise = (async () => {
|
|
218
|
+
const newAccessToken = await refreshServiceToken(stored.refreshToken);
|
|
219
|
+
if (newAccessToken && serviceTokenCache) {
|
|
220
|
+
writeTokens({
|
|
221
|
+
...stored,
|
|
222
|
+
accessToken: newAccessToken,
|
|
223
|
+
expiresAt: serviceTokenCache.expiresAt
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return newAccessToken;
|
|
227
|
+
})();
|
|
228
|
+
try {
|
|
229
|
+
return await serviceRefreshPromise;
|
|
230
|
+
} finally {
|
|
231
|
+
serviceRefreshPromise = null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
205
234
|
if (refreshPromise) return refreshPromise;
|
|
206
235
|
refreshPromise = refreshToken(stored);
|
|
207
236
|
try {
|
|
@@ -265,7 +294,7 @@ async function refreshToken(stored) {
|
|
|
265
294
|
export {
|
|
266
295
|
initSentry,
|
|
267
296
|
Sentry,
|
|
268
|
-
|
|
297
|
+
DEFAULT_SYNC_URL,
|
|
269
298
|
DEFAULT_TARGET_NAME,
|
|
270
299
|
writeEnvConfig,
|
|
271
300
|
getActiveEnv,
|
|
@@ -273,10 +302,11 @@ export {
|
|
|
273
302
|
WORKOS_API_URL,
|
|
274
303
|
WORKOS_AUTH_URL,
|
|
275
304
|
AUTH_STORE_PATH,
|
|
305
|
+
getSiteUrl,
|
|
276
306
|
readTokens,
|
|
277
307
|
writeTokens,
|
|
278
308
|
getSelectedOrg,
|
|
279
309
|
setSelectedOrg,
|
|
280
310
|
getValidToken
|
|
281
311
|
};
|
|
282
|
-
//# sourceMappingURL=chunk-
|
|
312
|
+
//# sourceMappingURL=chunk-TXQRKU2Y.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/auth/token-store.ts","../src/config.ts","../src/sentry.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { AUTH_STORE_PATH, CONVEX_URL, WORKOS_API_URL } 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 /** WorkOS client ID used to obtain these tokens (needed for refresh) */\n workosClientId?: string;\n /** Token type: \"oauth\" (default) for WorkOS JWT, \"service\" for device flow tokens */\n tokenType?: \"oauth\" | \"service\";\n}\n\nfunction ensureDir(): void {\n const dir = path.dirname(AUTH_STORE_PATH);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n}\n\nexport function readTokens(): StoredAuth | null {\n try {\n const data = fs.readFileSync(AUTH_STORE_PATH, \"utf-8\");\n return JSON.parse(data) as StoredAuth;\n } catch {\n return null;\n }\n}\n\nexport function writeTokens(auth: StoredAuth): void {\n ensureDir();\n fs.writeFileSync(AUTH_STORE_PATH, 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\nlet serviceTokenCache: ServiceTokenCache | null = null;\nlet serviceRefreshPromise: Promise<string | null> | null = null;\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(): string {\n const explicit = process.env.CONVEX_SITE_URL;\n if (explicit) return explicit.replace(/\\/$/, \"\");\n return CONVEX_URL.replace(\".convex.cloud\", \".convex.site\");\n}\n\nasync function refreshServiceToken(\n refreshToken: string,\n): Promise<string | null> {\n try {\n const response = await fetch(`${getSiteUrl()}/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 serviceTokenCache = {\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// Mutex to prevent concurrent token refresh races\nlet refreshPromise: Promise<string | null> | null = null;\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(): Promise<string | null> {\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 if (\n serviceTokenCache &&\n serviceTokenCache.expiresAt > Date.now() + 60_000\n ) {\n return serviceTokenCache.accessToken;\n }\n\n if (serviceRefreshPromise) return serviceRefreshPromise;\n serviceRefreshPromise = refreshServiceToken(envToken);\n try {\n return await serviceRefreshPromise;\n } finally {\n serviceRefreshPromise = null;\n }\n }\n\n // Static service token (legacy) or plain env override\n if (envToken) return envToken;\n\n // Stored token path (OAuth or device flow)\n const stored = readTokens();\n if (!stored) {\n console.error(\"[fml] Auth: no stored tokens\");\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 // Device flow tokens: refresh via /api/tokens/refresh (same as sandbox tokens)\n if (stored.tokenType === \"service\") {\n if (serviceRefreshPromise) return serviceRefreshPromise;\n serviceRefreshPromise = (async () => {\n const newAccessToken = await refreshServiceToken(stored.refreshToken);\n if (newAccessToken && serviceTokenCache) {\n writeTokens({\n ...stored,\n accessToken: newAccessToken,\n expiresAt: serviceTokenCache.expiresAt,\n });\n }\n return newAccessToken;\n })();\n try {\n return await serviceRefreshPromise;\n } finally {\n serviceRefreshPromise = null;\n }\n }\n\n // OAuth tokens: refresh via WorkOS\n if (refreshPromise) return refreshPromise;\n\n refreshPromise = refreshToken(stored);\n try {\n return await refreshPromise;\n } finally {\n refreshPromise = null;\n }\n}\n\nasync function refreshToken(stored: StoredAuth): 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);\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/**\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 config = readEnvConfig();\n const name = config.active;\n\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 === name);\n if (target) {\n const convexUrl = target.url.replace(\".convex.site\", \".convex.cloud\");\n return { name, convexUrl };\n }\n } catch {\n // Panopticon config not available\n }\n\n return { name, convexUrl: null };\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\nconst env = getActiveEnv();\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 env.convexUrl ??\n DEFAULT_PROD_URL\n).replace(/\\/$/, \"\");\n\n/** WorkOS API base URL */\nexport const WORKOS_API_URL = \"https://api.workos.com\";\n\n/** WorkOS authorization base URL */\nexport const WORKOS_AUTH_URL = \"https://auth.fml.inc\";\n\n/** Path to auth token store (per-environment) */\nexport const AUTH_STORE_PATH = path.join(FML_DATA_DIR, `auth.${env.name}.json`);\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","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","path","fs","getSiteUrl","refreshToken"]}
|