@usefillo/cli 0.8.0 → 0.10.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.js CHANGED
@@ -1294,6 +1294,10 @@ function _default(innerType, defaultValue) {
1294
1294
  }
1295
1295
 
1296
1296
  // ../core/dist/index.js
1297
+ var CONTENT_KINDS = ["heading", "paragraph", "divider"];
1298
+ function isField(block) {
1299
+ return !CONTENT_KINDS.includes(block.kind);
1300
+ }
1297
1301
  var C = (iso2, name, dialCode, lengths = [], groups = [], example = "") => ({ iso2, name, dialCode, lengths, groups, example });
1298
1302
  var PHONE_COUNTRIES = [
1299
1303
  C("US", "United States", "1", [10], [3, 3, 4], "2015550123"),
@@ -1415,7 +1419,11 @@ var blockSchema = looseObject({
1415
1419
  var pageSchema = object({
1416
1420
  id: idSchema,
1417
1421
  title: optional(string2().check(_maxLength(500))),
1418
- blocks: array(blockSchema).check(_maxLength(500))
1422
+ blocks: array(blockSchema).check(_maxLength(500)),
1423
+ // Optional conditional page flow. Kept as unknown here (this schema is strict,
1424
+ // so it would otherwise be dropped at parse) and normalized by hand below once
1425
+ // the page-id set is known — a jump to a missing page is dropped, never kept.
1426
+ next: optional(unknown())
1419
1427
  });
1420
1428
  var schemaShape = object({
1421
1429
  version: literal(1),
@@ -1426,7 +1434,7 @@ var schemaShape = object({
1426
1434
  });
1427
1435
  var MAX_SCHEMA_VERSION = 1;
1428
1436
  var FILLO_SCHEMA_VERSION = 1;
1429
- var FILLO_SDK_VERSION = true ? "0.8.0" : "0.0.0-dev";
1437
+ var FILLO_SDK_VERSION = true ? "0.10.0" : "0.0.0-dev";
1430
1438
  function str(value, max, fallback = "") {
1431
1439
  return typeof value === "string" ? value.trim().slice(0, max) : fallback;
1432
1440
  }
@@ -1481,6 +1489,23 @@ function conditions(value) {
1481
1489
  });
1482
1490
  return normalized.length ? normalized : void 0;
1483
1491
  }
1492
+ function jumpRules(value, pageIds, sourceId, allowedFieldIds) {
1493
+ if (!Array.isArray(value)) return void 0;
1494
+ const rules = value.slice(0, 50).flatMap((item) => {
1495
+ if (!item || typeof item !== "object") return [];
1496
+ const rec = item;
1497
+ const to = typeof rec.to === "string" ? rec.to : void 0;
1498
+ if (!to || to !== "end" && !pageIds.has(to)) return [];
1499
+ if (to === sourceId) return [];
1500
+ const rawWhen = rec.when;
1501
+ if (rawWhen !== void 0 && !Array.isArray(rawWhen)) return [];
1502
+ const when2 = conditions(rawWhen);
1503
+ if (Array.isArray(rawWhen) && rawWhen.length > 0 && when2 === void 0) return [];
1504
+ if (when2 && when2.some((c) => !allowedFieldIds.has(c.fieldId))) return [];
1505
+ return [{ when: when2 ?? [], to }];
1506
+ });
1507
+ return rules.length ? rules : void 0;
1508
+ }
1484
1509
  function baseBlock(rec) {
1485
1510
  return {
1486
1511
  id: str(rec.id, 128),
@@ -1524,6 +1549,17 @@ function normalizeResponseLimit(value) {
1524
1549
  onRepeat
1525
1550
  };
1526
1551
  }
1552
+ function normalizeTrust(value) {
1553
+ if (!value || typeof value !== "object") return void 0;
1554
+ const rec = value;
1555
+ const unverified = rec.unverified === "allow" || rec.unverified === "quarantine" ? rec.unverified : void 0;
1556
+ const challenge = rec.challenge === "turnstile" ? "turnstile" : void 0;
1557
+ if (!unverified && !challenge) return void 0;
1558
+ return {
1559
+ ...unverified ? { unverified } : {},
1560
+ ...challenge ? { challenge } : {}
1561
+ };
1562
+ }
1527
1563
  function normalizeSettings(value) {
1528
1564
  const rec = value && typeof value === "object" ? value : {};
1529
1565
  const submitMode = rec.submitMode === "button" || rec.submitMode === "auto" ? rec.submitMode : void 0;
@@ -1535,6 +1571,7 @@ function normalizeSettings(value) {
1535
1571
  redirectUrl: normalizeUrl(rec.redirectUrl),
1536
1572
  showProgress: bool(rec.showProgress),
1537
1573
  responseLimit: normalizeResponseLimit(rec.responseLimit),
1574
+ trust: normalizeTrust(rec.trust),
1538
1575
  notifyEmail: email2().check(_maxLength(254)).safeParse(rec.notifyEmail).success ? rec.notifyEmail : void 0,
1539
1576
  sendReceipt: bool(rec.sendReceipt),
1540
1577
  saveProgress: bool(rec.saveProgress),
@@ -1674,15 +1711,24 @@ function normalizeFormSchema(input) {
1674
1711
  if (parsed.data.version > MAX_SCHEMA_VERSION) {
1675
1712
  return { ok: false, error: `Unsupported schema version: ${parsed.data.version}` };
1676
1713
  }
1677
- const pages = parsed.data.pages.map((page) => {
1714
+ const rebuilt = parsed.data.pages.map((page) => {
1678
1715
  const blocks = page.blocks.flatMap((raw) => {
1679
1716
  const block = normalizeBlock(raw);
1680
1717
  return block ? [block] : [];
1681
1718
  });
1719
+ return { id: str(page.id, 128), title: optionalStr(page.title, 500), blocks, next: page.next };
1720
+ });
1721
+ const targetIds = new Set(rebuilt.map((p) => p.id));
1722
+ const priorFieldIds = /* @__PURE__ */ new Set();
1723
+ const pages = rebuilt.map((page) => {
1724
+ for (const block of page.blocks) {
1725
+ if (isField(block)) priorFieldIds.add(block.id);
1726
+ }
1682
1727
  return cleanObject({
1683
- id: str(page.id, 128),
1684
- title: optionalStr(page.title, 500),
1685
- blocks
1728
+ id: page.id,
1729
+ title: page.title,
1730
+ blocks: page.blocks,
1731
+ next: jumpRules(page.next, targetIds, page.id, new Set(priorFieldIds))
1686
1732
  });
1687
1733
  });
1688
1734
  const pageIds = /* @__PURE__ */ new Set();
@@ -1881,6 +1927,7 @@ function openBrowser(url) {
1881
1927
  } catch {
1882
1928
  return;
1883
1929
  }
1930
+ if (process.env.CI === "true") return;
1884
1931
  try {
1885
1932
  if (process.platform === "win32") {
1886
1933
  const child = spawn("cmd", ["/c", "start", "", safeUrl], { stdio: "ignore", detached: true });
@@ -1917,7 +1964,7 @@ async function readJson(res) {
1917
1964
  die(`Unexpected non-JSON response from ${API} (${res.status}).`);
1918
1965
  }
1919
1966
  }
1920
- async function login(flags) {
1967
+ async function login(flags, options2 = {}) {
1921
1968
  const apiBase = flagString(flags, "api")?.replace(/\/$/, "") ?? API;
1922
1969
  const run = flagString(flags, "run");
1923
1970
  const progressToken = flagString(flags, "token");
@@ -1966,7 +2013,9 @@ Login failed (${r.status}).`);
1966
2013
  console.log("\n");
1967
2014
  await whoami(apiBase);
1968
2015
  console.log(
1969
- run ? `
2016
+ options2.continueToAgentRun ? `
2017
+ ${dim("Workspace approved. Connecting this setup...")}
2018
+ ` : run ? `
1970
2019
  ${dim("Return to the Fillo setup prompt and run its next command.")}
1971
2020
  ` : `
1972
2021
  ${dim("Now run:")} fillo push form.json
@@ -1997,10 +2046,174 @@ async function list() {
1997
2046
  if (!forms.length) return console.log(" No forms yet.");
1998
2047
  for (const f of forms) {
1999
2048
  console.log(
2000
- ` ${f.status === "published" ? "\x1B[32m\u25CF\x1B[0m" : "\u25CB"} ${terminalText(f.name)} ${dim(f.id)}`
2049
+ ` ${f.status === "published" ? "\x1B[32m\u25CF\x1B[0m" : "\u25CB"} ${terminalText(f.name)} ${dim(f.id)} ${dim(f.url)}`
2001
2050
  );
2002
2051
  }
2003
2052
  }
2053
+ async function status(handle) {
2054
+ if (!handle) die("Usage: fillo status <formId|handle>");
2055
+ const res = await api(`/cli/forms/${encodeURIComponent(handle)}`, { token: requireToken() });
2056
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
2057
+ if (res.status === 404) {
2058
+ try {
2059
+ JSON.parse(await res.text());
2060
+ } catch {
2061
+ die(
2062
+ "This Fillo server does not support `fillo status` yet. Update the deployment, or check the form in the dashboard."
2063
+ );
2064
+ }
2065
+ die(`No form matches "${handle}" in this workspace. Run \`fillo list\` to see its forms.`);
2066
+ }
2067
+ const body = await readJson(res);
2068
+ if (!res.ok || !body.form) die(body.error ?? `status failed (${res.status}).`);
2069
+ const form = body.form;
2070
+ console.log(
2071
+ ` ${form.status === "published" ? "\x1B[32m\u25CF\x1B[0m" : "\u25CB"} ${terminalText(form.name)} ${dim(form.id)}`
2072
+ );
2073
+ console.log(` Status: ${bold(form.staged ? "staged" : form.status)}`);
2074
+ if (form.status === "published") console.log(` Live at ${terminalText(form.url)}`);
2075
+ else console.log(` ${dim(`Publishes to ${form.url}`)}`);
2076
+ if (form.staged) {
2077
+ console.log(` ${dim("Staged changes are waiting for review in the Fillo dashboard.")}`);
2078
+ }
2079
+ if (form.warning) {
2080
+ const pending = form.status !== "published" || form.staged === true;
2081
+ console.log(` ${dim(pending ? `Before publishing: ${form.warning}` : form.warning)}`);
2082
+ }
2083
+ if (form.warningUrl) console.log(` Storage settings: ${terminalText(form.warningUrl)}`);
2084
+ }
2085
+ async function publish(handle, flags) {
2086
+ if (!handle) die("Usage: fillo publish <formId|handle> [--allow-breaking]");
2087
+ const res = await api(`/cli/forms/${encodeURIComponent(handle)}/publish`, {
2088
+ method: "POST",
2089
+ token: requireToken(),
2090
+ body: JSON.stringify(
2091
+ flags["allow-breaking"] === true ? { allowBreaking: true } : {}
2092
+ )
2093
+ });
2094
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
2095
+ if (res.status === 404) {
2096
+ try {
2097
+ JSON.parse(await res.text());
2098
+ } catch {
2099
+ die(
2100
+ "This Fillo server does not support `fillo publish` yet. Update the deployment, or publish the form from the dashboard."
2101
+ );
2102
+ }
2103
+ die(`No form matches "${handle}" in this workspace. Run \`fillo list\` to see its forms.`);
2104
+ }
2105
+ const body = await readJson(res);
2106
+ if (res.status === 409 && body.code === "breaking_changes") {
2107
+ const fields = Array.isArray(body.breakingFields) ? body.breakingFields.filter((f) => typeof f === "string") : [];
2108
+ console.error(
2109
+ "\x1B[31m\u2717\x1B[0m Not published \u2014 the staged changes remove or re-type fields that existing responses answered."
2110
+ );
2111
+ if (fields.length) console.error(` Fields: ${terminalText(fields.join(", "))}`);
2112
+ console.error(
2113
+ ` ${dim("Recorded answers are kept, but the live form, grid, and exports stop showing these fields.")}`
2114
+ );
2115
+ console.error(" Re-run with --allow-breaking to publish anyway.");
2116
+ process.exit(1);
2117
+ }
2118
+ if (res.status === 409 && typeof body.warningUrl === "string" && body.warningUrl) {
2119
+ console.error(`\x1B[31m\u2717\x1B[0m ${terminalText(body.error ?? `publish failed (${res.status}).`)}`);
2120
+ console.error(` Storage settings: ${terminalText(body.warningUrl)}`);
2121
+ process.exit(1);
2122
+ }
2123
+ if (!res.ok || !body.form) die(body.error ?? `publish failed (${res.status}).`);
2124
+ const form = body.form;
2125
+ if (body.changed === false) {
2126
+ console.log(
2127
+ `
2128
+ \x1B[32m\u2713\x1B[0m ${bold(terminalText(form.name))} is already live \u2014 nothing staged to publish.`
2129
+ );
2130
+ } else {
2131
+ console.log(`
2132
+ \x1B[32m\u2713\x1B[0m Published ${bold(terminalText(form.name))} ${dim(form.id)}`);
2133
+ }
2134
+ console.log(` Live at ${terminalText(form.url)}
2135
+ `);
2136
+ }
2137
+ async function loadResponseData(file) {
2138
+ let value;
2139
+ if (file === "-") {
2140
+ try {
2141
+ const chunks = [];
2142
+ let bytes = 0;
2143
+ for await (const chunk of process.stdin) {
2144
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
2145
+ bytes += buffer.byteLength;
2146
+ if (bytes > MAX_STDIN_SCHEMA_BYTES) {
2147
+ die("The response data on stdin is too large (maximum 1 MB).");
2148
+ }
2149
+ chunks.push(buffer);
2150
+ }
2151
+ const input = Buffer.concat(chunks).toString("utf8");
2152
+ if (!input.trim()) {
2153
+ die("stdin is empty - pipe one JSON answer object to `fillo test-response <form> -`.");
2154
+ }
2155
+ value = JSON.parse(input);
2156
+ } catch (error) {
2157
+ die(`Couldn't read response data from stdin: ${error.message}`);
2158
+ }
2159
+ } else {
2160
+ const abs = isAbsolute(file) ? file : resolve(process.cwd(), file);
2161
+ if (!abs.endsWith(".json")) {
2162
+ die("Test response data must be a .json file (or - for JSON on stdin).");
2163
+ }
2164
+ try {
2165
+ value = JSON.parse(readFileSync(abs, "utf8"));
2166
+ } catch (error) {
2167
+ die(`Couldn't read ${file}: ${error.message}`);
2168
+ }
2169
+ }
2170
+ if (!isRecord(value)) {
2171
+ die("Test response data must be one JSON object keyed by field id.");
2172
+ }
2173
+ return value;
2174
+ }
2175
+ async function testResponse(handle, file) {
2176
+ if (!handle || !file) {
2177
+ die("Usage: fillo test-response <formId|handle> <answers.json|->");
2178
+ }
2179
+ const token = requireToken();
2180
+ const data = await loadResponseData(file);
2181
+ const res = await api(`/cli/forms/${encodeURIComponent(handle)}/test-response`, {
2182
+ method: "POST",
2183
+ token,
2184
+ body: JSON.stringify({ data })
2185
+ });
2186
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
2187
+ if (res.status === 404) {
2188
+ try {
2189
+ JSON.parse(await res.text());
2190
+ } catch {
2191
+ die(
2192
+ "This Fillo server does not support `fillo test-response` yet. Update the deployment, then retry."
2193
+ );
2194
+ }
2195
+ die(`No form matches "${handle}" in this workspace. Run \`fillo list\` to see its forms.`);
2196
+ }
2197
+ const body = await readJson(res);
2198
+ if (res.status === 422 && body.errors && typeof body.errors === "object") {
2199
+ console.error("\x1B[31m\u2717\x1B[0m Test response failed server validation.");
2200
+ for (const [field2, message] of Object.entries(body.errors)) {
2201
+ console.error(` ${terminalText(field2)}: ${terminalText(message)}`);
2202
+ }
2203
+ process.exit(1);
2204
+ }
2205
+ if (!res.ok || !body.id || body.preview !== true) {
2206
+ die(body.error ?? `test response failed (${res.status}).`);
2207
+ }
2208
+ console.log(
2209
+ `
2210
+ \x1B[32m\u2713\x1B[0m Test response passed the ${bold(body.schema ?? "current")} schema ${dim(body.id)}`
2211
+ );
2212
+ console.log(
2213
+ ` ${dim("Preview only \u2014 excluded from responses, limits, delivery, and analytics; auto-deletes after 7 days.")}
2214
+ `
2215
+ );
2216
+ }
2004
2217
  async function loadSchema(file, allowCode) {
2005
2218
  if (file === "-") {
2006
2219
  try {
@@ -2114,6 +2327,7 @@ function printSynced(body, requestedStage) {
2114
2327
  console.log(` Live at ${terminalText(`${API}/f/${body.slug}`)}`);
2115
2328
  }
2116
2329
  if (body.warning) console.log(` ${dim(`Before publishing: ${body.warning}`)}`);
2330
+ if (body.warningUrl) console.log(` Storage settings: ${terminalText(body.warningUrl)}`);
2117
2331
  console.log(` Embed: <FilloForm formId="${terminalText(formId)}" />
2118
2332
  `);
2119
2333
  }
@@ -2203,7 +2417,14 @@ async function push(file, flags) {
2203
2417
  })
2204
2418
  });
2205
2419
  const body = await readJson(res);
2206
- if (!res.ok || !body.formId) die(body.error ?? `push failed (${res.status}).`);
2420
+ if (!res.ok || !body.formId) {
2421
+ if (res.status === 409 && typeof body.warningUrl === "string" && body.warningUrl) {
2422
+ console.error(`\x1B[31m\u2717\x1B[0m ${terminalText(body.error ?? `push failed (${res.status}).`)}`);
2423
+ console.error(` Storage settings: ${terminalText(body.warningUrl)}`);
2424
+ process.exit(1);
2425
+ }
2426
+ die(body.error ?? `push failed (${res.status}).`);
2427
+ }
2207
2428
  printPushed(body.formId, body.url, !!body.updated);
2208
2429
  }
2209
2430
  return;
@@ -2242,6 +2463,18 @@ async function init(flags) {
2242
2463
  `);
2243
2464
  }
2244
2465
  var AGENT_ACTIONS = ["claim_required", "storage_required", "publish_required"];
2466
+ var AGENT_EVENT_STATUSES = [
2467
+ "created",
2468
+ "connected",
2469
+ "asking",
2470
+ "planning",
2471
+ "installing",
2472
+ "editing",
2473
+ "checking",
2474
+ "needs_action",
2475
+ "done",
2476
+ "error"
2477
+ ];
2245
2478
  var FORM_STATUSES = ["draft", "published"];
2246
2479
  function enumFlag(flags, key, allowed) {
2247
2480
  const value = flags[key];
@@ -2265,46 +2498,39 @@ async function agent(subcommand, flags) {
2265
2498
  return agentHelp();
2266
2499
  }
2267
2500
  if (!run || !token) {
2268
- die("Usage: fillo agent <connect|event> --run <id> --token <token> [--api <url>]");
2501
+ die("Usage: fillo agent <bootstrap|connect|event> --run <id> --token <token> [--api <url>]");
2269
2502
  }
2270
2503
  if (flags.account !== void 0 && flags.account !== true) {
2271
2504
  die("--account does not take a value.");
2272
2505
  }
2273
- if (subcommand === "connect") {
2274
- const account = flags.account === true ? await attachAgentAccount(apiBase, run, token) : void 0;
2275
- await postAgentEvent(apiBase, run, token, {
2276
- status: "connected",
2277
- message: "Agent connected. Reading the app now."
2278
- });
2279
- console.log(` \x1B[32m\u2713\x1B[0m Live progress connected.`);
2280
- if (account) {
2281
- console.log(` \x1B[32m\u2713\x1B[0m ${bold(account.workspace)}`);
2282
- console.log(` ${dim("Publishable key:")} ${account.publishableKey}`);
2506
+ if (subcommand === "bootstrap") {
2507
+ installSkill(flags);
2508
+ if (flags.account === true) {
2509
+ await login(flags, { continueToAgentRun: true });
2283
2510
  }
2284
- console.log(` ${dim("Report next steps with:")}`);
2285
- console.log(
2286
- ` fillo agent event --api ${terminalText(apiBase)} --run ${terminalText(run)} --token <same-token> --status editing --message "Editing the form screen"`
2287
- );
2288
- return;
2511
+ return connectAgentRun(apiBase, run, token, flags.account === true);
2512
+ }
2513
+ if (subcommand === "connect") {
2514
+ return connectAgentRun(apiBase, run, token, flags.account === true);
2289
2515
  }
2290
2516
  if (flags.account === true) {
2291
- die("--account can only be used with `fillo agent connect`.");
2517
+ die("--account can only be used with `fillo agent bootstrap` or `fillo agent connect`.");
2292
2518
  }
2293
2519
  if (subcommand === "event") {
2294
- const status = flagString(flags, "status");
2520
+ const status2 = enumFlag(flags, "status", AGENT_EVENT_STATUSES);
2295
2521
  const message = flagString(flags, "message");
2296
- if (!status) die("Usage: fillo agent event --status <status> --message <short update>");
2522
+ if (!status2) die('Usage: fillo agent event --status <status> --message "<what the human does next>"');
2297
2523
  const action = enumFlag(flags, "action", AGENT_ACTIONS);
2298
2524
  const formStatus = enumFlag(flags, "form-status", FORM_STATUSES);
2299
2525
  const formId = optionalStringFlag(flags, "form-id");
2300
- if ((status === "done" || status === "needs_action") && !formId) {
2301
- die(`--form-id is required when --status is ${status}.`);
2526
+ if ((status2 === "done" || status2 === "needs_action") && !formId) {
2527
+ die(`--form-id is required when --status is ${status2}.`);
2302
2528
  }
2303
- if (status === "needs_action" && !action) {
2529
+ if (status2 === "needs_action" && !action) {
2304
2530
  die("--action is required when --status is needs_action.");
2305
2531
  }
2306
2532
  await postAgentEvent(apiBase, run, token, {
2307
- status,
2533
+ status: status2,
2308
2534
  message,
2309
2535
  appUrl: flagString(flags, "app-url"),
2310
2536
  action,
@@ -2317,16 +2543,44 @@ async function agent(subcommand, flags) {
2317
2543
  }
2318
2544
  die(`Unknown agent command: ${subcommand}`);
2319
2545
  }
2320
- var SKILL_AGENTS = [
2321
- "agents",
2322
- "codex",
2323
- "claude",
2324
- "cursor",
2325
- "copilot",
2326
- "gemini"
2327
- ];
2546
+ async function connectAgentRun(apiBase, run, token, attachAccount) {
2547
+ const account = attachAccount ? await attachAgentAccount(apiBase, run, token) : void 0;
2548
+ await postAgentEvent(apiBase, run, token, {
2549
+ status: "connected",
2550
+ message: "Agent connected. Reading the app now."
2551
+ });
2552
+ console.log(` \x1B[32m\u2713\x1B[0m Live progress connected.`);
2553
+ if (account) {
2554
+ console.log(` \x1B[32m\u2713\x1B[0m ${bold(account.workspace)}`);
2555
+ console.log(` ${dim("Publishable key:")} ${account.publishableKey}`);
2556
+ }
2557
+ console.log(` ${dim("Report next steps with:")}`);
2558
+ console.log(
2559
+ ` fillo agent event --api ${terminalText(apiBase)} --run ${terminalText(run)} --token <same-token> --status editing --message "Editing the form screen"`
2560
+ );
2561
+ console.log(
2562
+ ` ${dim("For needs_action or done, lead --message with what the human does next \u2014 max 180 chars, longer is cut off.")}`
2563
+ );
2564
+ }
2565
+ var SKILL_AGENT_DIRECTORIES = {
2566
+ shared: ".agents/skills",
2567
+ universal: ".agents/skills",
2568
+ agents: ".agents/skills",
2569
+ codex: ".agents/skills",
2570
+ cursor: ".agents/skills",
2571
+ copilot: ".agents/skills",
2572
+ "github-copilot": ".agents/skills",
2573
+ gemini: ".agents/skills",
2574
+ "gemini-cli": ".agents/skills",
2575
+ amp: ".agents/skills",
2576
+ cline: ".agents/skills",
2577
+ opencode: ".agents/skills",
2578
+ warp: ".agents/skills",
2579
+ claude: ".claude/skills",
2580
+ "claude-code": ".claude/skills"
2581
+ };
2328
2582
  function isSkillAgent(value) {
2329
- return SKILL_AGENTS.some((agent2) => agent2 === value);
2583
+ return Object.prototype.hasOwnProperty.call(SKILL_AGENT_DIRECTORIES, value);
2330
2584
  }
2331
2585
  function findProjectRoot(start) {
2332
2586
  let current = resolve(start);
@@ -2337,19 +2591,25 @@ function findProjectRoot(start) {
2337
2591
  current = parent;
2338
2592
  }
2339
2593
  }
2340
- function detectSkillAgent() {
2341
- if (process.env.CLAUDECODE === "1" || process.env.CLAUDE_CODE === "1") {
2342
- return "claude";
2343
- }
2344
- return "agents";
2345
- }
2346
- function parseSkillAgent(flags) {
2347
- if (flags.agent === true || flags.agent === "") {
2348
- die(`--agent requires one of: ${SKILL_AGENTS.join(", ")}`);
2594
+ function parseSkillDirectories(flags) {
2595
+ const agent2 = flagString(flags, "agent");
2596
+ const customDirectory = flagString(flags, "dir");
2597
+ if (agent2 && customDirectory) die("Choose either --agent or --dir, not both.");
2598
+ if (customDirectory) {
2599
+ if (isAbsolute(customDirectory)) {
2600
+ die("--dir must be relative to the project root, or to your home directory with --global.");
2601
+ }
2602
+ const parts = customDirectory.split(/[\\/]+/).filter((part) => part && part !== ".");
2603
+ if (parts.length === 0 || parts.some((part) => part === "..")) {
2604
+ die("--dir must name a skill directory inside the selected project or home scope.");
2605
+ }
2606
+ return [join(...parts)];
2349
2607
  }
2350
- const value = flagString(flags, "agent") ?? detectSkillAgent();
2351
- if (isSkillAgent(value)) return value;
2352
- die(`--agent must be one of: ${SKILL_AGENTS.join(", ")}`);
2608
+ if (!agent2) return [SKILL_AGENT_DIRECTORIES.shared, SKILL_AGENT_DIRECTORIES.claude];
2609
+ if (isSkillAgent(agent2)) return [SKILL_AGENT_DIRECTORIES[agent2]];
2610
+ die(
2611
+ `Unknown agent ${agent2}. Use --agent shared, --agent claude, or --dir <agent-skill-directory>.`
2612
+ );
2353
2613
  }
2354
2614
  function skillFiles(root, relative2 = "") {
2355
2615
  const directory = join(root, relative2);
@@ -2436,52 +2696,69 @@ function installSkill(flags) {
2436
2696
  if (flags.global === true && flags.project === true) {
2437
2697
  die("Choose either --project or --global, not both.");
2438
2698
  }
2439
- const agent2 = parseSkillAgent(flags);
2699
+ const skillDirectories = parseSkillDirectories(flags);
2440
2700
  const global = flags.global === true;
2441
- const agentDirectory = agent2 === "claude" ? ".claude" : ".agents";
2442
2701
  const scopeRoot = global ? homedir() : findProjectRoot(process.cwd());
2443
- const skillsRoot = join(scopeRoot, agentDirectory, "skills");
2444
- const target = join(skillsRoot, "build-with-fillo");
2445
- const destinationParts = [agentDirectory, "skills", "build-with-fillo"];
2446
2702
  if (!existsSync(BUNDLED_SKILL_DIR)) {
2447
2703
  die("This CLI package is missing its bundled Fillo skill. Reinstall @usefillo/cli@latest.");
2448
2704
  }
2449
- assertSafeSkillDestination(scopeRoot, destinationParts);
2450
- const targetExists = pathEntryExists(target);
2451
- const updatingManagedSkill = targetExists && isCliManagedFilloSkill(target);
2452
- if (targetExists) {
2453
- if (sameSkillBundle(BUNDLED_SKILL_DIR, target)) {
2454
- console.log(`
2455
- \x1B[32m\u2713\x1B[0m Fillo skill is already installed.`);
2456
- console.log(` ${dim(target)}
2457
- `);
2458
- return;
2459
- }
2460
- if (!updatingManagedSkill && flags.force !== true) {
2705
+ const destinations = Array.from(
2706
+ new Set(skillDirectories)
2707
+ ).map((skillDirectory) => {
2708
+ const skillsRoot = resolve(scopeRoot, skillDirectory);
2709
+ const target = join(skillsRoot, "build-with-fillo");
2710
+ const relativeSkillsRoot = relative(scopeRoot, skillsRoot);
2711
+ if (relativeSkillsRoot === ".." || relativeSkillsRoot.startsWith(`..${sep}`) || isAbsolute(relativeSkillsRoot)) {
2712
+ die("The skill destination must stay inside the selected project or home scope.");
2713
+ }
2714
+ const destinationParts = [
2715
+ ...relativeSkillsRoot.split(sep).filter(Boolean),
2716
+ "build-with-fillo"
2717
+ ];
2718
+ assertSafeSkillDestination(scopeRoot, destinationParts);
2719
+ const targetExists = pathEntryExists(target);
2720
+ const current = targetExists && sameSkillBundle(BUNDLED_SKILL_DIR, target);
2721
+ const updatingManagedSkill = targetExists && isCliManagedFilloSkill(target);
2722
+ if (targetExists && !current && !updatingManagedSkill && flags.force !== true) {
2461
2723
  die(`A different skill already exists at ${target}. Re-run with --force to replace it.`);
2462
2724
  }
2463
- }
2464
- mkdirSync(skillsRoot, { recursive: true });
2465
- assertSafeSkillDestination(scopeRoot, destinationParts.slice(0, 2));
2466
- const staging = join(skillsRoot, `.build-with-fillo.install-${randomUUID()}`);
2467
- try {
2468
- cpSync(BUNDLED_SKILL_DIR, staging, { recursive: true, errorOnExist: true });
2469
- assertSafeSkillDestination(scopeRoot, destinationParts);
2470
- if (!targetExists && pathEntryExists(target)) {
2471
- throw new Error(`Skill destination changed during install: ${target}`);
2725
+ return {
2726
+ skillsRoot,
2727
+ target,
2728
+ destinationParts,
2729
+ targetExists,
2730
+ current,
2731
+ updatingManagedSkill
2732
+ };
2733
+ });
2734
+ for (const destination of destinations) {
2735
+ if (destination.current) continue;
2736
+ mkdirSync(destination.skillsRoot, { recursive: true });
2737
+ assertSafeSkillDestination(scopeRoot, destination.destinationParts.slice(0, 2));
2738
+ const staging = join(
2739
+ destination.skillsRoot,
2740
+ `.build-with-fillo.install-${randomUUID()}`
2741
+ );
2742
+ try {
2743
+ cpSync(BUNDLED_SKILL_DIR, staging, { recursive: true, errorOnExist: true });
2744
+ assertSafeSkillDestination(scopeRoot, destination.destinationParts);
2745
+ if (!destination.targetExists && pathEntryExists(destination.target)) {
2746
+ throw new Error(`Skill destination changed during install: ${destination.target}`);
2747
+ }
2748
+ moveSkillIntoPlace(staging, destination.target);
2749
+ } finally {
2750
+ if (pathEntryExists(staging)) rmSync(staging, { recursive: true, force: true });
2472
2751
  }
2473
- moveSkillIntoPlace(staging, target);
2474
- } finally {
2475
- if (pathEntryExists(staging)) rmSync(staging, { recursive: true, force: true });
2476
2752
  }
2753
+ const changed = destinations.filter((destination) => !destination.current);
2754
+ const action = changed.length === 0 ? "Fillo skill is already installed" : changed.every((destination) => destination.updatingManagedSkill) ? `Updated ${bold("Build with Fillo")}` : `Installed ${bold("Build with Fillo")}`;
2755
+ console.log(`
2756
+ \x1B[32m\u2713\x1B[0m ${action}.`);
2757
+ for (const destination of destinations) console.log(` ${dim(destination.target)}`);
2758
+ console.log(`
2759
+ Ask your agent to use ${bold("build-with-fillo")}.`);
2477
2760
  console.log(
2478
- `
2479
- \x1B[32m\u2713\x1B[0m ${updatingManagedSkill ? "Updated" : "Installed"} ${bold("Build with Fillo")} for ${agent2}.`
2480
- );
2481
- console.log(` ${dim(target)}`);
2482
- console.log(
2483
- `
2484
- Ask your agent to read ${terminalText(join(target, "SKILL.md"))} and add a Fillo form.
2761
+ ` ${dim(`If it does not discover skills, point it to ${join(destinations[0].target, "SKILL.md")}.`)}
2485
2762
  `
2486
2763
  );
2487
2764
  }
@@ -2534,15 +2811,22 @@ function agentHelp() {
2534
2811
  ${bold("fillo agent")} \u2014 report live progress back to a Fillo prompt modal
2535
2812
 
2536
2813
  ${bold("Commands")}
2814
+ agent bootstrap Install the skill and connect this coding-agent run
2815
+ ${dim("--account approve and attach an existing workspace in the browser")}
2537
2816
  agent connect Connect a coding-agent run to the open browser modal
2538
2817
  ${dim("--account attach the workspace from `fillo login`")}
2539
2818
  agent event Send a short progress update
2540
- ${dim("--status <created|connected|asking|planning|installing|editing|checking|needs_action|done|error>")}
2541
- ${dim('--message "Short update"')}
2819
+ ${dim(`--status <${AGENT_EVENT_STATUSES.join("|")}>`)}
2820
+ ${dim('--message "What the human does next" (max 180 chars)')}
2542
2821
  ${dim("--form-id <id> --form-status <draft|published> --form-name <name>")}
2543
2822
  ${dim("--action <claim_required|storage_required|publish_required>")}
2544
2823
  ${dim("--app-url <localhost-url>")}
2545
2824
  ${dim("--run <id> --token <token> --api <url>")}
2825
+
2826
+ ${dim("For --status needs_action or done, lead the message with what the human does")}
2827
+ ${dim('next, in one or two sentences, plus the form URL or form id \u2014 e.g. "Connect')}
2828
+ ${dim('storage in Fillo, publish the form, then submit one test response".')}
2829
+ ${dim("Max 180 characters \u2014 longer is cut off. Never list changed files in the message.")}
2546
2830
  `);
2547
2831
  }
2548
2832
  function skillHelp() {
@@ -2551,11 +2835,12 @@ function skillHelp() {
2551
2835
 
2552
2836
  ${bold("Commands")}
2553
2837
  skill install Install into this project (default)
2554
- ${dim("--agent <agents|codex|claude|cursor|copilot|gemini>")}
2838
+ ${dim("--agent <shared|claude> choose a standard destination")}
2839
+ ${dim("--dir <path> install into any agent's skill directory")}
2555
2840
  ${dim("--global install for the current user")}
2556
2841
  ${dim("--force replace a different existing copy")}
2557
2842
 
2558
- ${dim("Codex, Cursor, Copilot, and Gemini use shared .agents/skills; Claude Code uses .claude/skills.")}
2843
+ ${dim("Default: open Agent Skills path (.agents/skills) plus Claude Code (.claude/skills).")}
2559
2844
  `);
2560
2845
  }
2561
2846
  function printVersion() {
@@ -2582,8 +2867,14 @@ function help() {
2582
2867
  ${dim("--stage stage beside the live form for dashboard review")}
2583
2868
  ${dim("--draft alias with a handle; legacy one-off draft without")}
2584
2869
  ${dim("--allow-code allow a .mjs/.js schema (executes the file)")}
2585
- list List the workspace's forms
2586
- agent <cmd> Report live progress to a Fillo prompt modal
2870
+ list List the workspace's forms and their live URLs
2871
+ status <form> Show one form's status, live URL, and publish blockers
2872
+ ${dim("<form> is a form id or handle")}
2873
+ publish <form> Publish staged changes (or a draft form) \u2014 prints the live URL
2874
+ ${dim("--allow-breaking confirm removing/re-typing fields responses answered")}
2875
+ test-response <form> <file|->
2876
+ Validate answers against staged changes without real delivery
2877
+ agent <cmd> Prepare an agent run and report live progress
2587
2878
  skill install Install the Build with Fillo Agent Skill
2588
2879
 
2589
2880
  ${dim(`API: ${API} \xB7 set FILLO_API to override`)}
@@ -2592,6 +2883,7 @@ function help() {
2592
2883
  }
2593
2884
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
2594
2885
  "account",
2886
+ "allow-breaking",
2595
2887
  "allow-code",
2596
2888
  "draft",
2597
2889
  "stage",
@@ -2612,6 +2904,9 @@ var FLAGS_BY_COMMAND = {
2612
2904
  push: ["handle", "stage", "draft", "allow-code"],
2613
2905
  list: [],
2614
2906
  ls: [],
2907
+ status: [],
2908
+ publish: ["allow-breaking"],
2909
+ "test-response": [],
2615
2910
  agent: [
2616
2911
  "run",
2617
2912
  "token",
@@ -2625,7 +2920,7 @@ var FLAGS_BY_COMMAND = {
2625
2920
  "form-name",
2626
2921
  "form-status"
2627
2922
  ],
2628
- skill: ["agent", "global", "project", "force"],
2923
+ skill: ["agent", "dir", "global", "project", "force"],
2629
2924
  help: [],
2630
2925
  version: []
2631
2926
  };
@@ -2711,6 +3006,12 @@ async function main() {
2711
3006
  case "list":
2712
3007
  case "ls":
2713
3008
  return list();
3009
+ case "status":
3010
+ return status(positional[0]);
3011
+ case "publish":
3012
+ return publish(positional[0], flags);
3013
+ case "test-response":
3014
+ return testResponse(positional[0], positional[1]);
2714
3015
  case "agent":
2715
3016
  return agent(positional[0], flags);
2716
3017
  case "skill":