@floomhq/floom 1.0.52 → 1.0.54
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/dist/cli.js +16 -0
- package/dist/launch.js +82 -0
- package/dist/sync.js +55 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ import { auditSkills } from "./audit.js";
|
|
|
21
21
|
import { share } from "./share.js";
|
|
22
22
|
import { feedback } from "./feedback.js";
|
|
23
23
|
import { status } from "./status.js";
|
|
24
|
+
import { launchGate } from "./launch.js";
|
|
24
25
|
import { libraryAddSkill, libraryCreate, libraryList, libraryRemoveSkill, librarySubscribe, libraryUnsubscribe, moveSkill, } from "./library.js";
|
|
25
26
|
import { c, symbols } from "./ui.js";
|
|
26
27
|
import { printError, FloomError } from "./errors.js";
|
|
@@ -129,6 +130,8 @@ function commandUsage() {
|
|
|
129
130
|
${c.dim(`Flags: install|status|logs|run, --target ${TARGET_HINT}|all`)}
|
|
130
131
|
${c.cyan("audit skills")} Read-only skill quality and duplicate report
|
|
131
132
|
${c.dim("Flags: --json, --fix-plan")}
|
|
133
|
+
${c.cyan("launch gate")} Check pinned release identity for launch evidence
|
|
134
|
+
${c.dim("Flags: --json")}
|
|
132
135
|
|
|
133
136
|
${c.bold("Examples")}
|
|
134
137
|
${c.cyan("npx -y @floomhq/floom add")} ${c.dim("https://floom.dev/s/ffas93ud --setup")}
|
|
@@ -337,6 +340,9 @@ function subcommandUsage(cmd) {
|
|
|
337
340
|
case "feedback":
|
|
338
341
|
feedbackUsage();
|
|
339
342
|
return true;
|
|
343
|
+
case "launch":
|
|
344
|
+
commandUsage();
|
|
345
|
+
return true;
|
|
340
346
|
case "library":
|
|
341
347
|
case "lib":
|
|
342
348
|
libraryUsage();
|
|
@@ -1209,6 +1215,16 @@ async function main() {
|
|
|
1209
1215
|
});
|
|
1210
1216
|
return;
|
|
1211
1217
|
}
|
|
1218
|
+
case "launch": {
|
|
1219
|
+
const sub = rest[0];
|
|
1220
|
+
if (sub !== "gate")
|
|
1221
|
+
throw new FloomError("Unknown launch command.", `Try \`${CLI_COMMAND} launch gate --json\`.`);
|
|
1222
|
+
const args = rest.slice(1);
|
|
1223
|
+
const json = args.includes("--json");
|
|
1224
|
+
rejectArgs(args.filter((arg) => arg !== "--json"), `Try \`${CLI_COMMAND} launch gate --json\`.`);
|
|
1225
|
+
await launchGate({ json });
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1212
1228
|
case "status":
|
|
1213
1229
|
await status(parseStatusFlags(rest));
|
|
1214
1230
|
return;
|
package/dist/launch.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { CONFIG_DIR, readConfig, resolveApiUrl } from "./config.js";
|
|
3
|
+
import { floomFetch } from "./lib/api.js";
|
|
4
|
+
import { CLI_VERSION } from "./version.js";
|
|
5
|
+
import { c, symbols } from "./ui.js";
|
|
6
|
+
import { FloomError } from "./errors.js";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
async function readDaemonStatus() {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(await readFile(join(CONFIG_DIR, "daemon-status.json"), "utf8"));
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
if (err.code === "ENOENT")
|
|
14
|
+
return null;
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function getJson(url, action, token) {
|
|
19
|
+
try {
|
|
20
|
+
const res = await floomFetch(url, action, {
|
|
21
|
+
...(token ? { token } : {}),
|
|
22
|
+
timeoutMs: 8_000,
|
|
23
|
+
rateLimitRetries: 0,
|
|
24
|
+
});
|
|
25
|
+
return (await res.json());
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function launchGate(opts) {
|
|
32
|
+
const cfg = await readConfig();
|
|
33
|
+
const apiUrl = resolveApiUrl(cfg ?? undefined);
|
|
34
|
+
const [health, cliVersion, daemon] = await Promise.all([
|
|
35
|
+
getJson(`${apiUrl}/api/v1/health`, "check launch health", cfg?.accessToken),
|
|
36
|
+
getJson(`${apiUrl}/api/v1/cli-version`, "check CLI version", cfg?.accessToken),
|
|
37
|
+
readDaemonStatus(),
|
|
38
|
+
]);
|
|
39
|
+
const targetResults = daemon?.last_run ?? {};
|
|
40
|
+
const targets = daemon?.targets ?? [];
|
|
41
|
+
const daemonTargetsOk = targets.length > 0 && targets.every((target) => targetResults[target]?.ok === true);
|
|
42
|
+
const releaseAligned = health?.version === CLI_VERSION && cliVersion?.latest === CLI_VERSION;
|
|
43
|
+
const payload = {
|
|
44
|
+
ok: Boolean(health?.ok && releaseAligned && daemon?.running && daemon?.version === CLI_VERSION),
|
|
45
|
+
release: {
|
|
46
|
+
cli: CLI_VERSION,
|
|
47
|
+
web: health?.version ?? null,
|
|
48
|
+
cli_latest: cliVersion?.latest ?? null,
|
|
49
|
+
cli_min: cliVersion?.min ?? null,
|
|
50
|
+
release_aligned: releaseAligned,
|
|
51
|
+
},
|
|
52
|
+
health: health ?? null,
|
|
53
|
+
daemon: daemon ? {
|
|
54
|
+
running: daemon.running === true,
|
|
55
|
+
version: daemon.version ?? null,
|
|
56
|
+
hostname: daemon.hostname ?? null,
|
|
57
|
+
targets,
|
|
58
|
+
all_targets_ok: daemonTargetsOk,
|
|
59
|
+
last_completed_at: daemon.last_completed_at ?? null,
|
|
60
|
+
last_run: targetResults,
|
|
61
|
+
} : null,
|
|
62
|
+
escalations: [
|
|
63
|
+
...(releaseAligned ? [] : ["CLI, web health, and server latest versions are not aligned."]),
|
|
64
|
+
...(daemon?.running && daemon.version === CLI_VERSION ? [] : ["Daemon is not running on the pinned CLI version."]),
|
|
65
|
+
...(targets.length === 0 || daemonTargetsOk ? [] : ["Latest daemon cycle has not yet passed for every configured target."]),
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
if (opts.json) {
|
|
69
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
process.stdout.write(`\n${symbols.dot} ${c.bold("Floom launch gate")}\n\n`);
|
|
73
|
+
process.stdout.write(` ${c.dim("CLI:")} ${payload.release.cli}\n`);
|
|
74
|
+
process.stdout.write(` ${c.dim("Web:")} ${payload.release.web ?? "unknown"}\n`);
|
|
75
|
+
process.stdout.write(` ${c.dim("Daemon:")} ${payload.daemon?.running ? `running (${payload.daemon.version})` : "not running"}\n`);
|
|
76
|
+
if (payload.escalations.length > 0) {
|
|
77
|
+
for (const escalation of payload.escalations)
|
|
78
|
+
process.stdout.write(` ${symbols.bullet} ${escalation}\n`);
|
|
79
|
+
throw new FloomError("Launch gate did not pass.", "Run with --json for machine-readable details.");
|
|
80
|
+
}
|
|
81
|
+
process.stdout.write(`\n${symbols.ok} Launch gate passed for the pinned local release identity.\n\n`);
|
|
82
|
+
}
|
package/dist/sync.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
|
-
import { lstat, mkdir, open } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, open, rm, rmdir } from "node:fs/promises";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import ora from "ora";
|
|
@@ -224,6 +224,40 @@ async function overwriteTrackedFile(root, target, body, expectedHash) {
|
|
|
224
224
|
await parent.close();
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
|
+
async function deleteSyncedFile(root, target, expectedHash) {
|
|
228
|
+
const state = await localState(target);
|
|
229
|
+
if (state.kind === "missing")
|
|
230
|
+
return;
|
|
231
|
+
if (state.kind === "conflict")
|
|
232
|
+
throw conflictError(state.reason, "EEXIST");
|
|
233
|
+
if (state.hash !== expectedHash)
|
|
234
|
+
throw conflictError("local file changed since the last Floom sync", "EEXIST");
|
|
235
|
+
await rm(target);
|
|
236
|
+
await pruneEmptyParents(root, dirname(target));
|
|
237
|
+
}
|
|
238
|
+
async function pruneEmptyParents(root, startDir) {
|
|
239
|
+
const resolvedRoot = resolve(root);
|
|
240
|
+
let current = resolve(startDir);
|
|
241
|
+
for (;;) {
|
|
242
|
+
const relativeCurrent = relative(resolvedRoot, current);
|
|
243
|
+
if (!relativeCurrent || relativeCurrent === ".." || relativeCurrent.startsWith(`..${sep}`) || isAbsolute(relativeCurrent))
|
|
244
|
+
return;
|
|
245
|
+
try {
|
|
246
|
+
await rmdir(current);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
const code = err.code;
|
|
250
|
+
if (code === "ENOENT") {
|
|
251
|
+
current = dirname(current);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (code === "ENOTEMPTY" || code === "EEXIST" || code === "ENOTDIR")
|
|
255
|
+
return;
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
current = dirname(current);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
227
261
|
async function ensureSafeParentDirectory(root, target) {
|
|
228
262
|
const resolvedRoot = resolve(root);
|
|
229
263
|
const resolvedParent = resolve(dirname(target));
|
|
@@ -446,6 +480,26 @@ export async function sync(opts = {}) {
|
|
|
446
480
|
noteConflict(target, "local file changed since the last Floom sync");
|
|
447
481
|
continue;
|
|
448
482
|
}
|
|
483
|
+
try {
|
|
484
|
+
await deleteSyncedFile(root, target, entry.hash);
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
const code = err.code;
|
|
488
|
+
if (code === "ENOENT") {
|
|
489
|
+
unmarkSynced(manifest, key);
|
|
490
|
+
manifestChanged = true;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (code === "ELOOP") {
|
|
494
|
+
noteManifestConflict(key, "path contains a symbolic link");
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (code === "ENOTDIR" || code === "EISDIR" || code === "EEXIST") {
|
|
498
|
+
noteManifestConflict(key, err instanceof Error ? err.message : "local file changed since the last Floom sync");
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
throw err;
|
|
502
|
+
}
|
|
449
503
|
unmarkSynced(manifest, key);
|
|
450
504
|
manifestChanged = true;
|
|
451
505
|
}
|