@floomhq/floom 1.0.14 → 1.0.17

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/doctor.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import { homedir } from "node:os";
2
+ import { execFile as execFileCb } from "node:child_process";
3
+ import { delimiter } from "node:path";
2
4
  import { join } from "node:path";
3
- import { stat, readFile, access, readdir, constants } from "node:fs/promises";
5
+ import { stat, readFile, access, readdir, constants, realpath } from "node:fs/promises";
6
+ import { promisify } from "node:util";
4
7
  import { readConfig, CONFIG_PATH, resolveApiUrl } from "./config.js";
5
8
  import { floomFetch } from "./lib/api.js";
6
9
  import { c, symbols } from "./ui.js";
7
10
  import { CLI_VERSION, compareSemverish, formatVersionLabel } from "./version.js";
8
- import { resolveSkillsDir } from "./targets.js";
11
+ const execFile = promisify(execFileCb);
9
12
  function statusBadge(s) {
10
13
  if (s === "ok")
11
14
  return c.green(symbols.ok);
@@ -19,7 +22,7 @@ async function checkAuth() {
19
22
  return {
20
23
  name: "Auth",
21
24
  status: "ok",
22
- detail: "Receiver mode ready: add shared links without an account. Sign in to publish, save, follow, list, or sync.",
25
+ detail: "Receiver mode ready. Sign in only when publishing or listing your own skills.",
23
26
  };
24
27
  }
25
28
  const apiUrl = resolveApiUrl(cfg);
@@ -89,7 +92,7 @@ async function checkMcp() {
89
92
  return {
90
93
  name: "MCP",
91
94
  status: "ok",
92
- detail: "Optional MCP not registered. `npx -y @floomhq/floom add` still writes local files; MCP keeps signed-in libraries synced.",
95
+ detail: "Optional MCP not registered. `npx -y @floomhq/floom add` still writes local skill files.",
93
96
  };
94
97
  }
95
98
  return {
@@ -98,24 +101,31 @@ async function checkMcp() {
98
101
  detail: `Registered with: ${found.map((f) => f.name).join(", ")}`,
99
102
  };
100
103
  }
104
+ function targetSkillsDir(target) {
105
+ if (target === "codex") {
106
+ const codexHome = process.env.CODEX_HOME ?? join(homedir(), ".codex");
107
+ return process.env.CODEX_SKILLS_DIR ?? join(codexHome, "skills");
108
+ }
109
+ return process.env.CLAUDE_SKILLS_DIR ?? join(homedir(), ".claude", "skills");
110
+ }
101
111
  async function checkTargetDir(target) {
102
- const dir = resolveSkillsDir(target);
112
+ const dir = targetSkillsDir(target);
103
113
  try {
104
114
  const s = await stat(dir);
105
115
  if (!s.isDirectory()) {
106
116
  return {
107
- name: `${target} dir`,
117
+ name: "Target dir",
108
118
  status: "fail",
109
119
  detail: `${dir} exists but is not a directory.`,
110
120
  };
111
121
  }
112
122
  try {
113
123
  await access(dir, constants.W_OK);
114
- return { name: `${target} dir`, status: "ok", detail: `${dir} (writable)` };
124
+ return { name: "Target dir", status: "ok", detail: `${dir} (writable)` };
115
125
  }
116
126
  catch {
117
127
  return {
118
- name: `${target} dir`,
128
+ name: "Target dir",
119
129
  status: "fail",
120
130
  detail: `${dir} is not writable.`,
121
131
  hint: `Try: chmod u+w ${dir}`,
@@ -125,26 +135,26 @@ async function checkTargetDir(target) {
125
135
  catch (err) {
126
136
  if (err.code === "ENOENT") {
127
137
  return {
128
- name: `${target} dir`,
138
+ name: "Target dir",
129
139
  status: "warn",
130
140
  detail: `${dir} does not exist yet.`,
131
- hint: "It will be created on first `npx -y @floomhq/floom add`.",
141
+ hint: `It will be created on first \`npx -y @floomhq/floom add <link> --target ${target}\`.`,
132
142
  };
133
143
  }
134
144
  throw err;
135
145
  }
136
146
  }
137
147
  async function checkLastSync(target) {
138
- const dir = resolveSkillsDir(target);
148
+ const dir = targetSkillsDir(target);
139
149
  try {
140
150
  const entries = await readdir(dir);
141
151
  const skills = entries.filter((e) => e.endsWith(".md") || !e.startsWith("."));
142
152
  if (skills.length === 0) {
143
153
  return {
144
- name: `${target} sync`,
154
+ name: "Last sync",
145
155
  status: "warn",
146
156
  detail: "No synced skills found yet.",
147
- hint: "Run `npx -y @floomhq/floom add <link>` to install your first skill.",
157
+ hint: `Run \`npx -y @floomhq/floom add <link> --target ${target}\` to install your first skill.`,
148
158
  };
149
159
  }
150
160
  // Find most recently modified entry
@@ -161,7 +171,7 @@ async function checkLastSync(target) {
161
171
  }
162
172
  if (newest.mtime === 0) {
163
173
  return {
164
- name: `${target} sync`,
174
+ name: "Last sync",
165
175
  status: "warn",
166
176
  detail: `${skills.length} entries, no readable mtime.`,
167
177
  };
@@ -175,7 +185,7 @@ async function checkLastSync(target) {
175
185
  ? `${Math.round(ageSec / 3600)}h ago`
176
186
  : `${Math.round(ageSec / 86400)}d ago`;
177
187
  return {
178
- name: `${target} sync`,
188
+ name: "Last sync",
179
189
  status: "ok",
180
190
  detail: `${newest.name} — ${human} (${skills.length} total)`,
181
191
  };
@@ -183,13 +193,13 @@ async function checkLastSync(target) {
183
193
  catch (err) {
184
194
  if (err.code === "ENOENT") {
185
195
  return {
186
- name: `${target} sync`,
196
+ name: "Last sync",
187
197
  status: "warn",
188
198
  detail: "Skills dir not created yet.",
189
199
  };
190
200
  }
191
201
  return {
192
- name: `${target} sync`,
202
+ name: "Last sync",
193
203
  status: "warn",
194
204
  detail: `Cannot read skills dir: ${err.message}`,
195
205
  };
@@ -216,7 +226,7 @@ async function checkVersion() {
216
226
  name: "Version",
217
227
  status: "fail",
218
228
  detail: `CLI ${formatVersionLabel(CLI_VERSION)} below required ${formatVersionLabel(data.min)}.`,
219
- hint: "Run `npm i -g @floomhq/floom` to upgrade.",
229
+ hint: "Run `npm i -g @floomhq/floom` to upgrade, then use `floom`.",
220
230
  };
221
231
  }
222
232
  if (data.latest && compareSemverish(CLI_VERSION, data.latest) < 0) {
@@ -224,7 +234,7 @@ async function checkVersion() {
224
234
  name: "Version",
225
235
  status: "warn",
226
236
  detail: `CLI ${formatVersionLabel(CLI_VERSION)}, latest is ${formatVersionLabel(data.latest)}.`,
227
- hint: "Run `npm i -g @floomhq/floom` to upgrade.",
237
+ hint: "Run `npm i -g @floomhq/floom` to upgrade, then use `floom`.",
228
238
  };
229
239
  }
230
240
  return { name: "Version", status: "ok", detail: `CLI ${formatVersionLabel(CLI_VERSION)} (current)` };
@@ -237,32 +247,103 @@ async function checkVersion() {
237
247
  };
238
248
  }
239
249
  }
250
+ async function findPathExecutables(name) {
251
+ const seen = new Set();
252
+ const out = [];
253
+ const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
254
+ for (const dir of pathDirs) {
255
+ const candidate = join(dir, name);
256
+ if (seen.has(candidate))
257
+ continue;
258
+ seen.add(candidate);
259
+ try {
260
+ await access(candidate, constants.X_OK);
261
+ out.push(candidate);
262
+ }
263
+ catch {
264
+ // not present or not executable
265
+ }
266
+ }
267
+ return out;
268
+ }
269
+ async function safeRealpath(path) {
270
+ if (!path)
271
+ return null;
272
+ try {
273
+ return await realpath(path);
274
+ }
275
+ catch {
276
+ return null;
277
+ }
278
+ }
279
+ async function checkCliCommand() {
280
+ const bins = await findPathExecutables("floom");
281
+ if (bins.length === 0) {
282
+ return {
283
+ name: "CLI command",
284
+ status: "ok",
285
+ detail: "`floom` is not globally installed. `npx -y @floomhq/floom ...` is ready.",
286
+ };
287
+ }
288
+ const active = bins[0] ?? "";
289
+ const activeReal = await safeRealpath(active);
290
+ const currentReal = await safeRealpath(process.argv[1]);
291
+ if (activeReal && currentReal && activeReal === currentReal) {
292
+ return {
293
+ name: "CLI command",
294
+ status: "ok",
295
+ detail: `floom resolves to ${active}`,
296
+ };
297
+ }
298
+ const activeText = await readExecutableSample(active);
299
+ const activeVersion = await readExecutableVersion(active);
300
+ const looksLikeOldRuntime = active.includes(".floom/repo") ||
301
+ active.includes("/skills-minimal/") ||
302
+ activeText.includes("unknown command") ||
303
+ activeText.includes("Floom runtime") ||
304
+ (activeVersion !== null && compareSemverish(activeVersion, CLI_VERSION) < 0);
305
+ return {
306
+ name: "CLI command",
307
+ status: looksLikeOldRuntime ? "warn" : "ok",
308
+ detail: looksLikeOldRuntime
309
+ ? `Another floom command is first on PATH: ${active}${activeVersion ? ` (${formatVersionLabel(activeVersion)})` : ""}`
310
+ : `floom resolves to ${active}`,
311
+ ...(looksLikeOldRuntime
312
+ ? {
313
+ hint: `If \`floom doctor\` opens the old runtime CLI, remove that file or move it later in PATH, then run \`npm i -g @floomhq/floom\`. Inspect with: command -v floom`,
314
+ }
315
+ : {}),
316
+ };
317
+ }
318
+ async function readExecutableSample(path) {
319
+ try {
320
+ return (await readFile(path, "utf8")).slice(0, 4000);
321
+ }
322
+ catch {
323
+ return "";
324
+ }
325
+ }
326
+ async function readExecutableVersion(path) {
327
+ try {
328
+ const { stdout, stderr } = await execFile(path, ["--version"], { timeout: 1500 });
329
+ const text = `${stdout}\n${stderr}`;
330
+ return text.match(/\d+\.\d+\.\d+(?:[-+][A-Za-z0-9._-]+)?/)?.[0] ?? null;
331
+ }
332
+ catch {
333
+ return null;
334
+ }
335
+ }
240
336
  export async function doctor(opts = {}) {
241
337
  const target = opts.target ?? "claude";
338
+ process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)}, target: ${target})`)}\n\n`);
242
339
  const checks = await Promise.all([
340
+ checkCliCommand(),
243
341
  checkAuth(),
244
342
  checkMcp(),
245
343
  checkTargetDir(target),
246
344
  checkLastSync(target),
247
345
  checkVersion(),
248
346
  ]);
249
- const anyFail = checks.some((c) => c.status === "fail");
250
- const anyWarn = checks.some((c) => c.status === "warn");
251
- const status = anyFail ? "fail" : anyWarn ? "warn" : "ok";
252
- if (opts.json) {
253
- process.stdout.write(`${JSON.stringify({
254
- ok: !anyFail,
255
- status,
256
- version: CLI_VERSION,
257
- target,
258
- config_path: CONFIG_PATH,
259
- checks,
260
- }, null, 2)}\n`);
261
- if (anyFail)
262
- process.exit(1);
263
- return;
264
- }
265
- process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)})`)}\n\n`);
266
347
  // Compute column widths for clean table output.
267
348
  const nameW = Math.max(...checks.map((c) => c.name.length), 6);
268
349
  for (const check of checks) {
@@ -272,6 +353,8 @@ export async function doctor(opts = {}) {
272
353
  process.stdout.write(` ${c.dim("→ " + check.hint)}\n`);
273
354
  }
274
355
  }
356
+ const anyFail = checks.some((c) => c.status === "fail");
357
+ const anyWarn = checks.some((c) => c.status === "warn");
275
358
  process.stdout.write("\n");
276
359
  if (anyFail) {
277
360
  process.stdout.write(` ${c.red("✗ Some checks failed.")} See hints above.\n\n`);
@@ -279,10 +362,8 @@ export async function doctor(opts = {}) {
279
362
  }
280
363
  if (anyWarn) {
281
364
  process.stdout.write(` ${c.yellow("! All critical checks passed, with warnings.")}\n\n`);
282
- process.stdout.write(` ${c.dim("MCP sync is optional and account-backed; add still works without MCP.")}\n\n`);
283
365
  process.exit(0);
284
366
  }
285
367
  process.stdout.write(` ${c.green("✓ All checks passed.")} Floom is healthy.\n\n`);
286
- process.stdout.write(` ${c.dim("MCP sync is optional and account-backed; add still works without MCP.")}\n`);
287
368
  process.stdout.write(` ${c.dim("Config: " + CONFIG_PATH)}\n\n`);
288
369
  }
package/dist/errors.js CHANGED
@@ -23,7 +23,7 @@ export function friendlyHttp(status, action) {
23
23
  if (/fetch|inspect|add|install|show|get|search|list|info/i.test(action)) {
24
24
  return new FloomError("Skill not found.", "Check the link or slug, then try again.");
25
25
  }
26
- return new FloomError("Skill not found.", "Publish as a new skill for now. Publisher-side version updates are planned for a later release.");
26
+ return new FloomError("Skill not found.", "Run `npx -y @floomhq/floom publish <path>` to create a new one.");
27
27
  }
28
28
  if (status === 413) {
29
29
  return new FloomError("That file is too large to publish.");
package/dist/info.js CHANGED
@@ -28,7 +28,7 @@ export async function info(opts) {
28
28
  const spinner = opts.json ? null : ora({ text: c.dim(`Loading ${slug}...`), color: "yellow" }).start();
29
29
  let detail;
30
30
  try {
31
- detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "info skill metadata", cfg?.accessToken);
31
+ detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "load skill metadata", cfg?.accessToken);
32
32
  }
33
33
  finally {
34
34
  spinner?.stop();
package/dist/init.js CHANGED
@@ -1,5 +1,5 @@
1
- import { writeFile, access } from "node:fs/promises";
2
- import { dirname, resolve, basename } from "node:path";
1
+ import { writeFile, access, mkdir } from "node:fs/promises";
2
+ import { dirname, resolve, basename, extname, join } from "node:path";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { stdin as input, stdout as output } from "node:process";
5
5
  import { c, symbols } from "./ui.js";
@@ -30,7 +30,7 @@ version: 1.0
30
30
  `,
31
31
  "brand-voice": `---
32
32
  title: Brand voice
33
- description: Help an agent write in our company voice.
33
+ description: Keep agent writing aligned with our company voice.
34
34
  type: knowledge
35
35
  installs_as: memory
36
36
  version: 1.0
@@ -38,29 +38,27 @@ version: 1.0
38
38
 
39
39
  # Brand Voice
40
40
 
41
- ## Use when
42
- - Writing customer-facing copy
43
- - Rewriting drafts to match our tone
44
- - Reviewing messaging before it ships
41
+ ## Use this when
42
+
43
+ - Writing external copy
44
+ - Editing founder posts
45
+ - Drafting website, email, or support text
45
46
 
46
47
  ## Voice rules
47
- - Sound clear, direct, and useful.
48
- - Prefer concrete nouns and short sentences.
49
- - Avoid hype, filler, and generic AI language.
50
48
 
51
- ## Words we use
52
- - Replace this list with approved terms.
49
+ - Be clear and specific.
50
+ - Use short paragraphs.
51
+ - Avoid hype, filler, and vague claims.
53
52
 
54
- ## Words we avoid
55
- - Replace this list with banned or overused terms.
53
+ ## Company facts
56
54
 
57
- ## Examples
58
- - Before:
59
- - After:
55
+ - Add product facts here.
56
+ - Add banned phrases here.
57
+ - Add preferred examples here.
60
58
  `,
61
59
  "pr-review": `---
62
60
  title: PR review
63
- description: Review code changes with risk-first feedback.
61
+ description: Review pull requests for correctness, regressions, and missing tests.
64
62
  type: workflow
65
63
  installs_as: claude_skill
66
64
  version: 1.0
@@ -68,21 +66,26 @@ version: 1.0
68
66
 
69
67
  # PR Review
70
68
 
71
- ## Use when
72
- - Reviewing a pull request or diff
73
- - Checking a change before merge
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
74
80
 
75
- ## Review order
76
- 1. Correctness and regressions
77
- 2. Security and data safety
78
- 3. Tests and missing edge cases
79
- 4. Maintainability
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.
80
85
 
81
86
  ## Output
82
- - Lead with findings.
83
- - Include file paths and line references.
84
- - Keep style comments out unless they affect behavior.
85
- - If no issues are found, say that clearly and name any test gaps.
87
+
88
+ List findings first, ordered by severity, with file and line references.
86
89
  `,
87
90
  sales: `---
88
91
  title: Sales research
@@ -94,27 +97,25 @@ version: 1.0
94
97
 
95
98
  # Sales Research
96
99
 
97
- ## Use when
98
- - Preparing for a prospect call
99
- - Writing a relevant outbound message
100
- - Summarizing account context for the team
100
+ ## Goal
101
+
102
+ Prepare useful context before contacting an account.
101
103
 
102
- ## Gather
103
- - Company
104
- - Buyer persona
105
- - Recent trigger
104
+ ## Research checklist
105
+
106
+ - Company description
107
+ - Current priorities or trigger events
108
+ - Relevant people
106
109
  - Likely pain
107
- - Existing tools or workflow
110
+ - Specific reason to reach out
108
111
 
109
112
  ## Output
110
- - 5 bullet account summary
111
- - 3 likely pain points
112
- - 2 tailored opener angles
113
- - 1 clear next action
113
+
114
+ Return a short account brief and a first-message angle.
114
115
  `,
115
116
  support: `---
116
117
  title: Support tone
117
- description: Answer support tickets with a clear and calm company voice.
118
+ description: Keep support replies concise, helpful, and calm.
118
119
  type: instruction
119
120
  installs_as: memory
120
121
  version: 1.0
@@ -122,68 +123,59 @@ version: 1.0
122
123
 
123
124
  # Support Tone
124
125
 
125
- ## Use when
126
- - Replying to customer support messages
127
- - Summarizing customer issues
128
- - Drafting escalation notes
129
-
130
126
  ## Rules
131
- - Acknowledge the issue in plain language.
127
+
128
+ - Acknowledge the issue directly.
132
129
  - Give the next concrete step.
133
- - Do not over-apologize.
134
- - Do not invent product behavior.
135
- - Escalate when data, billing, or account access is involved.
136
-
137
- ## Reply shape
138
- 1. Short acknowledgement
139
- 2. Direct answer or next step
140
- 3. What happens next
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.
141
136
  `,
142
137
  onboarding: `---
143
- title: Team onboarding
144
- description: Help a new teammate understand how this team works.
138
+ title: Onboarding context
139
+ description: Give agents the context needed to help new teammates.
145
140
  type: knowledge
146
141
  installs_as: memory
147
142
  version: 1.0
148
143
  ---
149
144
 
150
- # Team Onboarding
145
+ # Onboarding Context
151
146
 
152
- ## Use when
153
- - A new teammate asks how work gets done
154
- - An agent needs company or team context
155
- - Creating first-week task plans
147
+ ## Company
156
148
 
157
- ## Team context
158
- - Mission:
159
- - Customers:
160
- - Current priorities:
161
- - Tools:
149
+ - Add what the company does.
150
+ - Add important product areas.
162
151
 
163
152
  ## How we work
164
- - Decision rules:
165
- - Review process:
166
- - Communication norms:
167
- - Definition of done:
168
-
169
- ## First-week checklist
170
- - Read:
171
- - Set up:
172
- - Ask:
173
- - Ship:
153
+
154
+ - Add team norms.
155
+ - Add review expectations.
156
+ - Add recurring workflows.
157
+
158
+ ## Useful links
159
+
160
+ - Add docs and repositories.
174
161
  `,
175
162
  };
176
- export async function init(filename, template = "generic") {
177
- const target = filename ?? "skill.md";
178
- const filePath = resolve(process.cwd(), target);
179
- const body = TEMPLATES[template];
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);
180
172
  const exists = await fileExists(filePath);
181
173
  if (exists) {
182
174
  if (!process.stdin.isTTY) {
183
- throw new FloomError(`${target} already exists.`, "Re-run with a different filename, or delete it first.");
175
+ throw new FloomError(`${outputTarget} already exists.`, "Re-run with a different filename, or delete it first.");
184
176
  }
185
177
  const rl = createInterface({ input, output });
186
- const answer = (await rl.question(`${c.yellow("?")} ${target} already exists. Overwrite? ${c.dim("(y/N)")} `)).trim().toLowerCase();
178
+ const answer = (await rl.question(`${c.yellow("?")} ${outputTarget} already exists. Overwrite? ${c.dim("(y/N)")} `)).trim().toLowerCase();
187
179
  rl.close();
188
180
  if (answer !== "y" && answer !== "yes") {
189
181
  process.stdout.write(`\n${c.dim("Cancelled. Nothing was written.")}\n\n`);
@@ -191,7 +183,9 @@ export async function init(filename, template = "generic") {
191
183
  }
192
184
  }
193
185
  try {
194
- await writeFile(filePath, body, "utf8");
186
+ if (folderPath)
187
+ await mkdir(folderPath, { recursive: true });
188
+ await writeFile(filePath, TEMPLATES[template], "utf8");
195
189
  }
196
190
  catch (err) {
197
191
  const code = err.code;
@@ -201,14 +195,15 @@ export async function init(filename, template = "generic") {
201
195
  if (code === "EISDIR") {
202
196
  throw new FloomError(`That's a directory, not a file: ${target}`);
203
197
  }
204
- throw new FloomError(`Couldn't create ${target}: ${err.message}`);
198
+ throw new FloomError(`Couldn't create ${outputTarget}: ${err.message}`);
205
199
  }
206
- process.stdout.write(`\n${symbols.ok} Created ${c.bold(basename(filePath))}\n`);
207
- process.stdout.write(` ${c.dim(`Template: ${template}`)}\n`);
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`);
208
203
  process.stdout.write(`\n ${c.bold("Next")}\n`);
209
204
  process.stdout.write(` ${c.dim("1.")} Fill in the title, description, and instructions.\n`);
210
- process.stdout.write(` ${c.dim("2.")} Check it: ${c.cyan(`npx -y @floomhq/floom scan ${shellQuote(target)}`)}\n`);
211
- process.stdout.write(` ${c.dim("3.")} Publish: ${c.cyan(`npx -y @floomhq/floom publish ${shellQuote(target)} --public`)}\n\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`);
212
207
  }
213
208
  function shellQuote(value) {
214
209
  if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))