@fml-inc/fml 0.8.0-rc.1 → 0.8.0-rc.2
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/README.md +27 -16
- package/dist/{chunk-CK5MEL5J.js → chunk-3KISRT57.js} +1 -1
- package/dist/{chunk-DEMVOVVY.js → chunk-56TPYMNK.js} +244 -29
- package/dist/{chunk-T5YVLSUC.js → chunk-7V7G7NVF.js} +4 -0
- package/dist/{chunk-7DR5X7XI.js → chunk-A6HAEVJX.js} +136 -52
- package/dist/{chunk-F66IBM3Z.js → chunk-DZKYW327.js} +2 -2
- package/dist/{chunk-I4GZPTBM.js → chunk-ENW2GJNO.js} +1 -1
- package/dist/{chunk-P3QLARXE.js → chunk-R45CWWZO.js} +3 -3
- package/dist/{chunk-HUR2HCUG.js → chunk-WZLLO563.js} +2 -2
- package/dist/{chunk-JXSSPQ5B.js → chunk-YSU5CS2V.js} +1 -1
- package/dist/cli.js +1310 -1158
- package/dist/hook-handler.js +290 -40
- package/dist/mcp/server.js +7 -7
- package/dist/observer-server.js +8 -8
- package/dist/sync-cycle.js +3 -3
- package/dist/{token-store-R7WXEPLI.js → token-store-BT5THIAJ.js} +1 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
createFmlClient,
|
|
4
4
|
getAuthenticatedClient
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-R45CWWZO.js";
|
|
6
6
|
import {
|
|
7
7
|
OBSERVER_HEARTBEAT_STALE_MS,
|
|
8
8
|
getProcessIdentity,
|
|
@@ -10,13 +10,13 @@ import {
|
|
|
10
10
|
isPidAlive,
|
|
11
11
|
readObserverStateStatus,
|
|
12
12
|
removeObserverStateIfOwned
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-WZLLO563.js";
|
|
14
14
|
import {
|
|
15
15
|
readJsonFile,
|
|
16
16
|
setHookCaptureDisabled,
|
|
17
17
|
writeJsonFile,
|
|
18
18
|
writeTextFileAtomic
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-ENW2GJNO.js";
|
|
20
20
|
import {
|
|
21
21
|
CODEX_HOOK_EVENTS,
|
|
22
22
|
GEMINI_HOOK_EVENTS,
|
|
@@ -24,8 +24,8 @@ import {
|
|
|
24
24
|
isObserverHealth,
|
|
25
25
|
readHookInboxStatus,
|
|
26
26
|
readObserverAuthToken
|
|
27
|
-
} from "./chunk-
|
|
28
|
-
import "./chunk-
|
|
27
|
+
} from "./chunk-56TPYMNK.js";
|
|
28
|
+
import "./chunk-7V7G7NVF.js";
|
|
29
29
|
import {
|
|
30
30
|
claudeConfigDirectory,
|
|
31
31
|
createCliRawSync,
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
initializeSyncSettings,
|
|
35
35
|
resolvePackageRoot,
|
|
36
36
|
runRawSyncOnce
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-YSU5CS2V.js";
|
|
38
38
|
import {
|
|
39
39
|
DEFAULT_PROD_URL,
|
|
40
40
|
DEFAULT_TARGET_NAME,
|
|
@@ -76,7 +76,7 @@ import {
|
|
|
76
76
|
storeServiceRefreshToken,
|
|
77
77
|
writeEnvConfig,
|
|
78
78
|
writeTokens
|
|
79
|
-
} from "./chunk-
|
|
79
|
+
} from "./chunk-A6HAEVJX.js";
|
|
80
80
|
import "./chunk-MLKGABMK.js";
|
|
81
81
|
|
|
82
82
|
// src/cli.ts
|
|
@@ -124,7 +124,7 @@ var POLL_INTERVAL_MS = 50;
|
|
|
124
124
|
var LOG_ROTATE_BYTES = 10 * 1024 * 1024;
|
|
125
125
|
var LOG_ROTATE_FILES = 3;
|
|
126
126
|
function expectedObserverVersion() {
|
|
127
|
-
return true ? "0.8.0-rc.
|
|
127
|
+
return true ? "0.8.0-rc.2+bed6eaed" : "dev";
|
|
128
128
|
}
|
|
129
129
|
function delay(ms) {
|
|
130
130
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -776,20 +776,58 @@ function probeLocalStorage() {
|
|
|
776
776
|
});
|
|
777
777
|
}
|
|
778
778
|
}
|
|
779
|
+
function humanCheckResult(result) {
|
|
780
|
+
switch (result.label) {
|
|
781
|
+
case "Auth":
|
|
782
|
+
return { label: "Account", detail: result.detail };
|
|
783
|
+
case "Environment": {
|
|
784
|
+
const environmentName = result.detail.replace(
|
|
785
|
+
/ has no backend URL$/u,
|
|
786
|
+
""
|
|
787
|
+
);
|
|
788
|
+
return {
|
|
789
|
+
label: "Environment",
|
|
790
|
+
detail: result.status === "ok" ? result.detail.split(" \u2192 ")[0] ?? result.detail : `No service URL is configured for ${environmentName}. Run \`fml env <name-or-url>\`.`
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
case "Organization":
|
|
794
|
+
return { label: "Organization", detail: result.detail };
|
|
795
|
+
case "Local data":
|
|
796
|
+
return { label: "Local storage", detail: result.detail };
|
|
797
|
+
case "Database lease":
|
|
798
|
+
return {
|
|
799
|
+
label: "Runtime state",
|
|
800
|
+
detail: result.detail.replace("observer", "FML service")
|
|
801
|
+
};
|
|
802
|
+
case "Raw sync":
|
|
803
|
+
return {
|
|
804
|
+
label: "Agent activity sync",
|
|
805
|
+
detail: result.detail.replace("observer state", "FML service state").replace("raw sync", "agent activity sync")
|
|
806
|
+
};
|
|
807
|
+
case "API":
|
|
808
|
+
return { label: "FML API", detail: result.detail };
|
|
809
|
+
case "Observer":
|
|
810
|
+
return {
|
|
811
|
+
label: "FML service",
|
|
812
|
+
detail: result.detail.replace("observer", "FML service").replace("continuous sync", "agent activity sync").replace("raw sync", "agent activity sync")
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
779
817
|
function reportResult(result, spinner) {
|
|
780
|
-
const text = `${result.label} \u2014 ${result.detail}`;
|
|
781
818
|
if (spinner) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (result.status === "fail") spinner.fail(text);
|
|
785
|
-
return;
|
|
819
|
+
spinner.indent = 0;
|
|
820
|
+
spinner.stop();
|
|
786
821
|
}
|
|
822
|
+
const humanResult = humanCheckResult(result);
|
|
823
|
+
if (!humanResult) return;
|
|
787
824
|
const icon = result.status === "ok" ? pc.green("\u2713") : result.status === "warn" ? pc.yellow("!") : pc.red("\u2717");
|
|
788
|
-
console.log(` ${icon} ${
|
|
825
|
+
console.log(` ${icon} ${humanResult.label} \u2014 ${humanResult.detail}`);
|
|
789
826
|
}
|
|
790
827
|
async function handleDoctor(opts) {
|
|
791
828
|
const environment = getActiveEnv();
|
|
792
829
|
const isTTY = !opts.json && process.stdout.isTTY === true;
|
|
830
|
+
if (!opts.json) console.log("FML doctor\n");
|
|
793
831
|
const checks = [];
|
|
794
832
|
const startSpinner = (label) => isTTY ? ora({ text: `Checking ${label}...`, indent: 2 }).start() : null;
|
|
795
833
|
const record = (result, spinner) => {
|
|
@@ -909,7 +947,7 @@ async function handleDoctor(opts) {
|
|
|
909
947
|
}
|
|
910
948
|
}
|
|
911
949
|
{
|
|
912
|
-
const spinner = startSpinner("
|
|
950
|
+
const spinner = startSpinner("agent activity sync");
|
|
913
951
|
try {
|
|
914
952
|
try {
|
|
915
953
|
probeDurableDirectory(FML_OBSERVER_DIR, {
|
|
@@ -977,7 +1015,7 @@ async function handleDoctor(opts) {
|
|
|
977
1015
|
}
|
|
978
1016
|
}
|
|
979
1017
|
{
|
|
980
|
-
const spinner = startSpinner("
|
|
1018
|
+
const spinner = startSpinner("FML service");
|
|
981
1019
|
const rawSync2 = createRawArtifactSync({ stateDir: FML_OBSERVER_DIR });
|
|
982
1020
|
const syncEnabled = rawSync2.getSettings().enabled;
|
|
983
1021
|
const observer = await readObserverRuntimeStatus();
|
|
@@ -1018,6 +1056,9 @@ async function handleDoctor(opts) {
|
|
|
1018
1056
|
console.log(`
|
|
1019
1057
|
${parts.join(", ")}
|
|
1020
1058
|
`);
|
|
1059
|
+
if (failed > 0) {
|
|
1060
|
+
console.log("Fix the failed checks above, then run `fml doctor` again.");
|
|
1061
|
+
}
|
|
1021
1062
|
}
|
|
1022
1063
|
|
|
1023
1064
|
// src/commands/install.ts
|
|
@@ -1264,24 +1305,6 @@ var CLAUDE_PLUGIN_INSTALLER = Object.freeze({
|
|
|
1264
1305
|
})
|
|
1265
1306
|
});
|
|
1266
1307
|
|
|
1267
|
-
// src/banner.ts
|
|
1268
|
-
import pc2 from "picocolors";
|
|
1269
|
-
var LOGO_LINES = [
|
|
1270
|
-
" \u2584\u2588\u2588\u2580 \u2588\u2588\u2584\u2588\u2588\u2584\u2588\u2588 \u2588\u2588",
|
|
1271
|
-
" \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
|
|
1272
|
-
"\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
|
|
1273
|
-
" \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588",
|
|
1274
|
-
" \u2580\u2580 \u2580\u2580 \u2580\u2580 \u2580\u2580\u2580\u2580"
|
|
1275
|
-
];
|
|
1276
|
-
function printBanner() {
|
|
1277
|
-
console.log("");
|
|
1278
|
-
for (let i = 0; i < LOGO_LINES.length; i++) {
|
|
1279
|
-
const color = i < 3 ? pc2.blue : pc2.cyan;
|
|
1280
|
-
console.log(" " + color(LOGO_LINES[i]));
|
|
1281
|
-
}
|
|
1282
|
-
console.log("");
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
1308
|
// src/bin-utils.ts
|
|
1286
1309
|
import fs3 from "fs";
|
|
1287
1310
|
import path4 from "path";
|
|
@@ -4132,7 +4155,7 @@ function rollbackMutation2(mutation) {
|
|
|
4132
4155
|
}
|
|
4133
4156
|
function warnSkippedPlan(plan, error, warn) {
|
|
4134
4157
|
warn(
|
|
4135
|
-
`
|
|
4158
|
+
`Warning: skipped ${plan.displayName} MCP config at ${plan.configPath}: ${error}`
|
|
4136
4159
|
);
|
|
4137
4160
|
}
|
|
4138
4161
|
function applyPlan2(plan, warn) {
|
|
@@ -4176,14 +4199,17 @@ function reportPlan(plan, warn, log) {
|
|
|
4176
4199
|
for (const note of plan.notes) log(note);
|
|
4177
4200
|
}
|
|
4178
4201
|
function configureFmlDirectMcp(pluginRoot, options = {}) {
|
|
4179
|
-
const
|
|
4202
|
+
const emitWarning = (message) => {
|
|
4203
|
+
(options.warn ?? console.warn)(message);
|
|
4204
|
+
options.onFailure?.(message);
|
|
4205
|
+
};
|
|
4180
4206
|
const log = options.log ?? console.log;
|
|
4181
4207
|
const context = plannerContext2(pluginRoot, options);
|
|
4182
4208
|
const changes = [];
|
|
4183
4209
|
for (const capability of DIRECT_MCP_CAPABILITIES) {
|
|
4184
4210
|
const plan = capability.planInstall(context);
|
|
4185
|
-
reportPlan(plan,
|
|
4186
|
-
if (!applyPlan2(plan,
|
|
4211
|
+
reportPlan(plan, emitWarning, log);
|
|
4212
|
+
if (!applyPlan2(plan, emitWarning)) continue;
|
|
4187
4213
|
changes.push({
|
|
4188
4214
|
targetId: plan.targetId,
|
|
4189
4215
|
displayName: plan.displayName,
|
|
@@ -4867,14 +4893,13 @@ async function handleInstall(opts = {}) {
|
|
|
4867
4893
|
homeDir: os5.homedir(),
|
|
4868
4894
|
packageRoot: pluginRoot
|
|
4869
4895
|
});
|
|
4870
|
-
console.log("
|
|
4871
|
-
console.log("[1/5] Creating FML directories...");
|
|
4896
|
+
console.log("Configuring FML integrations...");
|
|
4872
4897
|
for (const root of configuredManagedRoots()) {
|
|
4873
4898
|
const preparation = prepareManagedDirectory(root);
|
|
4874
4899
|
if (!preparation.managed) {
|
|
4875
4900
|
const recovery = process.platform === "win32" ? "use a dedicated FML directory in a native location; custom Windows roots require manual cleanup" : "use a dedicated FML directory or follow the inspected custom-root adoption steps in https://github.com/fml-inc/fml/blob/main/docs/plugin-cleanup.md#adopt-an-existing-custom-root";
|
|
4876
4901
|
console.warn(
|
|
4877
|
-
`
|
|
4902
|
+
`Warning: ${root.directory} could not be safely marked as FML-owned because it is non-empty, symlinked, or not owned by the current user; ${recovery} before using \`fml uninstall --purge\`.`
|
|
4878
4903
|
);
|
|
4879
4904
|
}
|
|
4880
4905
|
}
|
|
@@ -4885,11 +4910,8 @@ async function handleInstall(opts = {}) {
|
|
|
4885
4910
|
FML_OBSERVER_DIR
|
|
4886
4911
|
])) {
|
|
4887
4912
|
fs9.mkdirSync(directory, { recursive: true, mode: 448 });
|
|
4888
|
-
console.log(` ${directory}`);
|
|
4889
4913
|
}
|
|
4890
4914
|
initializeSyncSettings(path21.join(FML_OBSERVER_DIR, "sync.json"));
|
|
4891
|
-
console.log();
|
|
4892
|
-
console.log("[2/5] Installing agent-native /fml skills...");
|
|
4893
4915
|
let skillReady = false;
|
|
4894
4916
|
try {
|
|
4895
4917
|
const skillResults = installManagedAgentSkills(pluginRoot);
|
|
@@ -4897,23 +4919,17 @@ async function handleInstall(opts = {}) {
|
|
|
4897
4919
|
for (const result of skillResults) {
|
|
4898
4920
|
const targets = result.displayNames.join(", ");
|
|
4899
4921
|
if (result.status === "preserved") {
|
|
4900
|
-
console.
|
|
4901
|
-
`
|
|
4902
|
-
);
|
|
4903
|
-
} else {
|
|
4904
|
-
console.log(
|
|
4905
|
-
` ${targets}: ${result.destinationPath} (${result.status})`
|
|
4922
|
+
console.warn(
|
|
4923
|
+
`Warning: preserved ${result.destinationPath} for ${targets}: ${result.reason}`
|
|
4906
4924
|
);
|
|
4907
4925
|
}
|
|
4908
4926
|
}
|
|
4909
4927
|
} catch (error) {
|
|
4910
4928
|
const message = error instanceof Error ? error.message : String(error);
|
|
4911
|
-
console.
|
|
4912
|
-
`
|
|
4929
|
+
console.warn(
|
|
4930
|
+
`Warning: agent-native skill setup could not be completed: ${message}`
|
|
4913
4931
|
);
|
|
4914
4932
|
}
|
|
4915
|
-
console.log();
|
|
4916
|
-
console.log("[3/5] Writing plugin manifest...");
|
|
4917
4933
|
const packageJson = readJsonFile(path21.join(pluginRoot, "package.json"));
|
|
4918
4934
|
const generatedManifest = readJsonFile(paths.pluginManifestPath);
|
|
4919
4935
|
const version = typeof generatedManifest?.version === "string" ? generatedManifest.version : typeof packageJson?.version === "string" ? packageJson.version : "0.0.0-dev";
|
|
@@ -4921,17 +4937,14 @@ async function handleInstall(opts = {}) {
|
|
|
4921
4937
|
paths.pluginManifestPath,
|
|
4922
4938
|
CLAUDE_PLUGIN_INSTALLER.pluginManifest(version)
|
|
4923
4939
|
);
|
|
4924
|
-
console.log(` Version: ${version}
|
|
4925
|
-
`);
|
|
4926
|
-
console.log("[4/5] Registering the FML plugin...");
|
|
4927
4940
|
const claudeBin = resolveBin("claude");
|
|
4928
4941
|
const marketplaceResult = readJsonFileForUpdate(
|
|
4929
4942
|
paths.marketplaceManifestPath
|
|
4930
4943
|
);
|
|
4931
4944
|
let marketplaceReady = false;
|
|
4932
4945
|
if (!marketplaceResult.ok) {
|
|
4933
|
-
console.
|
|
4934
|
-
`
|
|
4946
|
+
console.warn(
|
|
4947
|
+
`Warning: preserved unreadable marketplace config ${paths.marketplaceManifestPath}: ${marketplaceResult.error}`
|
|
4935
4948
|
);
|
|
4936
4949
|
} else {
|
|
4937
4950
|
fs9.mkdirSync(paths.marketplaceDir, { recursive: true, mode: 448 });
|
|
@@ -4941,8 +4954,8 @@ async function handleInstall(opts = {}) {
|
|
|
4941
4954
|
marketplaceSymlinkType()
|
|
4942
4955
|
);
|
|
4943
4956
|
if (linkUpdate === "preserved") {
|
|
4944
|
-
console.
|
|
4945
|
-
`
|
|
4957
|
+
console.warn(
|
|
4958
|
+
`Warning: preserved non-symlink marketplace path ${paths.marketplaceLinkPath}; remove it manually to enable the FML Claude plugin`
|
|
4946
4959
|
);
|
|
4947
4960
|
} else {
|
|
4948
4961
|
const marketplace = CLAUDE_PLUGIN_INSTALLER.addMarketplaceEntry(
|
|
@@ -4955,8 +4968,8 @@ async function handleInstall(opts = {}) {
|
|
|
4955
4968
|
const settingsResult = readJsonFileForUpdate(paths.settingsPath);
|
|
4956
4969
|
let settingsReady = false;
|
|
4957
4970
|
if (!settingsResult.ok) {
|
|
4958
|
-
console.
|
|
4959
|
-
`
|
|
4971
|
+
console.warn(
|
|
4972
|
+
`Warning: preserved unreadable Claude settings ${paths.settingsPath}: ${settingsResult.error}`
|
|
4960
4973
|
);
|
|
4961
4974
|
} else if (marketplaceReady) {
|
|
4962
4975
|
const settings = CLAUDE_PLUGIN_INSTALLER.enableInSettings(
|
|
@@ -4966,21 +4979,23 @@ async function handleInstall(opts = {}) {
|
|
|
4966
4979
|
writeJsonFile(paths.settingsPath, settings);
|
|
4967
4980
|
settingsReady = true;
|
|
4968
4981
|
}
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4982
|
+
let mcpReady = true;
|
|
4983
|
+
let claudePluginReady = true;
|
|
4984
|
+
configureFmlDirectMcp(pluginRoot, {
|
|
4985
|
+
warn: (message) => console.warn(message.replace(/^warn:/u, "Warning:")),
|
|
4986
|
+
onFailure: () => {
|
|
4987
|
+
mcpReady = false;
|
|
4988
|
+
}
|
|
4989
|
+
});
|
|
4972
4990
|
let hooksReady = true;
|
|
4973
|
-
|
|
4991
|
+
configureFmlHooks(pluginRoot, {
|
|
4974
4992
|
onFailure: () => {
|
|
4975
4993
|
hooksReady = false;
|
|
4976
4994
|
}
|
|
4977
4995
|
});
|
|
4978
|
-
for (const target2 of hookTargets) {
|
|
4979
|
-
console.log(` Hooks: ${target2.displayName} -> fml`);
|
|
4980
|
-
}
|
|
4981
4996
|
if (!marketplaceReady || !settingsReady) {
|
|
4982
|
-
console.
|
|
4983
|
-
"
|
|
4997
|
+
console.warn(
|
|
4998
|
+
"Warning: skipped Claude plugin registration until its existing config is repaired"
|
|
4984
4999
|
);
|
|
4985
5000
|
} else if (claudeBin) {
|
|
4986
5001
|
try {
|
|
@@ -4995,16 +5010,14 @@ async function handleInstall(opts = {}) {
|
|
|
4995
5010
|
timeout: command.timeoutMs
|
|
4996
5011
|
});
|
|
4997
5012
|
}
|
|
4998
|
-
console.log(" Plugin registered via Claude Code CLI");
|
|
4999
5013
|
} catch (error) {
|
|
5014
|
+
claudePluginReady = false;
|
|
5000
5015
|
const message = error instanceof Error ? error.message : String(error);
|
|
5001
|
-
console.
|
|
5016
|
+
console.warn(`Warning: Claude plugin registration failed: ${message}`);
|
|
5002
5017
|
}
|
|
5003
5018
|
} else {
|
|
5004
|
-
console.
|
|
5019
|
+
console.warn("Warning: claude CLI not found; plugin files are ready");
|
|
5005
5020
|
}
|
|
5006
|
-
console.log();
|
|
5007
|
-
console.log("[5/5] Initializing the FML environment...");
|
|
5008
5021
|
if (!envConfigExists()) {
|
|
5009
5022
|
writeEnvConfig({
|
|
5010
5023
|
schemaVersion: 1,
|
|
@@ -5013,9 +5026,6 @@ async function handleInstall(opts = {}) {
|
|
|
5013
5026
|
[DEFAULT_TARGET_NAME]: { convexUrl: DEFAULT_PROD_URL }
|
|
5014
5027
|
}
|
|
5015
5028
|
});
|
|
5016
|
-
console.log(` ${DEFAULT_TARGET_NAME}: ${DEFAULT_PROD_URL}`);
|
|
5017
|
-
} else {
|
|
5018
|
-
console.log(" Preserved existing environment selection");
|
|
5019
5029
|
}
|
|
5020
5030
|
if (!skillReady || !marketplaceReady || !settingsReady || !hooksReady) {
|
|
5021
5031
|
throw new Error(
|
|
@@ -5030,21 +5040,20 @@ async function handleInstall(opts = {}) {
|
|
|
5030
5040
|
`FML install is incomplete: hook capture remains paused because its state could not be cleared: ${message}`
|
|
5031
5041
|
);
|
|
5032
5042
|
}
|
|
5033
|
-
console.log("");
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5043
|
+
console.log("\u2713 Agent skills configured");
|
|
5044
|
+
if (mcpReady && claudePluginReady) {
|
|
5045
|
+
console.log("\u2713 Coding-agent integrations configured");
|
|
5046
|
+
console.log("FML integrations configured.");
|
|
5047
|
+
} else {
|
|
5048
|
+
console.warn(
|
|
5049
|
+
"Warning: Coding-agent integrations were not fully configured. See warnings above."
|
|
5050
|
+
);
|
|
5051
|
+
console.log("FML integration configuration finished with warnings.");
|
|
5052
|
+
}
|
|
5041
5053
|
}
|
|
5042
5054
|
|
|
5043
|
-
// src/commands/
|
|
5044
|
-
import
|
|
5045
|
-
import os7 from "os";
|
|
5046
|
-
import path27 from "path";
|
|
5047
|
-
import readline from "readline/promises";
|
|
5055
|
+
// src/commands/sync.ts
|
|
5056
|
+
import { statSync } from "fs";
|
|
5048
5057
|
|
|
5049
5058
|
// src/commands/otel-config.ts
|
|
5050
5059
|
import fs11 from "fs";
|
|
@@ -6040,7 +6049,7 @@ function applyPlan3(plan, warn, commitOwnership) {
|
|
|
6040
6049
|
} catch (error) {
|
|
6041
6050
|
const detail = error instanceof Error ? error.message : String(error);
|
|
6042
6051
|
warn(
|
|
6043
|
-
`
|
|
6052
|
+
`Warning: skipped ${plan.displayName} OpenTelemetry at ${plan.configPath}: ${detail}`
|
|
6044
6053
|
);
|
|
6045
6054
|
return null;
|
|
6046
6055
|
}
|
|
@@ -6049,7 +6058,7 @@ function applyPlan3(plan, warn, commitOwnership) {
|
|
|
6049
6058
|
} catch (error) {
|
|
6050
6059
|
const detail = error instanceof Error ? error.message : String(error);
|
|
6051
6060
|
warn(
|
|
6052
|
-
`
|
|
6061
|
+
`Warning: could not record ${plan.displayName} OpenTelemetry ownership at ${plan.configPath}: ${detail}`
|
|
6053
6062
|
);
|
|
6054
6063
|
}
|
|
6055
6064
|
}
|
|
@@ -6058,7 +6067,7 @@ function applyPlan3(plan, warn, commitOwnership) {
|
|
|
6058
6067
|
const uniquePaths2 = new Set(plan.mutations.map((mutation) => mutation.path));
|
|
6059
6068
|
if (uniquePaths2.size !== plan.mutations.length) {
|
|
6060
6069
|
warn(
|
|
6061
|
-
`
|
|
6070
|
+
`Warning: skipped ${plan.displayName} OpenTelemetry at ${plan.configPath}: plan contains duplicate paths`
|
|
6062
6071
|
);
|
|
6063
6072
|
return null;
|
|
6064
6073
|
}
|
|
@@ -6089,7 +6098,7 @@ function applyPlan3(plan, warn, commitOwnership) {
|
|
|
6089
6098
|
const detail = error instanceof Error ? error.message : String(error);
|
|
6090
6099
|
const rollbackDetail = rollbackErrors.length === 0 ? "" : `; rollback failed: ${rollbackErrors.join("; ")}`;
|
|
6091
6100
|
warn(
|
|
6092
|
-
`
|
|
6101
|
+
`Warning: skipped ${plan.displayName} OpenTelemetry at ${plan.configPath}: ${detail}${rollbackDetail}`
|
|
6093
6102
|
);
|
|
6094
6103
|
return null;
|
|
6095
6104
|
}
|
|
@@ -6124,7 +6133,7 @@ function loadOtelConfigState(options, warn) {
|
|
|
6124
6133
|
)
|
|
6125
6134
|
};
|
|
6126
6135
|
} catch (error) {
|
|
6127
|
-
warn(`
|
|
6136
|
+
warn(`Warning: ${error instanceof Error ? error.message : String(error)}`);
|
|
6128
6137
|
return null;
|
|
6129
6138
|
}
|
|
6130
6139
|
}
|
|
@@ -6171,7 +6180,7 @@ function planSafely(capability, operation, context, warn) {
|
|
|
6171
6180
|
return operation === "install" ? capability.planInstall(context) : capability.planUninstall(context);
|
|
6172
6181
|
} catch (error) {
|
|
6173
6182
|
warn(
|
|
6174
|
-
`
|
|
6183
|
+
`Warning: skipped ${capability.displayName} OpenTelemetry: ${error instanceof Error ? error.message : String(error)}`
|
|
6175
6184
|
);
|
|
6176
6185
|
return null;
|
|
6177
6186
|
}
|
|
@@ -6181,7 +6190,7 @@ function pruneMissingRecordedEntry(state, key, entry, warn) {
|
|
|
6181
6190
|
commitStateEntry(state, key, null);
|
|
6182
6191
|
} catch (error) {
|
|
6183
6192
|
warn(
|
|
6184
|
-
`
|
|
6193
|
+
`Warning: could not prune missing ${entry.targetId} OpenTelemetry ownership at ${entry.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
6185
6194
|
);
|
|
6186
6195
|
}
|
|
6187
6196
|
}
|
|
@@ -6283,13 +6292,13 @@ function removeFmlOtel(observer, options = {}) {
|
|
|
6283
6292
|
}
|
|
6284
6293
|
if (candidate.recorded && candidate.plan.mutations.length === 0 && candidate.plan.warnings.length === 0 && state) {
|
|
6285
6294
|
warn(
|
|
6286
|
-
`
|
|
6295
|
+
`Warning: preserved ${candidate.plan.displayName} OpenTelemetry at ${candidate.plan.configPath}: managed fields were modified or removed after installation`
|
|
6287
6296
|
);
|
|
6288
6297
|
try {
|
|
6289
6298
|
commitStateEntry(state, key, null);
|
|
6290
6299
|
} catch (error) {
|
|
6291
6300
|
warn(
|
|
6292
|
-
`
|
|
6301
|
+
`Warning: could not relinquish ${candidate.plan.displayName} OpenTelemetry ownership at ${candidate.plan.configPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
6293
6302
|
);
|
|
6294
6303
|
}
|
|
6295
6304
|
}
|
|
@@ -6297,420 +6306,390 @@ function removeFmlOtel(observer, options = {}) {
|
|
|
6297
6306
|
return changes;
|
|
6298
6307
|
}
|
|
6299
6308
|
|
|
6300
|
-
// src/commands/
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
try {
|
|
6304
|
-
setHookCaptureDisabled(true);
|
|
6305
|
-
return true;
|
|
6306
|
-
} catch (error) {
|
|
6307
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
6308
|
-
console.warn(
|
|
6309
|
-
`Could not persist the hook-capture pause at ${FML_HOOK_CAPTURE_DISABLED_FILE}: ${message}`
|
|
6310
|
-
);
|
|
6311
|
-
return false;
|
|
6312
|
-
}
|
|
6309
|
+
// src/commands/sync.ts
|
|
6310
|
+
function rawSync() {
|
|
6311
|
+
return createCliRawSync();
|
|
6313
6312
|
}
|
|
6314
|
-
function
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
return
|
|
6319
|
-
} catch {
|
|
6320
|
-
return [logicalPath];
|
|
6313
|
+
function formatBytes(bytes) {
|
|
6314
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
6315
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
6316
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
6317
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
6321
6318
|
}
|
|
6319
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
|
|
6322
6320
|
}
|
|
6323
|
-
function
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
}
|
|
6332
|
-
if (directoryStats.isSymbolicLink()) {
|
|
6333
|
-
if (configuredRoot) return false;
|
|
6334
|
-
const containsLogicalMarker = preservedPaths.some(
|
|
6335
|
-
(preservedPath) => pathContains(resolvedDirectory, preservedPath)
|
|
6336
|
-
);
|
|
6337
|
-
if (!containsLogicalMarker) {
|
|
6338
|
-
fs12.rmSync(directory, { force: true });
|
|
6339
|
-
return true;
|
|
6340
|
-
}
|
|
6341
|
-
return false;
|
|
6342
|
-
}
|
|
6343
|
-
const directoryPaths = [resolvedDirectory];
|
|
6344
|
-
try {
|
|
6345
|
-
const canonicalDirectory = fs12.realpathSync(directory);
|
|
6346
|
-
if (!pathsAreSame(canonicalDirectory, resolvedDirectory)) {
|
|
6347
|
-
directoryPaths.push(canonicalDirectory);
|
|
6348
|
-
}
|
|
6349
|
-
} catch {
|
|
6350
|
-
}
|
|
6351
|
-
const containsPreservedPath = directoryPaths.some(
|
|
6352
|
-
(directoryPath) => preservedPaths.some(
|
|
6353
|
-
(preservedPath) => pathContains(directoryPath, preservedPath)
|
|
6354
|
-
)
|
|
6321
|
+
function formatDuration(durationMs) {
|
|
6322
|
+
if (durationMs < 1e3) return `${Math.round(durationMs)} ms`;
|
|
6323
|
+
return `${(durationMs / 1e3).toFixed(2)} s`;
|
|
6324
|
+
}
|
|
6325
|
+
function printRepositoryResolution(metrics) {
|
|
6326
|
+
if (metrics.attempted === 0 && metrics.budgetSkipped === 0) return;
|
|
6327
|
+
console.log(
|
|
6328
|
+
` Repo checks: ${metrics.attempted} (${metrics.resolved} resolved, ${metrics.unresolved} unresolved, ${metrics.cacheHits} cached, ${metrics.budgetSkipped} skipped by budget)`
|
|
6355
6329
|
);
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
return true;
|
|
6359
|
-
}
|
|
6360
|
-
const isPreservedPath = directoryPaths.some(
|
|
6361
|
-
(directoryPath) => preservedPaths.some(
|
|
6362
|
-
(preservedPath) => pathsAreSame(directoryPath, path27.resolve(preservedPath))
|
|
6363
|
-
)
|
|
6330
|
+
console.log(
|
|
6331
|
+
` Repo check time: ${formatDuration(metrics.elapsedMs)} total, ${formatDuration(metrics.slowestMs)} slowest`
|
|
6364
6332
|
);
|
|
6365
|
-
if (isPreservedPath) return true;
|
|
6366
|
-
if (!directoryStats.isDirectory()) return false;
|
|
6367
|
-
let entries;
|
|
6368
|
-
try {
|
|
6369
|
-
entries = fs12.readdirSync(directory);
|
|
6370
|
-
} catch (error) {
|
|
6371
|
-
if (error.code === "ENOENT") return true;
|
|
6372
|
-
throw error;
|
|
6373
|
-
}
|
|
6374
|
-
let complete = true;
|
|
6375
|
-
for (const entry of entries) {
|
|
6376
|
-
const entryPath = path27.join(directory, entry);
|
|
6377
|
-
if (!purgeDirectoryPreservingPaths(entryPath, preservedPaths, false)) {
|
|
6378
|
-
complete = false;
|
|
6379
|
-
}
|
|
6380
|
-
}
|
|
6381
|
-
return complete;
|
|
6382
6333
|
}
|
|
6383
|
-
function
|
|
6384
|
-
|
|
6385
|
-
return purgeDirectoryPreservingPaths(
|
|
6386
|
-
directory,
|
|
6387
|
-
preservedFiles.flatMap(
|
|
6388
|
-
(preservedFile) => resolvePreservedPaths(preservedFile)
|
|
6389
|
-
),
|
|
6390
|
-
true
|
|
6391
|
-
);
|
|
6334
|
+
function writeJson(value) {
|
|
6335
|
+
console.log(JSON.stringify(value, null, 2));
|
|
6392
6336
|
}
|
|
6393
|
-
function
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
if (error.code === "ENOENT") return;
|
|
6400
|
-
try {
|
|
6401
|
-
fs12.writeFileSync(marker, FML_MANAGED_DIRECTORY_MARKER_CONTENT, {
|
|
6402
|
-
encoding: "utf8",
|
|
6403
|
-
flag: "wx",
|
|
6404
|
-
mode: 384
|
|
6405
|
-
});
|
|
6406
|
-
} catch (markerError) {
|
|
6407
|
-
if (markerError.code === "EEXIST" && hasManagedDirectoryMarker(directory)) {
|
|
6408
|
-
throw error;
|
|
6409
|
-
}
|
|
6410
|
-
const removalMessage = error instanceof Error ? error.message : String(error);
|
|
6411
|
-
const markerMessage = markerError instanceof Error ? markerError.message : String(markerError);
|
|
6412
|
-
throw new Error(
|
|
6413
|
-
`could not remove ${directory} (${removalMessage}) or restore its FML ownership marker (${markerMessage})`
|
|
6414
|
-
);
|
|
6415
|
-
}
|
|
6416
|
-
throw error;
|
|
6337
|
+
function warnRetainedLegacyDatabase(retained) {
|
|
6338
|
+
if (retained.pendingEvents !== null && retained.pendingHookEvents !== null && retained.pendingOtlpEnvelopes !== null) {
|
|
6339
|
+
console.warn(
|
|
6340
|
+
`Warning: Retained legacy observer database at ${retained.databaseFile} with ${retained.pendingEvents} pending rows (${retained.pendingHookEvents} hook, ${retained.pendingOtlpEnvelopes} OTLP); FML will not sync those rows automatically.`
|
|
6341
|
+
);
|
|
6342
|
+
return;
|
|
6417
6343
|
}
|
|
6344
|
+
console.warn(
|
|
6345
|
+
`Warning: Retained legacy observer database at ${retained.databaseFile}; pending row count unavailable. FML will not sync that database automatically.`
|
|
6346
|
+
);
|
|
6418
6347
|
}
|
|
6419
|
-
function
|
|
6420
|
-
const
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
installedPlugins,
|
|
6424
|
-
CLAUDE_PLUGIN_INSTALLER.pluginId
|
|
6348
|
+
function printStatus(status) {
|
|
6349
|
+
const { queue } = status;
|
|
6350
|
+
console.log(
|
|
6351
|
+
`Raw artifact sync: ${status.settings.enabled ? "enabled" : "disabled"}`
|
|
6425
6352
|
);
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
)
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
)
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
}
|
|
6438
|
-
writeJsonFile(paths.installedPluginsPath, update.installedPlugins);
|
|
6439
|
-
console.log(" Cleaned installed_plugins.json");
|
|
6353
|
+
console.log(` State: ${FML_OBSERVER_DIR}`);
|
|
6354
|
+
console.log(` Tracked: ${queue.tracked}`);
|
|
6355
|
+
console.log(` Pending: ${queue.pending}`);
|
|
6356
|
+
console.log(` Raw work: ${queue.rawPending}`);
|
|
6357
|
+
console.log(` Metadata: ${queue.metadataPending}`);
|
|
6358
|
+
console.log(` Uploaded: ${queue.uploaded}`);
|
|
6359
|
+
console.log(` Failed: ${queue.failed}`);
|
|
6360
|
+
console.log(
|
|
6361
|
+
` Watermark: ${formatBytes(queue.uploadedBytes)} / ${formatBytes(queue.totalBytes)}`
|
|
6362
|
+
);
|
|
6363
|
+
console.log(` Remaining: ${formatBytes(queue.remainingBytes)}`);
|
|
6440
6364
|
}
|
|
6441
|
-
function
|
|
6442
|
-
return
|
|
6443
|
-
|
|
6444
|
-
|
|
6365
|
+
function syncExitCode(result) {
|
|
6366
|
+
if (result.outcome === "failed" || result.outcome === "exhausted") return 1;
|
|
6367
|
+
if (result.outcome === "busy") return 1;
|
|
6368
|
+
return 0;
|
|
6445
6369
|
}
|
|
6446
|
-
async function
|
|
6447
|
-
const
|
|
6448
|
-
if (
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
);
|
|
6458
|
-
return false;
|
|
6459
|
-
}
|
|
6460
|
-
const rl = readline.createInterface({
|
|
6461
|
-
input,
|
|
6462
|
-
output: opts.output ?? process.stdout
|
|
6463
|
-
});
|
|
6464
|
-
try {
|
|
6465
|
-
const closed = new Promise((resolve) => {
|
|
6466
|
-
rl.once("close", () => resolve(null));
|
|
6467
|
-
});
|
|
6468
|
-
const answer = await Promise.race([
|
|
6469
|
-
rl.question(`${warning}
|
|
6470
|
-
Type y to continue: `).catch(() => null),
|
|
6471
|
-
closed
|
|
6472
|
-
]);
|
|
6473
|
-
if (answer !== null && answer.trim().toLowerCase() === "y") return true;
|
|
6474
|
-
} finally {
|
|
6475
|
-
rl.close();
|
|
6476
|
-
}
|
|
6477
|
-
console.error("Uninstall canceled; nothing was removed.");
|
|
6478
|
-
return false;
|
|
6479
|
-
}
|
|
6480
|
-
function refuseUnsafePurge(roots) {
|
|
6481
|
-
const unsafeRoots = unsafePurgeRoots(roots);
|
|
6482
|
-
if (unsafeRoots.length === 0) return false;
|
|
6483
|
-
console.error("Refusing to purge unsafe FML directories:");
|
|
6484
|
-
for (const root of unsafeRoots) {
|
|
6485
|
-
console.error(` - ${root.directory}: ${root.reason}`);
|
|
6370
|
+
async function handleSyncRun(opts = {}) {
|
|
6371
|
+
const sync2 = rawSync();
|
|
6372
|
+
if (!sync2.getSettings().enabled) {
|
|
6373
|
+
const result2 = {
|
|
6374
|
+
ok: true,
|
|
6375
|
+
outcome: "disabled",
|
|
6376
|
+
status: sync2.status()
|
|
6377
|
+
};
|
|
6378
|
+
if (opts.json) writeJson(result2);
|
|
6379
|
+
else printStatus(result2.status);
|
|
6380
|
+
return;
|
|
6486
6381
|
}
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
if (!await confirmPurge(opts, purgeRoots)) {
|
|
6498
|
-
process.exitCode = 1;
|
|
6499
|
-
return;
|
|
6382
|
+
if (opts.dryRun) {
|
|
6383
|
+
const discovery = sync2.discover();
|
|
6384
|
+
const bySource = {};
|
|
6385
|
+
let totalBytes = 0;
|
|
6386
|
+
for (const candidate of discovery.candidates) {
|
|
6387
|
+
bySource[candidate.source] = (bySource[candidate.source] ?? 0) + 1;
|
|
6388
|
+
try {
|
|
6389
|
+
totalBytes += statSync(candidate.sourcePath).size;
|
|
6390
|
+
} catch {
|
|
6391
|
+
}
|
|
6500
6392
|
}
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6393
|
+
const result2 = {
|
|
6394
|
+
ok: true,
|
|
6395
|
+
dryRun: true,
|
|
6396
|
+
targetUrl: getSiteUrl(),
|
|
6397
|
+
discovered: {
|
|
6398
|
+
count: discovery.candidates.length,
|
|
6399
|
+
totalBytes,
|
|
6400
|
+
bySource
|
|
6401
|
+
},
|
|
6402
|
+
repositoryResolution: discovery.repositoryResolution,
|
|
6403
|
+
oversized: discovery.oversized,
|
|
6404
|
+
status: sync2.status()
|
|
6405
|
+
};
|
|
6406
|
+
if (opts.json) writeJson(result2);
|
|
6407
|
+
else {
|
|
6408
|
+
console.log("FML raw artifact sync dry-run");
|
|
6409
|
+
console.log(` Target: ${result2.targetUrl}`);
|
|
6410
|
+
console.log(` Discovered: ${result2.discovered.count}`);
|
|
6411
|
+
console.log(` Bytes: ${formatBytes(result2.discovered.totalBytes)}`);
|
|
6412
|
+
for (const [source, count] of Object.entries(bySource).sort()) {
|
|
6413
|
+
console.log(` ${source}: ${count}`);
|
|
6414
|
+
}
|
|
6415
|
+
printRepositoryResolution(discovery.repositoryResolution);
|
|
6416
|
+
console.log(` Oversized: ${discovery.oversized.length}`);
|
|
6417
|
+
if (discovery.compressedArtifactsUnsupportedReason) {
|
|
6418
|
+
console.log(
|
|
6419
|
+
` Compressed: skipped \u2014 ${discovery.compressedArtifactsUnsupportedReason}`
|
|
6420
|
+
);
|
|
6504
6421
|
}
|
|
6505
6422
|
}
|
|
6506
|
-
if (refuseUnsafePurge(purgeRoots)) return;
|
|
6507
|
-
}
|
|
6508
|
-
console.log("Uninstalling fml...\n");
|
|
6509
|
-
const otelDestination = readLocalObserverOtelDestination({
|
|
6510
|
-
endpointFile: FML_OBSERVER_ENDPOINT_FILE,
|
|
6511
|
-
authTokenFile: FML_OBSERVER_AUTH_TOKEN_FILE
|
|
6512
|
-
});
|
|
6513
|
-
let observer;
|
|
6514
|
-
try {
|
|
6515
|
-
observer = await stopObserver();
|
|
6516
|
-
} catch (error) {
|
|
6517
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
6518
|
-
console.error(
|
|
6519
|
-
"Could not safely stop the FML observer; uninstall was aborted. Repair the reported files and rerun `fml uninstall`."
|
|
6520
|
-
);
|
|
6521
|
-
process.exitCode = 1;
|
|
6522
6423
|
return;
|
|
6523
6424
|
}
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
);
|
|
6425
|
+
const attempt = await runRawSyncOnce();
|
|
6426
|
+
if (attempt.kind === "unauthenticated") {
|
|
6427
|
+
console.error("Raw artifact sync requires an FML login.");
|
|
6428
|
+
console.error("Run `fml login` or set FML_TOKEN.");
|
|
6528
6429
|
process.exitCode = 1;
|
|
6529
6430
|
return;
|
|
6530
6431
|
}
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
});
|
|
6537
|
-
console.log("[1/7] Unregistering the Claude Code plugin...");
|
|
6538
|
-
const claudeBin = resolveBin("claude");
|
|
6539
|
-
if (claudeBin) {
|
|
6540
|
-
try {
|
|
6541
|
-
const command = CLAUDE_PLUGIN_INSTALLER.uninstallCommand;
|
|
6542
|
-
execBinSync(claudeBin, [...command.args], {
|
|
6543
|
-
timeout: command.timeoutMs
|
|
6544
|
-
});
|
|
6545
|
-
} catch {
|
|
6432
|
+
if (attempt.kind === "disabled") {
|
|
6433
|
+
if (opts.json) {
|
|
6434
|
+
writeJson({ ok: true, outcome: "disabled", status: attempt.status });
|
|
6435
|
+
} else {
|
|
6436
|
+
printStatus(attempt.status);
|
|
6546
6437
|
}
|
|
6438
|
+
return;
|
|
6547
6439
|
}
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
);
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6440
|
+
const { result, targetUrl } = attempt;
|
|
6441
|
+
if (opts.json) {
|
|
6442
|
+
writeJson({ ok: syncExitCode(result) === 0, targetUrl, result });
|
|
6443
|
+
} else {
|
|
6444
|
+
console.log("FML raw artifact sync");
|
|
6445
|
+
console.log(` Target: ${targetUrl}`);
|
|
6446
|
+
console.log(` Outcome: ${result.outcome}`);
|
|
6447
|
+
console.log(` Sync passes: ${result.syncPasses}`);
|
|
6448
|
+
console.log(` Discovered: ${result.discovery.candidates.length}`);
|
|
6449
|
+
if (result.discovery.compressedArtifactsUnsupportedReason) {
|
|
6450
|
+
console.log(
|
|
6451
|
+
` Compressed: skipped \u2014 ${result.discovery.compressedArtifactsUnsupportedReason}`
|
|
6452
|
+
);
|
|
6453
|
+
}
|
|
6454
|
+
printRepositoryResolution(result.discovery.repositoryResolution);
|
|
6455
|
+
console.log(` Uploaded files: ${result.upload.uploaded}`);
|
|
6456
|
+
console.log(` Uploaded chunks: ${result.upload.uploadedChunks}`);
|
|
6457
|
+
console.log(
|
|
6458
|
+
` Uploaded bytes: ${formatBytes(result.upload.uploadedBytes)}`
|
|
6559
6459
|
);
|
|
6460
|
+
console.log(` Failed: ${result.upload.failed}`);
|
|
6461
|
+
console.log(` Exhausted: ${result.upload.exhausted}`);
|
|
6462
|
+
if (result.upload.retryAtMs !== void 0) {
|
|
6463
|
+
console.log(
|
|
6464
|
+
` Retry at: ${new Date(result.upload.retryAtMs).toISOString()}`
|
|
6465
|
+
);
|
|
6466
|
+
}
|
|
6467
|
+
console.log(` Drained: ${result.drained ? "yes" : "no"}`);
|
|
6468
|
+
if (result.outcome === "pending") {
|
|
6469
|
+
console.log(
|
|
6470
|
+
" Next: More data remains; run `fml sync` again or `fml start` for continuous syncing."
|
|
6471
|
+
);
|
|
6472
|
+
} else if (result.outcome === "backing_off") {
|
|
6473
|
+
const nextRetryAtMs = result.upload.nextRetryAtMs;
|
|
6474
|
+
console.log(
|
|
6475
|
+
` Next: Remaining items are backing off after upload errors${nextRetryAtMs !== void 0 ? `; next retry due ${new Date(nextRetryAtMs).toISOString()}` : ""}. \`fml start\` retries automatically.`
|
|
6476
|
+
);
|
|
6477
|
+
}
|
|
6478
|
+
console.log("");
|
|
6479
|
+
printStatus(result.status);
|
|
6560
6480
|
}
|
|
6561
|
-
|
|
6562
|
-
|
|
6481
|
+
process.exitCode = syncExitCode(result);
|
|
6482
|
+
}
|
|
6483
|
+
function setRawSyncEnabled(enabled) {
|
|
6484
|
+
return rawSync().setEnabled(enabled);
|
|
6485
|
+
}
|
|
6486
|
+
async function handleSyncEnable(opts = {}) {
|
|
6487
|
+
const sync2 = rawSync();
|
|
6488
|
+
const settings = sync2.setEnabled(true);
|
|
6489
|
+
const isolationWarning = observerOnlyOverrideWarning();
|
|
6490
|
+
if (isolationWarning) console.warn(isolationWarning);
|
|
6491
|
+
let retainedLegacyDatabase;
|
|
6563
6492
|
try {
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6493
|
+
const observer = await startObserver({
|
|
6494
|
+
onLegacyDatabaseRetained: (retained) => {
|
|
6495
|
+
retainedLegacyDatabase = retained;
|
|
6496
|
+
if (!opts.json) warnRetainedLegacyDatabase(retained);
|
|
6497
|
+
}
|
|
6498
|
+
});
|
|
6499
|
+
setHookCaptureDisabled(false);
|
|
6500
|
+
if (observer.status === "already_running") {
|
|
6501
|
+
await wakeObserverSync();
|
|
6502
|
+
}
|
|
6503
|
+
const port = observer.runtime.health?.port;
|
|
6504
|
+
const authToken = port ? readObserverAuthToken(FML_OBSERVER_AUTH_TOKEN_FILE) : null;
|
|
6505
|
+
if (port && authToken) {
|
|
6506
|
+
configureFmlOtel({
|
|
6507
|
+
baseUrl: `http://127.0.0.1:${port}`,
|
|
6508
|
+
authToken
|
|
6509
|
+
});
|
|
6510
|
+
} else if (!opts.json) {
|
|
6511
|
+
console.warn(
|
|
6512
|
+
"Warning: Could not configure native OpenTelemetry: the healthy observer endpoint or local token is unavailable."
|
|
6513
|
+
);
|
|
6514
|
+
}
|
|
6515
|
+
if (opts.json) {
|
|
6516
|
+
writeJson({
|
|
6517
|
+
ok: true,
|
|
6518
|
+
sync: settings,
|
|
6519
|
+
observer,
|
|
6520
|
+
...observer.retainedLegacyDatabase ? { retainedLegacyDatabase: observer.retainedLegacyDatabase } : {}
|
|
6521
|
+
});
|
|
6522
|
+
} else {
|
|
6523
|
+
const pid = observer.runtime.health?.pid;
|
|
6524
|
+
if (observer.status === "started") {
|
|
6525
|
+
console.log(
|
|
6526
|
+
pid ? `FML observer started (PID ${pid}).` : "FML observer started."
|
|
6573
6527
|
);
|
|
6528
|
+
console.log("Agent activity will sync automatically.");
|
|
6574
6529
|
} else {
|
|
6575
6530
|
console.log(
|
|
6576
|
-
`
|
|
6531
|
+
pid ? `FML observer is already running (PID ${pid}).` : "FML observer is already running."
|
|
6577
6532
|
);
|
|
6533
|
+
console.log("Agent activity sync is on.");
|
|
6578
6534
|
}
|
|
6579
6535
|
}
|
|
6580
6536
|
} catch (error) {
|
|
6581
6537
|
const message = error instanceof Error ? error.message : String(error);
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6538
|
+
if (opts.json) {
|
|
6539
|
+
writeJson({
|
|
6540
|
+
ok: false,
|
|
6541
|
+
sync: settings,
|
|
6542
|
+
error: message,
|
|
6543
|
+
...retainedLegacyDatabase ? { retainedLegacyDatabase } : {}
|
|
6544
|
+
});
|
|
6545
|
+
} else console.error(message);
|
|
6546
|
+
process.exitCode = 1;
|
|
6591
6547
|
}
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
);
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
console.log("[4/7] Removing FML hook registrations...");
|
|
6605
|
-
let hookRemovalComplete = true;
|
|
6606
|
-
for (const target2 of removeFmlHooks({
|
|
6607
|
-
onFailure: () => {
|
|
6608
|
-
hookRemovalComplete = false;
|
|
6548
|
+
}
|
|
6549
|
+
async function handleSyncDisable(opts = {}) {
|
|
6550
|
+
const sync2 = rawSync();
|
|
6551
|
+
const settings = sync2.setEnabled(false);
|
|
6552
|
+
const observerWoken = await wakeObserverSync();
|
|
6553
|
+
if (opts.json) {
|
|
6554
|
+
writeJson({ ok: true, sync: settings, observerWoken });
|
|
6555
|
+
} else {
|
|
6556
|
+
console.log("Agent activity sync is paused.");
|
|
6557
|
+
const observer = await readObserverRuntimeStatus();
|
|
6558
|
+
if (observer.running) {
|
|
6559
|
+
console.log("FML observer remains available for local integrations");
|
|
6609
6560
|
}
|
|
6610
|
-
})) {
|
|
6611
|
-
console.log(` Removed FML hooks from ${target2.displayName}`);
|
|
6612
|
-
}
|
|
6613
|
-
if (!hookRemovalComplete) {
|
|
6614
|
-
process.exitCode = 1;
|
|
6615
|
-
console.warn(
|
|
6616
|
-
" Some FML hook registrations were preserved; repair the paths reported above and rerun `fml uninstall`."
|
|
6617
|
-
);
|
|
6618
|
-
}
|
|
6619
|
-
console.log("[5/7] Removing native OpenTelemetry configuration...");
|
|
6620
|
-
for (const target2 of removeFmlOtel(otelDestination)) {
|
|
6621
|
-
console.log(` Removed FML OpenTelemetry from ${target2.displayName}`);
|
|
6622
6561
|
}
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6562
|
+
}
|
|
6563
|
+
async function handleSyncStatus(opts = {}) {
|
|
6564
|
+
const status = rawSync().status();
|
|
6565
|
+
const observer = await readObserverRuntimeStatus();
|
|
6566
|
+
if (opts.json) writeJson({ ok: true, status, observer });
|
|
6567
|
+
else {
|
|
6568
|
+
printStatus(status);
|
|
6569
|
+
const state = observer.stateStatus.state;
|
|
6570
|
+
console.log(
|
|
6571
|
+
` Observer: ${observer.healthy ? "running" : observer.running ? "unhealthy" : "stopped"}`
|
|
6626
6572
|
);
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
}
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
purgeComplete = false;
|
|
6637
|
-
} else {
|
|
6638
|
-
for (const root of purgeRoots) {
|
|
6639
|
-
if (refuseUnsafePurge([root])) {
|
|
6640
|
-
purgeComplete = false;
|
|
6641
|
-
continue;
|
|
6642
|
-
}
|
|
6643
|
-
const containsObserverDirectory = pathContains(
|
|
6644
|
-
root.directory,
|
|
6645
|
-
FML_OBSERVER_DIR
|
|
6573
|
+
if (state) {
|
|
6574
|
+
console.log(` PID: ${state.pid}`);
|
|
6575
|
+
console.log(` Phase: ${state.phase}`);
|
|
6576
|
+
if (state.lastOutcome) {
|
|
6577
|
+
console.log(` Last run: ${state.lastOutcome}`);
|
|
6578
|
+
}
|
|
6579
|
+
if (state.nextAttemptAtMs) {
|
|
6580
|
+
console.log(
|
|
6581
|
+
` Next run: ${new Date(state.nextAttemptAtMs).toISOString()}`
|
|
6646
6582
|
);
|
|
6647
|
-
const customManagedRoot = !root.nativeDefault;
|
|
6648
|
-
try {
|
|
6649
|
-
const purged = purgeDirectoryPreservingFiles(root.directory, [
|
|
6650
|
-
...customManagedRoot ? [managedDirectoryMarker(root.directory)] : [],
|
|
6651
|
-
...containsObserverDirectory && hookCapturePaused ? [FML_HOOK_CAPTURE_DISABLED_FILE] : []
|
|
6652
|
-
]);
|
|
6653
|
-
if (!purged) {
|
|
6654
|
-
purgeComplete = false;
|
|
6655
|
-
console.warn(
|
|
6656
|
-
` Preserved ${root.directory}: a symbolic link prevents a complete safe purge`
|
|
6657
|
-
);
|
|
6658
|
-
continue;
|
|
6659
|
-
}
|
|
6660
|
-
if (customManagedRoot && !containsObserverDirectory) {
|
|
6661
|
-
finishCustomRootPurge(root.directory);
|
|
6662
|
-
}
|
|
6663
|
-
console.log(` Purged ${root.directory}`);
|
|
6664
|
-
} catch (error) {
|
|
6665
|
-
purgeComplete = false;
|
|
6666
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
6667
|
-
console.warn(
|
|
6668
|
-
` Preserved ${root.directory}: the safe purge was incomplete: ${message}`
|
|
6669
|
-
);
|
|
6670
|
-
}
|
|
6671
6583
|
}
|
|
6672
6584
|
}
|
|
6673
|
-
|
|
6585
|
+
console.log(` Log: ${observer.logFile}`);
|
|
6586
|
+
}
|
|
6587
|
+
}
|
|
6588
|
+
async function handleSyncReset(opts = {}) {
|
|
6589
|
+
const result = rawSync().reset();
|
|
6590
|
+
if (opts.json) writeJson({ ok: !result.skipped, reset: result });
|
|
6591
|
+
else if (result.skipped) {
|
|
6592
|
+
console.error(
|
|
6593
|
+
"Raw artifact reset skipped: another sync owns the queue lock"
|
|
6594
|
+
);
|
|
6674
6595
|
} else {
|
|
6675
6596
|
console.log(
|
|
6676
|
-
|
|
6597
|
+
`Reset raw upload watermarks for ${result.reset} artifact${result.reset === 1 ? "" : "s"}.`
|
|
6677
6598
|
);
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
console.
|
|
6682
|
-
"
|
|
6599
|
+
if (result.removed > 0) {
|
|
6600
|
+
console.log(`Removed ${result.removed} missing artifact entries.`);
|
|
6601
|
+
}
|
|
6602
|
+
console.log(
|
|
6603
|
+
"The next enabled sync will replay each artifact from byte zero."
|
|
6683
6604
|
);
|
|
6684
6605
|
}
|
|
6685
|
-
if (
|
|
6606
|
+
if (result.skipped) {
|
|
6686
6607
|
process.exitCode = 1;
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
);
|
|
6608
|
+
} else {
|
|
6609
|
+
await wakeObserverSync();
|
|
6690
6610
|
}
|
|
6691
|
-
console.log("\nDone! FML has been uninstalled.");
|
|
6692
6611
|
}
|
|
6693
6612
|
|
|
6694
|
-
// src/commands/
|
|
6695
|
-
async function
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6613
|
+
// src/commands/daemon.ts
|
|
6614
|
+
async function handleFmlStart() {
|
|
6615
|
+
await handleSyncEnable();
|
|
6616
|
+
}
|
|
6617
|
+
async function stopFml(opts = {}) {
|
|
6618
|
+
setRawSyncEnabled(false);
|
|
6619
|
+
const result = await stopObserver({ force: opts.force });
|
|
6620
|
+
let stopped = result.status === "stopped" || result.status === "killed" || result.status === "not_running" || result.status === "stale_state";
|
|
6621
|
+
if (stopped) {
|
|
6622
|
+
try {
|
|
6623
|
+
setHookCaptureDisabled(true);
|
|
6624
|
+
} catch (error) {
|
|
6625
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6626
|
+
console.error(
|
|
6627
|
+
`FML observer stopped, but the hook-capture pause could not be persisted: ${message}`
|
|
6628
|
+
);
|
|
6629
|
+
process.exitCode = 1;
|
|
6630
|
+
stopped = false;
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6633
|
+
switch (result.status) {
|
|
6634
|
+
case "stopped":
|
|
6635
|
+
console.log(`FML observer stopped (PID ${result.pid}).`);
|
|
6636
|
+
console.log("Agent activity sync is paused.");
|
|
6637
|
+
break;
|
|
6638
|
+
case "killed":
|
|
6639
|
+
console.log(`FML observer stopped (PID ${result.pid}).`);
|
|
6640
|
+
console.log("Agent activity sync is paused.");
|
|
6641
|
+
break;
|
|
6642
|
+
case "not_running":
|
|
6643
|
+
console.log("FML observer is already stopped.");
|
|
6644
|
+
console.log("Agent activity sync is paused.");
|
|
6645
|
+
break;
|
|
6646
|
+
case "stale_state":
|
|
6647
|
+
console.log(
|
|
6648
|
+
"FML observer is stopped. Old process state will be cleaned on the next start."
|
|
6649
|
+
);
|
|
6650
|
+
console.log("Agent activity sync is paused.");
|
|
6651
|
+
break;
|
|
6652
|
+
case "permission_denied":
|
|
6653
|
+
console.error(
|
|
6654
|
+
`Could not stop FML observer PID ${result.pid}: permission denied`
|
|
6655
|
+
);
|
|
6656
|
+
process.exitCode = 1;
|
|
6657
|
+
break;
|
|
6658
|
+
case "foreign_listener":
|
|
6659
|
+
console.error("Refused to stop a different local observer instance");
|
|
6660
|
+
process.exitCode = 1;
|
|
6661
|
+
break;
|
|
6662
|
+
case "ambiguous_live_process":
|
|
6663
|
+
console.error(
|
|
6664
|
+
`Refused to treat unverifiable live PID ${result.pid} as a stopped FML observer`
|
|
6665
|
+
);
|
|
6666
|
+
process.exitCode = 1;
|
|
6667
|
+
break;
|
|
6668
|
+
case "unresponsive":
|
|
6669
|
+
console.error(
|
|
6670
|
+
`Refused to signal unverifiable FML observer PID ${result.pid}; retry with \`fml stop --force\` to escalate`
|
|
6671
|
+
);
|
|
6672
|
+
process.exitCode = 1;
|
|
6673
|
+
break;
|
|
6674
|
+
case "signal_sent":
|
|
6675
|
+
console.error(`FML observer PID ${result.pid} did not stop`);
|
|
6676
|
+
process.exitCode = 1;
|
|
6677
|
+
break;
|
|
6678
|
+
case "force_stop_unverified":
|
|
6679
|
+
console.error(
|
|
6680
|
+
`Could not corroborate that PID ${result.pid} is the FML observer; refusing to signal it. If the observer is not running, remove ${result.stateFile}, ${result.lockFile}, ${result.databaseStateFile}, and ${result.databaseLockFile} manually.`
|
|
6681
|
+
);
|
|
6682
|
+
process.exitCode = 1;
|
|
6683
|
+
break;
|
|
6684
|
+
}
|
|
6685
|
+
return stopped;
|
|
6686
|
+
}
|
|
6687
|
+
async function handleFmlStop(opts = {}) {
|
|
6688
|
+
await stopFml(opts);
|
|
6689
|
+
}
|
|
6690
|
+
async function handleFmlRestart() {
|
|
6691
|
+
if (await stopFml()) {
|
|
6692
|
+
await handleFmlStart();
|
|
6714
6693
|
}
|
|
6715
6694
|
}
|
|
6716
6695
|
|
|
@@ -7194,8 +7173,9 @@ async function runServiceTokenLogin(refreshToken) {
|
|
|
7194
7173
|
}
|
|
7195
7174
|
await selectOrg(environment);
|
|
7196
7175
|
console.log("Logged in with FML service token.");
|
|
7197
|
-
console.log(
|
|
7198
|
-
|
|
7176
|
+
console.log(
|
|
7177
|
+
"Queries and agent activity sync will use the stored service token."
|
|
7178
|
+
);
|
|
7199
7179
|
}
|
|
7200
7180
|
async function handleServiceTokenLogin(refreshToken) {
|
|
7201
7181
|
try {
|
|
@@ -7212,16 +7192,12 @@ async function handleServiceTokenLogin(refreshToken) {
|
|
|
7212
7192
|
process.exit(1);
|
|
7213
7193
|
}
|
|
7214
7194
|
}
|
|
7215
|
-
async function
|
|
7216
|
-
if (opts?.serviceToken) {
|
|
7217
|
-
await handleServiceTokenLogin();
|
|
7218
|
-
return;
|
|
7219
|
-
}
|
|
7195
|
+
async function runInteractiveLogin(opts = {}) {
|
|
7220
7196
|
const environment = captureLoginEnvironment();
|
|
7221
7197
|
const envName = environment.envName;
|
|
7222
7198
|
const existingToken = await getValidToken({ env: envName });
|
|
7223
7199
|
if (existingToken) {
|
|
7224
|
-
const { readTokens: readTokens2 } = await import("./token-store-
|
|
7200
|
+
const { readTokens: readTokens2 } = await import("./token-store-BT5THIAJ.js");
|
|
7225
7201
|
const stored = readTokens2(envName);
|
|
7226
7202
|
if (stored?.tokenType === "service") {
|
|
7227
7203
|
const label = stored.user.id === SERVICE_TOKEN_LOGIN_USER_ID ? "a service token" : `${stored.user.name} (${stored.user.email})`;
|
|
@@ -7233,23 +7209,27 @@ async function handleLogin(opts) {
|
|
|
7233
7209
|
}
|
|
7234
7210
|
await linkGitHubIdentity(environment);
|
|
7235
7211
|
await selectOrg(environment);
|
|
7236
|
-
|
|
7237
|
-
process.exit(0);
|
|
7212
|
+
return;
|
|
7238
7213
|
}
|
|
7239
7214
|
console.log(`Signing in to FML (${envName})...`);
|
|
7215
|
+
const useDeviceFlow = opts.device || !await canOpenBrowser();
|
|
7216
|
+
let result;
|
|
7217
|
+
if (useDeviceFlow) {
|
|
7218
|
+
result = await deviceLogin(environment);
|
|
7219
|
+
} else {
|
|
7220
|
+
result = await login(environment);
|
|
7221
|
+
}
|
|
7222
|
+
console.log(`Logged in as ${result.name} (${result.email})`);
|
|
7223
|
+
await linkGitHubIdentity(environment);
|
|
7224
|
+
await selectOrg(environment);
|
|
7225
|
+
}
|
|
7226
|
+
async function handleLogin(opts) {
|
|
7227
|
+
if (opts?.serviceToken) {
|
|
7228
|
+
await handleServiceTokenLogin();
|
|
7229
|
+
return;
|
|
7230
|
+
}
|
|
7240
7231
|
try {
|
|
7241
|
-
|
|
7242
|
-
let result;
|
|
7243
|
-
if (useDeviceFlow) {
|
|
7244
|
-
result = await deviceLogin(environment);
|
|
7245
|
-
} else {
|
|
7246
|
-
result = await login(environment);
|
|
7247
|
-
}
|
|
7248
|
-
console.log(`
|
|
7249
|
-
Logged in as ${result.name} (${result.email})`);
|
|
7250
|
-
await linkGitHubIdentity(environment);
|
|
7251
|
-
await selectOrg(environment);
|
|
7252
|
-
console.log("You're all set! Restart Claude Code to use FML tools.");
|
|
7232
|
+
await runInteractiveLogin({ device: opts?.device });
|
|
7253
7233
|
process.exit(0);
|
|
7254
7234
|
} catch (err) {
|
|
7255
7235
|
Sentry.captureException(err);
|
|
@@ -7259,742 +7239,891 @@ Logged in as ${result.name} (${result.email})`);
|
|
|
7259
7239
|
}
|
|
7260
7240
|
}
|
|
7261
7241
|
|
|
7262
|
-
// src/commands/
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7242
|
+
// src/commands/setup.ts
|
|
7243
|
+
var defaultDependencies = {
|
|
7244
|
+
install: () => handleInstall(),
|
|
7245
|
+
getActiveToken: () => getValidToken({ env: getActiveEnv().name }),
|
|
7246
|
+
login: () => runInteractiveLogin(),
|
|
7247
|
+
stop: () => handleFmlStop(),
|
|
7248
|
+
start: () => handleFmlStart()
|
|
7249
|
+
};
|
|
7250
|
+
async function runStep(name, operation) {
|
|
7251
|
+
const priorExitCode = process.exitCode;
|
|
7252
|
+
await operation();
|
|
7253
|
+
if (process.exitCode && process.exitCode !== priorExitCode) {
|
|
7254
|
+
throw new Error(`FML setup could not complete ${name}.`);
|
|
7255
|
+
}
|
|
7256
|
+
}
|
|
7257
|
+
async function handleSetup(dependencies = defaultDependencies) {
|
|
7258
|
+
if (process.exitCode) {
|
|
7259
|
+
throw new Error("FML setup cannot run after an earlier command failure.");
|
|
7260
|
+
}
|
|
7261
|
+
console.log("Setting up FML...");
|
|
7262
|
+
await runStep("integration installation", dependencies.install);
|
|
7263
|
+
const token = await dependencies.getActiveToken();
|
|
7264
|
+
if (token) {
|
|
7265
|
+
console.log("Already signed in.");
|
|
7266
|
+
} else {
|
|
7267
|
+
await runStep("login", dependencies.login);
|
|
7268
|
+
}
|
|
7269
|
+
await runStep("observer stop", dependencies.stop);
|
|
7270
|
+
await runStep("observer start", dependencies.start);
|
|
7271
|
+
console.log("FML is ready.");
|
|
7272
|
+
console.log("Start a new agent session to load FML tools.");
|
|
7273
|
+
console.log("Run `fml doctor` to verify setup.");
|
|
7267
7274
|
}
|
|
7268
7275
|
|
|
7269
|
-
// src/commands/
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
+
// src/commands/uninstall.ts
|
|
7277
|
+
import fs12 from "fs";
|
|
7278
|
+
import os7 from "os";
|
|
7279
|
+
import path27 from "path";
|
|
7280
|
+
import readline from "readline/promises";
|
|
7281
|
+
var PURGE_WARNING = "This deletes FML credentials for ALL environments, logs, sync state, and every pending best-effort OTLP envelope.";
|
|
7282
|
+
function tryDisableHookCapture() {
|
|
7283
|
+
try {
|
|
7284
|
+
setHookCaptureDisabled(true);
|
|
7285
|
+
return true;
|
|
7286
|
+
} catch (error) {
|
|
7287
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7288
|
+
console.warn(
|
|
7289
|
+
`Warning: Could not persist the hook-capture pause at ${FML_HOOK_CAPTURE_DISABLED_FILE}: ${message}`
|
|
7290
|
+
);
|
|
7291
|
+
return false;
|
|
7276
7292
|
}
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7293
|
+
}
|
|
7294
|
+
function resolvePreservedPaths(preservedFile) {
|
|
7295
|
+
const logicalPath = path27.resolve(preservedFile);
|
|
7296
|
+
try {
|
|
7297
|
+
const canonicalPath = fs12.realpathSync(preservedFile);
|
|
7298
|
+
return pathsAreSame(canonicalPath, logicalPath) ? [logicalPath] : [logicalPath, canonicalPath];
|
|
7299
|
+
} catch {
|
|
7300
|
+
return [logicalPath];
|
|
7285
7301
|
}
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
if (!currentId && !current) {
|
|
7296
|
-
console.log("\nNo org selected. Run `fml org <slug>` to select one.");
|
|
7297
|
-
}
|
|
7298
|
-
return;
|
|
7302
|
+
}
|
|
7303
|
+
function purgeDirectoryPreservingPaths(directory, preservedPaths, configuredRoot = false) {
|
|
7304
|
+
const resolvedDirectory = path27.resolve(directory);
|
|
7305
|
+
let directoryStats;
|
|
7306
|
+
try {
|
|
7307
|
+
directoryStats = fs12.lstatSync(directory);
|
|
7308
|
+
} catch (error) {
|
|
7309
|
+
if (error.code === "ENOENT") return true;
|
|
7310
|
+
throw error;
|
|
7299
7311
|
}
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
console.error(
|
|
7305
|
-
`Organization "${slug}" not found. Available: ${orgs.map((o) => o.slug ?? o.name).join(", ")}`
|
|
7312
|
+
if (directoryStats.isSymbolicLink()) {
|
|
7313
|
+
if (configuredRoot) return false;
|
|
7314
|
+
const containsLogicalMarker = preservedPaths.some(
|
|
7315
|
+
(preservedPath) => pathContains(resolvedDirectory, preservedPath)
|
|
7306
7316
|
);
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
}
|
|
7313
|
-
|
|
7314
|
-
// src/commands/open.ts
|
|
7315
|
-
var DEFAULT_APP_BASE_URL = "https://app.fml.app";
|
|
7316
|
-
function appBaseUrl() {
|
|
7317
|
-
const configured = process.env.FML_APP_URL?.trim();
|
|
7318
|
-
return (configured || DEFAULT_APP_BASE_URL).replace(/\/+$/, "");
|
|
7319
|
-
}
|
|
7320
|
-
async function handleOpen(opts) {
|
|
7321
|
-
const environment = getActiveEnv();
|
|
7322
|
-
const api = await getAuthenticatedClient(environment);
|
|
7323
|
-
if (!api) {
|
|
7324
|
-
console.error("Not authenticated. Run `fml login` first.");
|
|
7325
|
-
process.exit(1);
|
|
7317
|
+
if (!containsLogicalMarker) {
|
|
7318
|
+
fs12.rmSync(directory, { force: true });
|
|
7319
|
+
return true;
|
|
7320
|
+
}
|
|
7321
|
+
return false;
|
|
7326
7322
|
}
|
|
7327
|
-
|
|
7323
|
+
const directoryPaths = [resolvedDirectory];
|
|
7328
7324
|
try {
|
|
7329
|
-
const
|
|
7330
|
-
if (
|
|
7331
|
-
|
|
7332
|
-
process.exit(1);
|
|
7325
|
+
const canonicalDirectory = fs12.realpathSync(directory);
|
|
7326
|
+
if (!pathsAreSame(canonicalDirectory, resolvedDirectory)) {
|
|
7327
|
+
directoryPaths.push(canonicalDirectory);
|
|
7333
7328
|
}
|
|
7334
|
-
const selected = getSelectedOrg(environment.name);
|
|
7335
|
-
const org = selected && orgs.find((candidate) => {
|
|
7336
|
-
const candidateSlug = candidate.slug ?? candidate.name;
|
|
7337
|
-
return candidateSlug === selected || candidate.name === selected;
|
|
7338
|
-
}) || orgs[0];
|
|
7339
|
-
slug = org.slug ?? org.name;
|
|
7340
7329
|
} catch {
|
|
7341
|
-
console.error("Could not fetch organizations. Opening base URL.");
|
|
7342
|
-
slug = "";
|
|
7343
7330
|
}
|
|
7344
|
-
const
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7331
|
+
const containsPreservedPath = directoryPaths.some(
|
|
7332
|
+
(directoryPath) => preservedPaths.some(
|
|
7333
|
+
(preservedPath) => pathContains(directoryPath, preservedPath)
|
|
7334
|
+
)
|
|
7335
|
+
);
|
|
7336
|
+
if (!containsPreservedPath) {
|
|
7337
|
+
fs12.rmSync(directory, { recursive: true, force: true });
|
|
7338
|
+
return true;
|
|
7351
7339
|
}
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
if (
|
|
7358
|
-
if (
|
|
7359
|
-
|
|
7340
|
+
const isPreservedPath = directoryPaths.some(
|
|
7341
|
+
(directoryPath) => preservedPaths.some(
|
|
7342
|
+
(preservedPath) => pathsAreSame(directoryPath, path27.resolve(preservedPath))
|
|
7343
|
+
)
|
|
7344
|
+
);
|
|
7345
|
+
if (isPreservedPath) return true;
|
|
7346
|
+
if (!directoryStats.isDirectory()) return false;
|
|
7347
|
+
let entries;
|
|
7348
|
+
try {
|
|
7349
|
+
entries = fs12.readdirSync(directory);
|
|
7350
|
+
} catch (error) {
|
|
7351
|
+
if (error.code === "ENOENT") return true;
|
|
7352
|
+
throw error;
|
|
7360
7353
|
}
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7354
|
+
let complete = true;
|
|
7355
|
+
for (const entry of entries) {
|
|
7356
|
+
const entryPath = path27.join(directory, entry);
|
|
7357
|
+
if (!purgeDirectoryPreservingPaths(entryPath, preservedPaths, false)) {
|
|
7358
|
+
complete = false;
|
|
7359
|
+
}
|
|
7366
7360
|
}
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
"evictedQuarantinedRecords"
|
|
7361
|
+
return complete;
|
|
7362
|
+
}
|
|
7363
|
+
function purgeDirectoryPreservingFiles(directory, preservedFiles) {
|
|
7364
|
+
if (hasSymlinkedParentBelowTrustedAnchor(directory)) return false;
|
|
7365
|
+
return purgeDirectoryPreservingPaths(
|
|
7366
|
+
directory,
|
|
7367
|
+
preservedFiles.flatMap(
|
|
7368
|
+
(preservedFile) => resolvePreservedPaths(preservedFile)
|
|
7369
|
+
),
|
|
7370
|
+
true
|
|
7378
7371
|
);
|
|
7379
|
-
const maxBytes = record.maxBytes;
|
|
7380
|
-
const quarantineMaxAgeMs = record.quarantineMaxAgeMs;
|
|
7381
|
-
if (record.unit !== "logical_bytes" || logicalBytes === void 0 || evictedRecords === void 0 || evictedBytes === void 0 || evictedPendingRecords === void 0 || evictedQuarantinedRecords === void 0 || maxBytes !== null && (typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes < 1)) {
|
|
7382
|
-
return void 0;
|
|
7383
|
-
}
|
|
7384
|
-
const lastEvictedAtMsValue = nonNegativeInteger("lastEvictedAtMs");
|
|
7385
|
-
const lastEvictedAtMs = lastEvictedAtMsValue !== void 0 && lastEvictedAtMsValue <= 864e13 ? lastEvictedAtMsValue : void 0;
|
|
7386
|
-
return {
|
|
7387
|
-
logicalBytes,
|
|
7388
|
-
maxBytes,
|
|
7389
|
-
...quarantineMaxAgeMs === null ? { quarantineMaxAgeMs: null } : typeof quarantineMaxAgeMs === "number" && Number.isSafeInteger(quarantineMaxAgeMs) && quarantineMaxAgeMs > 0 ? { quarantineMaxAgeMs } : {},
|
|
7390
|
-
evictedRecords,
|
|
7391
|
-
evictedBytes,
|
|
7392
|
-
evictedPendingRecords,
|
|
7393
|
-
evictedQuarantinedRecords,
|
|
7394
|
-
...lastEvictedAtMs === void 0 ? {} : { lastEvictedAtMs }
|
|
7395
|
-
};
|
|
7396
7372
|
}
|
|
7397
|
-
function
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7373
|
+
function finishCustomRootPurge(directory) {
|
|
7374
|
+
const marker = managedDirectoryMarker(directory);
|
|
7375
|
+
fs12.rmSync(marker, { force: true });
|
|
7376
|
+
try {
|
|
7377
|
+
fs12.rmdirSync(directory);
|
|
7378
|
+
} catch (error) {
|
|
7379
|
+
if (error.code === "ENOENT") return;
|
|
7380
|
+
try {
|
|
7381
|
+
fs12.writeFileSync(marker, FML_MANAGED_DIRECTORY_MARKER_CONTENT, {
|
|
7382
|
+
encoding: "utf8",
|
|
7383
|
+
flag: "wx",
|
|
7384
|
+
mode: 384
|
|
7385
|
+
});
|
|
7386
|
+
} catch (markerError) {
|
|
7387
|
+
if (markerError.code === "EEXIST" && hasManagedDirectoryMarker(directory)) {
|
|
7388
|
+
throw error;
|
|
7389
|
+
}
|
|
7390
|
+
const removalMessage = error instanceof Error ? error.message : String(error);
|
|
7391
|
+
const markerMessage = markerError instanceof Error ? markerError.message : String(markerError);
|
|
7392
|
+
throw new Error(
|
|
7393
|
+
`could not remove ${directory} (${removalMessage}) or restore its FML ownership marker (${markerMessage})`
|
|
7394
|
+
);
|
|
7395
|
+
}
|
|
7396
|
+
throw error;
|
|
7414
7397
|
}
|
|
7415
|
-
return {
|
|
7416
|
-
logicalBytes,
|
|
7417
|
-
bodyBytes,
|
|
7418
|
-
maxBytes,
|
|
7419
|
-
droppedRecords,
|
|
7420
|
-
droppedLogicalBytes,
|
|
7421
|
-
droppedBodyBytes,
|
|
7422
|
-
...typeof record.lastDroppedAtMs === "number" && Number.isSafeInteger(record.lastDroppedAtMs) && record.lastDroppedAtMs >= 0 && Number.isFinite(new Date(record.lastDroppedAtMs).getTime()) ? { lastDroppedAtMs: record.lastDroppedAtMs } : {},
|
|
7423
|
-
...typeof record.lastDropReason === "string" ? { lastDropReason: record.lastDropReason } : {}
|
|
7424
|
-
};
|
|
7425
7398
|
}
|
|
7426
|
-
function
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7399
|
+
function cleanInstalledPluginScopes(paths) {
|
|
7400
|
+
const installedPlugins = readJsonFile(paths.installedPluginsPath);
|
|
7401
|
+
if (!installedPlugins) return;
|
|
7402
|
+
const update = CLAUDE_PLUGIN_INSTALLER.removeInstalledPlugin(
|
|
7403
|
+
installedPlugins,
|
|
7404
|
+
CLAUDE_PLUGIN_INSTALLER.pluginId
|
|
7405
|
+
);
|
|
7406
|
+
if (!update.changed) return;
|
|
7407
|
+
for (const settingsPath2 of CLAUDE_PLUGIN_INSTALLER.projectSettingsPaths(
|
|
7408
|
+
installedPlugins,
|
|
7409
|
+
CLAUDE_PLUGIN_INSTALLER.pluginId
|
|
7410
|
+
)) {
|
|
7411
|
+
removeEnabledPluginFromSettings(
|
|
7412
|
+
settingsPath2,
|
|
7413
|
+
CLAUDE_PLUGIN_INSTALLER.pluginId
|
|
7414
|
+
);
|
|
7433
7415
|
}
|
|
7434
|
-
|
|
7435
|
-
phase: record.phase,
|
|
7436
|
-
pending: record.pending,
|
|
7437
|
-
...typeof record.quarantined === "number" && Number.isSafeInteger(record.quarantined) && record.quarantined >= 0 ? { quarantined: record.quarantined } : {},
|
|
7438
|
-
...typeof record.lastError === "string" ? { lastError: record.lastError } : {},
|
|
7439
|
-
...typeof record.lastWarning === "string" ? { lastWarning: record.lastWarning } : {},
|
|
7440
|
-
...typeof record.nextAttemptAtMs === "number" && Number.isSafeInteger(record.nextAttemptAtMs) && record.nextAttemptAtMs >= 0 && Number.isFinite(new Date(record.nextAttemptAtMs).getTime()) ? { nextAttemptAtMs: record.nextAttemptAtMs } : {}
|
|
7441
|
-
};
|
|
7416
|
+
writeJsonFile(paths.installedPluginsPath, update.installedPlugins);
|
|
7442
7417
|
}
|
|
7443
|
-
function
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
}
|
|
7448
|
-
const hooks = value.hooks;
|
|
7449
|
-
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) return null;
|
|
7450
|
-
const record = hooks;
|
|
7451
|
-
const spool = hookSpoolStatus(record.spool);
|
|
7452
|
-
return {
|
|
7453
|
-
...status,
|
|
7454
|
-
...typeof record.rejected === "number" && Number.isSafeInteger(record.rejected) && record.rejected >= 0 ? { rejected: record.rejected } : {},
|
|
7455
|
-
...typeof record.lastRejectedReason === "string" ? { lastRejectedReason: record.lastRejectedReason } : {},
|
|
7456
|
-
...typeof record.lastRejectedTarget === "string" ? { lastRejectedTarget: record.lastRejectedTarget } : {},
|
|
7457
|
-
...spool === void 0 ? {} : { spool }
|
|
7458
|
-
};
|
|
7418
|
+
function purgeWarning(roots) {
|
|
7419
|
+
return `${PURGE_WARNING}
|
|
7420
|
+
Purge roots:
|
|
7421
|
+
${roots.map((root) => ` - ${root.directory}`).join("\n")}`;
|
|
7459
7422
|
}
|
|
7460
|
-
function
|
|
7461
|
-
const
|
|
7462
|
-
if (
|
|
7463
|
-
|
|
7423
|
+
async function confirmPurge(opts, roots) {
|
|
7424
|
+
const warning = purgeWarning(roots);
|
|
7425
|
+
if (opts.assumeYes) {
|
|
7426
|
+
console.log(warning);
|
|
7427
|
+
return true;
|
|
7464
7428
|
}
|
|
7465
|
-
const
|
|
7466
|
-
if (
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
...spool === void 0 ? {} : { spool }
|
|
7471
|
-
};
|
|
7472
|
-
}
|
|
7473
|
-
async function handleStatus() {
|
|
7474
|
-
const { name, convexUrl } = getActiveEnv();
|
|
7475
|
-
console.log(`Environment: ${name}`);
|
|
7476
|
-
console.log(` Convex: ${convexUrl ?? "not configured"}
|
|
7477
|
-
`);
|
|
7478
|
-
const tokens = readTokens(name);
|
|
7479
|
-
if (!tokens) {
|
|
7480
|
-
console.log("Authentication:");
|
|
7481
|
-
console.log(" Not authenticated. Run `fml login` to sign in.");
|
|
7482
|
-
} else {
|
|
7483
|
-
const tokenValid = await getValidToken({ env: name });
|
|
7484
|
-
console.log("Authentication:");
|
|
7485
|
-
if (tokens.tokenType === "service" && tokens.user.id === SERVICE_TOKEN_LOGIN_USER_ID) {
|
|
7486
|
-
console.log(" Auth: service token");
|
|
7487
|
-
} else {
|
|
7488
|
-
console.log(` User: ${tokens.user.name} (${tokens.user.email})`);
|
|
7489
|
-
}
|
|
7490
|
-
console.log(
|
|
7491
|
-
` Token: ${tokenValid ? "valid" : "expired (needs refresh/re-login)"}`
|
|
7492
|
-
);
|
|
7493
|
-
console.log(
|
|
7494
|
-
` Org: ${tokens.orgSlug ?? "(none \u2014 run `fml org <slug>`)"}`
|
|
7429
|
+
const input = opts.input ?? process.stdin;
|
|
7430
|
+
if (input.isTTY !== true) {
|
|
7431
|
+
console.error(
|
|
7432
|
+
`${warning}
|
|
7433
|
+
Re-run with --yes to confirm in a non-interactive session.`
|
|
7495
7434
|
);
|
|
7435
|
+
return false;
|
|
7496
7436
|
}
|
|
7497
|
-
const
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
const protectedStatus = observer.healthy ? await readObserverProtectedStatus() : null;
|
|
7514
|
-
const observerState = observer.stateStatus.state;
|
|
7515
|
-
console.log("\nLocal observer:");
|
|
7516
|
-
console.log(
|
|
7517
|
-
` Status: ${observer.healthy ? "running" : observer.running ? "unhealthy" : "stopped"}`
|
|
7518
|
-
);
|
|
7519
|
-
if (observerState) {
|
|
7520
|
-
console.log(` PID: ${observerState.pid}`);
|
|
7521
|
-
console.log(` Phase: ${observerState.phase}`);
|
|
7522
|
-
if (observerState.lastOutcome) {
|
|
7523
|
-
console.log(` Last: ${observerState.lastOutcome}`);
|
|
7524
|
-
}
|
|
7437
|
+
const rl = readline.createInterface({
|
|
7438
|
+
input,
|
|
7439
|
+
output: opts.output ?? process.stdout
|
|
7440
|
+
});
|
|
7441
|
+
try {
|
|
7442
|
+
const closed = new Promise((resolve) => {
|
|
7443
|
+
rl.once("close", () => resolve(null));
|
|
7444
|
+
});
|
|
7445
|
+
const answer = await Promise.race([
|
|
7446
|
+
rl.question(`${warning}
|
|
7447
|
+
Type y to continue: `).catch(() => null),
|
|
7448
|
+
closed
|
|
7449
|
+
]);
|
|
7450
|
+
if (answer !== null && answer.trim().toLowerCase() === "y") return true;
|
|
7451
|
+
} finally {
|
|
7452
|
+
rl.close();
|
|
7525
7453
|
}
|
|
7526
|
-
console.
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
console.
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7454
|
+
console.error("Uninstall canceled; nothing was removed.");
|
|
7455
|
+
return false;
|
|
7456
|
+
}
|
|
7457
|
+
function refuseUnsafePurge(roots) {
|
|
7458
|
+
const unsafeRoots = unsafePurgeRoots(roots);
|
|
7459
|
+
if (unsafeRoots.length === 0) return false;
|
|
7460
|
+
console.error("Refusing to purge unsafe FML directories:");
|
|
7461
|
+
for (const root of unsafeRoots) {
|
|
7462
|
+
console.error(` - ${root.directory}: ${root.reason}`);
|
|
7463
|
+
}
|
|
7464
|
+
console.error(
|
|
7465
|
+
"Inspect and remove those roots manually, or reconfigure FML to fresh dedicated empty data, state, log, and observer directories and run `fml install` before retrying."
|
|
7466
|
+
);
|
|
7467
|
+
process.exitCode = 1;
|
|
7468
|
+
return true;
|
|
7469
|
+
}
|
|
7470
|
+
async function handleUninstall(opts) {
|
|
7471
|
+
const purgeRoots = opts.purge ? configuredManagedRoots() : [];
|
|
7472
|
+
if (opts.purge) {
|
|
7473
|
+
if (refuseUnsafePurge(purgeRoots)) return;
|
|
7474
|
+
if (!await confirmPurge(opts, purgeRoots)) {
|
|
7475
|
+
process.exitCode = 1;
|
|
7476
|
+
return;
|
|
7545
7477
|
}
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
);
|
|
7550
|
-
if (hooks.spool.quarantineMaxAgeMs !== void 0) {
|
|
7551
|
-
console.log(
|
|
7552
|
-
` Quarantine retention: ${hooks.spool.quarantineMaxAgeMs === null ? "unbounded" : `${Math.round(hooks.spool.quarantineMaxAgeMs / 864e5)} day(s)`}`
|
|
7553
|
-
);
|
|
7554
|
-
}
|
|
7555
|
-
if (hooks.spool.evictedRecords > 0) {
|
|
7556
|
-
console.log(
|
|
7557
|
-
` Evicted: ${hooks.spool.evictedRecords} record(s), ${formatBytes(hooks.spool.evictedBytes)} (${hooks.spool.evictedQuarantinedRecords} quarantined, ${hooks.spool.evictedPendingRecords} pending)`
|
|
7558
|
-
);
|
|
7559
|
-
if (hooks.spool.lastEvictedAtMs !== void 0) {
|
|
7560
|
-
console.log(
|
|
7561
|
-
` Last eviction: ${new Date(hooks.spool.lastEvictedAtMs).toISOString()}`
|
|
7562
|
-
);
|
|
7563
|
-
}
|
|
7478
|
+
for (const root of purgeRoots) {
|
|
7479
|
+
if (!fs12.existsSync(root.directory) && pathContains(root.directory, FML_OBSERVER_DIR)) {
|
|
7480
|
+
prepareManagedDirectory(root);
|
|
7564
7481
|
}
|
|
7565
7482
|
}
|
|
7566
|
-
if (
|
|
7483
|
+
if (refuseUnsafePurge(purgeRoots)) return;
|
|
7567
7484
|
}
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
7485
|
+
console.log("Removing FML integrations...\n");
|
|
7486
|
+
const otelDestination = readLocalObserverOtelDestination({
|
|
7487
|
+
endpointFile: FML_OBSERVER_ENDPOINT_FILE,
|
|
7488
|
+
authTokenFile: FML_OBSERVER_AUTH_TOKEN_FILE
|
|
7489
|
+
});
|
|
7490
|
+
let observer;
|
|
7491
|
+
try {
|
|
7492
|
+
observer = await stopObserver();
|
|
7493
|
+
} catch (error) {
|
|
7494
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
7495
|
+
console.error(
|
|
7496
|
+
"Could not safely stop the FML observer; uninstall was aborted. Repair the reported files and rerun `fml uninstall`."
|
|
7573
7497
|
);
|
|
7574
|
-
|
|
7575
|
-
|
|
7498
|
+
process.exitCode = 1;
|
|
7499
|
+
return;
|
|
7500
|
+
}
|
|
7501
|
+
if (observer.status === "permission_denied" || observer.status === "foreign_listener" || observer.status === "ambiguous_live_process" || observer.status === "unresponsive" || observer.status === "signal_sent") {
|
|
7502
|
+
console.error(
|
|
7503
|
+
`Could not safely stop FML observer PID ${observer.pid}; uninstall was aborted.`
|
|
7576
7504
|
);
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7505
|
+
process.exitCode = 1;
|
|
7506
|
+
return;
|
|
7507
|
+
}
|
|
7508
|
+
let hookCapturePaused = tryDisableHookCapture();
|
|
7509
|
+
createRawArtifactSync({ stateDir: FML_OBSERVER_DIR }).setEnabled(false);
|
|
7510
|
+
const paths = CLAUDE_PLUGIN_INSTALLER.resolvePaths({
|
|
7511
|
+
env: process.env,
|
|
7512
|
+
homeDir: os7.homedir()
|
|
7513
|
+
});
|
|
7514
|
+
const claudeBin = resolveBin("claude");
|
|
7515
|
+
if (claudeBin) {
|
|
7516
|
+
try {
|
|
7517
|
+
const command = CLAUDE_PLUGIN_INSTALLER.uninstallCommand;
|
|
7518
|
+
execBinSync(claudeBin, [...command.args], {
|
|
7519
|
+
timeout: command.timeoutMs
|
|
7520
|
+
});
|
|
7521
|
+
} catch {
|
|
7586
7522
|
}
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7523
|
+
}
|
|
7524
|
+
cleanInstalledPluginScopes(paths);
|
|
7525
|
+
const settings = readJsonFile(paths.settingsPath);
|
|
7526
|
+
if (settings) {
|
|
7527
|
+
const marketplace2 = readJsonFile(paths.marketplaceManifestPath);
|
|
7528
|
+
const keepLocalMarketplace = CLAUDE_PLUGIN_INSTALLER.hasOtherLocalMarketplacePlugins(
|
|
7529
|
+
settings,
|
|
7530
|
+
marketplace2
|
|
7531
|
+
);
|
|
7532
|
+
writeJsonFile(
|
|
7533
|
+
paths.settingsPath,
|
|
7534
|
+
CLAUDE_PLUGIN_INSTALLER.disableInSettings(settings, keepLocalMarketplace)
|
|
7535
|
+
);
|
|
7536
|
+
}
|
|
7537
|
+
let skillsRemoved = true;
|
|
7538
|
+
try {
|
|
7539
|
+
for (const result of removeManagedAgentSkills(resolvePackageRoot())) {
|
|
7540
|
+
if (result.status !== "preserved" && !result.reason) continue;
|
|
7541
|
+
skillsRemoved = false;
|
|
7542
|
+
const targets = result.displayNames.join(", ");
|
|
7543
|
+
console.warn(
|
|
7544
|
+
`Warning: Preserved ${result.destinationPath} for ${targets}: ${result.reason ?? "cleanup could not be verified"}`
|
|
7590
7545
|
);
|
|
7591
7546
|
}
|
|
7547
|
+
} catch (error) {
|
|
7548
|
+
skillsRemoved = false;
|
|
7549
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7550
|
+
console.warn(
|
|
7551
|
+
`Warning: Agent skill cleanup could not be completed: ${message}`
|
|
7552
|
+
);
|
|
7592
7553
|
}
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
console.
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7554
|
+
let integrationsRemoved = true;
|
|
7555
|
+
if (removeMarketplaceLink(paths.marketplaceLinkPath) === "preserved") {
|
|
7556
|
+
integrationsRemoved = false;
|
|
7557
|
+
console.warn(
|
|
7558
|
+
`Warning: Preserved non-symlink marketplace path ${paths.marketplaceLinkPath}`
|
|
7559
|
+
);
|
|
7560
|
+
}
|
|
7561
|
+
const marketplace = readJsonFile(paths.marketplaceManifestPath);
|
|
7562
|
+
if (marketplace && Array.isArray(marketplace.plugins)) {
|
|
7563
|
+
writeJsonFile(
|
|
7564
|
+
paths.marketplaceManifestPath,
|
|
7565
|
+
CLAUDE_PLUGIN_INSTALLER.removeMarketplaceEntry(
|
|
7566
|
+
marketplace,
|
|
7567
|
+
CLAUDE_PLUGIN_INSTALLER.pluginName
|
|
7568
|
+
)
|
|
7569
|
+
);
|
|
7570
|
+
}
|
|
7571
|
+
fs12.rmSync(paths.pluginCacheDir, { recursive: true, force: true });
|
|
7572
|
+
const reportIntegrationWarning = (message) => {
|
|
7573
|
+
integrationsRemoved = false;
|
|
7574
|
+
console.warn(message.replace(/^warn:/u, "Warning:"));
|
|
7575
|
+
};
|
|
7576
|
+
let hookRemovalComplete = true;
|
|
7577
|
+
removeFmlHooks({
|
|
7578
|
+
onFailure: (target2) => {
|
|
7579
|
+
hookRemovalComplete = false;
|
|
7580
|
+
reportIntegrationWarning(
|
|
7581
|
+
`Warning: Preserved FML hook registration at ${target2.configPath}: ${target2.reason}`
|
|
7604
7582
|
);
|
|
7605
|
-
if (usedPercent >= 80) {
|
|
7606
|
-
console.log(
|
|
7607
|
-
` Attention: the oldest pending telemetry is dropped when the spool reaches ${formatBytes(otlp.spool.maxBytes)}`
|
|
7608
|
-
);
|
|
7609
|
-
}
|
|
7610
|
-
if (otlp.spool.droppedRecords > 0) {
|
|
7611
|
-
console.log(
|
|
7612
|
-
` Dropped: ${otlp.spool.droppedRecords} envelope(s), ${formatBytes(otlp.spool.droppedLogicalBytes)} logical; ${formatBytes(otlp.spool.droppedBodyBytes)} encoded bodies`
|
|
7613
|
-
);
|
|
7614
|
-
if (otlp.spool.lastDropReason) {
|
|
7615
|
-
console.log(
|
|
7616
|
-
` Last drop: ${otlp.spool.lastDropReason}${otlp.spool.lastDroppedAtMs === void 0 ? "" : ` at ${new Date(otlp.spool.lastDroppedAtMs).toISOString()}`}`
|
|
7617
|
-
);
|
|
7618
|
-
}
|
|
7619
|
-
}
|
|
7620
7583
|
}
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7584
|
+
});
|
|
7585
|
+
if (!hookRemovalComplete) {
|
|
7586
|
+
integrationsRemoved = false;
|
|
7587
|
+
process.exitCode = 1;
|
|
7588
|
+
console.warn(
|
|
7589
|
+
"Warning: Some FML hook registrations were preserved; repair the paths reported above and rerun `fml uninstall`."
|
|
7590
|
+
);
|
|
7591
|
+
}
|
|
7592
|
+
removeFmlOtel(otelDestination, { warn: reportIntegrationWarning });
|
|
7593
|
+
if (!otelDestination) {
|
|
7594
|
+
integrationsRemoved = false;
|
|
7595
|
+
console.warn(
|
|
7596
|
+
"Warning: The local observer endpoint or token is unavailable; only recorded OpenTelemetry profiles could be cleaned"
|
|
7597
|
+
);
|
|
7598
|
+
}
|
|
7599
|
+
removeFmlDirectMcp("all", {
|
|
7600
|
+
log: () => void 0,
|
|
7601
|
+
warn: reportIntegrationWarning
|
|
7602
|
+
});
|
|
7603
|
+
let purgeComplete = true;
|
|
7604
|
+
if (opts.purge) {
|
|
7605
|
+
if (refuseUnsafePurge(purgeRoots)) {
|
|
7606
|
+
purgeComplete = false;
|
|
7607
|
+
} else {
|
|
7608
|
+
for (const root of purgeRoots) {
|
|
7609
|
+
if (refuseUnsafePurge([root])) {
|
|
7610
|
+
purgeComplete = false;
|
|
7611
|
+
continue;
|
|
7612
|
+
}
|
|
7613
|
+
const containsObserverDirectory = pathContains(
|
|
7614
|
+
root.directory,
|
|
7615
|
+
FML_OBSERVER_DIR
|
|
7616
|
+
);
|
|
7617
|
+
const customManagedRoot = !root.nativeDefault;
|
|
7618
|
+
try {
|
|
7619
|
+
const purged = purgeDirectoryPreservingFiles(root.directory, [
|
|
7620
|
+
...customManagedRoot ? [managedDirectoryMarker(root.directory)] : [],
|
|
7621
|
+
...containsObserverDirectory && hookCapturePaused ? [FML_HOOK_CAPTURE_DISABLED_FILE] : []
|
|
7622
|
+
]);
|
|
7623
|
+
if (!purged) {
|
|
7624
|
+
purgeComplete = false;
|
|
7625
|
+
console.warn(
|
|
7626
|
+
`Warning: Preserved ${root.directory}: a symbolic link prevents a complete safe purge`
|
|
7627
|
+
);
|
|
7628
|
+
continue;
|
|
7629
|
+
}
|
|
7630
|
+
if (customManagedRoot && !containsObserverDirectory) {
|
|
7631
|
+
finishCustomRootPurge(root.directory);
|
|
7632
|
+
}
|
|
7633
|
+
} catch (error) {
|
|
7634
|
+
purgeComplete = false;
|
|
7635
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7636
|
+
console.warn(
|
|
7637
|
+
`Warning: Preserved ${root.directory}: the safe purge was incomplete: ${message}`
|
|
7638
|
+
);
|
|
7639
|
+
}
|
|
7640
|
+
}
|
|
7627
7641
|
}
|
|
7642
|
+
if (!hookCapturePaused) hookCapturePaused = tryDisableHookCapture();
|
|
7643
|
+
}
|
|
7644
|
+
if (!hookCapturePaused) {
|
|
7645
|
+
process.exitCode = 1;
|
|
7646
|
+
integrationsRemoved = false;
|
|
7647
|
+
console.warn(
|
|
7648
|
+
"Warning: FML cleanup is incomplete; cached agent processes may keep invoking hooks until they exit."
|
|
7649
|
+
);
|
|
7650
|
+
}
|
|
7651
|
+
if (opts.purge && !purgeComplete) {
|
|
7652
|
+
process.exitCode = 1;
|
|
7653
|
+
console.warn(
|
|
7654
|
+
"Warning: Some local data remains because a purge target could not be removed safely. Repair the reported path and rerun `fml uninstall --purge`."
|
|
7655
|
+
);
|
|
7656
|
+
}
|
|
7657
|
+
if (skillsRemoved) {
|
|
7658
|
+
console.log("\u2713 Agent skills removed");
|
|
7659
|
+
} else {
|
|
7660
|
+
console.warn(
|
|
7661
|
+
"Warning: Agent skills were not fully removed. See warnings above."
|
|
7662
|
+
);
|
|
7663
|
+
}
|
|
7664
|
+
if (integrationsRemoved) {
|
|
7665
|
+
console.log("\u2713 Coding-agent integrations removed");
|
|
7666
|
+
} else {
|
|
7667
|
+
console.warn(
|
|
7668
|
+
"Warning: Coding-agent integrations were not fully removed. See warnings above."
|
|
7669
|
+
);
|
|
7670
|
+
}
|
|
7671
|
+
if (!opts.purge) {
|
|
7672
|
+
console.log("\u2713 Local data preserved");
|
|
7673
|
+
} else if (purgeComplete && hookCapturePaused) {
|
|
7674
|
+
console.log("\u2713 Local data deleted");
|
|
7675
|
+
} else {
|
|
7676
|
+
console.warn(
|
|
7677
|
+
"Warning: Local data was not fully deleted. See warnings above."
|
|
7678
|
+
);
|
|
7679
|
+
}
|
|
7680
|
+
const removalComplete = skillsRemoved && integrationsRemoved && (!opts.purge || purgeComplete && hookCapturePaused);
|
|
7681
|
+
if (removalComplete) {
|
|
7682
|
+
console.log("\nFML integrations removed.");
|
|
7683
|
+
} else {
|
|
7684
|
+
console.warn(
|
|
7685
|
+
"FML integration removal is incomplete; review the warnings above."
|
|
7686
|
+
);
|
|
7628
7687
|
}
|
|
7629
7688
|
}
|
|
7630
7689
|
|
|
7631
|
-
// src/commands/
|
|
7632
|
-
|
|
7633
|
-
function
|
|
7634
|
-
return
|
|
7635
|
-
}
|
|
7636
|
-
function
|
|
7637
|
-
|
|
7638
|
-
if (
|
|
7639
|
-
|
|
7640
|
-
|
|
7690
|
+
// src/commands/update.ts
|
|
7691
|
+
var SEMVER_PATTERN = /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
7692
|
+
function getCurrentVersion() {
|
|
7693
|
+
return true ? "0.8.0-rc.2+bed6eaed" : "unknown";
|
|
7694
|
+
}
|
|
7695
|
+
function resolveUpdateChannel(version) {
|
|
7696
|
+
const match = SEMVER_PATTERN.exec(version);
|
|
7697
|
+
if (!match) return "latest";
|
|
7698
|
+
const prerelease = match[1];
|
|
7699
|
+
if (!prerelease) return "latest";
|
|
7700
|
+
return prerelease.split(".").every((identifier) => {
|
|
7701
|
+
return !(identifier.length > 1 && identifier.startsWith("0") && /^[0-9]+$/u.test(identifier));
|
|
7702
|
+
}) ? "next" : "latest";
|
|
7703
|
+
}
|
|
7704
|
+
function resolveUpdateTarget(version) {
|
|
7705
|
+
return `@fml-inc/fml@${resolveUpdateChannel(version)}`;
|
|
7706
|
+
}
|
|
7707
|
+
function createUpdateEnvironment(env) {
|
|
7708
|
+
const updateEnv = { ...env };
|
|
7709
|
+
for (const key of Object.keys(updateEnv)) {
|
|
7710
|
+
if (key.toLowerCase() === "npm_config_registry") {
|
|
7711
|
+
delete updateEnv[key];
|
|
7712
|
+
}
|
|
7713
|
+
}
|
|
7714
|
+
return updateEnv;
|
|
7715
|
+
}
|
|
7716
|
+
function formatUpdateError(err) {
|
|
7717
|
+
if (!(err instanceof Error)) return String(err);
|
|
7718
|
+
const stderr = err.stderr;
|
|
7719
|
+
if (typeof stderr === "string") return stderr;
|
|
7720
|
+
if (Buffer.isBuffer(stderr)) return stderr.toString();
|
|
7721
|
+
return err.message;
|
|
7722
|
+
}
|
|
7723
|
+
async function handleUpdate(overrides = {}) {
|
|
7724
|
+
const dependencies = {
|
|
7725
|
+
currentVersion: getCurrentVersion(),
|
|
7726
|
+
env: process.env,
|
|
7727
|
+
resolveBin,
|
|
7728
|
+
execBinSync,
|
|
7729
|
+
captureException: Sentry.captureException,
|
|
7730
|
+
log: console.log,
|
|
7731
|
+
error: console.error,
|
|
7732
|
+
exit: process.exit,
|
|
7733
|
+
...overrides
|
|
7734
|
+
};
|
|
7735
|
+
const channel = resolveUpdateChannel(dependencies.currentVersion);
|
|
7736
|
+
const target2 = resolveUpdateTarget(dependencies.currentVersion);
|
|
7737
|
+
dependencies.log(`Current version: ${dependencies.currentVersion}`);
|
|
7738
|
+
dependencies.log(`Updating FML from the ${channel} channel...
|
|
7739
|
+
`);
|
|
7740
|
+
try {
|
|
7741
|
+
const npm = dependencies.resolveBin("npm");
|
|
7742
|
+
if (!npm) throw new Error("npm was not found on PATH");
|
|
7743
|
+
dependencies.execBinSync(npm, ["install", "--global", target2], {
|
|
7744
|
+
timeout: 12e4,
|
|
7745
|
+
env: createUpdateEnvironment(dependencies.env)
|
|
7746
|
+
});
|
|
7747
|
+
dependencies.log(`
|
|
7748
|
+
FML update complete (${channel} channel).`);
|
|
7749
|
+
dependencies.log("Run `fml setup` to refresh integrations.");
|
|
7750
|
+
} catch (err) {
|
|
7751
|
+
dependencies.captureException(err);
|
|
7752
|
+
dependencies.error("Update failed:", formatUpdateError(err));
|
|
7753
|
+
dependencies.exit(1);
|
|
7641
7754
|
}
|
|
7642
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
|
|
7643
7755
|
}
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
}
|
|
7648
|
-
|
|
7649
|
-
if (metrics.attempted === 0 && metrics.budgetSkipped === 0) return;
|
|
7756
|
+
|
|
7757
|
+
// src/commands/logout.ts
|
|
7758
|
+
async function handleLogout() {
|
|
7759
|
+
const { name } = getActiveEnv();
|
|
7760
|
+
await clearStoredCredentials(name);
|
|
7650
7761
|
console.log(
|
|
7651
|
-
`
|
|
7762
|
+
`Logged out. Stored credentials for the ${name} environment were cleared.`
|
|
7652
7763
|
);
|
|
7653
|
-
|
|
7654
|
-
|
|
7764
|
+
}
|
|
7765
|
+
|
|
7766
|
+
// src/commands/org.ts
|
|
7767
|
+
async function handleOrg(slug) {
|
|
7768
|
+
const environment = getActiveEnv();
|
|
7769
|
+
const token = await getValidToken({ env: environment.name });
|
|
7770
|
+
if (!token) {
|
|
7771
|
+
console.error("Not logged in. Run `fml login` first.");
|
|
7772
|
+
process.exit(1);
|
|
7773
|
+
}
|
|
7774
|
+
const api = createFmlClient(token, {
|
|
7775
|
+
envName: environment.name,
|
|
7776
|
+
siteUrl: getSiteUrlForConvexUrl(environment.convexUrl)
|
|
7777
|
+
});
|
|
7778
|
+
const orgs = await api.queryOrgs();
|
|
7779
|
+
if (orgs.length === 0) {
|
|
7780
|
+
console.error("No organizations found.");
|
|
7781
|
+
process.exit(1);
|
|
7782
|
+
}
|
|
7783
|
+
const current = getSelectedOrg(environment.name);
|
|
7784
|
+
const currentId = getSelectedOrgId(environment.name);
|
|
7785
|
+
if (!slug) {
|
|
7786
|
+
console.log("Organizations:\n");
|
|
7787
|
+
for (const org of orgs) {
|
|
7788
|
+
const orgSlug = org.slug ?? org.name;
|
|
7789
|
+
const marker = org._id === currentId || !currentId && orgSlug === current ? " (selected)" : "";
|
|
7790
|
+
console.log(` ${orgSlug}${marker}`);
|
|
7791
|
+
}
|
|
7792
|
+
if (!currentId && !current) {
|
|
7793
|
+
console.log("\nNo org selected. Run `fml org <slug>` to select one.");
|
|
7794
|
+
}
|
|
7795
|
+
return;
|
|
7796
|
+
}
|
|
7797
|
+
const match = orgs.find(
|
|
7798
|
+
(o) => (o.slug ?? o.name) === slug || o.name === slug
|
|
7655
7799
|
);
|
|
7800
|
+
if (!match) {
|
|
7801
|
+
console.error(
|
|
7802
|
+
`Organization "${slug}" not found. Available: ${orgs.map((o) => o.slug ?? o.name).join(", ")}`
|
|
7803
|
+
);
|
|
7804
|
+
process.exit(1);
|
|
7805
|
+
}
|
|
7806
|
+
const matchSlug = match.slug ?? match.name;
|
|
7807
|
+
await setSelectedOrg(matchSlug, match._id, environment.name);
|
|
7808
|
+
console.log(`Selected org: ${match.name} (${matchSlug})`);
|
|
7656
7809
|
}
|
|
7657
|
-
|
|
7658
|
-
|
|
7810
|
+
|
|
7811
|
+
// src/commands/open.ts
|
|
7812
|
+
var DEFAULT_APP_BASE_URL = "https://app.fml.inc";
|
|
7813
|
+
function appBaseUrl() {
|
|
7814
|
+
const configured = process.env.FML_APP_URL?.trim();
|
|
7815
|
+
return (configured || DEFAULT_APP_BASE_URL).replace(/\/+$/, "");
|
|
7659
7816
|
}
|
|
7660
|
-
function
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7817
|
+
async function handleOpen(opts) {
|
|
7818
|
+
const environment = getActiveEnv();
|
|
7819
|
+
const api = await getAuthenticatedClient(environment);
|
|
7820
|
+
if (!api) {
|
|
7821
|
+
console.error("Not signed in. Run `fml login` first.");
|
|
7822
|
+
process.exit(1);
|
|
7823
|
+
}
|
|
7824
|
+
let slug;
|
|
7825
|
+
try {
|
|
7826
|
+
const orgs = await api.queryOrgs();
|
|
7827
|
+
if (orgs.length === 0) {
|
|
7828
|
+
console.error("No organizations are available for this account.");
|
|
7829
|
+
process.exit(1);
|
|
7830
|
+
}
|
|
7831
|
+
const selected = getSelectedOrg(environment.name);
|
|
7832
|
+
const org = selected && orgs.find((candidate) => {
|
|
7833
|
+
const candidateSlug = candidate.slug ?? candidate.name;
|
|
7834
|
+
return candidateSlug === selected || candidate.name === selected;
|
|
7835
|
+
}) || orgs[0];
|
|
7836
|
+
slug = org.slug ?? org.name;
|
|
7837
|
+
} catch {
|
|
7838
|
+
console.error(
|
|
7839
|
+
"Could not load your organizations. Opening the FML dashboard."
|
|
7664
7840
|
);
|
|
7665
|
-
|
|
7841
|
+
slug = "";
|
|
7842
|
+
}
|
|
7843
|
+
const baseUrl = appBaseUrl();
|
|
7844
|
+
const url = slug ? `${baseUrl}/${slug}` : baseUrl;
|
|
7845
|
+
if (opts.json) {
|
|
7846
|
+
console.log(JSON.stringify({ url }));
|
|
7847
|
+
} else {
|
|
7848
|
+
const openBrowser = (await import("open")).default;
|
|
7849
|
+
await openBrowser(url);
|
|
7666
7850
|
}
|
|
7667
|
-
console.warn(
|
|
7668
|
-
`Retained legacy observer database at ${retained.databaseFile}; pending row count unavailable. FML will not sync that database automatically.`
|
|
7669
|
-
);
|
|
7670
7851
|
}
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
)
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
console.log(` Metadata: ${queue.metadataPending}`);
|
|
7681
|
-
console.log(` Uploaded: ${queue.uploaded}`);
|
|
7682
|
-
console.log(` Failed: ${queue.failed}`);
|
|
7683
|
-
console.log(
|
|
7684
|
-
` Watermark: ${formatBytes2(queue.uploadedBytes)} / ${formatBytes2(queue.totalBytes)}`
|
|
7685
|
-
);
|
|
7686
|
-
console.log(` Remaining: ${formatBytes2(queue.remainingBytes)}`);
|
|
7852
|
+
|
|
7853
|
+
// src/commands/status.ts
|
|
7854
|
+
function formatBytes2(bytes) {
|
|
7855
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
7856
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
7857
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
7858
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
7859
|
+
}
|
|
7860
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
|
|
7687
7861
|
}
|
|
7688
|
-
function
|
|
7689
|
-
if (
|
|
7690
|
-
|
|
7691
|
-
|
|
7862
|
+
function hookSpoolStatus(value) {
|
|
7863
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
7864
|
+
return void 0;
|
|
7865
|
+
}
|
|
7866
|
+
const record = value;
|
|
7867
|
+
const nonNegativeInteger = (field) => {
|
|
7868
|
+
const candidate = record[field];
|
|
7869
|
+
return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0 ? candidate : void 0;
|
|
7870
|
+
};
|
|
7871
|
+
const logicalBytes = nonNegativeInteger("logicalBytes");
|
|
7872
|
+
const evictedRecords = nonNegativeInteger("evictedRecords");
|
|
7873
|
+
const evictedBytes = nonNegativeInteger("evictedBytes");
|
|
7874
|
+
const evictedPendingRecords = nonNegativeInteger("evictedPendingRecords");
|
|
7875
|
+
const evictedQuarantinedRecords = nonNegativeInteger(
|
|
7876
|
+
"evictedQuarantinedRecords"
|
|
7877
|
+
);
|
|
7878
|
+
const maxBytes = record.maxBytes;
|
|
7879
|
+
const quarantineMaxAgeMs = record.quarantineMaxAgeMs;
|
|
7880
|
+
if (record.unit !== "logical_bytes" || logicalBytes === void 0 || evictedRecords === void 0 || evictedBytes === void 0 || evictedPendingRecords === void 0 || evictedQuarantinedRecords === void 0 || maxBytes !== null && (typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes < 1)) {
|
|
7881
|
+
return void 0;
|
|
7882
|
+
}
|
|
7883
|
+
const lastEvictedAtMsValue = nonNegativeInteger("lastEvictedAtMs");
|
|
7884
|
+
const lastEvictedAtMs = lastEvictedAtMsValue !== void 0 && lastEvictedAtMsValue <= 864e13 ? lastEvictedAtMsValue : void 0;
|
|
7885
|
+
return {
|
|
7886
|
+
logicalBytes,
|
|
7887
|
+
maxBytes,
|
|
7888
|
+
...quarantineMaxAgeMs === null ? { quarantineMaxAgeMs: null } : typeof quarantineMaxAgeMs === "number" && Number.isSafeInteger(quarantineMaxAgeMs) && quarantineMaxAgeMs > 0 ? { quarantineMaxAgeMs } : {},
|
|
7889
|
+
evictedRecords,
|
|
7890
|
+
evictedBytes,
|
|
7891
|
+
evictedPendingRecords,
|
|
7892
|
+
evictedQuarantinedRecords,
|
|
7893
|
+
...lastEvictedAtMs === void 0 ? {} : { lastEvictedAtMs }
|
|
7894
|
+
};
|
|
7692
7895
|
}
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
const result2 = {
|
|
7697
|
-
ok: true,
|
|
7698
|
-
outcome: "disabled",
|
|
7699
|
-
status: sync2.status()
|
|
7700
|
-
};
|
|
7701
|
-
if (opts.json) writeJson(result2);
|
|
7702
|
-
else printStatus(result2.status);
|
|
7703
|
-
return;
|
|
7896
|
+
function otlpSpoolStatus(value) {
|
|
7897
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
7898
|
+
return void 0;
|
|
7704
7899
|
}
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
const
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
dryRun: true,
|
|
7719
|
-
targetUrl: getSiteUrl(),
|
|
7720
|
-
discovered: {
|
|
7721
|
-
count: discovery.candidates.length,
|
|
7722
|
-
totalBytes,
|
|
7723
|
-
bySource
|
|
7724
|
-
},
|
|
7725
|
-
repositoryResolution: discovery.repositoryResolution,
|
|
7726
|
-
oversized: discovery.oversized,
|
|
7727
|
-
status: sync2.status()
|
|
7728
|
-
};
|
|
7729
|
-
if (opts.json) writeJson(result2);
|
|
7730
|
-
else {
|
|
7731
|
-
console.log("FML raw artifact sync dry-run");
|
|
7732
|
-
console.log(` Target: ${result2.targetUrl}`);
|
|
7733
|
-
console.log(` Discovered: ${result2.discovered.count}`);
|
|
7734
|
-
console.log(` Bytes: ${formatBytes2(result2.discovered.totalBytes)}`);
|
|
7735
|
-
for (const [source, count] of Object.entries(bySource).sort()) {
|
|
7736
|
-
console.log(` ${source}: ${count}`);
|
|
7737
|
-
}
|
|
7738
|
-
printRepositoryResolution(discovery.repositoryResolution);
|
|
7739
|
-
console.log(` Oversized: ${discovery.oversized.length}`);
|
|
7740
|
-
if (discovery.compressedArtifactsUnsupportedReason) {
|
|
7741
|
-
console.log(
|
|
7742
|
-
` Compressed: skipped \u2014 ${discovery.compressedArtifactsUnsupportedReason}`
|
|
7743
|
-
);
|
|
7744
|
-
}
|
|
7745
|
-
}
|
|
7746
|
-
return;
|
|
7900
|
+
const record = value;
|
|
7901
|
+
const nonNegativeInteger = (field) => {
|
|
7902
|
+
const candidate = record[field];
|
|
7903
|
+
return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0 ? candidate : void 0;
|
|
7904
|
+
};
|
|
7905
|
+
const logicalBytes = nonNegativeInteger("logicalBytes");
|
|
7906
|
+
const bodyBytes = nonNegativeInteger("bodyBytes");
|
|
7907
|
+
const maxBytes = nonNegativeInteger("maxBytes");
|
|
7908
|
+
const droppedRecords = nonNegativeInteger("droppedRecords");
|
|
7909
|
+
const droppedLogicalBytes = nonNegativeInteger("droppedLogicalBytes");
|
|
7910
|
+
const droppedBodyBytes = nonNegativeInteger("droppedBodyBytes");
|
|
7911
|
+
if (record.unit !== "logical_bytes" || logicalBytes === void 0 || bodyBytes === void 0 || maxBytes === void 0 || maxBytes < 1 || droppedRecords === void 0 || droppedLogicalBytes === void 0 || droppedBodyBytes === void 0 || bodyBytes > logicalBytes || droppedBodyBytes > droppedLogicalBytes) {
|
|
7912
|
+
return void 0;
|
|
7747
7913
|
}
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7914
|
+
return {
|
|
7915
|
+
logicalBytes,
|
|
7916
|
+
bodyBytes,
|
|
7917
|
+
maxBytes,
|
|
7918
|
+
droppedRecords,
|
|
7919
|
+
droppedLogicalBytes,
|
|
7920
|
+
droppedBodyBytes,
|
|
7921
|
+
...typeof record.lastDroppedAtMs === "number" && Number.isSafeInteger(record.lastDroppedAtMs) && record.lastDroppedAtMs >= 0 && Number.isFinite(new Date(record.lastDroppedAtMs).getTime()) ? { lastDroppedAtMs: record.lastDroppedAtMs } : {},
|
|
7922
|
+
...typeof record.lastDropReason === "string" ? { lastDropReason: record.lastDropReason } : {}
|
|
7923
|
+
};
|
|
7924
|
+
}
|
|
7925
|
+
function observerQueueStatus(value, key) {
|
|
7926
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
7927
|
+
const queue = value[key];
|
|
7928
|
+
if (!queue || typeof queue !== "object" || Array.isArray(queue)) return null;
|
|
7929
|
+
const record = queue;
|
|
7930
|
+
if (typeof record.phase !== "string" || typeof record.pending !== "number" || !Number.isSafeInteger(record.pending) || record.pending < 0) {
|
|
7931
|
+
return null;
|
|
7754
7932
|
}
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
}
|
|
7759
|
-
|
|
7760
|
-
}
|
|
7761
|
-
|
|
7933
|
+
return {
|
|
7934
|
+
phase: record.phase,
|
|
7935
|
+
pending: record.pending,
|
|
7936
|
+
...typeof record.quarantined === "number" && Number.isSafeInteger(record.quarantined) && record.quarantined >= 0 ? { quarantined: record.quarantined } : {},
|
|
7937
|
+
...typeof record.lastError === "string" ? { lastError: record.lastError } : {},
|
|
7938
|
+
...typeof record.lastWarning === "string" ? { lastWarning: record.lastWarning } : {},
|
|
7939
|
+
...typeof record.nextAttemptAtMs === "number" && Number.isSafeInteger(record.nextAttemptAtMs) && record.nextAttemptAtMs >= 0 && Number.isFinite(new Date(record.nextAttemptAtMs).getTime()) ? { nextAttemptAtMs: record.nextAttemptAtMs } : {}
|
|
7940
|
+
};
|
|
7941
|
+
}
|
|
7942
|
+
function hookStatus(value) {
|
|
7943
|
+
const status = observerQueueStatus(value, "hooks");
|
|
7944
|
+
if (!status || !value || typeof value !== "object" || Array.isArray(value)) {
|
|
7945
|
+
return null;
|
|
7762
7946
|
}
|
|
7763
|
-
const
|
|
7764
|
-
if (
|
|
7765
|
-
|
|
7947
|
+
const hooks = value.hooks;
|
|
7948
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) return null;
|
|
7949
|
+
const record = hooks;
|
|
7950
|
+
const spool = hookSpoolStatus(record.spool);
|
|
7951
|
+
return {
|
|
7952
|
+
...status,
|
|
7953
|
+
...typeof record.rejected === "number" && Number.isSafeInteger(record.rejected) && record.rejected >= 0 ? { rejected: record.rejected } : {},
|
|
7954
|
+
...typeof record.lastRejectedReason === "string" ? { lastRejectedReason: record.lastRejectedReason } : {},
|
|
7955
|
+
...typeof record.lastRejectedTarget === "string" ? { lastRejectedTarget: record.lastRejectedTarget } : {},
|
|
7956
|
+
...spool === void 0 ? {} : { spool }
|
|
7957
|
+
};
|
|
7958
|
+
}
|
|
7959
|
+
function otlpStatus(value) {
|
|
7960
|
+
const status = observerQueueStatus(value, "otlp");
|
|
7961
|
+
if (!status || !value || typeof value !== "object" || Array.isArray(value)) {
|
|
7962
|
+
return null;
|
|
7963
|
+
}
|
|
7964
|
+
const otlp = value.otlp;
|
|
7965
|
+
if (!otlp || typeof otlp !== "object" || Array.isArray(otlp)) return null;
|
|
7966
|
+
const spool = otlpSpoolStatus(otlp.spool);
|
|
7967
|
+
return {
|
|
7968
|
+
...status,
|
|
7969
|
+
...spool === void 0 ? {} : { spool }
|
|
7970
|
+
};
|
|
7971
|
+
}
|
|
7972
|
+
async function handleStatus() {
|
|
7973
|
+
const { name, convexUrl } = getActiveEnv();
|
|
7974
|
+
console.log(`Environment: ${name}`);
|
|
7975
|
+
console.log(` Convex: ${convexUrl ?? "not configured"}
|
|
7976
|
+
`);
|
|
7977
|
+
const tokens = readTokens(name);
|
|
7978
|
+
if (!tokens) {
|
|
7979
|
+
console.log("Authentication:");
|
|
7980
|
+
console.log(" Not authenticated. Run `fml login` to sign in.");
|
|
7766
7981
|
} else {
|
|
7767
|
-
|
|
7768
|
-
console.log(
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
console.log(
|
|
7774
|
-
` Compressed: skipped \u2014 ${result.discovery.compressedArtifactsUnsupportedReason}`
|
|
7775
|
-
);
|
|
7982
|
+
const tokenValid = await getValidToken({ env: name });
|
|
7983
|
+
console.log("Authentication:");
|
|
7984
|
+
if (tokens.tokenType === "service" && tokens.user.id === SERVICE_TOKEN_LOGIN_USER_ID) {
|
|
7985
|
+
console.log(" Auth: service token");
|
|
7986
|
+
} else {
|
|
7987
|
+
console.log(` User: ${tokens.user.name} (${tokens.user.email})`);
|
|
7776
7988
|
}
|
|
7777
|
-
printRepositoryResolution(result.discovery.repositoryResolution);
|
|
7778
|
-
console.log(` Uploaded files: ${result.upload.uploaded}`);
|
|
7779
|
-
console.log(` Uploaded chunks: ${result.upload.uploadedChunks}`);
|
|
7780
7989
|
console.log(
|
|
7781
|
-
`
|
|
7990
|
+
` Token: ${tokenValid ? "valid" : "expired (needs refresh/re-login)"}`
|
|
7782
7991
|
);
|
|
7783
|
-
console.log(
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7992
|
+
console.log(
|
|
7993
|
+
` Org: ${tokens.orgSlug ?? "(none \u2014 run `fml org <slug>`)"}`
|
|
7994
|
+
);
|
|
7995
|
+
}
|
|
7996
|
+
const syncStatus = createRawArtifactSync({
|
|
7997
|
+
stateDir: FML_OBSERVER_DIR
|
|
7998
|
+
}).status();
|
|
7999
|
+
console.log("\nRaw artifact sync:");
|
|
8000
|
+
console.log(` Enabled: ${syncStatus.settings.enabled ? "yes" : "no"}`);
|
|
8001
|
+
console.log(` Tracked: ${syncStatus.queue.tracked}`);
|
|
8002
|
+
console.log(` Pending: ${syncStatus.queue.pending}`);
|
|
8003
|
+
console.log(` Raw work: ${syncStatus.queue.rawPending}`);
|
|
8004
|
+
console.log(` Metadata: ${syncStatus.queue.metadataPending}`);
|
|
8005
|
+
console.log(` Uploaded: ${syncStatus.queue.uploaded}`);
|
|
8006
|
+
console.log(` Failed: ${syncStatus.queue.failed}`);
|
|
8007
|
+
console.log(
|
|
8008
|
+
` Watermark: ${formatBytes2(syncStatus.queue.uploadedBytes)} / ${formatBytes2(syncStatus.queue.totalBytes)}`
|
|
8009
|
+
);
|
|
8010
|
+
console.log(` Remaining: ${formatBytes2(syncStatus.queue.remainingBytes)}`);
|
|
8011
|
+
const observer = await readObserverRuntimeStatus();
|
|
8012
|
+
const protectedStatus = observer.healthy ? await readObserverProtectedStatus() : null;
|
|
8013
|
+
const observerState = observer.stateStatus.state;
|
|
8014
|
+
console.log("\nLocal observer:");
|
|
8015
|
+
console.log(
|
|
8016
|
+
` Status: ${observer.healthy ? "running" : observer.running ? "unhealthy" : "stopped"}`
|
|
8017
|
+
);
|
|
8018
|
+
if (observerState) {
|
|
8019
|
+
console.log(` PID: ${observerState.pid}`);
|
|
8020
|
+
console.log(` Phase: ${observerState.phase}`);
|
|
8021
|
+
if (observerState.lastOutcome) {
|
|
8022
|
+
console.log(` Last: ${observerState.lastOutcome}`);
|
|
7800
8023
|
}
|
|
7801
|
-
console.log("");
|
|
7802
|
-
printStatus(result.status);
|
|
7803
8024
|
}
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
try {
|
|
7816
|
-
const observer = await startObserver({
|
|
7817
|
-
onLegacyDatabaseRetained: (retained) => {
|
|
7818
|
-
retainedLegacyDatabase = retained;
|
|
7819
|
-
if (!opts.json) warnRetainedLegacyDatabase(retained);
|
|
7820
|
-
}
|
|
7821
|
-
});
|
|
7822
|
-
setHookCaptureDisabled(false);
|
|
7823
|
-
if (observer.status === "already_running") {
|
|
7824
|
-
await wakeObserverSync();
|
|
8025
|
+
console.log(` Log: ${observer.logFile}`);
|
|
8026
|
+
const hooks = hookStatus(protectedStatus);
|
|
8027
|
+
const hookInbox = readHookInboxStatus();
|
|
8028
|
+
console.log("\nHook capture:");
|
|
8029
|
+
if (!hooks) {
|
|
8030
|
+
console.log(` Status: ${observer.healthy ? "unavailable" : "stopped"}`);
|
|
8031
|
+
} else {
|
|
8032
|
+
console.log(` Status: ${hooks.phase}`);
|
|
8033
|
+
console.log(` Pending: ${hooks.pending}`);
|
|
8034
|
+
if (hooks.quarantined && hooks.quarantined > 0) {
|
|
8035
|
+
console.log(` Quarantined: ${hooks.quarantined}`);
|
|
7825
8036
|
}
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
})) {
|
|
7833
|
-
if (!opts.json) {
|
|
7834
|
-
console.log(`OpenTelemetry: ${target2.displayName} -> FML observer`);
|
|
7835
|
-
}
|
|
8037
|
+
if (hooks.rejected && hooks.rejected > 0) {
|
|
8038
|
+
console.log(` Rejected: ${hooks.rejected}`);
|
|
8039
|
+
if (hooks.lastRejectedReason) {
|
|
8040
|
+
console.log(
|
|
8041
|
+
` Last rejection: ${hooks.lastRejectedReason}${hooks.lastRejectedTarget ? ` (${hooks.lastRejectedTarget})` : ""}`
|
|
8042
|
+
);
|
|
7836
8043
|
}
|
|
7837
|
-
} else if (!opts.json) {
|
|
7838
|
-
console.warn(
|
|
7839
|
-
"Could not configure native OpenTelemetry: the healthy observer endpoint or local token is unavailable."
|
|
7840
|
-
);
|
|
7841
8044
|
}
|
|
7842
|
-
if (
|
|
7843
|
-
writeJson({
|
|
7844
|
-
ok: true,
|
|
7845
|
-
sync: settings,
|
|
7846
|
-
observer,
|
|
7847
|
-
...observer.retainedLegacyDatabase ? { retainedLegacyDatabase: observer.retainedLegacyDatabase } : {}
|
|
7848
|
-
});
|
|
7849
|
-
} else {
|
|
7850
|
-
console.log("FML raw artifact sync is enabled");
|
|
7851
|
-
const pid = observer.runtime.health?.pid;
|
|
8045
|
+
if (hooks.spool) {
|
|
7852
8046
|
console.log(
|
|
7853
|
-
|
|
8047
|
+
` Spool: ${formatBytes2(hooks.spool.logicalBytes)} / ${hooks.spool.maxBytes === null ? "unbounded" : formatBytes2(hooks.spool.maxBytes)} (logical content)`
|
|
7854
8048
|
);
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
writeJson({
|
|
7860
|
-
ok: false,
|
|
7861
|
-
sync: settings,
|
|
7862
|
-
error: message,
|
|
7863
|
-
...retainedLegacyDatabase ? { retainedLegacyDatabase } : {}
|
|
7864
|
-
});
|
|
7865
|
-
} else console.error(message);
|
|
7866
|
-
process.exitCode = 1;
|
|
7867
|
-
}
|
|
7868
|
-
}
|
|
7869
|
-
async function handleSyncDisable(opts = {}) {
|
|
7870
|
-
const sync2 = rawSync();
|
|
7871
|
-
const settings = sync2.setEnabled(false);
|
|
7872
|
-
const observerWoken = await wakeObserverSync();
|
|
7873
|
-
if (opts.json) {
|
|
7874
|
-
writeJson({ ok: true, sync: settings, observerWoken });
|
|
7875
|
-
} else {
|
|
7876
|
-
console.log("FML raw artifact sync is disabled");
|
|
7877
|
-
const observer = await readObserverRuntimeStatus();
|
|
7878
|
-
if (observer.running) {
|
|
7879
|
-
console.log("FML observer remains available for local integrations");
|
|
7880
|
-
}
|
|
7881
|
-
}
|
|
7882
|
-
}
|
|
7883
|
-
async function handleSyncStatus(opts = {}) {
|
|
7884
|
-
const status = rawSync().status();
|
|
7885
|
-
const observer = await readObserverRuntimeStatus();
|
|
7886
|
-
if (opts.json) writeJson({ ok: true, status, observer });
|
|
7887
|
-
else {
|
|
7888
|
-
printStatus(status);
|
|
7889
|
-
const state = observer.stateStatus.state;
|
|
7890
|
-
console.log(
|
|
7891
|
-
` Observer: ${observer.healthy ? "running" : observer.running ? "unhealthy" : "stopped"}`
|
|
7892
|
-
);
|
|
7893
|
-
if (state) {
|
|
7894
|
-
console.log(` PID: ${state.pid}`);
|
|
7895
|
-
console.log(` Phase: ${state.phase}`);
|
|
7896
|
-
if (state.lastOutcome) {
|
|
7897
|
-
console.log(` Last run: ${state.lastOutcome}`);
|
|
8049
|
+
if (hooks.spool.quarantineMaxAgeMs !== void 0) {
|
|
8050
|
+
console.log(
|
|
8051
|
+
` Quarantine retention: ${hooks.spool.quarantineMaxAgeMs === null ? "unbounded" : `${Math.round(hooks.spool.quarantineMaxAgeMs / 864e5)} day(s)`}`
|
|
8052
|
+
);
|
|
7898
8053
|
}
|
|
7899
|
-
if (
|
|
8054
|
+
if (hooks.spool.evictedRecords > 0) {
|
|
7900
8055
|
console.log(
|
|
7901
|
-
`
|
|
8056
|
+
` Evicted: ${hooks.spool.evictedRecords} record(s), ${formatBytes2(hooks.spool.evictedBytes)} (${hooks.spool.evictedQuarantinedRecords} quarantined, ${hooks.spool.evictedPendingRecords} pending)`
|
|
7902
8057
|
);
|
|
8058
|
+
if (hooks.spool.lastEvictedAtMs !== void 0) {
|
|
8059
|
+
console.log(
|
|
8060
|
+
` Last eviction: ${new Date(hooks.spool.lastEvictedAtMs).toISOString()}`
|
|
8061
|
+
);
|
|
8062
|
+
}
|
|
7903
8063
|
}
|
|
7904
8064
|
}
|
|
7905
|
-
console.log(`
|
|
8065
|
+
if (hooks.lastError) console.log(` Error: ${hooks.lastError}`);
|
|
7906
8066
|
}
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
const result = rawSync().reset();
|
|
7910
|
-
if (opts.json) writeJson({ ok: !result.skipped, reset: result });
|
|
7911
|
-
else if (result.skipped) {
|
|
7912
|
-
console.error(
|
|
7913
|
-
"Raw artifact reset skipped: another sync owns the queue lock"
|
|
7914
|
-
);
|
|
8067
|
+
if (hookInbox.unreadable) {
|
|
8068
|
+
console.log(" Inbox: unavailable");
|
|
7915
8069
|
} else {
|
|
7916
8070
|
console.log(
|
|
7917
|
-
`
|
|
8071
|
+
` Inbox: ${hookInbox.pendingEntries} pending (${formatBytes2(hookInbox.pendingBytes)})`
|
|
7918
8072
|
);
|
|
7919
|
-
if (result.removed > 0) {
|
|
7920
|
-
console.log(`Removed ${result.removed} missing artifact entries.`);
|
|
7921
|
-
}
|
|
7922
8073
|
console.log(
|
|
7923
|
-
"
|
|
8074
|
+
` Inbox limits: ${hookInbox.maxBytes === null ? "unbounded bytes" : formatBytes2(hookInbox.maxBytes)}, ${hookInbox.maxEntries === null ? "unbounded entries" : `${hookInbox.maxEntries} entries`}`
|
|
7924
8075
|
);
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
} else {
|
|
7929
|
-
await wakeObserverSync();
|
|
7930
|
-
}
|
|
7931
|
-
}
|
|
7932
|
-
|
|
7933
|
-
// src/commands/daemon.ts
|
|
7934
|
-
async function handleFmlStart() {
|
|
7935
|
-
await handleSyncEnable();
|
|
7936
|
-
}
|
|
7937
|
-
async function handleFmlStop(opts = {}) {
|
|
7938
|
-
setRawSyncEnabled(false);
|
|
7939
|
-
const result = await stopObserver({ force: opts.force });
|
|
7940
|
-
if (result.status === "stopped" || result.status === "killed" || result.status === "not_running" || result.status === "stale_state") {
|
|
7941
|
-
try {
|
|
7942
|
-
setHookCaptureDisabled(true);
|
|
7943
|
-
} catch (error) {
|
|
7944
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7945
|
-
console.error(
|
|
7946
|
-
`FML observer stopped, but the hook-capture pause could not be persisted: ${message}`
|
|
8076
|
+
if (hookInbox.capacityRejectedEntries > 0) {
|
|
8077
|
+
console.log(
|
|
8078
|
+
` Inbox capacity refusals: ${hookInbox.capacityRejectedEntries} record(s), ${formatBytes2(hookInbox.capacityRejectedBytes)}`
|
|
7947
8079
|
);
|
|
7948
|
-
|
|
8080
|
+
if (hookInbox.lastCapacityRejectedAtMs !== void 0) {
|
|
8081
|
+
console.log(
|
|
8082
|
+
` Last inbox refusal: ${new Date(hookInbox.lastCapacityRejectedAtMs).toISOString()}${hookInbox.lastCapacityRejectionReason ? ` (${hookInbox.lastCapacityRejectionReason})` : ""}`
|
|
8083
|
+
);
|
|
8084
|
+
}
|
|
7949
8085
|
}
|
|
7950
|
-
|
|
7951
|
-
switch (result.status) {
|
|
7952
|
-
case "stopped":
|
|
7953
|
-
console.log(`FML observer stopped (PID ${result.pid})`);
|
|
7954
|
-
break;
|
|
7955
|
-
case "killed":
|
|
7956
|
-
console.log(`FML observer force stopped (PID ${result.pid})`);
|
|
7957
|
-
break;
|
|
7958
|
-
case "not_running":
|
|
7959
|
-
console.log("FML observer was not running");
|
|
7960
|
-
break;
|
|
7961
|
-
case "stale_state":
|
|
8086
|
+
if (hookInbox.retiredEntries > 0) {
|
|
7962
8087
|
console.log(
|
|
7963
|
-
|
|
7964
|
-
);
|
|
7965
|
-
break;
|
|
7966
|
-
case "permission_denied":
|
|
7967
|
-
console.error(
|
|
7968
|
-
`Could not stop FML observer PID ${result.pid}: permission denied`
|
|
7969
|
-
);
|
|
7970
|
-
process.exitCode = 1;
|
|
7971
|
-
break;
|
|
7972
|
-
case "foreign_listener":
|
|
7973
|
-
console.error("Refused to stop a different local observer instance");
|
|
7974
|
-
process.exitCode = 1;
|
|
7975
|
-
break;
|
|
7976
|
-
case "ambiguous_live_process":
|
|
7977
|
-
console.error(
|
|
7978
|
-
`Refused to treat unverifiable live PID ${result.pid} as a stopped FML observer`
|
|
8088
|
+
` Inbox retired: ${hookInbox.retiredEntries} record(s), ${formatBytes2(hookInbox.retiredBytes)}`
|
|
7979
8089
|
);
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
8090
|
+
}
|
|
8091
|
+
}
|
|
8092
|
+
const otlp = otlpStatus(protectedStatus);
|
|
8093
|
+
console.log("\nNative OpenTelemetry:");
|
|
8094
|
+
if (!otlp) {
|
|
8095
|
+
console.log(` Status: ${observer.healthy ? "unavailable" : "stopped"}`);
|
|
8096
|
+
} else {
|
|
8097
|
+
console.log(` Status: ${otlp.phase}`);
|
|
8098
|
+
console.log(` Pending: ${otlp.pending}`);
|
|
8099
|
+
if (otlp.spool) {
|
|
8100
|
+
const usedPercent = otlp.spool.logicalBytes / otlp.spool.maxBytes * 100;
|
|
8101
|
+
console.log(
|
|
8102
|
+
` Spool: ${formatBytes2(otlp.spool.logicalBytes)} / ${formatBytes2(otlp.spool.maxBytes)} (${usedPercent.toFixed(1)}%; ${formatBytes2(otlp.spool.bodyBytes)} encoded bodies)`
|
|
7985
8103
|
);
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
8104
|
+
if (usedPercent >= 80) {
|
|
8105
|
+
console.log(
|
|
8106
|
+
` Attention: the oldest pending telemetry is dropped when the spool reaches ${formatBytes2(otlp.spool.maxBytes)}`
|
|
8107
|
+
);
|
|
8108
|
+
}
|
|
8109
|
+
if (otlp.spool.droppedRecords > 0) {
|
|
8110
|
+
console.log(
|
|
8111
|
+
` Dropped: ${otlp.spool.droppedRecords} envelope(s), ${formatBytes2(otlp.spool.droppedLogicalBytes)} logical; ${formatBytes2(otlp.spool.droppedBodyBytes)} encoded bodies`
|
|
8112
|
+
);
|
|
8113
|
+
if (otlp.spool.lastDropReason) {
|
|
8114
|
+
console.log(
|
|
8115
|
+
` Last drop: ${otlp.spool.lastDropReason}${otlp.spool.lastDroppedAtMs === void 0 ? "" : ` at ${new Date(otlp.spool.lastDroppedAtMs).toISOString()}`}`
|
|
8116
|
+
);
|
|
8117
|
+
}
|
|
8118
|
+
}
|
|
8119
|
+
}
|
|
8120
|
+
if (otlp.lastError) console.log(` Error: ${otlp.lastError}`);
|
|
8121
|
+
if (otlp.lastWarning) console.log(` Warning: ${otlp.lastWarning}`);
|
|
8122
|
+
if (otlp.nextAttemptAtMs !== void 0) {
|
|
8123
|
+
console.log(
|
|
8124
|
+
` Next attempt: ${new Date(otlp.nextAttemptAtMs).toISOString()}`
|
|
7995
8125
|
);
|
|
7996
|
-
|
|
7997
|
-
break;
|
|
8126
|
+
}
|
|
7998
8127
|
}
|
|
7999
8128
|
}
|
|
8000
8129
|
|
|
@@ -8002,7 +8131,7 @@ async function handleFmlStop(opts = {}) {
|
|
|
8002
8131
|
function handleEnvShow() {
|
|
8003
8132
|
const { name, convexUrl } = getActiveEnv();
|
|
8004
8133
|
console.log(`Environment: ${name}`);
|
|
8005
|
-
console.log(`
|
|
8134
|
+
console.log(` Service URL: ${convexUrl ?? "not configured"}`);
|
|
8006
8135
|
}
|
|
8007
8136
|
function handleEnvSwitch(target2) {
|
|
8008
8137
|
let name = target2;
|
|
@@ -8025,17 +8154,20 @@ function handleEnvSwitch(target2) {
|
|
|
8025
8154
|
return;
|
|
8026
8155
|
}
|
|
8027
8156
|
if (!convexUrl) {
|
|
8028
|
-
console.error(`
|
|
8157
|
+
console.error(`Environment ${name} has no configured service URL.`);
|
|
8029
8158
|
console.error(
|
|
8030
|
-
`
|
|
8159
|
+
`Set FML_CONVEX_URL=https://<deployment>.convex.cloud and run \`fml env ${name}\` again.`
|
|
8031
8160
|
);
|
|
8032
8161
|
process.exitCode = 1;
|
|
8033
8162
|
return;
|
|
8034
8163
|
}
|
|
8035
8164
|
setActiveEnvironment(name, convexUrl);
|
|
8036
|
-
console.log(`
|
|
8037
|
-
console.log(
|
|
8038
|
-
console.log("
|
|
8165
|
+
console.log(`Environment switched to ${name}.`);
|
|
8166
|
+
console.log(`Service URL: ${convexUrl}`);
|
|
8167
|
+
console.log("CLI commands and agent activity sync now use this environment.");
|
|
8168
|
+
console.log(
|
|
8169
|
+
"Start a new agent session to use the new environment and credentials."
|
|
8170
|
+
);
|
|
8039
8171
|
}
|
|
8040
8172
|
|
|
8041
8173
|
// src/commands/data.ts
|
|
@@ -8592,7 +8724,7 @@ async function handleConfigDetail(opts) {
|
|
|
8592
8724
|
// src/commands/tools.ts
|
|
8593
8725
|
import { readFileSync } from "fs";
|
|
8594
8726
|
import path28 from "path";
|
|
8595
|
-
var pluginVersion = true ? "0.8.0-rc.
|
|
8727
|
+
var pluginVersion = true ? "0.8.0-rc.2+bed6eaed" : void 0;
|
|
8596
8728
|
async function getApi() {
|
|
8597
8729
|
const api = await getAuthenticatedClient();
|
|
8598
8730
|
if (!api) {
|
|
@@ -8725,13 +8857,31 @@ async function handleToolsCall(name, opts) {
|
|
|
8725
8857
|
console.log(JSON.stringify(result.result, null, 2));
|
|
8726
8858
|
}
|
|
8727
8859
|
|
|
8860
|
+
// src/banner.ts
|
|
8861
|
+
import pc2 from "picocolors";
|
|
8862
|
+
var LOGO_LINES = [
|
|
8863
|
+
" \u2584\u2588\u2588\u2580 \u2588\u2588\u2584\u2588\u2588\u2584\u2588\u2588 \u2588\u2588",
|
|
8864
|
+
" \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
|
|
8865
|
+
"\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
|
|
8866
|
+
" \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588",
|
|
8867
|
+
" \u2580\u2580 \u2580\u2580 \u2580\u2580 \u2580\u2580\u2580\u2580"
|
|
8868
|
+
];
|
|
8869
|
+
function printBanner() {
|
|
8870
|
+
console.log("");
|
|
8871
|
+
for (let i = 0; i < LOGO_LINES.length; i++) {
|
|
8872
|
+
const color = i < 3 ? pc2.blue : pc2.cyan;
|
|
8873
|
+
console.log(" " + color(LOGO_LINES[i]));
|
|
8874
|
+
}
|
|
8875
|
+
console.log("");
|
|
8876
|
+
}
|
|
8877
|
+
|
|
8728
8878
|
// src/cli.ts
|
|
8729
8879
|
await initSentry();
|
|
8730
8880
|
if (process.argv.length <= 2 || process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
8731
8881
|
printBanner();
|
|
8732
8882
|
}
|
|
8733
8883
|
var program = new Command().name("fml").description("FML CLI and agent tools").version(
|
|
8734
|
-
true ? "0.8.0-rc.
|
|
8884
|
+
true ? "0.8.0-rc.2+bed6eaed" : "dev"
|
|
8735
8885
|
);
|
|
8736
8886
|
function formatCommandInventory(cmd, prefix = "") {
|
|
8737
8887
|
const name = prefix ? `${prefix} ${cmd.name()}` : cmd.name();
|
|
@@ -8760,11 +8910,12 @@ tools.command("call").description("Invoke a backend tool by name with JSON args"
|
|
|
8760
8910
|
program.command("mcp-server", { hidden: true }).description("Run the FML MCP server on stdio").action(async () => {
|
|
8761
8911
|
await import("./mcp/server.js");
|
|
8762
8912
|
});
|
|
8763
|
-
program.command("
|
|
8913
|
+
program.command("setup").description("Install integrations, sign in if needed, and start FML").action(() => handleSetup());
|
|
8914
|
+
program.command("install").description("Register FML agent integrations and the MCP server").action(() => handleInstall());
|
|
8764
8915
|
program.command("uninstall").description("Remove FML agent integrations and optionally local data").option("--purge", "Also remove all data, logs, and auth tokens").option("-y, --yes", "Skip the purge confirmation prompt").action(
|
|
8765
8916
|
(opts) => handleUninstall({ purge: opts.purge, assumeYes: opts.yes })
|
|
8766
8917
|
);
|
|
8767
|
-
program.command("update").description("Update fml
|
|
8918
|
+
program.command("update").description("Update fml from its release channel").action(() => handleUpdate());
|
|
8768
8919
|
program.command("login").description("Sign in to your FML account").option("--device", "Use device authorization flow (no browser needed)").option(
|
|
8769
8920
|
"--service-token",
|
|
8770
8921
|
"Sign in with an org service refresh token (prompts for fml_srt_*)"
|
|
@@ -8774,28 +8925,29 @@ program.command("login").description("Sign in to your FML account").option("--de
|
|
|
8774
8925
|
program.command("logout").description("Sign out and clear stored credentials").action(() => handleLogout());
|
|
8775
8926
|
program.command("org").description("Show or select organization").argument("[slug]", "Org slug to select").action((slug) => handleOrg(slug));
|
|
8776
8927
|
program.command("status").description("Show auth, environment, and raw sync watermarks").action(() => handleStatus());
|
|
8777
|
-
program.command("doctor").description("Check FML configuration,
|
|
8928
|
+
program.command("doctor").description("Check FML configuration, agent activity sync, and connectivity").option("--json", "Output as JSON").action((opts) => handleDoctor(opts));
|
|
8778
8929
|
program.command("open").description("Open FML dashboard in browser").option("--json", "Output URL as JSON instead of opening browser").action((opts) => handleOpen(opts));
|
|
8779
8930
|
program.command("env").description("Show or switch FML backend environment").argument("[target]", "Environment name or backend URL").action((target2) => {
|
|
8780
8931
|
if (target2) handleEnvSwitch(target2);
|
|
8781
8932
|
else handleEnvShow();
|
|
8782
8933
|
});
|
|
8783
|
-
program.command("start").description("Start the local observer and continuous
|
|
8784
|
-
program.command("
|
|
8785
|
-
|
|
8786
|
-
sync.command("
|
|
8934
|
+
program.command("start").description("Start the local observer and continuous agent activity sync").action(() => handleFmlStart());
|
|
8935
|
+
program.command("restart").description("Restart the local observer and continuous agent activity sync").action(() => handleFmlRestart());
|
|
8936
|
+
program.command("stop").description("Stop the local observer and pause agent activity sync").option("--force", "Escalate to SIGTERM/SIGKILL if the observer is hung").action((opts) => handleFmlStop({ force: opts.force }));
|
|
8937
|
+
var sync = program.command("sync").description("Sync Agent activity to FML").option("--dry-run", "Discover artifacts without changing queue state").option("--json", "Output as JSON").action((opts) => handleSyncRun(opts));
|
|
8938
|
+
sync.command("enable").description("Enable Agent activity sync").option("--json", "Output as JSON").action(
|
|
8787
8939
|
(_opts, command) => handleSyncEnable(command.optsWithGlobals())
|
|
8788
8940
|
);
|
|
8789
|
-
sync.command("disable").description("
|
|
8941
|
+
sync.command("disable").description("Pause Agent activity sync").option("--json", "Output as JSON").action(
|
|
8790
8942
|
(_opts, command) => handleSyncDisable(command.optsWithGlobals())
|
|
8791
8943
|
);
|
|
8792
|
-
sync.command("status").description("Show
|
|
8944
|
+
sync.command("status").description("Show Agent activity queue watermarks").option("--json", "Output as JSON").action(
|
|
8793
8945
|
(_opts, command) => handleSyncStatus(command.optsWithGlobals())
|
|
8794
8946
|
);
|
|
8795
|
-
sync.command("reset").description("Replay
|
|
8947
|
+
sync.command("reset").description("Replay Agent activity uploads from byte zero").option("--json", "Output as JSON").action(
|
|
8796
8948
|
(_opts, command) => handleSyncReset(command.optsWithGlobals())
|
|
8797
8949
|
);
|
|
8798
|
-
var artifacts = program.command("artifacts", { hidden: true }).description("Compatibility alias for
|
|
8950
|
+
var artifacts = program.command("artifacts", { hidden: true }).description("Compatibility alias for Agent activity sync");
|
|
8799
8951
|
artifacts.command("sync").option("--dry-run", "Discover artifacts without changing queue state").option("--json", "Output as JSON").action((opts) => handleSyncRun(opts));
|
|
8800
8952
|
program.command("activity").description("Activity summary \u2014 sessions, prompts, tools, costs").option("--since <duration>", 'Time window, e.g. "24h", "7d"').action((opts) => handleActivity(opts));
|
|
8801
8953
|
program.command("sessions").description("List recent sessions").option("--since <duration>", 'Time filter, e.g. "24h", "7d"').option("--limit <n>", "Max sessions to return").action((opts) => handleSessions(opts));
|