@floomhq/floom 1.0.63 → 2.0.0
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/index.d.ts +1 -0
- package/dist/index.js +3663 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -25
- package/package.json +37 -45
- package/LICENSE +0 -21
- package/README.md +0 -89
- package/bin/floom.js +0 -2
- package/dist/audit.js +0 -236
- package/dist/cli.js +0 -1313
- package/dist/config.js +0 -85
- package/dist/daemon.js +0 -450
- package/dist/delete.js +0 -55
- package/dist/doctor.js +0 -381
- package/dist/errors.js +0 -71
- package/dist/feedback.js +0 -34
- package/dist/info.js +0 -78
- package/dist/init.js +0 -221
- package/dist/install.js +0 -305
- package/dist/launch.js +0 -83
- package/dist/lib/api.js +0 -142
- package/dist/lib/skill-labels.js +0 -140
- package/dist/library.js +0 -102
- package/dist/list.js +0 -79
- package/dist/login.js +0 -259
- package/dist/mcp.js +0 -20
- package/dist/package.js +0 -507
- package/dist/publish.js +0 -240
- package/dist/push-watch.js +0 -372
- package/dist/scan.js +0 -24
- package/dist/search.js +0 -54
- package/dist/secrets.js +0 -119
- package/dist/setup.js +0 -301
- package/dist/share.js +0 -70
- package/dist/status.js +0 -181
- package/dist/sync-manifest.js +0 -314
- package/dist/sync.js +0 -581
- package/dist/targets.js +0 -49
- package/dist/ui.js +0 -28
- package/dist/whoami.js +0 -64
package/dist/init.js
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { writeFile, access, mkdir } from "node:fs/promises";
|
|
2
|
-
import { dirname, resolve, basename, extname, join } from "node:path";
|
|
3
|
-
import { createInterface } from "node:readline/promises";
|
|
4
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
5
|
-
import { c, symbols } from "./ui.js";
|
|
6
|
-
import { FloomError } from "./errors.js";
|
|
7
|
-
const TEMPLATES = {
|
|
8
|
-
generic: `---
|
|
9
|
-
title:
|
|
10
|
-
description:
|
|
11
|
-
version: 1.0
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
# Goal
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
# When to use
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# Inputs
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# Instructions
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# Output
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# Examples
|
|
30
|
-
`,
|
|
31
|
-
"brand-voice": `---
|
|
32
|
-
title: Brand voice
|
|
33
|
-
description: Keep agent writing aligned with our company voice.
|
|
34
|
-
type: knowledge
|
|
35
|
-
installs_as: memory
|
|
36
|
-
version: 1.0
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
# Brand Voice
|
|
40
|
-
|
|
41
|
-
## Use this when
|
|
42
|
-
|
|
43
|
-
- Writing external copy
|
|
44
|
-
- Editing founder posts
|
|
45
|
-
- Drafting website, email, or support text
|
|
46
|
-
|
|
47
|
-
## Voice rules
|
|
48
|
-
|
|
49
|
-
- Be clear and specific.
|
|
50
|
-
- Use short paragraphs.
|
|
51
|
-
- Avoid hype, filler, and vague claims.
|
|
52
|
-
|
|
53
|
-
## Company facts
|
|
54
|
-
|
|
55
|
-
- Add product facts here.
|
|
56
|
-
- Add banned phrases here.
|
|
57
|
-
- Add preferred examples here.
|
|
58
|
-
`,
|
|
59
|
-
"pr-review": `---
|
|
60
|
-
title: PR review
|
|
61
|
-
description: Review pull requests for correctness, regressions, and missing tests.
|
|
62
|
-
type: workflow
|
|
63
|
-
installs_as: claude_skill
|
|
64
|
-
version: 1.0
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
# PR Review
|
|
68
|
-
|
|
69
|
-
## Goal
|
|
70
|
-
|
|
71
|
-
Find concrete bugs, regressions, security issues, and missing tests before code merges.
|
|
72
|
-
|
|
73
|
-
## Inputs
|
|
74
|
-
|
|
75
|
-
- Diff
|
|
76
|
-
- Test output
|
|
77
|
-
- Relevant files and callers
|
|
78
|
-
|
|
79
|
-
## Review steps
|
|
80
|
-
|
|
81
|
-
1. Read the diff and affected call sites.
|
|
82
|
-
2. Check behavior changes against the stated intent.
|
|
83
|
-
3. Look for edge cases, auth/security issues, data loss, and broken contracts.
|
|
84
|
-
4. Verify tests cover the changed behavior.
|
|
85
|
-
|
|
86
|
-
## Output
|
|
87
|
-
|
|
88
|
-
List findings first, ordered by severity, with file and line references.
|
|
89
|
-
`,
|
|
90
|
-
sales: `---
|
|
91
|
-
title: Sales research
|
|
92
|
-
description: Prepare concise account research and outreach context.
|
|
93
|
-
type: workflow
|
|
94
|
-
installs_as: memory
|
|
95
|
-
version: 1.0
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
# Sales Research
|
|
99
|
-
|
|
100
|
-
## Goal
|
|
101
|
-
|
|
102
|
-
Prepare useful context before contacting an account.
|
|
103
|
-
|
|
104
|
-
## Research checklist
|
|
105
|
-
|
|
106
|
-
- Company description
|
|
107
|
-
- Current priorities or trigger events
|
|
108
|
-
- Relevant people
|
|
109
|
-
- Likely pain
|
|
110
|
-
- Specific reason to reach out
|
|
111
|
-
|
|
112
|
-
## Output
|
|
113
|
-
|
|
114
|
-
Return a short account brief and a first-message angle.
|
|
115
|
-
`,
|
|
116
|
-
support: `---
|
|
117
|
-
title: Support tone
|
|
118
|
-
description: Keep support replies concise, helpful, and calm.
|
|
119
|
-
type: instruction
|
|
120
|
-
installs_as: memory
|
|
121
|
-
version: 1.0
|
|
122
|
-
---
|
|
123
|
-
|
|
124
|
-
# Support Tone
|
|
125
|
-
|
|
126
|
-
## Rules
|
|
127
|
-
|
|
128
|
-
- Acknowledge the issue directly.
|
|
129
|
-
- Give the next concrete step.
|
|
130
|
-
- Avoid blaming the user.
|
|
131
|
-
- Avoid long explanations unless the user asks.
|
|
132
|
-
|
|
133
|
-
## Output
|
|
134
|
-
|
|
135
|
-
Write the reply in plain language with a clear next action.
|
|
136
|
-
`,
|
|
137
|
-
onboarding: `---
|
|
138
|
-
title: Onboarding context
|
|
139
|
-
description: Give agents the context needed to help new teammates.
|
|
140
|
-
type: knowledge
|
|
141
|
-
installs_as: memory
|
|
142
|
-
version: 1.0
|
|
143
|
-
---
|
|
144
|
-
|
|
145
|
-
# Onboarding Context
|
|
146
|
-
|
|
147
|
-
## Company
|
|
148
|
-
|
|
149
|
-
- Add what the company does.
|
|
150
|
-
- Add important product areas.
|
|
151
|
-
|
|
152
|
-
## How we work
|
|
153
|
-
|
|
154
|
-
- Add team norms.
|
|
155
|
-
- Add review expectations.
|
|
156
|
-
- Add recurring workflows.
|
|
157
|
-
|
|
158
|
-
## Useful links
|
|
159
|
-
|
|
160
|
-
- Add docs and repositories.
|
|
161
|
-
`,
|
|
162
|
-
};
|
|
163
|
-
export const INIT_TEMPLATES = Object.keys(TEMPLATES);
|
|
164
|
-
const CLI_COMMAND = "npx -y @floomhq/floom";
|
|
165
|
-
export async function init(filename, opts = {}) {
|
|
166
|
-
const target = filename ?? "skill";
|
|
167
|
-
const template = opts.template ?? "generic";
|
|
168
|
-
const folderMode = extname(target).toLowerCase() !== ".md";
|
|
169
|
-
const outputTarget = folderMode ? join(target, "SKILL.md") : target;
|
|
170
|
-
const folderPath = folderMode ? resolve(process.cwd(), target) : null;
|
|
171
|
-
const filePath = resolve(process.cwd(), outputTarget);
|
|
172
|
-
const exists = await fileExists(filePath);
|
|
173
|
-
if (exists) {
|
|
174
|
-
if (!process.stdin.isTTY) {
|
|
175
|
-
throw new FloomError(`${outputTarget} already exists.`, "Re-run with a different filename, or delete it first.");
|
|
176
|
-
}
|
|
177
|
-
const rl = createInterface({ input, output });
|
|
178
|
-
const answer = (await rl.question(`${c.yellow("?")} ${outputTarget} already exists. Overwrite? ${c.dim("(y/N)")} `)).trim().toLowerCase();
|
|
179
|
-
rl.close();
|
|
180
|
-
if (answer !== "y" && answer !== "yes") {
|
|
181
|
-
process.stdout.write(`\n${c.dim("Cancelled. Nothing was written.")}\n\n`);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
try {
|
|
186
|
-
if (folderPath)
|
|
187
|
-
await mkdir(folderPath, { recursive: true });
|
|
188
|
-
await writeFile(filePath, TEMPLATES[template], "utf8");
|
|
189
|
-
}
|
|
190
|
-
catch (err) {
|
|
191
|
-
const code = err.code;
|
|
192
|
-
if (code === "ENOENT") {
|
|
193
|
-
throw new FloomError(`Directory not found: ${dirname(target)}`, "Create the directory first, or choose a filename in the current directory.");
|
|
194
|
-
}
|
|
195
|
-
if (code === "EISDIR") {
|
|
196
|
-
throw new FloomError(`That's a directory, not a file: ${target}`);
|
|
197
|
-
}
|
|
198
|
-
throw new FloomError(`Couldn't create ${outputTarget}: ${err.message}`);
|
|
199
|
-
}
|
|
200
|
-
process.stdout.write(`\n${symbols.ok} Created ${c.bold(folderMode ? outputTarget : basename(filePath))}\n`);
|
|
201
|
-
if (template !== "generic")
|
|
202
|
-
process.stdout.write(` ${c.dim(`Template: ${template}`)}\n`);
|
|
203
|
-
process.stdout.write(`\n ${c.bold("Next")}\n`);
|
|
204
|
-
process.stdout.write(` ${c.dim("1.")} Fill in the title, description, and instructions.\n`);
|
|
205
|
-
process.stdout.write(` ${c.dim("2.")} Check it: ${c.cyan(`${CLI_COMMAND} scan ${shellQuote(folderMode ? target : outputTarget)}`)}\n`);
|
|
206
|
-
process.stdout.write(` ${c.dim("3.")} Publish: ${c.cyan(`${CLI_COMMAND} publish ${shellQuote(folderMode ? target : outputTarget)} --type instruction --public`)}\n\n`);
|
|
207
|
-
}
|
|
208
|
-
function shellQuote(value) {
|
|
209
|
-
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
|
|
210
|
-
return value;
|
|
211
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
212
|
-
}
|
|
213
|
-
async function fileExists(p) {
|
|
214
|
-
try {
|
|
215
|
-
await access(p);
|
|
216
|
-
return true;
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
}
|
package/dist/install.js
DELETED
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import { constants } from "node:fs";
|
|
2
|
-
import { lstat, mkdir, open } from "node:fs/promises";
|
|
3
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
-
import ora from "ora";
|
|
5
|
-
import { readConfig, resolveApiUrl } from "./config.js";
|
|
6
|
-
import { getJson } from "./lib/api.js";
|
|
7
|
-
import { c, symbols } from "./ui.js";
|
|
8
|
-
import { FloomError } from "./errors.js";
|
|
9
|
-
import { normalizeRemotePackageFiles, packageHash, sha256Bytes } from "./package.js";
|
|
10
|
-
import { manifestKey, markSynced, readSyncManifest, withSyncLock, writeSyncManifest } from "./sync-manifest.js";
|
|
11
|
-
import { targetLabel, targetSkillsDir, targetSkillsDirEnv } from "./targets.js";
|
|
12
|
-
const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
13
|
-
const FD_PATH_ROOT = "/proc/self/fd";
|
|
14
|
-
function slugFromInput(input) {
|
|
15
|
-
const trimmed = input.trim();
|
|
16
|
-
try {
|
|
17
|
-
const url = new URL(trimmed);
|
|
18
|
-
const last = url.pathname.split("/").filter(Boolean).at(-1) ?? "";
|
|
19
|
-
return last.replace(/\.(md|json)$/i, "");
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return trimmed.replace(/\.(md|json)$/i, "");
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
function skillPath(root, slug) {
|
|
26
|
-
return join(root, slug, "SKILL.md");
|
|
27
|
-
}
|
|
28
|
-
function legacySkillPath(root, slug) {
|
|
29
|
-
return join(root, `${slug}.md`);
|
|
30
|
-
}
|
|
31
|
-
function skillsDirHint(target) {
|
|
32
|
-
return targetSkillsDirEnv(target);
|
|
33
|
-
}
|
|
34
|
-
function setupCommand(target) {
|
|
35
|
-
return `npx -y @floomhq/floom setup --target ${target} --yes`;
|
|
36
|
-
}
|
|
37
|
-
async function readLocalFile(path) {
|
|
38
|
-
try {
|
|
39
|
-
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
40
|
-
try {
|
|
41
|
-
const stat = await handle.stat();
|
|
42
|
-
if (!stat.isFile())
|
|
43
|
-
throw new FloomError("Local path is blocked by an existing file or directory.");
|
|
44
|
-
return await handle.readFile();
|
|
45
|
-
}
|
|
46
|
-
finally {
|
|
47
|
-
await handle.close();
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
catch (err) {
|
|
51
|
-
const code = err.code;
|
|
52
|
-
if (code === "ENOENT")
|
|
53
|
-
return null;
|
|
54
|
-
if (code === "ELOOP")
|
|
55
|
-
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
|
|
56
|
-
if (code === "ENOTDIR" || code === "EISDIR") {
|
|
57
|
-
throw new FloomError("Local path is blocked by an existing file or directory.");
|
|
58
|
-
}
|
|
59
|
-
throw err;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
async function localPackageHash(root, slug, target, files) {
|
|
63
|
-
const main = await readLocalFile(target);
|
|
64
|
-
if (main === null) {
|
|
65
|
-
const legacy = await readLocalFile(legacySkillPath(root, slug));
|
|
66
|
-
if (legacy !== null && files.length === 0)
|
|
67
|
-
return packageHash(legacy.toString("utf8"), []);
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
const localFiles = [];
|
|
71
|
-
for (const file of files) {
|
|
72
|
-
const bytes = await readLocalFile(join(dirname(target), file.path));
|
|
73
|
-
if (bytes === null)
|
|
74
|
-
return null;
|
|
75
|
-
localFiles.push({ path: file.path, bytes, sha256: file.sha256 });
|
|
76
|
-
}
|
|
77
|
-
return packageHash(main.toString("utf8"), localFiles);
|
|
78
|
-
}
|
|
79
|
-
async function markInstallSynced(root, slug, files) {
|
|
80
|
-
const manifest = await readSyncManifest();
|
|
81
|
-
for (const file of files) {
|
|
82
|
-
markSynced(manifest, manifestKey(root, file.target), slug, file.hash);
|
|
83
|
-
}
|
|
84
|
-
await writeSyncManifest(manifest);
|
|
85
|
-
}
|
|
86
|
-
async function preflightInstallPackage(root, files, opts) {
|
|
87
|
-
for (const file of files) {
|
|
88
|
-
await ensureSafeParentDirectory(root, file.target);
|
|
89
|
-
const existing = await readLocalFile(file.target);
|
|
90
|
-
if (existing === null)
|
|
91
|
-
continue;
|
|
92
|
-
if (sha256Buffer(existing) === file.hash)
|
|
93
|
-
continue;
|
|
94
|
-
if (opts.force)
|
|
95
|
-
continue;
|
|
96
|
-
return file.target;
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
function sha256Buffer(input) {
|
|
101
|
-
return sha256Bytes(input);
|
|
102
|
-
}
|
|
103
|
-
async function writeInstallFile(root, target, body) {
|
|
104
|
-
const parent = await openSafeParentDirectory(root, target);
|
|
105
|
-
let handle = null;
|
|
106
|
-
try {
|
|
107
|
-
handle = await open(childCreatePath(parent, dirname(target), basename(target)), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
108
|
-
await writeAll(handle, body);
|
|
109
|
-
}
|
|
110
|
-
finally {
|
|
111
|
-
await handle?.close();
|
|
112
|
-
await parent.close();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
async function overwriteInstallFile(root, target, body) {
|
|
116
|
-
const parent = await openSafeParentDirectory(root, target);
|
|
117
|
-
const handle = await open(childCreatePath(parent, dirname(target), basename(target)), constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW, 0o600);
|
|
118
|
-
try {
|
|
119
|
-
const stat = await handle.stat();
|
|
120
|
-
if (!stat.isFile())
|
|
121
|
-
throw new FloomError("Local path is blocked by an existing file or directory.");
|
|
122
|
-
await writeAll(handle, body);
|
|
123
|
-
}
|
|
124
|
-
finally {
|
|
125
|
-
await handle.close();
|
|
126
|
-
await parent.close();
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
async function openSafeParentDirectory(root, target) {
|
|
130
|
-
await ensureSafeParentDirectory(root, target);
|
|
131
|
-
return open(dirname(target), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
132
|
-
}
|
|
133
|
-
function childCreatePath(parent, fallbackParent, name) {
|
|
134
|
-
if (process.platform === "linux")
|
|
135
|
-
return `${FD_PATH_ROOT}/${parent.fd}/${name}`;
|
|
136
|
-
return join(resolve(fallbackParent), name);
|
|
137
|
-
}
|
|
138
|
-
async function writeAll(handle, body) {
|
|
139
|
-
const buffer = Buffer.isBuffer(body) ? body : Buffer.from(body, "utf8");
|
|
140
|
-
let offset = 0;
|
|
141
|
-
while (offset < buffer.length) {
|
|
142
|
-
const result = await handle.write(buffer, offset, buffer.length - offset, offset);
|
|
143
|
-
if (result.bytesWritten === 0)
|
|
144
|
-
throw new FloomError("Failed to write local skill file.");
|
|
145
|
-
offset += result.bytesWritten;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
async function ensureSafeParentDirectory(root, target) {
|
|
149
|
-
const resolvedRoot = resolve(root);
|
|
150
|
-
const resolvedParent = resolve(dirname(target));
|
|
151
|
-
const relativeParent = relative(resolvedRoot, resolvedParent);
|
|
152
|
-
if (relativeParent === ".." || relativeParent.startsWith(`..${sep}`) || isAbsolute(relativeParent)) {
|
|
153
|
-
throw new FloomError("Invalid skill target path.");
|
|
154
|
-
}
|
|
155
|
-
try {
|
|
156
|
-
await mkdir(resolvedRoot, { recursive: true, mode: 0o700 });
|
|
157
|
-
}
|
|
158
|
-
catch (err) {
|
|
159
|
-
if (err.code === "EEXIST") {
|
|
160
|
-
throw new FloomError("Skills directory points to a file, not a directory.", "Set the skills directory env var to a directory, or remove the file blocking it.");
|
|
161
|
-
}
|
|
162
|
-
throw err;
|
|
163
|
-
}
|
|
164
|
-
await assertSafeDirectory(resolvedRoot);
|
|
165
|
-
if (!relativeParent || relativeParent === ".")
|
|
166
|
-
return;
|
|
167
|
-
let current = resolvedRoot;
|
|
168
|
-
for (const segment of relativeParent.split(sep).filter(Boolean)) {
|
|
169
|
-
current = join(current, segment);
|
|
170
|
-
try {
|
|
171
|
-
await assertSafeDirectory(current);
|
|
172
|
-
}
|
|
173
|
-
catch (err) {
|
|
174
|
-
if (err.code !== "ENOENT")
|
|
175
|
-
throw err;
|
|
176
|
-
await mkdir(current, { mode: 0o700 });
|
|
177
|
-
await assertSafeDirectory(current);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
async function assertSafeDirectory(path) {
|
|
182
|
-
const stat = await lstat(path);
|
|
183
|
-
if (stat.isSymbolicLink()) {
|
|
184
|
-
throw new FloomError("Local path contains a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
|
|
185
|
-
}
|
|
186
|
-
if (!stat.isDirectory()) {
|
|
187
|
-
throw new FloomError("Local path is blocked by an existing file or directory.");
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
export async function install(slugInput, opts = {}) {
|
|
191
|
-
const targetAgent = opts.target ?? "claude";
|
|
192
|
-
const root = targetSkillsDir(targetAgent);
|
|
193
|
-
const slug = slugFromInput(slugInput);
|
|
194
|
-
if (!SLUG_RE.test(slug)) {
|
|
195
|
-
throw new FloomError(`Invalid skill slug: ${slugInput}`);
|
|
196
|
-
}
|
|
197
|
-
const cfg = await readConfig();
|
|
198
|
-
const apiUrl = resolveApiUrl(cfg);
|
|
199
|
-
const spinner = ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
|
|
200
|
-
let detail;
|
|
201
|
-
try {
|
|
202
|
-
detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "fetch skill", cfg?.accessToken);
|
|
203
|
-
if (!detail || typeof detail.body_md !== "string") {
|
|
204
|
-
throw new FloomError("Invalid skill response.");
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
catch (err) {
|
|
208
|
-
spinner.stop();
|
|
209
|
-
throw err;
|
|
210
|
-
}
|
|
211
|
-
const target = skillPath(root, slug);
|
|
212
|
-
const remotePackageFiles = normalizeRemotePackageFiles(detail.package_files ?? detail.files);
|
|
213
|
-
const installFiles = [
|
|
214
|
-
{ target, bytes: detail.body_md, hash: sha256Bytes(detail.body_md) },
|
|
215
|
-
...remotePackageFiles.map((file) => ({
|
|
216
|
-
target: join(dirname(target), file.path),
|
|
217
|
-
bytes: file.bytes,
|
|
218
|
-
hash: file.sha256,
|
|
219
|
-
})),
|
|
220
|
-
];
|
|
221
|
-
const remoteHash = packageHash(detail.body_md, remotePackageFiles);
|
|
222
|
-
let action = "installed";
|
|
223
|
-
let manifestWarning = null;
|
|
224
|
-
await withSyncLock(async () => {
|
|
225
|
-
try {
|
|
226
|
-
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
227
|
-
}
|
|
228
|
-
catch (err) {
|
|
229
|
-
if (err.code === "EEXIST") {
|
|
230
|
-
throw new FloomError(`${skillsDirHint(targetAgent)} points to a file, not a directory.`, `Set ${skillsDirHint(targetAgent)} to a directory, or remove the file blocking it.`);
|
|
231
|
-
}
|
|
232
|
-
throw err;
|
|
233
|
-
}
|
|
234
|
-
await ensureSafeParentDirectory(root, target);
|
|
235
|
-
const existing = await localPackageHash(root, slug, target, remotePackageFiles);
|
|
236
|
-
const conflictingTarget = await preflightInstallPackage(root, installFiles, opts.force ? { force: true } : {});
|
|
237
|
-
if (conflictingTarget) {
|
|
238
|
-
throw new FloomError("Local skill already exists with different content.", `Run \`npx -y @floomhq/floom add <link> --force\` to replace it, or move the local file first: ${relative(root, conflictingTarget).split(sep).join("/")}`);
|
|
239
|
-
}
|
|
240
|
-
if (existing === remoteHash) {
|
|
241
|
-
action = "unchanged";
|
|
242
|
-
}
|
|
243
|
-
else if (opts.force) {
|
|
244
|
-
try {
|
|
245
|
-
await overwriteInstallFile(root, target, detail.body_md);
|
|
246
|
-
for (const file of remotePackageFiles) {
|
|
247
|
-
await overwriteInstallFile(root, join(dirname(target), file.path), file.bytes);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
catch (err) {
|
|
251
|
-
const code = err.code;
|
|
252
|
-
if (code === "ELOOP")
|
|
253
|
-
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
|
|
254
|
-
throw err;
|
|
255
|
-
}
|
|
256
|
-
action = "updated";
|
|
257
|
-
}
|
|
258
|
-
else if (existing !== null) {
|
|
259
|
-
throw new FloomError("Local skill already exists with different content.", "Run `npx -y @floomhq/floom add <link> --force` to replace it, or move the local file first.");
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
try {
|
|
263
|
-
await writeInstallFile(root, target, detail.body_md);
|
|
264
|
-
for (const file of remotePackageFiles) {
|
|
265
|
-
await writeInstallFile(root, join(dirname(target), file.path), file.bytes);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
catch (err) {
|
|
269
|
-
const code = err.code;
|
|
270
|
-
if (code === "EEXIST") {
|
|
271
|
-
throw new FloomError("Local skill already exists with different content.", "Move or delete the local file, then run `npx -y @floomhq/floom add` again.");
|
|
272
|
-
}
|
|
273
|
-
if (code === "ELOOP") {
|
|
274
|
-
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
|
|
275
|
-
}
|
|
276
|
-
if (code === "ENOENT") {
|
|
277
|
-
throw new FloomError("Local path changed during install.", "Run `npx -y @floomhq/floom add` again.");
|
|
278
|
-
}
|
|
279
|
-
throw err;
|
|
280
|
-
}
|
|
281
|
-
action = "installed";
|
|
282
|
-
}
|
|
283
|
-
try {
|
|
284
|
-
await markInstallSynced(root, slug, installFiles);
|
|
285
|
-
}
|
|
286
|
-
catch (err) {
|
|
287
|
-
manifestWarning = err instanceof Error ? err.message : String(err);
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
spinner.stop();
|
|
291
|
-
process.stdout.write(`\n${symbols.ok} [floom] ${action} ${c.bold(slug)}\n`);
|
|
292
|
-
process.stdout.write(` ${c.dim(dirname(target))}\n\n`);
|
|
293
|
-
if (manifestWarning) {
|
|
294
|
-
process.stdout.write(` ${c.yellow("!")} ${c.dim(`Installed, but sync tracking was not updated: ${manifestWarning}`)}\n\n`);
|
|
295
|
-
}
|
|
296
|
-
process.stdout.write(` ${c.bold("Next")}\n`);
|
|
297
|
-
if (opts.setup) {
|
|
298
|
-
process.stdout.write(` ${c.dim("1.")} Floom is connecting ${targetLabel(targetAgent)} now.\n`);
|
|
299
|
-
process.stdout.write(` ${c.dim("2.")} Tell your agent to use ${c.bold(slug)} when it matches the task.\n\n`);
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
process.stdout.write(` ${c.dim("1.")} Tell your agent to use ${c.bold(slug)} when it matches the task.\n`);
|
|
303
|
-
process.stdout.write(` ${c.dim("2.")} One-time setup: ${c.cyan(setupCommand(targetAgent))}\n\n`);
|
|
304
|
-
}
|
|
305
|
-
}
|
package/dist/launch.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
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 daemonAligned = daemon?.running === true && daemon.version === CLI_VERSION;
|
|
44
|
-
const payload = {
|
|
45
|
-
ok: Boolean(health?.ok && releaseAligned && daemonAligned && daemonTargetsOk),
|
|
46
|
-
release: {
|
|
47
|
-
cli: CLI_VERSION,
|
|
48
|
-
web: health?.version ?? null,
|
|
49
|
-
cli_latest: cliVersion?.latest ?? null,
|
|
50
|
-
cli_min: cliVersion?.min ?? null,
|
|
51
|
-
release_aligned: releaseAligned,
|
|
52
|
-
},
|
|
53
|
-
health: health ?? null,
|
|
54
|
-
daemon: daemon ? {
|
|
55
|
-
running: daemon.running === true,
|
|
56
|
-
version: daemon.version ?? null,
|
|
57
|
-
hostname: daemon.hostname ?? null,
|
|
58
|
-
targets,
|
|
59
|
-
all_targets_ok: daemonTargetsOk,
|
|
60
|
-
last_completed_at: daemon.last_completed_at ?? null,
|
|
61
|
-
last_run: targetResults,
|
|
62
|
-
} : null,
|
|
63
|
-
escalations: [
|
|
64
|
-
...(releaseAligned ? [] : ["CLI, web health, and server latest versions are not aligned."]),
|
|
65
|
-
...(daemonAligned ? [] : ["Daemon is not running on the pinned CLI version."]),
|
|
66
|
-
...(targets.length === 0 || daemonTargetsOk ? [] : ["Latest daemon cycle has not yet passed for every configured target."]),
|
|
67
|
-
],
|
|
68
|
-
};
|
|
69
|
-
if (opts.json) {
|
|
70
|
-
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
process.stdout.write(`\n${symbols.dot} ${c.bold("Floom launch gate")}\n\n`);
|
|
74
|
-
process.stdout.write(` ${c.dim("CLI:")} ${payload.release.cli}\n`);
|
|
75
|
-
process.stdout.write(` ${c.dim("Web:")} ${payload.release.web ?? "unknown"}\n`);
|
|
76
|
-
process.stdout.write(` ${c.dim("Daemon:")} ${payload.daemon?.running ? `running (${payload.daemon.version})` : "not running"}\n`);
|
|
77
|
-
if (payload.escalations.length > 0) {
|
|
78
|
-
for (const escalation of payload.escalations)
|
|
79
|
-
process.stdout.write(` ${symbols.bullet} ${escalation}\n`);
|
|
80
|
-
throw new FloomError("Launch gate did not pass.", "Run with --json for machine-readable details.");
|
|
81
|
-
}
|
|
82
|
-
process.stdout.write(`\n${symbols.ok} Launch gate passed for the pinned local release identity.\n\n`);
|
|
83
|
-
}
|