@usefillo/cli 0.16.0 → 0.19.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
@@ -182,10 +182,13 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
182
182
  // (`--device` is the alias). Presence-only.
183
183
  "headless",
184
184
  "device",
185
- // Workspace commands (storage/slack/delete): presence-only switches.
185
+ // Workspace commands (storage/slack/discord/delete): presence-only switches.
186
186
  "force-path-style",
187
187
  "channels",
188
188
  "refresh",
189
+ // `discord enable --auto-join`: widens a role grant so a non-member can be
190
+ // added to the server. Presence-only — it is never "=false".
191
+ "auto-join",
189
192
  "yes",
190
193
  "also-unpublish",
191
194
  "cancel"
@@ -292,8 +295,8 @@ async function startLoopbackServer() {
292
295
  let handler = null;
293
296
  let served = false;
294
297
  const server = createServer((req, res) => {
295
- const respond = (status4, message) => {
296
- res.statusCode = status4;
298
+ const respond = (status5, message) => {
299
+ res.statusCode = status5;
297
300
  res.setHeader("Content-Type", "text/html; charset=utf-8");
298
301
  res.setHeader("Cache-Control", "no-store");
299
302
  res.end(resultPage(message));
@@ -592,6 +595,11 @@ async function fetchWhoami(apiBase = API) {
592
595
  if (!workspace) die("Fillo returned an invalid workspace identity.");
593
596
  return {
594
597
  workspace,
598
+ ...typeof me.workspaceId === "string" ? { workspaceId: terminalText(me.workspaceId) } : {},
599
+ ...typeof me.workspaceSlug === "string" ? { workspaceSlug: terminalText(me.workspaceSlug) } : {},
600
+ ...typeof me.project === "string" ? { project: terminalText(me.project) } : {},
601
+ ...typeof me.projectId === "string" ? { projectId: terminalText(me.projectId) } : {},
602
+ ...typeof me.projectSlug === "string" ? { projectSlug: terminalText(me.projectSlug) } : {},
595
603
  canPublishFileFields: typeof me.canPublishFileFields === "boolean" ? me.canPublishFileFields : void 0
596
604
  };
597
605
  }
@@ -607,10 +615,15 @@ async function whoami(flags) {
607
615
  return emitResult({
608
616
  connected: true,
609
617
  workspace: me.workspace,
618
+ ...me.workspaceId ? { workspaceId: me.workspaceId } : {},
619
+ ...me.workspaceSlug ? { workspaceSlug: me.workspaceSlug } : {},
620
+ ...me.project ? { project: me.project } : {},
621
+ ...me.projectId ? { projectId: me.projectId } : {},
622
+ ...me.projectSlug ? { projectSlug: me.projectSlug } : {},
610
623
  ...me.canPublishFileFields === void 0 ? {} : { canPublishFileFields: me.canPublishFileFields }
611
624
  });
612
625
  }
613
- printConnected(me.workspace);
626
+ printConnected(me.project ? `${me.workspace} / ${me.project}` : me.workspace);
614
627
  if (me.canPublishFileFields === true) {
615
628
  console.log(` ${dim("File uploads: storage ready \u2014 a file field can publish now.")}`);
616
629
  } else if (me.canPublishFileFields === false) {
@@ -1153,7 +1166,7 @@ function installSkill(flags, opts = {}) {
1153
1166
  for (const target of targets) console.log(` ${dim(target)}`);
1154
1167
  if (targets.length > 1) {
1155
1168
  console.log(
1156
- ` ${dim("Same bundle in both \u2014 cross-agent compatibility, not duplication.")}`
1169
+ ` ${dim("Same bundle in both \u2014 cross-agent compatibility, not duplication (want one folder? --agent <name> or --dir <path>).")}`
1157
1170
  );
1158
1171
  }
1159
1172
  console.log(`
@@ -1294,21 +1307,21 @@ async function agent(subcommand, flags) {
1294
1307
  die("--account can only be used with `fillo agent bootstrap` or `fillo agent connect`.");
1295
1308
  }
1296
1309
  if (subcommand === "event") {
1297
- const status4 = enumFlag(flags, "status", AGENT_EVENT_STATUSES);
1310
+ const status5 = enumFlag(flags, "status", AGENT_EVENT_STATUSES);
1298
1311
  const message = flagString(flags, "message");
1299
- if (!status4)
1312
+ if (!status5)
1300
1313
  die('Usage: fillo agent event --status <status> --message "<what the human does next>"');
1301
1314
  const action = enumFlag(flags, "action", AGENT_ACTIONS);
1302
1315
  const formStatus = enumFlag(flags, "form-status", FORM_STATUSES);
1303
1316
  const formId = optionalStringFlag(flags, "form-id");
1304
- if ((status4 === "done" || status4 === "needs_action") && !formId) {
1305
- die(`--form-id is required when --status is ${status4}.`);
1317
+ if ((status5 === "done" || status5 === "needs_action") && !formId) {
1318
+ die(`--form-id is required when --status is ${status5}.`);
1306
1319
  }
1307
- if (status4 === "needs_action" && !action) {
1320
+ if (status5 === "needs_action" && !action) {
1308
1321
  die("--action is required when --status is needs_action.");
1309
1322
  }
1310
1323
  await postAgentEvent(apiBase, run, token, {
1311
- status: status4,
1324
+ status: status5,
1312
1325
  message,
1313
1326
  appUrl: flagString(flags, "app-url"),
1314
1327
  action,
@@ -1469,13 +1482,13 @@ async function claim(flags) {
1469
1482
  const grant = await readJson(codeRes);
1470
1483
  const emailFlag = flagString(flags, "email");
1471
1484
  const savedEmail = cfg.email;
1472
- const { approval, sentTo, notice } = await sendClaimEmail(
1485
+ const { approval, sentTo, notice: notice2 } = await sendClaimEmail(
1473
1486
  grant,
1474
1487
  cfg.claimToken,
1475
1488
  emailFlag,
1476
1489
  savedEmail
1477
1490
  );
1478
- const alreadyClaimed = approval === "device" && notice === "already_claimed";
1491
+ const alreadyClaimed = approval === "device" && notice2 === "already_claimed";
1479
1492
  if (json) {
1480
1493
  emitProgress({
1481
1494
  status: "awaiting_claim",
@@ -1483,11 +1496,11 @@ async function claim(flags) {
1483
1496
  ...sentTo ? { email: sentTo } : {},
1484
1497
  verification_uri: grant.verification_uri,
1485
1498
  approval,
1486
- ...notice ? { notice } : {},
1499
+ ...notice2 ? { notice: notice2 } : {},
1487
1500
  note: AGENT_WAIT_LINE
1488
1501
  });
1489
1502
  } else {
1490
- printGuidance(grant, approval, sentTo, notice);
1503
+ printGuidance(grant, approval, sentTo, notice2);
1491
1504
  }
1492
1505
  await pollForGrant(grant, cfg.pk, approval, alreadyClaimed, json);
1493
1506
  }
@@ -1535,10 +1548,10 @@ async function sendClaimEmail(grant, claimToken, emailFlag, savedEmail) {
1535
1548
  if (!res.ok) die(body.error ?? `Couldn't send the claim email (${res.status}).`);
1536
1549
  return { approval: "device", sentTo: email3 };
1537
1550
  }
1538
- function printGuidance(grant, approval, sentTo, notice) {
1539
- if (notice === "already_claimed") {
1551
+ function printGuidance(grant, approval, sentTo, notice2) {
1552
+ if (notice2 === "already_claimed") {
1540
1553
  console.log("\n This workspace is already claimed.");
1541
- } else if (notice === "recent_email") {
1554
+ } else if (notice2 === "recent_email") {
1542
1555
  console.log(
1543
1556
  `
1544
1557
  A claim email was already sent recently${sentTo ? ` to ${bold(terminalText(sentTo))}` : ""} \u2014 open that one.`
@@ -1552,7 +1565,7 @@ function printGuidance(grant, approval, sentTo, notice) {
1552
1565
  console.log(`
1553
1566
  Your code: ${bold(grant.user_code)}
1554
1567
  `);
1555
- if (notice === "already_claimed") {
1568
+ if (notice2 === "already_claimed") {
1556
1569
  console.log(` Sign in at ${terminalText(grant.verification_uri)} and enter the code`);
1557
1570
  console.log(" to approve this terminal.");
1558
1571
  } else if (approval === "page") {
@@ -1612,11 +1625,11 @@ Claim failed: ${body.error}`);
1612
1625
  }
1613
1626
  if (!claimedAnnounced && Date.now() >= nextStatusAt) {
1614
1627
  nextStatusAt = Date.now() + statusEvery;
1615
- const status4 = await api(`/workspaces/claim-status?key=${encodeURIComponent(pk)}`).catch(
1628
+ const status5 = await api(`/workspaces/claim-status?key=${encodeURIComponent(pk)}`).catch(
1616
1629
  () => null
1617
1630
  );
1618
- if (status4?.ok) {
1619
- const state = await status4.json().catch(() => ({}));
1631
+ if (status5?.ok) {
1632
+ const state = await status5.json().catch(() => ({}));
1620
1633
  if (state.claimed === true) {
1621
1634
  claimedAnnounced = true;
1622
1635
  if (json) {
@@ -1858,6 +1871,403 @@ var deleteCommand = {
1858
1871
  help: deleteHelp
1859
1872
  };
1860
1873
 
1874
+ // src/lib/browser-connect.ts
1875
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
1876
+ function pollIntervalMs2(fallback) {
1877
+ const raw = process.env.FILLO_POLL_INTERVAL_MS;
1878
+ if (raw === void 0 || raw === "") return fallback;
1879
+ const value = Number(raw);
1880
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
1881
+ }
1882
+ async function connectViaBrowser(opts) {
1883
+ const interval = pollIntervalMs2(opts.intervalMs ?? 5e3);
1884
+ const deadline = Date.now() + (opts.timeoutMs ?? 6e5);
1885
+ const dots = !opts.json && !agentMode();
1886
+ if (opts.json) {
1887
+ emitProgress({
1888
+ status: "awaiting_connect",
1889
+ provider: opts.what,
1890
+ url: opts.startUrl,
1891
+ note: "Open the URL in your signed-in browser; this command waits. Do not retry in a loop."
1892
+ });
1893
+ } else {
1894
+ console.log(`
1895
+ Connect ${terminalText(opts.what)} in your signed-in browser:
1896
+ `);
1897
+ console.log(` ${terminalText(opts.startUrl)}
1898
+ `);
1899
+ console.log(" Open in your signed-in browser and approve access \u2014 this command waits.");
1900
+ if (agentMode()) {
1901
+ console.log(
1902
+ " Do not retry in a loop; ask the human to complete the browser step. This command waits."
1903
+ );
1904
+ }
1905
+ }
1906
+ if (dots) process.stdout.write(" Waiting for the connection");
1907
+ for (; ; ) {
1908
+ let connected2 = false;
1909
+ try {
1910
+ connected2 = await opts.poll();
1911
+ } catch {
1912
+ }
1913
+ if (connected2) {
1914
+ const { result, lines } = opts.onConnected();
1915
+ if (opts.json) {
1916
+ emitProgress({ status: "connected" });
1917
+ emitResult(result);
1918
+ return;
1919
+ }
1920
+ if (dots) process.stdout.write("\n");
1921
+ for (const line of lines) console.log(line);
1922
+ return;
1923
+ }
1924
+ if (Date.now() >= deadline) break;
1925
+ if (dots) process.stdout.write(".");
1926
+ await sleep3(interval);
1927
+ }
1928
+ if (opts.json) {
1929
+ emitResult({ connected: false, error: "timeout" });
1930
+ process.exit(1);
1931
+ }
1932
+ if (dots) process.stdout.write("\n");
1933
+ console.log(
1934
+ ` Timed out waiting for ${terminalText(opts.what)} to connect. Finish the browser step, then re-run this command.`
1935
+ );
1936
+ process.exit(1);
1937
+ }
1938
+
1939
+ // src/commands/discord.ts
1940
+ var WEBHOOK_URL_ENV = "FILLO_DISCORD_WEBHOOK_URL";
1941
+ var SNOWFLAKE = /^\d{17,20}$/;
1942
+ var EARLY_SIGNAL_CHOICES = ["5", "10", "25", "off"];
1943
+ function notice(json, line) {
1944
+ if (json) emitProgress({ status: "notice", notice: line });
1945
+ else console.log(` ${line}`);
1946
+ }
1947
+ async function fetchConnection(token) {
1948
+ const res = await api("/cli/discord", { token });
1949
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
1950
+ const body = await readJson(res);
1951
+ if (!res.ok) die(body.error ?? `discord status failed (${res.status}).`);
1952
+ return body;
1953
+ }
1954
+ async function fetchDestination(token, form) {
1955
+ const res = await api(`/cli/forms/${encodeURIComponent(form)}/discord`, { token });
1956
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
1957
+ const body = await readJson(res);
1958
+ if (res.status === 404) die(body.error ?? "Form not found in the selected project.");
1959
+ if (!res.ok) die(body.error ?? `discord status failed (${res.status}).`);
1960
+ return body;
1961
+ }
1962
+ async function putDestination(token, form, patch) {
1963
+ const res = await api(`/cli/forms/${encodeURIComponent(form)}/discord`, {
1964
+ method: "PUT",
1965
+ token,
1966
+ body: JSON.stringify(patch)
1967
+ });
1968
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
1969
+ const body = await readJson(res);
1970
+ if (res.status === 404) die(body.error ?? "Form not found in the selected project.");
1971
+ if (!res.ok) die(body.error ?? `discord update failed (${res.status}).`);
1972
+ return body;
1973
+ }
1974
+ async function projectId(token) {
1975
+ const res = await api("/cli/whoami", { token });
1976
+ const body = await readJson(res);
1977
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
1978
+ if (!res.ok || typeof body.projectId !== "string" || !body.projectId) {
1979
+ die(body.error ?? "Couldn't resolve the selected Fillo project.");
1980
+ }
1981
+ return body.projectId;
1982
+ }
1983
+ async function connect(flags) {
1984
+ const json = jsonMode(flags);
1985
+ const token = requireToken();
1986
+ const connection = await fetchConnection(token);
1987
+ if (!connection.appConfigured) {
1988
+ die(
1989
+ "This Fillo deployment has no Discord app, so there's no one-click channel picker. Run `fillo discord webhook` to paste a channel webhook URL instead."
1990
+ );
1991
+ }
1992
+ const startUrl = `${API}/api/integrations/discord/start?return=terminal&project=${encodeURIComponent(
1993
+ await projectId(token)
1994
+ )}`;
1995
+ let connected2 = null;
1996
+ await connectViaBrowser({
1997
+ json,
1998
+ what: "Discord",
1999
+ startUrl,
2000
+ poll: async () => {
2001
+ const snapshot = await fetchConnection(token);
2002
+ connected2 = snapshot;
2003
+ return snapshot.connected === true;
2004
+ },
2005
+ onConnected: () => {
2006
+ const label = connected2?.channelLabel ?? null;
2007
+ return {
2008
+ result: {
2009
+ connected: true,
2010
+ webhookId: connected2?.webhookId ?? null,
2011
+ channelLabel: label
2012
+ },
2013
+ lines: [
2014
+ ` ${okMark()} Discord connected${label ? ` (${terminalText(label)})` : ""}.`,
2015
+ " Turn a form on with `fillo discord enable <form>`."
2016
+ ]
2017
+ };
2018
+ }
2019
+ });
2020
+ }
2021
+ async function webhook(flags) {
2022
+ const json = jsonMode(flags);
2023
+ const token = requireToken();
2024
+ let url = process.env[WEBHOOK_URL_ENV]?.trim() ?? "";
2025
+ if (!url) {
2026
+ if (json || !isInteractive()) {
2027
+ die(
2028
+ `Set ${WEBHOOK_URL_ENV} to the channel webhook URL and re-run. It is never accepted as an argument \u2014 arguments land in shell history and \`ps\` output.`
2029
+ );
2030
+ }
2031
+ url = await readSecret(" Discord webhook URL (hidden): ").catch(() => "");
2032
+ if (!url) {
2033
+ die(`No webhook URL entered. Paste it at the prompt, or set ${WEBHOOK_URL_ENV} and re-run.`);
2034
+ }
2035
+ }
2036
+ const label = flagString(flags, "label");
2037
+ const res = await api("/cli/discord/webhook", {
2038
+ method: "POST",
2039
+ token,
2040
+ body: JSON.stringify({ url, ...label ? { label } : {} })
2041
+ });
2042
+ const body = await readJson(res);
2043
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
2044
+ if (!res.ok || !body.webhookId) die(body.error ?? `discord webhook failed (${res.status}).`);
2045
+ if (json) return emitResult(body);
2046
+ console.log(
2047
+ ` ${okMark()} Connected Discord${body.label ? ` (${terminalText(body.label)})` : ""}.`
2048
+ );
2049
+ console.log(" Turn a form on with `fillo discord enable <form>`.");
2050
+ }
2051
+ function printDestination(body) {
2052
+ if (!body.connected) {
2053
+ console.log(
2054
+ " Discord is not connected to this project. Run `fillo discord connect` (or `fillo discord webhook` to paste a channel URL)."
2055
+ );
2056
+ return;
2057
+ }
2058
+ if (!body.enabled) {
2059
+ console.log(
2060
+ ` Discord is connected${body.connectionLabel ? ` (${terminalText(body.connectionLabel)})` : ""} but this form isn't sending. Turn it on with \`fillo discord enable <form>\`.`
2061
+ );
2062
+ return;
2063
+ }
2064
+ console.log(
2065
+ ` ${okMark()} Sending to Discord${body.channelLabel ? ` \u2014 ${terminalText(body.channelLabel)}` : ""}`
2066
+ );
2067
+ const fields = body.includedFieldIds ?? [];
2068
+ console.log(
2069
+ ` ${dim("Fields:")} ${fields.length > 0 ? terminalText(fields.join(", ")) : "link only"}`
2070
+ );
2071
+ console.log(
2072
+ ` ${dim("Early signal:")} ${body.earlySignalLimit ? `first ${body.earlySignalLimit} responses in full (${body.earlySignalDelivered} sent)` : "off"}`
2073
+ );
2074
+ const grant = body.roleGrant;
2075
+ console.log(
2076
+ ` ${dim("Role grant:")} ${grant ? `${terminalText(grant.roleId)} in ${terminalText(grant.guildId)}${grant.autoJoin ? " (auto-join)" : ""}` : "off"}`
2077
+ );
2078
+ if (!body.webhookMatches) {
2079
+ console.log(
2080
+ `
2081
+ ${bold("This form points at a different webhook than the project does.")} It keeps failing until you re-run \`fillo discord enable <form>\`.`
2082
+ );
2083
+ }
2084
+ }
2085
+ async function status(form, flags) {
2086
+ if (!form) die("Usage: fillo discord status <form> \u2014 a form id, slug, or push handle.");
2087
+ const body = await fetchDestination(requireToken(), form);
2088
+ if (jsonMode(flags)) return emitResult(body);
2089
+ printDestination(body);
2090
+ }
2091
+ function parseFields(raw) {
2092
+ if (raw.trim().toLowerCase() === "none") return [];
2093
+ return raw.split(",").map((id) => id.trim()).filter(Boolean);
2094
+ }
2095
+ function parseRole(raw) {
2096
+ if (raw.trim().toLowerCase() === "none") return null;
2097
+ const [guildId, roleId, ...extra] = raw.trim().split("/");
2098
+ if (extra.length > 0 || !guildId || !roleId || !SNOWFLAKE.test(guildId) || !SNOWFLAKE.test(roleId)) {
2099
+ die(
2100
+ "--role must be <guildId>/<roleId> (Discord ids), or none to clear it. List them with `fillo discord roles [guildId]`."
2101
+ );
2102
+ }
2103
+ return { guildId, roleId };
2104
+ }
2105
+ async function enable(form, flags) {
2106
+ const json = jsonMode(flags);
2107
+ if (!form)
2108
+ die("Usage: fillo discord enable <form> [--fields a,b,c] [--early-signal 5|10|25|off]");
2109
+ const token = requireToken();
2110
+ const current = await fetchDestination(token, form);
2111
+ if (!current.connected || !current.connectionWebhookId) {
2112
+ die(
2113
+ "Discord isn't connected to this project. Run `fillo discord connect`, or `fillo discord webhook` to paste a channel URL."
2114
+ );
2115
+ }
2116
+ const patch = {};
2117
+ if (!current.enabled || !current.webhookMatches) {
2118
+ patch.enabled = true;
2119
+ patch.webhookId = current.connectionWebhookId;
2120
+ }
2121
+ const rawFields = flagString(flags, "fields");
2122
+ if (rawFields !== void 0) patch.includeFieldIds = parseFields(rawFields);
2123
+ const rawEarly = flagString(flags, "early-signal");
2124
+ if (rawEarly !== void 0) {
2125
+ if (!EARLY_SIGNAL_CHOICES.includes(rawEarly)) {
2126
+ die(`--early-signal must be one of: ${EARLY_SIGNAL_CHOICES.join(", ")}`);
2127
+ }
2128
+ patch.earlySignalLimit = rawEarly === "off" ? null : Number(rawEarly);
2129
+ }
2130
+ const rawRole = flagString(flags, "role");
2131
+ const autoJoin = flags["auto-join"] === true;
2132
+ if (rawRole !== void 0) {
2133
+ const grant = parseRole(rawRole);
2134
+ patch.roleGrant = grant ? { ...grant, ...autoJoin ? { autoJoin: true } : {} } : null;
2135
+ } else if (autoJoin) {
2136
+ die(
2137
+ "--auto-join needs --role <guildId>/<roleId> \u2014 it widens that grant, it isn't a setting on its own."
2138
+ );
2139
+ }
2140
+ if (Object.keys(patch).length === 0) {
2141
+ if (json) return emitResult(current);
2142
+ printDestination(current);
2143
+ return;
2144
+ }
2145
+ if (typeof patch.earlySignalLimit === "number") {
2146
+ notice(
2147
+ json,
2148
+ `Early signal sends every answer to this channel for the first ${patch.earlySignalLimit} responses.`
2149
+ );
2150
+ }
2151
+ if (patch.roleGrant && autoJoin) {
2152
+ notice(
2153
+ json,
2154
+ "Auto-join adds respondents to your Discord server with that role \u2014 Discord asks each person's permission first."
2155
+ );
2156
+ } else if (patch.roleGrant) {
2157
+ notice(
2158
+ json,
2159
+ "Respondents who verify with Discord get that role once their response is accepted."
2160
+ );
2161
+ }
2162
+ const body = await putDestination(token, form, patch);
2163
+ if (json) return emitResult(body);
2164
+ printDestination(body);
2165
+ }
2166
+ async function disable(form, flags) {
2167
+ if (!form) die("Usage: fillo discord disable <form> \u2014 a form id, slug, or push handle.");
2168
+ const token = requireToken();
2169
+ const body = await putDestination(token, form, { enabled: false });
2170
+ if (jsonMode(flags)) return emitResult(body);
2171
+ console.log(
2172
+ ` ${okMark()} This form no longer posts to Discord. The workspace's webhook stays connected.`
2173
+ );
2174
+ }
2175
+ async function roles(guildId, flags) {
2176
+ const json = jsonMode(flags);
2177
+ const token = requireToken();
2178
+ const path = guildId ? `/cli/discord/roles?guildId=${encodeURIComponent(guildId)}` : "/cli/discord/roles";
2179
+ const res = await api(path, { token });
2180
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
2181
+ const body = await readJson(res);
2182
+ if (!res.ok) die(body.error ?? `discord roles failed (${res.status}).`);
2183
+ if (json) return emitResult(body);
2184
+ if (!guildId) {
2185
+ const guilds = body.guilds ?? [];
2186
+ if (guilds.length === 0) {
2187
+ console.log(
2188
+ " No Discord servers yet \u2014 connect a channel webhook first with `fillo discord connect`."
2189
+ );
2190
+ return;
2191
+ }
2192
+ console.log("");
2193
+ printTable(
2194
+ ["SERVER", "ID"],
2195
+ guilds.map((guild) => [terminalText(guild.name ?? "(name unavailable)"), guild.id])
2196
+ );
2197
+ if (body.guildsUnavailable === true) {
2198
+ console.log(
2199
+ `
2200
+ ${dim("Discord didn't answer, so some names are missing. Ids still work.")}`
2201
+ );
2202
+ }
2203
+ console.log(`
2204
+ ${dim("List a server's roles: fillo discord roles <id>")}`);
2205
+ return;
2206
+ }
2207
+ const list5 = body.roles ?? [];
2208
+ if (list5.length === 0) {
2209
+ console.log(" That server has no roles Fillo could list.");
2210
+ return;
2211
+ }
2212
+ console.log("");
2213
+ printTable(
2214
+ ["ROLE", "ID", "GRANTABLE"],
2215
+ list5.map((role) => [terminalText(role.name), role.id, role.grantable ? "yes" : "no"])
2216
+ );
2217
+ console.log(`
2218
+ ${dim(`Use one: fillo discord enable <form> --role ${guildId}/<roleId>`)}`);
2219
+ console.log(
2220
+ ` ${dim("A role marked no sits above Fillo's own role, or belongs to another integration.")}`
2221
+ );
2222
+ }
2223
+ async function discord(subcommand, args, flags) {
2224
+ if (subcommand === void 0 || subcommand === "help") return discordHelp();
2225
+ if (subcommand === "connect") return connect(flags);
2226
+ if (subcommand === "webhook") return webhook(flags);
2227
+ if (subcommand === "status") return status(args[0], flags);
2228
+ if (subcommand === "enable") return enable(args[0], flags);
2229
+ if (subcommand === "disable") return disable(args[0], flags);
2230
+ if (subcommand === "roles") return roles(args[0], flags);
2231
+ die(
2232
+ `Unknown discord command: ${terminalText(subcommand)} (expected connect, webhook, status, enable, disable, or roles).`
2233
+ );
2234
+ }
2235
+ function discordHelp() {
2236
+ console.log(`
2237
+ ${boldRaw("fillo discord")} \u2014 post responses to a Discord channel, from the terminal
2238
+
2239
+ ${boldRaw("Commands")}
2240
+ discord connect Let Discord pick the channel and create the webhook
2241
+ ${dimRaw("opens an OAuth URL to approve; needs a Discord app on this deploy")}
2242
+ discord webhook Connect a channel webhook you paste
2243
+ ${dimRaw("--label <name> what Fillo shows for this channel")}
2244
+ ${dimRaw(`reads the URL from a hidden prompt, or ${WEBHOOK_URL_ENV}`)}
2245
+ discord status <form> What this form sends, to where, with which extras
2246
+ discord enable <form> Start sending \u2014 and set any of the options below
2247
+ ${dimRaw("--fields a,b,c up to three field ids (none = link only)")}
2248
+ ${dimRaw("--early-signal 5|10|25 first N responses arrive in full (off to stop)")}
2249
+ ${dimRaw("--role <guildId>/<roleId> grant that role on an accepted response")}
2250
+ ${dimRaw("--auto-join with --role: add non-members to the server")}
2251
+ discord disable <form> Stop this form posting (the webhook stays connected)
2252
+ discord roles [guildId] Your servers, or one server's roles and what Fillo can grant
2253
+
2254
+ ${dimRaw("The webhook URL is the whole credential, so it is NEVER an argument \u2014")}
2255
+ ${dimRaw("arguments land in shell history and `ps` output. Paste it at the hidden")}
2256
+ ${dimRaw(`prompt, or set ${WEBHOOK_URL_ENV} for an unattended run.`)}
2257
+ ${dimRaw("--early-signal and --auto-join widen what leaves Fillo, so each prints a")}
2258
+ ${dimRaw("one-line notice saying what was agreed to. Ask the human before setting them.")}
2259
+ ${dimRaw("Re-enabling against a different webhook clears early signal \u2014 that consent")}
2260
+ ${dimRaw("was given for one channel and is never carried into another.")}
2261
+ ${dimRaw("--json prints the raw server response on stdout.")}
2262
+ `);
2263
+ }
2264
+ var discordCommand = {
2265
+ name: "discord",
2266
+ flags: ["label", "fields", "early-signal", "role", "auto-join"],
2267
+ run: (args, flags) => discord(args[0], args.slice(1), flags),
2268
+ help: discordHelp
2269
+ };
2270
+
1861
2271
  // src/commands/forms.ts
1862
2272
  import { readFileSync as readFileSync3 } from "fs";
1863
2273
  import { isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
@@ -3258,6 +3668,48 @@ function sortForPicker(countries) {
3258
3668
  return [...countries].sort((a, b) => collator.compare(a.name, b.name));
3259
3669
  }
3260
3670
  var byIso = new Map(PHONE_COUNTRIES.map((c) => [c.iso2, c]));
3671
+ var DEFAULT_STRINGS = {
3672
+ back: "Back",
3673
+ next: "Next",
3674
+ submit: "Submit",
3675
+ submitting: "Submitting\u2026",
3676
+ uploading: "Uploading\u2026",
3677
+ optional: " (optional)",
3678
+ other: "Other",
3679
+ otherPrompt: "Other \u2014 please specify",
3680
+ otherPlaceholder: "Your answer",
3681
+ choosePlaceholder: "Choose\u2026",
3682
+ successTitle: "Thanks!",
3683
+ successMessage: "Your response has been recorded.",
3684
+ closed: "This form is no longer accepting responses.",
3685
+ notLive: "This form isn't live yet.",
3686
+ notOpenTitle: "This form isn't open yet",
3687
+ notOpenBody: "The form owner is still setting things up. Please check back soon.",
3688
+ closedTitle: "Responses are closed",
3689
+ submitFailed: "This form can't submit right now. Please try again in a moment.",
3690
+ loadFailedNotFound: "Form not found. Check the link or ask the form owner for help.",
3691
+ loadFailedNetwork: "Couldn't reach the server \u2014 check your connection and try again.",
3692
+ loadFailed: "This form could not be loaded.",
3693
+ renderFailed: "This form could not be rendered.",
3694
+ resumeNotice: "Picked up where you left off.",
3695
+ editNotice: "You're updating your earlier response.",
3696
+ resumeStartOver: "Start over",
3697
+ challengeUnavailable: "The verification check couldn't load. Refresh the page and try again."
3698
+ };
3699
+ var DEFAULT_RESPONDENT_ERROR_STRINGS = {
3700
+ submitFailed: DEFAULT_STRINGS.submitFailed,
3701
+ loadFailedNotFound: DEFAULT_STRINGS.loadFailedNotFound,
3702
+ loadFailedNetwork: DEFAULT_STRINGS.loadFailedNetwork,
3703
+ formUnavailable: "This form is unavailable.",
3704
+ formClosed: DEFAULT_STRINGS.closed,
3705
+ submitRateLimited: "Too many responses are being submitted right now. Wait a moment, then try again.",
3706
+ respondentUnrecognized: "This form couldn't verify who is responding. Ask the form owner for help.",
3707
+ fileUnavailable: "One of the uploaded files is no longer available. Remove it and upload it again.",
3708
+ scopeMissing: "We couldn't tell which entry this response belongs to. Answer the required field and try again.",
3709
+ challengeIncomplete: "Please complete the verification check, then submit.",
3710
+ challengeRetry: "That verification didn't go through. Please complete the check again and resubmit.",
3711
+ reviewAnswers: "We couldn't submit \u2014 please review your answers."
3712
+ };
3261
3713
  var fileValueSchema = object({
3262
3714
  fileId: string2().check(_minLength(1)),
3263
3715
  name: string2(),
@@ -3322,7 +3774,7 @@ var schemaShape = object({
3322
3774
  });
3323
3775
  var MAX_SCHEMA_VERSION = 1;
3324
3776
  var FILLO_SCHEMA_VERSION = 1;
3325
- var FILLO_SDK_VERSION = true ? "0.14.0" : "0.0.0-dev";
3777
+ var FILLO_SDK_VERSION = true ? "0.19.0" : "0.0.0-dev";
3326
3778
  function str(value, max, fallback = "") {
3327
3779
  return typeof value === "string" ? value.trim().slice(0, max) : fallback;
3328
3780
  }
@@ -4183,7 +4635,7 @@ async function list(flags) {
4183
4635
  }
4184
4636
  }
4185
4637
  var liveDot = () => ansiEnabled() ? "\x1B[32m\u25CF\x1B[0m" : "\u25CF";
4186
- async function status(handle, flags) {
4638
+ async function status2(handle, flags) {
4187
4639
  if (!handle) die("Usage: fillo status <formId|handle>");
4188
4640
  const res = await api(`/cli/forms/${encodeURIComponent(handle)}`, { token: requireToken() });
4189
4641
  if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
@@ -4395,6 +4847,7 @@ async function loadSchema(file, allowCode) {
4395
4847
  die(`Couldn't read ${file}: ${e.message}`);
4396
4848
  }
4397
4849
  }
4850
+ var PUSH_STORAGE_VALUES = /* @__PURE__ */ new Set(["gdrive", "box", "s3", "r2"]);
4398
4851
  function isRecord(value) {
4399
4852
  return typeof value === "object" && value !== null && !Array.isArray(value);
4400
4853
  }
@@ -4404,7 +4857,19 @@ function toPushItems(value, handle) {
4404
4857
  return values.map((item, index) => {
4405
4858
  if (isRecord(item) && "schema" in item) {
4406
4859
  const ownHandle = typeof item.id === "string" ? item.id : void 0;
4407
- return { handle: handle ?? ownHandle, schema: item.schema, theme: item.theme };
4860
+ if (item.storage !== void 0 && !PUSH_STORAGE_VALUES.has(String(item.storage))) {
4861
+ die(`Item ${index + 1} has an unsupported storage destination.`);
4862
+ }
4863
+ if (item.purpose !== void 0 && item.purpose !== "file_request") {
4864
+ die(`Item ${index + 1} has an unsupported form purpose.`);
4865
+ }
4866
+ return {
4867
+ handle: handle ?? ownHandle,
4868
+ schema: item.schema,
4869
+ theme: item.theme,
4870
+ ...typeof item.storage === "string" ? { storage: item.storage } : {},
4871
+ ...item.purpose === "file_request" ? { purpose: item.purpose } : {}
4872
+ };
4408
4873
  }
4409
4874
  if (values.length > 1 && !handle) {
4410
4875
  die(`Item ${index + 1} needs an id or --handle.`);
@@ -4414,9 +4879,9 @@ function toPushItems(value, handle) {
4414
4879
  }
4415
4880
  function printNotices(notices) {
4416
4881
  if (!Array.isArray(notices)) return;
4417
- for (const notice of notices) {
4418
- if (typeof notice === "string" && notice.trim()) {
4419
- console.log(` ${bold("Note:")} ${terminalText(notice)}`);
4882
+ for (const notice2 of notices) {
4883
+ if (typeof notice2 === "string" && notice2.trim()) {
4884
+ console.log(` ${bold("Note:")} ${terminalText(notice2)}`);
4420
4885
  }
4421
4886
  }
4422
4887
  }
@@ -4501,7 +4966,9 @@ async function pushWithSync(items, credential, requestedStage, results) {
4501
4966
  ...credential.key ? { key: credential.key } : {},
4502
4967
  id: handle,
4503
4968
  schema: item.schema,
4504
- theme: item.theme ?? null
4969
+ theme: item.theme ?? null,
4970
+ ...item.storage ? { storage: item.storage } : {},
4971
+ ...item.purpose ? { purpose: item.purpose } : {}
4505
4972
  })
4506
4973
  });
4507
4974
  const body = await readJson(res);
@@ -4566,6 +5033,8 @@ async function push(file, flags) {
4566
5033
  body: JSON.stringify({
4567
5034
  schema: item.schema,
4568
5035
  theme: item.theme ?? null,
5036
+ ...item.storage ? { storage: item.storage } : {},
5037
+ ...item.purpose ? { purpose: item.purpose } : {},
4569
5038
  publish: false
4570
5039
  })
4571
5040
  });
@@ -4593,6 +5062,8 @@ async function push(file, flags) {
4593
5062
  schema: item.schema,
4594
5063
  theme: item.theme ?? null,
4595
5064
  handle: item.handle,
5065
+ ...item.storage ? { storage: item.storage } : {},
5066
+ ...item.purpose ? { purpose: item.purpose } : {},
4596
5067
  publish: true
4597
5068
  })
4598
5069
  });
@@ -4608,11 +5079,16 @@ async function push(file, flags) {
4608
5079
  }
4609
5080
  die(body.error ?? `push failed (${res.status}).`);
4610
5081
  }
5082
+ const draft = body.status === "draft";
4611
5083
  if (results) {
4612
5084
  results.push(body);
4613
- emitProgress({ status: "pushed", formId: body.formId, published: true });
5085
+ emitProgress({
5086
+ status: "pushed",
5087
+ formId: body.formId,
5088
+ ...draft ? { draft: true } : { published: true }
5089
+ });
4614
5090
  } else {
4615
- printPushed(body.formId, body.url, !!body.updated, false, {
5091
+ printPushed(body.formId, body.url, !!body.updated, draft, {
4616
5092
  warning: body.warning,
4617
5093
  warningUrl: body.warningUrl,
4618
5094
  notices: body.notices
@@ -4643,7 +5119,7 @@ var listCommand = {
4643
5119
  var statusCommand = {
4644
5120
  name: "status",
4645
5121
  flags: [],
4646
- run: (args, flags) => status(args[0], flags)
5122
+ run: (args, flags) => status2(args[0], flags)
4647
5123
  };
4648
5124
  var publishCommand = {
4649
5125
  name: "publish",
@@ -4656,6 +5132,61 @@ var testResponseCommand = {
4656
5132
  run: (args) => testResponse(args[0], args[1])
4657
5133
  };
4658
5134
 
5135
+ // src/commands/branding.ts
5136
+ function printState(body) {
5137
+ const plan = body.plan === "pro" ? "Everything" : "Free";
5138
+ console.log(` Plan: ${plan}`);
5139
+ console.log(` Badge: ${body.poweredBy ? "shown" : "hidden"}`);
5140
+ if (body.canHide === false) {
5141
+ console.log(dim(" Turning it off needs the Everything plan (Settings \u2192 Plan)."));
5142
+ }
5143
+ }
5144
+ async function branding(subcommand, flags) {
5145
+ if (subcommand === "help") return brandingHelp();
5146
+ if (subcommand !== void 0 && subcommand !== "on" && subcommand !== "off") {
5147
+ die(`Unknown branding command: ${subcommand} (expected on, off, or nothing for status).`);
5148
+ }
5149
+ const token = requireToken();
5150
+ const res = subcommand === void 0 ? await api("/cli/workspace/branding", { token }) : await api("/cli/workspace/branding", {
5151
+ method: "PATCH",
5152
+ token,
5153
+ body: JSON.stringify({ show: subcommand === "on" })
5154
+ });
5155
+ const body = await readJson(res);
5156
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
5157
+ if (!res.ok) die(body.error ?? `branding ${subcommand ?? "status"} failed (${res.status}).`);
5158
+ if (jsonMode(flags)) return emitResult(body);
5159
+ console.log("");
5160
+ if (subcommand !== void 0) {
5161
+ console.log(
5162
+ ` ${okMark()} Badge ${body.poweredBy ? "shown" : "hidden"} on Fillo-rendered forms.
5163
+ `
5164
+ );
5165
+ }
5166
+ printState(body);
5167
+ console.log("");
5168
+ }
5169
+ function brandingHelp() {
5170
+ console.log(`
5171
+ ${boldRaw("fillo branding")} \u2014 the workspace's "Powered by Fillo" badge
5172
+
5173
+ ${boldRaw("Commands")}
5174
+ branding Print the current badge state
5175
+ branding off Hide the badge on Fillo-rendered forms (Everything plan)
5176
+ branding on Show the badge again
5177
+
5178
+ ${dimRaw("Hiding needs the Everything plan; a workspace owner or admin selects")}
5179
+ ${dimRaw("that in Settings \u2192 Plan. Headless embeds never show a badge.")}
5180
+ ${dimRaw("--json prints the raw server response on stdout.")}
5181
+ `);
5182
+ }
5183
+ var brandingCommand = {
5184
+ name: "branding",
5185
+ flags: [],
5186
+ run: (args, flags) => branding(args[0], flags),
5187
+ help: brandingHelp
5188
+ };
5189
+
4659
5190
  // src/commands/keys.ts
4660
5191
  var PRESETS = ["read", "agent", "full"];
4661
5192
  var EXPIRY_CHOICES = ["30d", "90d", "never"];
@@ -4804,7 +5335,7 @@ async function keys(subcommand, args, flags) {
4804
5335
  }
4805
5336
  function keysHelp() {
4806
5337
  console.log(`
4807
- ${boldRaw("fillo keys")} \u2014 workspace API keys (fsk_) for coding agents and integrations
5338
+ ${boldRaw("fillo keys")} \u2014 project API keys (fsk_) for coding agents and integrations
4808
5339
 
4809
5340
  ${boldRaw("Commands")}
4810
5341
  keys create --name <name> Mint a key \u2014 the plaintext is shown once, store it
@@ -5244,73 +5775,16 @@ var settingsCommand = {
5244
5775
  help: settingsHelp
5245
5776
  };
5246
5777
 
5247
- // src/lib/browser-connect.ts
5248
- var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
5249
- function pollIntervalMs2(fallback) {
5250
- const raw = process.env.FILLO_POLL_INTERVAL_MS;
5251
- if (raw === void 0 || raw === "") return fallback;
5252
- const value = Number(raw);
5253
- return Number.isFinite(value) && value >= 0 ? value : fallback;
5254
- }
5255
- async function connectViaBrowser(opts) {
5256
- const interval = pollIntervalMs2(opts.intervalMs ?? 5e3);
5257
- const deadline = Date.now() + (opts.timeoutMs ?? 6e5);
5258
- const dots = !opts.json && !agentMode();
5259
- if (opts.json) {
5260
- emitProgress({
5261
- status: "awaiting_connect",
5262
- provider: opts.what,
5263
- url: opts.startUrl,
5264
- note: "Open the URL in your signed-in browser; this command waits. Do not retry in a loop."
5265
- });
5266
- } else {
5267
- console.log(`
5268
- Connect ${terminalText(opts.what)} in your signed-in browser:
5269
- `);
5270
- console.log(` ${terminalText(opts.startUrl)}
5271
- `);
5272
- console.log(" Open in your signed-in browser and approve access \u2014 this command waits.");
5273
- if (agentMode()) {
5274
- console.log(
5275
- " Do not retry in a loop; ask the human to complete the browser step. This command waits."
5276
- );
5277
- }
5278
- }
5279
- if (dots) process.stdout.write(" Waiting for the connection");
5280
- for (; ; ) {
5281
- let connected2 = false;
5282
- try {
5283
- connected2 = await opts.poll();
5284
- } catch {
5285
- }
5286
- if (connected2) {
5287
- const { result, lines } = opts.onConnected();
5288
- if (opts.json) {
5289
- emitProgress({ status: "connected" });
5290
- emitResult(result);
5291
- return;
5292
- }
5293
- if (dots) process.stdout.write("\n");
5294
- for (const line of lines) console.log(line);
5295
- return;
5296
- }
5297
- if (Date.now() >= deadline) break;
5298
- if (dots) process.stdout.write(".");
5299
- await sleep3(interval);
5300
- }
5301
- if (opts.json) {
5302
- emitResult({ connected: false, error: "timeout" });
5303
- process.exit(1);
5778
+ // src/commands/slack.ts
5779
+ async function slackStartUrl(token) {
5780
+ const res = await api("/cli/whoami", { token });
5781
+ const body = await readJson(res);
5782
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
5783
+ if (!res.ok || typeof body.projectId !== "string" || !body.projectId) {
5784
+ die(body.error ?? "Couldn't resolve the selected Fillo project.");
5304
5785
  }
5305
- if (dots) process.stdout.write("\n");
5306
- console.log(
5307
- ` Timed out waiting for ${terminalText(opts.what)} to connect. Finish the browser step, then re-run this command.`
5308
- );
5309
- process.exit(1);
5786
+ return `${API}/api/integrations/slack/start?return=terminal&project=${encodeURIComponent(body.projectId)}`;
5310
5787
  }
5311
-
5312
- // src/commands/slack.ts
5313
- var SLACK_START_URL = `${API}/api/integrations/slack/start?return=terminal`;
5314
5788
  async function refresh(flags) {
5315
5789
  const json = jsonMode(flags);
5316
5790
  const token = requireToken();
@@ -5318,8 +5792,9 @@ async function refresh(flags) {
5318
5792
  const body = await readJson(res);
5319
5793
  if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
5320
5794
  if (res.status === 409) {
5795
+ const startUrl = await slackStartUrl(token);
5321
5796
  die(
5322
- `${body.error ?? "Reconnect Slack in the browser to refresh channels."} Reconnect at ${SLACK_START_URL}`
5797
+ `${body.error ?? "Reconnect Slack in the browser to refresh channels."} Reconnect at ${startUrl}`
5323
5798
  );
5324
5799
  }
5325
5800
  if (res.status === 429) {
@@ -5351,7 +5826,7 @@ function printChannels(channels) {
5351
5826
  console.log("");
5352
5827
  printTable(["NAME", "ID", "VISIBILITY"], rows);
5353
5828
  }
5354
- async function status2(flags) {
5829
+ async function status3(flags) {
5355
5830
  const json = jsonMode(flags);
5356
5831
  const showChannels = flags.channels === true;
5357
5832
  const token = requireToken();
@@ -5376,13 +5851,14 @@ async function status2(flags) {
5376
5851
  );
5377
5852
  }
5378
5853
  }
5379
- async function connect(flags) {
5854
+ async function connect2(flags) {
5380
5855
  const json = jsonMode(flags);
5381
5856
  const token = requireToken();
5857
+ const startUrl = await slackStartUrl(token);
5382
5858
  await connectViaBrowser({
5383
5859
  json,
5384
5860
  what: "Slack",
5385
- startUrl: SLACK_START_URL,
5861
+ startUrl,
5386
5862
  poll: async () => {
5387
5863
  const res = await api("/cli/slack", { token });
5388
5864
  if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
@@ -5401,12 +5877,12 @@ async function connect(flags) {
5401
5877
  }
5402
5878
  async function slack(subcommand, flags) {
5403
5879
  if (subcommand === "help") return slackHelp();
5404
- if (subcommand === "connect") return connect(flags);
5880
+ if (subcommand === "connect") return connect2(flags);
5405
5881
  if (subcommand !== void 0 && subcommand !== "status") {
5406
5882
  die(`Unknown slack command: ${terminalText(subcommand)} (expected status or connect).`);
5407
5883
  }
5408
5884
  if (flags.refresh === true) return refresh(flags);
5409
- return status2(flags);
5885
+ return status3(flags);
5410
5886
  }
5411
5887
  function slackHelp() {
5412
5888
  console.log(`
@@ -5455,7 +5931,7 @@ async function fetchStatus(token) {
5455
5931
  if (!res.ok || !body.providers) die(body.error ?? `storage status failed (${res.status}).`);
5456
5932
  return body;
5457
5933
  }
5458
- async function status3(flags) {
5934
+ async function status4(flags) {
5459
5935
  const token = requireToken();
5460
5936
  const body = await fetchStatus(token);
5461
5937
  if (jsonMode(flags)) return emitResult(body);
@@ -5622,7 +6098,7 @@ async function connectBrowserProvider(provider, flags) {
5622
6098
  }
5623
6099
  });
5624
6100
  }
5625
- async function connect2(providerArg, flags) {
6101
+ async function connect3(providerArg, flags) {
5626
6102
  const provider = resolveProviderArg(providerArg);
5627
6103
  if (!provider) {
5628
6104
  die("Usage: fillo storage connect <s3|drive|box> (r2 and gdrive are accepted aliases).");
@@ -5645,9 +6121,9 @@ async function disconnect(providerArg, flags) {
5645
6121
  console.log(` ${okMark()} Disconnected ${PROVIDER_LABEL[provider]} storage.`);
5646
6122
  }
5647
6123
  async function storage(subcommand, args, flags) {
5648
- if (subcommand === void 0 || subcommand === "status") return status3(flags);
6124
+ if (subcommand === void 0 || subcommand === "status") return status4(flags);
5649
6125
  if (subcommand === "help") return storageHelp();
5650
- if (subcommand === "connect") return connect2(args[0], flags);
6126
+ if (subcommand === "connect") return connect3(args[0], flags);
5651
6127
  if (subcommand === "disconnect") return disconnect(args[0], flags);
5652
6128
  die(
5653
6129
  `Unknown storage command: ${terminalText(subcommand)} (expected status, connect, or disconnect).`
@@ -5682,6 +6158,63 @@ var storageCommand = {
5682
6158
  help: storageHelp
5683
6159
  };
5684
6160
 
6161
+ // src/commands/tokens.ts
6162
+ var TOOLS = ["zapier", "n8n"];
6163
+ async function createConnector(flags) {
6164
+ const json = jsonMode(flags);
6165
+ const tool = enumFlag(flags, "tool", TOOLS);
6166
+ if (!tool) die(`Usage: fillo tokens create-connector --tool ${TOOLS.join("|")}`);
6167
+ const res = await api("/cli/tokens/connector", {
6168
+ method: "POST",
6169
+ token: requireToken(),
6170
+ body: JSON.stringify({ tool })
6171
+ });
6172
+ const body = await readJson(res);
6173
+ if (res.status === 401) die(body.error ?? "Invalid or missing CLI token \u2014 run `fillo login`");
6174
+ if (!res.ok || !body.token) die(body.error ?? `tokens create-connector failed (${res.status}).`);
6175
+ if (!/^fcli_[A-Za-z0-9._~-]{8,512}$/.test(body.token)) {
6176
+ die("Fillo returned an unexpected token format.");
6177
+ }
6178
+ if (json) return emitResult(body);
6179
+ console.log(
6180
+ `
6181
+ ${okMark()} Created a ${bold(terminalText(body.label ?? tool))} connector token`
6182
+ );
6183
+ console.log(`
6184
+ ${body.token}
6185
+ `);
6186
+ console.log(` ${bold("Store it now")} \u2014 Fillo cannot show this token again.`);
6187
+ console.log(
6188
+ " Treat it like a password: it can read and change this project's forms and responses."
6189
+ );
6190
+ console.log(" Revoke it any time in Settings \u2192 Connections.\n");
6191
+ }
6192
+ async function tokens(subcommand, flags) {
6193
+ if (subcommand === void 0 || subcommand === "help") return tokensHelp();
6194
+ if (subcommand === "create-connector") return createConnector(flags);
6195
+ die(`Unknown tokens command: ${terminalText(subcommand)} (expected create-connector).`);
6196
+ }
6197
+ function tokensHelp() {
6198
+ console.log(`
6199
+ ${boldRaw("fillo tokens")} \u2014 credentials for no-code connectors
6200
+
6201
+ ${boldRaw("Commands")}
6202
+ tokens create-connector --tool ${TOOLS.join("|")}
6203
+ Mint the connector's API token \u2014 shown once, store it
6204
+
6205
+ ${dimRaw("The token is non-expiring so a live automation doesn't break silently, and")}
6206
+ ${dimRaw("stays revocable in Settings \u2192 Connections. For coding agents and your own")}
6207
+ ${dimRaw("integrations use `fillo keys create` instead \u2014 those are scoped.")}
6208
+ ${dimRaw("--json prints the raw server response on stdout, token included.")}
6209
+ `);
6210
+ }
6211
+ var tokensCommand = {
6212
+ name: "tokens",
6213
+ flags: ["tool"],
6214
+ run: (args, flags) => tokens(args[0], flags),
6215
+ help: tokensHelp
6216
+ };
6217
+
5685
6218
  // src/commands/webhooks.ts
5686
6219
  var dateTime2 = (iso) => iso.slice(0, 16).replace("T", " ");
5687
6220
  async function list3(handle, flags) {
@@ -5812,6 +6345,115 @@ var webhooksCommand = {
5812
6345
  help: webhooksHelp
5813
6346
  };
5814
6347
 
6348
+ // src/commands/projects.ts
6349
+ function authFailure(status5) {
6350
+ if (status5 === 401) die("Token invalid \u2014 run `fillo login` again.");
6351
+ }
6352
+ function saveSelectedProject(project) {
6353
+ if (!project.publishableKey.startsWith("pk_")) {
6354
+ die("Fillo returned an invalid publishable key; the selected project was not saved locally.");
6355
+ }
6356
+ const {
6357
+ claimUrl: _claimUrl,
6358
+ email: _email2,
6359
+ name: _name,
6360
+ claimToken: _claimToken,
6361
+ ...current
6362
+ } = readConfig();
6363
+ writeConfig({ ...current, pk: project.publishableKey });
6364
+ }
6365
+ async function list4(flags) {
6366
+ const res = await api("/cli/projects", { token: requireToken() });
6367
+ const body = await readJson(res);
6368
+ authFailure(res.status);
6369
+ if (!res.ok || !Array.isArray(body.projects)) {
6370
+ die(body.error ?? `project list failed (${res.status}).`);
6371
+ }
6372
+ if (jsonMode(flags)) return emitResult(body);
6373
+ console.log(`
6374
+ ${bold("Projects")}`);
6375
+ printTable(
6376
+ ["", "NAME", "SLUG", "ID"],
6377
+ body.projects.map((project) => [
6378
+ project.current ? "*" : "",
6379
+ terminalText(project.name),
6380
+ terminalText(project.slug),
6381
+ terminalText(project.id)
6382
+ ])
6383
+ );
6384
+ console.log(`
6385
+ ${dim("* selected for this CLI login")}`);
6386
+ console.log(` ${dim("Switch with `fillo project select <id-or-slug>`.")}
6387
+ `);
6388
+ }
6389
+ async function create(name, flags) {
6390
+ if (!name) die('Usage: fillo project create "Project name"');
6391
+ const res = await api("/cli/projects", {
6392
+ method: "POST",
6393
+ token: requireToken(),
6394
+ body: JSON.stringify({ name, source: "cli" })
6395
+ });
6396
+ const body = await readJson(res);
6397
+ authFailure(res.status);
6398
+ if (!res.ok || !body.project || body.selected !== true) {
6399
+ die(body.error ?? `project create failed (${res.status}).`);
6400
+ }
6401
+ saveSelectedProject(body.project);
6402
+ if (jsonMode(flags)) return emitResult(body);
6403
+ console.log(` ${okMark()} Created and selected ${bold(terminalText(body.project.name))}.`);
6404
+ console.log(` ${dim("Forms, keys, origins, identities, and agent access are isolated here.")}`);
6405
+ console.log(` ${dim("Members, billing, storage, and usage totals stay with the workspace.")}`);
6406
+ }
6407
+ async function select(target, flags) {
6408
+ if (!target) die("Usage: fillo project select <id|slug|unique-name>");
6409
+ const res = await api("/cli/projects/select", {
6410
+ method: "POST",
6411
+ token: requireToken(),
6412
+ body: JSON.stringify({ project: target, source: "cli" })
6413
+ });
6414
+ const body = await readJson(res);
6415
+ authFailure(res.status);
6416
+ if (!res.ok || !body.project || body.selected !== true) {
6417
+ die(body.error ?? `project select failed (${res.status}).`);
6418
+ }
6419
+ saveSelectedProject(body.project);
6420
+ if (jsonMode(flags)) return emitResult(body);
6421
+ console.log(` ${okMark()} Selected ${bold(terminalText(body.project.name))}.`);
6422
+ console.log(` ${dim("Future CLI commands use this project until you select another.")}`);
6423
+ }
6424
+ async function projects(subcommand, args, flags) {
6425
+ if (subcommand === void 0 || subcommand === "list" || subcommand === "ls") {
6426
+ return list4(flags);
6427
+ }
6428
+ if (subcommand === "help") return projectHelp();
6429
+ if (subcommand === "create") return create(args.join(" ").trim() || void 0, flags);
6430
+ if (subcommand === "select" || subcommand === "use") {
6431
+ return select(args.join(" ").trim() || void 0, flags);
6432
+ }
6433
+ die(`Unknown project command: ${terminalText(subcommand)} (expected list, create, or select).`);
6434
+ }
6435
+ function projectHelp() {
6436
+ console.log(`
6437
+ ${boldRaw("fillo project")} \u2014 create and select isolated sites/apps
6438
+
6439
+ ${boldRaw("Commands")}
6440
+ project List projects in the current workspace
6441
+ project create <name> Create and select a project
6442
+ project select <id|slug|name> Select a project for future commands
6443
+
6444
+ ${dimRaw("Requires an ordinary `fillo login`. Project-pinned agent handoff")}
6445
+ ${dimRaw("tokens cannot list, create, or select sibling projects.")}
6446
+ ${dimRaw("--json prints the server response on stdout.")}
6447
+ `);
6448
+ }
6449
+ var projectCommand = {
6450
+ name: "project",
6451
+ aliases: ["projects"],
6452
+ flags: [],
6453
+ run: (args, flags) => projects(args[0], args.slice(1), flags),
6454
+ help: projectHelp
6455
+ };
6456
+
5815
6457
  // src/index.ts
5816
6458
  var REGISTRY = [
5817
6459
  initCommand,
@@ -5819,6 +6461,7 @@ var REGISTRY = [
5819
6461
  loginCommand,
5820
6462
  logoutCommand,
5821
6463
  whoamiCommand,
6464
+ projectCommand,
5822
6465
  keysCommand,
5823
6466
  pushCommand,
5824
6467
  listCommand,
@@ -5830,8 +6473,11 @@ var REGISTRY = [
5830
6473
  skillCommand,
5831
6474
  storageCommand,
5832
6475
  slackCommand,
6476
+ discordCommand,
6477
+ tokensCommand,
5833
6478
  webhooksCommand,
5834
6479
  settingsCommand,
6480
+ brandingCommand,
5835
6481
  membersCommand,
5836
6482
  deleteCommand
5837
6483
  ];
@@ -5862,15 +6508,17 @@ function help() {
5862
6508
  ${dimRaw("--headless approve a code instead of the local browser handoff")}
5863
6509
  ${dimRaw("--run <id> --token <token> bind a fresh agent handoff login")}
5864
6510
  logout Forget the stored credentials
5865
- whoami Show the signed-in workspace
5866
- keys <cmd> Workspace API keys for agents/CI: create, list, revoke
6511
+ whoami Show the signed-in workspace and selected project
6512
+ project <cmd> Isolated sites/apps: list, create, select
6513
+ ${dimRaw("project create <name> \xB7 project select <id|slug|name>")}
6514
+ keys <cmd> Project API keys for agents/CI: create, list, revoke
5867
6515
  ${dimRaw("keys create --name <n> --preset read|agent|full")}
5868
6516
  push <file|-> Create/update a form from JSON (file or stdin)
5869
6517
  ${dimRaw("--handle <name> idempotent id for re-pushes")}
5870
6518
  ${dimRaw("--stage stage beside the live form for dashboard review")}
5871
6519
  ${dimRaw("--draft alias with a handle; legacy one-off draft without")}
5872
6520
  ${dimRaw("--allow-code allow a .mjs/.js schema (executes the file)")}
5873
- list List the workspace's forms and their live URLs
6521
+ list List the project's forms and their live URLs
5874
6522
  status <form> Show one form's status, live URL, and publish blockers
5875
6523
  ${dimRaw("<form> is a form id or handle")}
5876
6524
  publish <form> Publish staged changes (or a draft form) \u2014 prints the live URL
@@ -5886,9 +6534,13 @@ function help() {
5886
6534
  storage <cmd> Inspect/connect upload destinations (status, connect, disconnect)
5887
6535
  ${dimRaw("storage connect s3 \xB7 connect drive|box \xB7 disconnect <p>")}
5888
6536
  slack <cmd> Slack notifications: status, --channels, --refresh, connect
6537
+ discord <cmd> Discord destination: connect, webhook, status, enable, disable, roles
6538
+ ${dimRaw("discord enable <form> --fields a,b --early-signal 10 --role <g>/<r>")}
6539
+ tokens <cmd> Connector credentials: create-connector --tool zapier|n8n
5889
6540
  webhooks <cmd> A form's signed webhooks: list, add, set, remove
5890
6541
  ${dimRaw("webhooks add <form> --url <u> shows the signing secret once")}
5891
6542
  settings <cmd> A form's operational settings: get, set key=value
6543
+ branding [on|off] The workspace's "Powered by Fillo" badge (off needs Everything)
5892
6544
  members <cmd> Workspace members: list, invite <email>, cancel-invite <id>
5893
6545
  delete <cmd> Irreversible deletes: delete form <form> \xB7 delete workspace
5894
6546
  ${dimRaw("typed confirmation required; --confirm for agents, --yes never skips")}