@fml-inc/fml 0.0.22
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 +11 -0
- package/bin/fml +3 -0
- package/bin/mcp-server +3 -0
- package/bin/sync-daemon +3 -0
- package/dist/chunk-4YEAQJPF.js +47 -0
- package/dist/chunk-4YEAQJPF.js.map +1 -0
- package/dist/chunk-DP2ILCME.js +282 -0
- package/dist/chunk-DP2ILCME.js.map +1 -0
- package/dist/chunk-EL2WSQBR.js +88 -0
- package/dist/chunk-EL2WSQBR.js.map +1 -0
- package/dist/chunk-MYCITM37.js +179 -0
- package/dist/chunk-MYCITM37.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/cli.js +1720 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon-J4WSZXVY.js +15 -0
- package/dist/daemon-J4WSZXVY.js.map +1 -0
- package/dist/esm-JQB7UAJD.js +14618 -0
- package/dist/esm-JQB7UAJD.js.map +1 -0
- package/dist/mcp/server.js +13033 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/postinstall.js +81 -0
- package/dist/postinstall.js.map +1 -0
- package/dist/token-store-67FJZTYS.js +17 -0
- package/dist/token-store-67FJZTYS.js.map +1 -0
- package/package.json +49 -0
package/bin/fml
ADDED
package/bin/mcp-server
ADDED
package/bin/sync-daemon
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/dirs.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
var home = os.homedir();
|
|
6
|
+
function dataDir() {
|
|
7
|
+
switch (process.platform) {
|
|
8
|
+
case "darwin":
|
|
9
|
+
return path.join(home, "Library", "Application Support", "fml");
|
|
10
|
+
case "win32":
|
|
11
|
+
return path.join(
|
|
12
|
+
process.env.APPDATA ?? path.join(home, "AppData", "Roaming"),
|
|
13
|
+
"fml"
|
|
14
|
+
);
|
|
15
|
+
default:
|
|
16
|
+
return path.join(
|
|
17
|
+
process.env.XDG_DATA_HOME ?? path.join(home, ".local", "share"),
|
|
18
|
+
"fml"
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function logDir() {
|
|
23
|
+
switch (process.platform) {
|
|
24
|
+
case "darwin":
|
|
25
|
+
return path.join(home, "Library", "Logs", "fml");
|
|
26
|
+
case "win32":
|
|
27
|
+
return path.join(
|
|
28
|
+
process.env.APPDATA ?? path.join(home, "AppData", "Roaming"),
|
|
29
|
+
"fml",
|
|
30
|
+
"logs"
|
|
31
|
+
);
|
|
32
|
+
default:
|
|
33
|
+
return path.join(
|
|
34
|
+
process.env.XDG_STATE_HOME ?? path.join(home, ".local", "state"),
|
|
35
|
+
"fml",
|
|
36
|
+
"log"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
var FML_DATA_DIR = dataDir();
|
|
41
|
+
var FML_LOG_DIR = logDir();
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
FML_DATA_DIR,
|
|
45
|
+
FML_LOG_DIR
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=chunk-4YEAQJPF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/dirs.ts"],"sourcesContent":["/**\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 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 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 — SQLite DBs, PID files, config, auth tokens */\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"],"mappings":";AAOA,OAAO,QAAQ;AACf,OAAO,QAAQ;AACf,OAAO,UAAU;AAEjB,IAAM,OAAO,GAAG,QAAQ;AAExB,SAAS,UAAkB;AACzB,UAAQ,QAAQ,UAAU;AAAA,IACxB,KAAK;AACH,aAAO,KAAK,KAAK,MAAM,WAAW,uBAAuB,KAAK;AAAA,IAChE,KAAK;AACH,aAAO,KAAK;AAAA,QACV,QAAQ,IAAI,WAAW,KAAK,KAAK,MAAM,WAAW,SAAS;AAAA,QAC3D;AAAA,MACF;AAAA,IACF;AACE,aAAO,KAAK;AAAA,QACV,QAAQ,IAAI,iBAAiB,KAAK,KAAK,MAAM,UAAU,OAAO;AAAA,QAC9D;AAAA,MACF;AAAA,EACJ;AACF;AAEA,SAAS,SAAiB;AACxB,UAAQ,QAAQ,UAAU;AAAA,IACxB,KAAK;AACH,aAAO,KAAK,KAAK,MAAM,WAAW,QAAQ,KAAK;AAAA,IACjD,KAAK;AACH,aAAO,KAAK;AAAA,QACV,QAAQ,IAAI,WAAW,KAAK,KAAK,MAAM,WAAW,SAAS;AAAA,QAC3D;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACE,aAAO,KAAK;AAAA,QACV,QAAQ,IAAI,kBAAkB,KAAK,KAAK,MAAM,UAAU,OAAO;AAAA,QAC/D;AAAA,QACA;AAAA,MACF;AAAA,EACJ;AACF;AAGO,IAAM,eAAe,QAAQ;AAG7B,IAAM,cAAc,OAAO;","names":[]}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FML_DATA_DIR
|
|
3
|
+
} from "./chunk-4YEAQJPF.js";
|
|
4
|
+
|
|
5
|
+
// src/auth/token-store.ts
|
|
6
|
+
import fs2 from "fs";
|
|
7
|
+
import path2 from "path";
|
|
8
|
+
|
|
9
|
+
// src/config.ts
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import os from "os";
|
|
12
|
+
import path from "path";
|
|
13
|
+
var DEFAULT_PROD_URL = "https://trustworthy-chihuahua-382.convex.cloud";
|
|
14
|
+
var DEFAULT_TARGET_NAME = "fml";
|
|
15
|
+
var ENV_CONFIG_PATH = path.join(FML_DATA_DIR, "env.json");
|
|
16
|
+
function readEnvConfig() {
|
|
17
|
+
try {
|
|
18
|
+
const raw = fs.readFileSync(ENV_CONFIG_PATH, "utf-8");
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
} catch {
|
|
21
|
+
return { active: DEFAULT_TARGET_NAME };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function writeEnvConfig(config) {
|
|
25
|
+
const dir = path.dirname(ENV_CONFIG_PATH);
|
|
26
|
+
if (!fs.existsSync(dir)) {
|
|
27
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
28
|
+
}
|
|
29
|
+
fs.writeFileSync(ENV_CONFIG_PATH, `${JSON.stringify(config, null, 2)}
|
|
30
|
+
`, {
|
|
31
|
+
mode: 384
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function getActiveEnv() {
|
|
35
|
+
const config = readEnvConfig();
|
|
36
|
+
const name = config.active;
|
|
37
|
+
try {
|
|
38
|
+
const panoDataDir = process.env.PANOPTICON_DATA_DIR ?? (process.platform === "darwin" ? path.join(
|
|
39
|
+
os.homedir(),
|
|
40
|
+
"Library",
|
|
41
|
+
"Application Support",
|
|
42
|
+
"panopticon"
|
|
43
|
+
) : process.platform === "win32" ? path.join(
|
|
44
|
+
process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"),
|
|
45
|
+
"panopticon"
|
|
46
|
+
) : path.join(os.homedir(), ".local", "share", "panopticon"));
|
|
47
|
+
const raw = fs.readFileSync(path.join(panoDataDir, "config.json"), "utf-8");
|
|
48
|
+
const panoConf = JSON.parse(raw);
|
|
49
|
+
const target = panoConf.sync?.targets?.find((t) => t.name === name);
|
|
50
|
+
if (target) {
|
|
51
|
+
const convexUrl = target.url.replace(".convex.site", ".convex.cloud");
|
|
52
|
+
return { name, convexUrl };
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
return { name, convexUrl: null };
|
|
57
|
+
}
|
|
58
|
+
var env = getActiveEnv();
|
|
59
|
+
var CONVEX_URL = (process.env.FML_CONVEX_URL ?? env.convexUrl ?? "").replace(/\/$/, "");
|
|
60
|
+
var WORKOS_API_URL = "https://api.workos.com";
|
|
61
|
+
var WORKOS_AUTH_URL = "https://auth.fml.inc";
|
|
62
|
+
var AUTH_STORE_PATH = path.join(FML_DATA_DIR, `auth.${env.name}.json`);
|
|
63
|
+
|
|
64
|
+
// src/sentry.ts
|
|
65
|
+
var initialized = false;
|
|
66
|
+
var sentry = null;
|
|
67
|
+
async function loadSentry() {
|
|
68
|
+
if (sentry) return sentry;
|
|
69
|
+
try {
|
|
70
|
+
sentry = await import("./esm-JQB7UAJD.js");
|
|
71
|
+
return sentry;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function initSentry() {
|
|
77
|
+
if (initialized) return;
|
|
78
|
+
initialized = true;
|
|
79
|
+
const mod = await loadSentry();
|
|
80
|
+
if (!mod) return;
|
|
81
|
+
const client = new mod.ServerRuntimeClient({
|
|
82
|
+
dsn: "https://4c243521c03917c886d0bf33bc0038c8@o4510167429873664.ingest.us.sentry.io/4511078213156864",
|
|
83
|
+
tracesSampleRate: 0,
|
|
84
|
+
integrations: [],
|
|
85
|
+
stackParser: mod.createStackParser(mod.nodeStackLineParser()),
|
|
86
|
+
transport: (options) => mod.createTransport(options, async (request) => {
|
|
87
|
+
const res = await fetch(options.url, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: { "Content-Type": "application/x-sentry-envelope" },
|
|
90
|
+
body: request.body
|
|
91
|
+
});
|
|
92
|
+
return { statusCode: res.status };
|
|
93
|
+
})
|
|
94
|
+
});
|
|
95
|
+
mod.setCurrentClient(client);
|
|
96
|
+
client.init();
|
|
97
|
+
}
|
|
98
|
+
var Sentry = {
|
|
99
|
+
captureException: (err) => sentry?.captureException(err),
|
|
100
|
+
captureMessage: (msg) => sentry?.captureMessage(msg)
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/auth/token-store.ts
|
|
104
|
+
function ensureDir() {
|
|
105
|
+
const dir = path2.dirname(AUTH_STORE_PATH);
|
|
106
|
+
if (!fs2.existsSync(dir)) {
|
|
107
|
+
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function readTokens() {
|
|
111
|
+
try {
|
|
112
|
+
const data = fs2.readFileSync(AUTH_STORE_PATH, "utf-8");
|
|
113
|
+
return JSON.parse(data);
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function writeTokens(auth) {
|
|
119
|
+
ensureDir();
|
|
120
|
+
fs2.writeFileSync(AUTH_STORE_PATH, JSON.stringify(auth, null, 2), {
|
|
121
|
+
mode: 384
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function getSelectedOrg() {
|
|
125
|
+
return readTokens()?.orgSlug ?? null;
|
|
126
|
+
}
|
|
127
|
+
function setSelectedOrg(orgSlug) {
|
|
128
|
+
const stored = readTokens();
|
|
129
|
+
if (!stored) return;
|
|
130
|
+
writeTokens({ ...stored, orgSlug });
|
|
131
|
+
}
|
|
132
|
+
var serviceTokenCache = null;
|
|
133
|
+
var serviceRefreshPromise = null;
|
|
134
|
+
var SERVICE_ACCESS_TOKEN_PATH = path2.join(FML_DATA_DIR, "access_token");
|
|
135
|
+
function getSiteUrl() {
|
|
136
|
+
const explicit = process.env.CONVEX_SITE_URL;
|
|
137
|
+
if (explicit) return explicit.replace(/\/$/, "");
|
|
138
|
+
return CONVEX_URL.replace(".convex.cloud", ".convex.site");
|
|
139
|
+
}
|
|
140
|
+
async function refreshServiceToken(refreshToken2) {
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetch(`${getSiteUrl()}/api/tokens/refresh`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: {
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
Authorization: `Bearer ${refreshToken2}`
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
console.error(
|
|
151
|
+
`[fml] Service token refresh failed: HTTP ${response.status}`
|
|
152
|
+
);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const data = await response.json();
|
|
156
|
+
if (!data.ok || !data.accessToken) {
|
|
157
|
+
console.error(`[fml] Service token refresh error: ${data.error}`);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
serviceTokenCache = {
|
|
161
|
+
accessToken: data.accessToken,
|
|
162
|
+
expiresAt: data.expiresAt
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
const dir = path2.dirname(SERVICE_ACCESS_TOKEN_PATH);
|
|
166
|
+
if (!fs2.existsSync(dir)) {
|
|
167
|
+
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
168
|
+
}
|
|
169
|
+
fs2.writeFileSync(SERVICE_ACCESS_TOKEN_PATH, data.accessToken, {
|
|
170
|
+
mode: 384
|
|
171
|
+
});
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
return data.accessToken;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
Sentry.captureException(err);
|
|
177
|
+
console.error(`[fml] Service token refresh error: ${err}`);
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
var refreshPromise = null;
|
|
182
|
+
async function getValidToken() {
|
|
183
|
+
const envToken = process.env.FML_TOKEN;
|
|
184
|
+
if (envToken?.startsWith("fml_srt_")) {
|
|
185
|
+
if (serviceTokenCache && serviceTokenCache.expiresAt > Date.now() + 6e4) {
|
|
186
|
+
return serviceTokenCache.accessToken;
|
|
187
|
+
}
|
|
188
|
+
if (serviceRefreshPromise) return serviceRefreshPromise;
|
|
189
|
+
serviceRefreshPromise = refreshServiceToken(envToken);
|
|
190
|
+
try {
|
|
191
|
+
return await serviceRefreshPromise;
|
|
192
|
+
} finally {
|
|
193
|
+
serviceRefreshPromise = null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (envToken) return envToken;
|
|
197
|
+
const stored = readTokens();
|
|
198
|
+
if (!stored) {
|
|
199
|
+
console.error("[fml] Auth: no stored tokens");
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
if (stored.expiresAt > Date.now() + 6e4) {
|
|
203
|
+
return stored.accessToken;
|
|
204
|
+
}
|
|
205
|
+
if (refreshPromise) return refreshPromise;
|
|
206
|
+
refreshPromise = refreshToken(stored);
|
|
207
|
+
try {
|
|
208
|
+
return await refreshPromise;
|
|
209
|
+
} finally {
|
|
210
|
+
refreshPromise = null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function refreshToken(stored) {
|
|
214
|
+
if (!stored.workosClientId) {
|
|
215
|
+
console.error(
|
|
216
|
+
"[fml] Auth: token expired, missing workosClientId \u2014 run `fml login`"
|
|
217
|
+
);
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const response = await fetch(
|
|
222
|
+
`${WORKOS_API_URL}/user_management/authenticate`,
|
|
223
|
+
{
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: { "Content-Type": "application/json" },
|
|
226
|
+
body: JSON.stringify({
|
|
227
|
+
grant_type: "refresh_token",
|
|
228
|
+
client_id: stored.workosClientId,
|
|
229
|
+
refresh_token: stored.refreshToken
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
console.error(
|
|
235
|
+
`[fml] Auth: refresh failed (HTTP ${response.status}) \u2014 token preserved`
|
|
236
|
+
);
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const data = await response.json();
|
|
240
|
+
const userName = [data.user.first_name, data.user.last_name].filter(Boolean).join(" ");
|
|
241
|
+
const refreshed = {
|
|
242
|
+
accessToken: data.access_token,
|
|
243
|
+
refreshToken: data.refresh_token,
|
|
244
|
+
expiresAt: Date.now() + (data.expires_in ?? 300) * 1e3,
|
|
245
|
+
user: {
|
|
246
|
+
id: data.user.id,
|
|
247
|
+
email: data.user.email,
|
|
248
|
+
name: userName || stored.user.name
|
|
249
|
+
},
|
|
250
|
+
orgSlug: stored.orgSlug,
|
|
251
|
+
workosClientId: stored.workosClientId
|
|
252
|
+
};
|
|
253
|
+
writeTokens(refreshed);
|
|
254
|
+
return refreshed.accessToken;
|
|
255
|
+
} catch (err) {
|
|
256
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
257
|
+
console.error(
|
|
258
|
+
`[fml] Auth: refresh failed (network error: ${msg}) \u2014 token preserved`
|
|
259
|
+
);
|
|
260
|
+
Sentry.captureException(err);
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export {
|
|
266
|
+
initSentry,
|
|
267
|
+
Sentry,
|
|
268
|
+
DEFAULT_PROD_URL,
|
|
269
|
+
DEFAULT_TARGET_NAME,
|
|
270
|
+
writeEnvConfig,
|
|
271
|
+
getActiveEnv,
|
|
272
|
+
CONVEX_URL,
|
|
273
|
+
WORKOS_API_URL,
|
|
274
|
+
WORKOS_AUTH_URL,
|
|
275
|
+
AUTH_STORE_PATH,
|
|
276
|
+
readTokens,
|
|
277
|
+
writeTokens,
|
|
278
|
+
getSelectedOrg,
|
|
279
|
+
setSelectedOrg,
|
|
280
|
+
getValidToken
|
|
281
|
+
};
|
|
282
|
+
//# sourceMappingURL=chunk-DP2ILCME.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}\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 // OAuth path (interactive user)\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 // Deduplicate concurrent refresh attempts\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/** 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/** Convex deployment URL (switches with `fml env`) */\nexport const CONVEX_URL: string = (\n process.env.FML_CONVEX_URL ?? env.convexUrl ?? \"\"\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","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","refreshToken"]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FML_DATA_DIR,
|
|
3
|
+
FML_LOG_DIR
|
|
4
|
+
} from "./chunk-4YEAQJPF.js";
|
|
5
|
+
|
|
6
|
+
// src/daemon-utils.ts
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { execFileSync } from "child_process";
|
|
10
|
+
var SYNC_PID_FILE = path.join(FML_DATA_DIR, "sync.pid");
|
|
11
|
+
var SYNC_LOG_FILE = path.join(FML_LOG_DIR, "sync.log");
|
|
12
|
+
function isDaemonRunning(pidFile) {
|
|
13
|
+
if (!fs.existsSync(pidFile)) return { running: false, pid: null };
|
|
14
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
15
|
+
try {
|
|
16
|
+
process.kill(pid, 0);
|
|
17
|
+
return { running: true, pid };
|
|
18
|
+
} catch {
|
|
19
|
+
return { running: false, pid };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function resolvePanopticonBin() {
|
|
23
|
+
try {
|
|
24
|
+
const result = execFileSync("which", ["panopticon"], {
|
|
25
|
+
encoding: "utf-8",
|
|
26
|
+
timeout: 5e3
|
|
27
|
+
}).trim();
|
|
28
|
+
if (result) return result;
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const resolved = import.meta.resolve("@fml-inc/panopticon/setup");
|
|
33
|
+
const pkgDir = path.resolve(new URL(resolved).pathname, "../..");
|
|
34
|
+
const bin = path.join(pkgDir, "bin", "panopticon");
|
|
35
|
+
if (fs.existsSync(bin)) return bin;
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function panopticonExec(...argsAndOpts) {
|
|
41
|
+
let timeout = 1e4;
|
|
42
|
+
const args = [];
|
|
43
|
+
for (const a of argsAndOpts) {
|
|
44
|
+
if (typeof a === "string") args.push(a);
|
|
45
|
+
else timeout = a.timeout ?? timeout;
|
|
46
|
+
}
|
|
47
|
+
const bin = resolvePanopticonBin();
|
|
48
|
+
if (!bin) return { ok: false, stdout: "panopticon binary not found" };
|
|
49
|
+
try {
|
|
50
|
+
const stdout = execFileSync("node", [bin, ...args], {
|
|
51
|
+
encoding: "utf-8",
|
|
52
|
+
timeout
|
|
53
|
+
});
|
|
54
|
+
return { ok: true, stdout };
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const e = err;
|
|
57
|
+
return { ok: false, stdout: (e.stdout ?? "") + (e.stderr ?? "") };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/commands/daemon.ts
|
|
62
|
+
function parsePanopticonRunning() {
|
|
63
|
+
const result = panopticonExec("status");
|
|
64
|
+
const serverLine = result.stdout.split("\n").find((l) => l.startsWith("Server:"));
|
|
65
|
+
return result.ok && /running/i.test(serverLine ?? "");
|
|
66
|
+
}
|
|
67
|
+
function isSyncRunning() {
|
|
68
|
+
return isDaemonRunning(SYNC_PID_FILE);
|
|
69
|
+
}
|
|
70
|
+
async function handleStart() {
|
|
71
|
+
const startResult = panopticonExec("start");
|
|
72
|
+
console.log(
|
|
73
|
+
startResult.stdout.trim() || (startResult.ok ? "Panopticon started" : "Failed to start panopticon")
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function handleStop() {
|
|
77
|
+
const stopResult = panopticonExec("stop");
|
|
78
|
+
console.log(stopResult.stdout.trim() || "Panopticon stopped");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
panopticonExec,
|
|
83
|
+
parsePanopticonRunning,
|
|
84
|
+
isSyncRunning,
|
|
85
|
+
handleStart,
|
|
86
|
+
handleStop
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=chunk-EL2WSQBR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/daemon-utils.ts","../src/commands/daemon.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { execFileSync } from \"node:child_process\";\nimport { FML_DATA_DIR, FML_LOG_DIR } from \"./dirs.js\";\n\nexport const SYNC_PID_FILE = path.join(FML_DATA_DIR, \"sync.pid\");\nexport const SYNC_LOG_FILE = path.join(FML_LOG_DIR, \"sync.log\");\n\nexport function isDaemonRunning(pidFile: string): {\n running: boolean;\n pid: number | null;\n} {\n if (!fs.existsSync(pidFile)) return { running: false, pid: null };\n const pid = parseInt(fs.readFileSync(pidFile, \"utf-8\").trim(), 10);\n try {\n process.kill(pid, 0);\n return { running: true, pid };\n } catch {\n return { running: false, pid };\n }\n}\n\n/** Resolve the panopticon CLI binary from PATH or the bundled dependency. */\nexport function resolvePanopticonBin(): string | null {\n // Try to find panopticon in PATH\n try {\n const result = execFileSync(\"which\", [\"panopticon\"], {\n encoding: \"utf-8\",\n timeout: 5_000,\n }).trim();\n if (result) return result;\n } catch {}\n\n // Fall back to the bundled dependency\n try {\n const resolved = import.meta.resolve(\"@fml-inc/panopticon/setup\");\n const pkgDir = path.resolve(new URL(resolved).pathname, \"../..\");\n const bin = path.join(pkgDir, \"bin\", \"panopticon\");\n if (fs.existsSync(bin)) return bin;\n } catch {}\n\n return null;\n}\n\n/** Run a panopticon CLI command and return stdout. */\nexport function panopticonExec(...args: string[]): {\n ok: boolean;\n stdout: string;\n};\nexport function panopticonExec(\n ...argsAndOpts: [...string[], { timeout?: number }]\n): { ok: boolean; stdout: string };\nexport function panopticonExec(\n ...argsAndOpts: Array<string | { timeout?: number }>\n): {\n ok: boolean;\n stdout: string;\n} {\n let timeout = 10_000;\n const args: string[] = [];\n for (const a of argsAndOpts) {\n if (typeof a === \"string\") args.push(a);\n else timeout = a.timeout ?? timeout;\n }\n const bin = resolvePanopticonBin();\n if (!bin) return { ok: false, stdout: \"panopticon binary not found\" };\n try {\n const stdout = execFileSync(\"node\", [bin, ...args], {\n encoding: \"utf-8\",\n timeout,\n });\n return { ok: true, stdout };\n } catch (err: unknown) {\n const e = err as { stdout?: string; stderr?: string; status?: number };\n return { ok: false, stdout: (e.stdout ?? \"\") + (e.stderr ?? \"\") };\n }\n}\n","import {\n isDaemonRunning,\n panopticonExec,\n SYNC_PID_FILE,\n} from \"../daemon-utils.js\";\n\n// ── Status helpers (used by doctor, status) ─────────────────────────────────\n\nexport function parsePanopticonRunning(): boolean {\n const result = panopticonExec(\"status\");\n const serverLine = result.stdout\n .split(\"\\n\")\n .find((l) => l.startsWith(\"Server:\"));\n return result.ok && /running/i.test(serverLine ?? \"\");\n}\n\nexport function isSyncRunning(): { running: boolean; pid: number | null } {\n return isDaemonRunning(SYNC_PID_FILE);\n}\n\n// ── Start / Stop (used by CLI, install, MCP) ────────────────────────────────\n\nexport async function handleStart(): Promise<void> {\n const startResult = panopticonExec(\"start\");\n console.log(\n startResult.stdout.trim() ||\n (startResult.ok ? \"Panopticon started\" : \"Failed to start panopticon\"),\n );\n}\n\nexport function handleStop(): void {\n const stopResult = panopticonExec(\"stop\");\n console.log(stopResult.stdout.trim() || \"Panopticon stopped\");\n}\n"],"mappings":";;;;;;AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,oBAAoB;AAGtB,IAAM,gBAAgB,KAAK,KAAK,cAAc,UAAU;AACxD,IAAM,gBAAgB,KAAK,KAAK,aAAa,UAAU;AAEvD,SAAS,gBAAgB,SAG9B;AACA,MAAI,CAAC,GAAG,WAAW,OAAO,EAAG,QAAO,EAAE,SAAS,OAAO,KAAK,KAAK;AAChE,QAAM,MAAM,SAAS,GAAG,aAAa,SAAS,OAAO,EAAE,KAAK,GAAG,EAAE;AACjE,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO,EAAE,SAAS,MAAM,IAAI;AAAA,EAC9B,QAAQ;AACN,WAAO,EAAE,SAAS,OAAO,IAAI;AAAA,EAC/B;AACF;AAGO,SAAS,uBAAsC;AAEpD,MAAI;AACF,UAAM,SAAS,aAAa,SAAS,CAAC,YAAY,GAAG;AAAA,MACnD,UAAU;AAAA,MACV,SAAS;AAAA,IACX,CAAC,EAAE,KAAK;AACR,QAAI,OAAQ,QAAO;AAAA,EACrB,QAAQ;AAAA,EAAC;AAGT,MAAI;AACF,UAAM,WAAW,YAAY,QAAQ,2BAA2B;AAChE,UAAM,SAAS,KAAK,QAAQ,IAAI,IAAI,QAAQ,EAAE,UAAU,OAAO;AAC/D,UAAM,MAAM,KAAK,KAAK,QAAQ,OAAO,YAAY;AACjD,QAAI,GAAG,WAAW,GAAG,EAAG,QAAO;AAAA,EACjC,QAAQ;AAAA,EAAC;AAET,SAAO;AACT;AAUO,SAAS,kBACX,aAIH;AACA,MAAI,UAAU;AACd,QAAM,OAAiB,CAAC;AACxB,aAAW,KAAK,aAAa;AAC3B,QAAI,OAAO,MAAM,SAAU,MAAK,KAAK,CAAC;AAAA,QACjC,WAAU,EAAE,WAAW;AAAA,EAC9B;AACA,QAAM,MAAM,qBAAqB;AACjC,MAAI,CAAC,IAAK,QAAO,EAAE,IAAI,OAAO,QAAQ,8BAA8B;AACpE,MAAI;AACF,UAAM,SAAS,aAAa,QAAQ,CAAC,KAAK,GAAG,IAAI,GAAG;AAAA,MAClD,UAAU;AAAA,MACV;AAAA,IACF,CAAC;AACD,WAAO,EAAE,IAAI,MAAM,OAAO;AAAA,EAC5B,SAAS,KAAc;AACrB,UAAM,IAAI;AACV,WAAO,EAAE,IAAI,OAAO,SAAS,EAAE,UAAU,OAAO,EAAE,UAAU,IAAI;AAAA,EAClE;AACF;;;ACpEO,SAAS,yBAAkC;AAChD,QAAM,SAAS,eAAe,QAAQ;AACtC,QAAM,aAAa,OAAO,OACvB,MAAM,IAAI,EACV,KAAK,CAAC,MAAM,EAAE,WAAW,SAAS,CAAC;AACtC,SAAO,OAAO,MAAM,WAAW,KAAK,cAAc,EAAE;AACtD;AAEO,SAAS,gBAA0D;AACxE,SAAO,gBAAgB,aAAa;AACtC;AAIA,eAAsB,cAA6B;AACjD,QAAM,cAAc,eAAe,OAAO;AAC1C,UAAQ;AAAA,IACN,YAAY,OAAO,KAAK,MACrB,YAAY,KAAK,uBAAuB;AAAA,EAC7C;AACF;AAEO,SAAS,aAAmB;AACjC,QAAM,aAAa,eAAe,MAAM;AACxC,UAAQ,IAAI,WAAW,OAAO,KAAK,KAAK,oBAAoB;AAC9D;","names":[]}
|