@usefillo/cli 0.16.1 → 0.20.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/README.md +35 -2
- package/dist/index.js +809 -115
- package/dist/skill/build-with-fillo/SKILL.md +146 -46
- package/dist/skill/build-with-fillo/agents/openai.yaml +2 -2
- package/dist/skill/build-with-fillo/references/auth-and-lifecycle.md +74 -28
- package/dist/skill/build-with-fillo/references/frameworks.md +22 -0
- package/dist/skill/build-with-fillo/references/operations.md +83 -6
- package/dist/skill/build-with-fillo/references/schema-and-ux.md +5 -0
- package/dist/skill/build-with-fillo/references/source-map.md +6 -2
- package/dist/skill/build-with-fillo/references/troubleshooting.md +3 -3
- package/package.json +3 -3
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 = (
|
|
296
|
-
res.statusCode =
|
|
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) {
|
|
@@ -802,6 +815,7 @@ async function provisionWorkspace(flags, usageHint, apiBase = API) {
|
|
|
802
815
|
}
|
|
803
816
|
}
|
|
804
817
|
const workspaceName = flagString(flags, "workspace-name") ?? defaultWorkspaceName();
|
|
818
|
+
const promptCopyId = flagString(flags, "pc")?.trim();
|
|
805
819
|
const res = await api(
|
|
806
820
|
"/workspaces/provision",
|
|
807
821
|
{
|
|
@@ -810,7 +824,8 @@ async function provisionWorkspace(flags, usageHint, apiBase = API) {
|
|
|
810
824
|
email: email3,
|
|
811
825
|
source: "cli",
|
|
812
826
|
...name ? { name } : {},
|
|
813
|
-
...workspaceName ? { workspaceName } : {}
|
|
827
|
+
...workspaceName ? { workspaceName } : {},
|
|
828
|
+
...promptCopyId ? { promptCopyId } : {}
|
|
814
829
|
})
|
|
815
830
|
},
|
|
816
831
|
apiBase
|
|
@@ -945,7 +960,7 @@ async function init(flags) {
|
|
|
945
960
|
}
|
|
946
961
|
var initCommand = {
|
|
947
962
|
name: "init",
|
|
948
|
-
flags: ["email", "name", "workspace-name"],
|
|
963
|
+
flags: ["email", "name", "workspace-name", "pc"],
|
|
949
964
|
run: (_args, flags) => init(flags)
|
|
950
965
|
};
|
|
951
966
|
|
|
@@ -1153,7 +1168,7 @@ function installSkill(flags, opts = {}) {
|
|
|
1153
1168
|
for (const target of targets) console.log(` ${dim(target)}`);
|
|
1154
1169
|
if (targets.length > 1) {
|
|
1155
1170
|
console.log(
|
|
1156
|
-
` ${dim("Same bundle in both \u2014 cross-agent compatibility, not duplication.")}`
|
|
1171
|
+
` ${dim("Same bundle in both \u2014 cross-agent compatibility, not duplication (want one folder? --agent <name> or --dir <path>).")}`
|
|
1157
1172
|
);
|
|
1158
1173
|
}
|
|
1159
1174
|
console.log(`
|
|
@@ -1197,7 +1212,7 @@ var skillCommand = {
|
|
|
1197
1212
|
};
|
|
1198
1213
|
|
|
1199
1214
|
// src/commands/agent.ts
|
|
1200
|
-
var BOOTSTRAP_USAGE = 'To set up a workspace I need
|
|
1215
|
+
var BOOTSTRAP_USAGE = 'To set up a workspace I need the human\'s real work email for the recovery link. Ask the human for their work email once \u2014 never invent an address or use a documentation placeholder \u2014 then re-run: npx @usefillo/cli@latest agent bootstrap --email <work-email> [--name "Your Name"]. (Already have an account? `fillo login`.)';
|
|
1201
1216
|
async function bootstrapStandalone(flags, apiBase) {
|
|
1202
1217
|
if (flags.account === true) {
|
|
1203
1218
|
die(
|
|
@@ -1294,21 +1309,21 @@ async function agent(subcommand, flags) {
|
|
|
1294
1309
|
die("--account can only be used with `fillo agent bootstrap` or `fillo agent connect`.");
|
|
1295
1310
|
}
|
|
1296
1311
|
if (subcommand === "event") {
|
|
1297
|
-
const
|
|
1312
|
+
const status5 = enumFlag(flags, "status", AGENT_EVENT_STATUSES);
|
|
1298
1313
|
const message = flagString(flags, "message");
|
|
1299
|
-
if (!
|
|
1314
|
+
if (!status5)
|
|
1300
1315
|
die('Usage: fillo agent event --status <status> --message "<what the human does next>"');
|
|
1301
1316
|
const action = enumFlag(flags, "action", AGENT_ACTIONS);
|
|
1302
1317
|
const formStatus = enumFlag(flags, "form-status", FORM_STATUSES);
|
|
1303
1318
|
const formId = optionalStringFlag(flags, "form-id");
|
|
1304
|
-
if ((
|
|
1305
|
-
die(`--form-id is required when --status is ${
|
|
1319
|
+
if ((status5 === "done" || status5 === "needs_action") && !formId) {
|
|
1320
|
+
die(`--form-id is required when --status is ${status5}.`);
|
|
1306
1321
|
}
|
|
1307
|
-
if (
|
|
1322
|
+
if (status5 === "needs_action" && !action) {
|
|
1308
1323
|
die("--action is required when --status is needs_action.");
|
|
1309
1324
|
}
|
|
1310
1325
|
await postAgentEvent(apiBase, run, token, {
|
|
1311
|
-
status:
|
|
1326
|
+
status: status5,
|
|
1312
1327
|
message,
|
|
1313
1328
|
appUrl: flagString(flags, "app-url"),
|
|
1314
1329
|
action,
|
|
@@ -1390,9 +1405,10 @@ function agentHelp() {
|
|
|
1390
1405
|
|
|
1391
1406
|
${boldRaw("Commands")}
|
|
1392
1407
|
agent bootstrap Set up a workspace and install the Build with Fillo skill
|
|
1393
|
-
${dimRaw("--email
|
|
1408
|
+
${dimRaw("--email <work-email> provision a fresh workspace (ask the human first; a TTY confirms git identity)")}
|
|
1394
1409
|
${dimRaw('--name "Your Name" optional display name')}
|
|
1395
1410
|
${dimRaw("--workspace-name <name> name the new workspace (default: this repo/directory)")}
|
|
1411
|
+
${dimRaw("--pc <uuid> optional marketing stitch id from /agents?pc=")}
|
|
1396
1412
|
${dimRaw("--api <url> target a different Fillo deployment (default: FILLO_API or https://fillo.so)")}
|
|
1397
1413
|
${dimRaw("--run <id> --token <token> instead: attach to an open browser modal")}
|
|
1398
1414
|
${dimRaw("--account with --run/--token: approve an existing workspace in the browser")}
|
|
@@ -1419,6 +1435,7 @@ var agentCommand = {
|
|
|
1419
1435
|
"email",
|
|
1420
1436
|
"name",
|
|
1421
1437
|
"workspace-name",
|
|
1438
|
+
"pc",
|
|
1422
1439
|
"run",
|
|
1423
1440
|
"token",
|
|
1424
1441
|
"api",
|
|
@@ -1469,13 +1486,13 @@ async function claim(flags) {
|
|
|
1469
1486
|
const grant = await readJson(codeRes);
|
|
1470
1487
|
const emailFlag = flagString(flags, "email");
|
|
1471
1488
|
const savedEmail = cfg.email;
|
|
1472
|
-
const { approval, sentTo, notice } = await sendClaimEmail(
|
|
1489
|
+
const { approval, sentTo, notice: notice2 } = await sendClaimEmail(
|
|
1473
1490
|
grant,
|
|
1474
1491
|
cfg.claimToken,
|
|
1475
1492
|
emailFlag,
|
|
1476
1493
|
savedEmail
|
|
1477
1494
|
);
|
|
1478
|
-
const alreadyClaimed = approval === "device" &&
|
|
1495
|
+
const alreadyClaimed = approval === "device" && notice2 === "already_claimed";
|
|
1479
1496
|
if (json) {
|
|
1480
1497
|
emitProgress({
|
|
1481
1498
|
status: "awaiting_claim",
|
|
@@ -1483,11 +1500,11 @@ async function claim(flags) {
|
|
|
1483
1500
|
...sentTo ? { email: sentTo } : {},
|
|
1484
1501
|
verification_uri: grant.verification_uri,
|
|
1485
1502
|
approval,
|
|
1486
|
-
...
|
|
1503
|
+
...notice2 ? { notice: notice2 } : {},
|
|
1487
1504
|
note: AGENT_WAIT_LINE
|
|
1488
1505
|
});
|
|
1489
1506
|
} else {
|
|
1490
|
-
printGuidance(grant, approval, sentTo,
|
|
1507
|
+
printGuidance(grant, approval, sentTo, notice2);
|
|
1491
1508
|
}
|
|
1492
1509
|
await pollForGrant(grant, cfg.pk, approval, alreadyClaimed, json);
|
|
1493
1510
|
}
|
|
@@ -1535,10 +1552,10 @@ async function sendClaimEmail(grant, claimToken, emailFlag, savedEmail) {
|
|
|
1535
1552
|
if (!res.ok) die(body.error ?? `Couldn't send the claim email (${res.status}).`);
|
|
1536
1553
|
return { approval: "device", sentTo: email3 };
|
|
1537
1554
|
}
|
|
1538
|
-
function printGuidance(grant, approval, sentTo,
|
|
1539
|
-
if (
|
|
1555
|
+
function printGuidance(grant, approval, sentTo, notice2) {
|
|
1556
|
+
if (notice2 === "already_claimed") {
|
|
1540
1557
|
console.log("\n This workspace is already claimed.");
|
|
1541
|
-
} else if (
|
|
1558
|
+
} else if (notice2 === "recent_email") {
|
|
1542
1559
|
console.log(
|
|
1543
1560
|
`
|
|
1544
1561
|
A claim email was already sent recently${sentTo ? ` to ${bold(terminalText(sentTo))}` : ""} \u2014 open that one.`
|
|
@@ -1552,7 +1569,7 @@ function printGuidance(grant, approval, sentTo, notice) {
|
|
|
1552
1569
|
console.log(`
|
|
1553
1570
|
Your code: ${bold(grant.user_code)}
|
|
1554
1571
|
`);
|
|
1555
|
-
if (
|
|
1572
|
+
if (notice2 === "already_claimed") {
|
|
1556
1573
|
console.log(` Sign in at ${terminalText(grant.verification_uri)} and enter the code`);
|
|
1557
1574
|
console.log(" to approve this terminal.");
|
|
1558
1575
|
} else if (approval === "page") {
|
|
@@ -1612,11 +1629,11 @@ Claim failed: ${body.error}`);
|
|
|
1612
1629
|
}
|
|
1613
1630
|
if (!claimedAnnounced && Date.now() >= nextStatusAt) {
|
|
1614
1631
|
nextStatusAt = Date.now() + statusEvery;
|
|
1615
|
-
const
|
|
1632
|
+
const status5 = await api(`/workspaces/claim-status?key=${encodeURIComponent(pk)}`).catch(
|
|
1616
1633
|
() => null
|
|
1617
1634
|
);
|
|
1618
|
-
if (
|
|
1619
|
-
const state = await
|
|
1635
|
+
if (status5?.ok) {
|
|
1636
|
+
const state = await status5.json().catch(() => ({}));
|
|
1620
1637
|
if (state.claimed === true) {
|
|
1621
1638
|
claimedAnnounced = true;
|
|
1622
1639
|
if (json) {
|
|
@@ -1858,6 +1875,441 @@ var deleteCommand = {
|
|
|
1858
1875
|
help: deleteHelp
|
|
1859
1876
|
};
|
|
1860
1877
|
|
|
1878
|
+
// src/lib/browser-connect.ts
|
|
1879
|
+
var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1880
|
+
function pollIntervalMs2(fallback) {
|
|
1881
|
+
const raw = process.env.FILLO_POLL_INTERVAL_MS;
|
|
1882
|
+
if (raw === void 0 || raw === "") return fallback;
|
|
1883
|
+
const value = Number(raw);
|
|
1884
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
1885
|
+
}
|
|
1886
|
+
async function connectViaBrowser(opts) {
|
|
1887
|
+
const interval = pollIntervalMs2(opts.intervalMs ?? 5e3);
|
|
1888
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 6e5);
|
|
1889
|
+
const dots = !opts.json && !agentMode();
|
|
1890
|
+
if (opts.json) {
|
|
1891
|
+
emitProgress({
|
|
1892
|
+
status: "awaiting_connect",
|
|
1893
|
+
provider: opts.what,
|
|
1894
|
+
url: opts.startUrl,
|
|
1895
|
+
note: "Open the URL in your signed-in browser; this command waits. Do not retry in a loop."
|
|
1896
|
+
});
|
|
1897
|
+
} else {
|
|
1898
|
+
console.log(`
|
|
1899
|
+
Connect ${terminalText(opts.what)} in your signed-in browser:
|
|
1900
|
+
`);
|
|
1901
|
+
console.log(` ${terminalText(opts.startUrl)}
|
|
1902
|
+
`);
|
|
1903
|
+
console.log(" Open in your signed-in browser and approve access \u2014 this command waits.");
|
|
1904
|
+
if (agentMode()) {
|
|
1905
|
+
console.log(
|
|
1906
|
+
" Do not retry in a loop; ask the human to complete the browser step. This command waits."
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
if (dots) process.stdout.write(" Waiting for the connection");
|
|
1911
|
+
for (; ; ) {
|
|
1912
|
+
let connected2 = false;
|
|
1913
|
+
try {
|
|
1914
|
+
connected2 = await opts.poll();
|
|
1915
|
+
} catch {
|
|
1916
|
+
}
|
|
1917
|
+
if (connected2) {
|
|
1918
|
+
const { result, lines } = opts.onConnected();
|
|
1919
|
+
if (opts.json) {
|
|
1920
|
+
emitProgress({ status: "connected" });
|
|
1921
|
+
emitResult(result);
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
if (dots) process.stdout.write("\n");
|
|
1925
|
+
for (const line of lines) console.log(line);
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
if (Date.now() >= deadline) break;
|
|
1929
|
+
if (dots) process.stdout.write(".");
|
|
1930
|
+
await sleep3(interval);
|
|
1931
|
+
}
|
|
1932
|
+
if (opts.json) {
|
|
1933
|
+
emitResult({ connected: false, error: "timeout" });
|
|
1934
|
+
process.exit(1);
|
|
1935
|
+
}
|
|
1936
|
+
if (dots) process.stdout.write("\n");
|
|
1937
|
+
console.log(
|
|
1938
|
+
` Timed out waiting for ${terminalText(opts.what)} to connect. Finish the browser step, then re-run this command.`
|
|
1939
|
+
);
|
|
1940
|
+
process.exit(1);
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// src/commands/discord.ts
|
|
1944
|
+
var WEBHOOK_URL_ENV = "FILLO_DISCORD_WEBHOOK_URL";
|
|
1945
|
+
var SNOWFLAKE = /^\d{17,20}$/;
|
|
1946
|
+
var EARLY_SIGNAL_CHOICES = ["5", "10", "25", "off"];
|
|
1947
|
+
function notice(json, line) {
|
|
1948
|
+
if (json) emitProgress({ status: "notice", notice: line });
|
|
1949
|
+
else console.log(` ${line}`);
|
|
1950
|
+
}
|
|
1951
|
+
async function fetchConnection(token) {
|
|
1952
|
+
const res = await api("/cli/discord", { token });
|
|
1953
|
+
if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
|
|
1954
|
+
const body = await readJson(res);
|
|
1955
|
+
if (!res.ok) die(body.error ?? `discord status failed (${res.status}).`);
|
|
1956
|
+
return body;
|
|
1957
|
+
}
|
|
1958
|
+
async function fetchDestination(token, form) {
|
|
1959
|
+
const res = await api(`/cli/forms/${encodeURIComponent(form)}/discord`, { token });
|
|
1960
|
+
if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
|
|
1961
|
+
const body = await readJson(res);
|
|
1962
|
+
if (res.status === 404) die(body.error ?? "Form not found in the selected project.");
|
|
1963
|
+
if (!res.ok) die(body.error ?? `discord status failed (${res.status}).`);
|
|
1964
|
+
return body;
|
|
1965
|
+
}
|
|
1966
|
+
async function putDestination(token, form, patch) {
|
|
1967
|
+
const res = await api(`/cli/forms/${encodeURIComponent(form)}/discord`, {
|
|
1968
|
+
method: "PUT",
|
|
1969
|
+
token,
|
|
1970
|
+
body: JSON.stringify(patch)
|
|
1971
|
+
});
|
|
1972
|
+
if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
|
|
1973
|
+
const body = await readJson(res);
|
|
1974
|
+
if (res.status === 404) die(body.error ?? "Form not found in the selected project.");
|
|
1975
|
+
if (!res.ok) die(body.error ?? `discord update failed (${res.status}).`);
|
|
1976
|
+
return body;
|
|
1977
|
+
}
|
|
1978
|
+
async function projectId(token) {
|
|
1979
|
+
const res = await api("/cli/whoami", { token });
|
|
1980
|
+
const body = await readJson(res);
|
|
1981
|
+
if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
|
|
1982
|
+
if (!res.ok || typeof body.projectId !== "string" || !body.projectId) {
|
|
1983
|
+
die(body.error ?? "Couldn't resolve the selected Fillo project.");
|
|
1984
|
+
}
|
|
1985
|
+
return body.projectId;
|
|
1986
|
+
}
|
|
1987
|
+
async function connect(flags) {
|
|
1988
|
+
const json = jsonMode(flags);
|
|
1989
|
+
const token = requireToken();
|
|
1990
|
+
const connection = await fetchConnection(token);
|
|
1991
|
+
if (!connection.appConfigured) {
|
|
1992
|
+
die(
|
|
1993
|
+
"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."
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
const startUrl = `${API}/api/integrations/discord/start?return=terminal&project=${encodeURIComponent(
|
|
1997
|
+
await projectId(token)
|
|
1998
|
+
)}`;
|
|
1999
|
+
let connected2 = null;
|
|
2000
|
+
await connectViaBrowser({
|
|
2001
|
+
json,
|
|
2002
|
+
what: "Discord",
|
|
2003
|
+
startUrl,
|
|
2004
|
+
poll: async () => {
|
|
2005
|
+
const snapshot = await fetchConnection(token);
|
|
2006
|
+
connected2 = snapshot;
|
|
2007
|
+
return snapshot.connected === true;
|
|
2008
|
+
},
|
|
2009
|
+
onConnected: () => {
|
|
2010
|
+
const label = connected2?.channelLabel ?? null;
|
|
2011
|
+
return {
|
|
2012
|
+
result: {
|
|
2013
|
+
connected: true,
|
|
2014
|
+
webhookId: connected2?.webhookId ?? null,
|
|
2015
|
+
channelLabel: label
|
|
2016
|
+
},
|
|
2017
|
+
lines: [
|
|
2018
|
+
` ${okMark()} Discord connected${label ? ` (${terminalText(label)})` : ""}.`,
|
|
2019
|
+
" Turn a form on with `fillo discord enable <form>`."
|
|
2020
|
+
]
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
async function webhook(flags) {
|
|
2026
|
+
const json = jsonMode(flags);
|
|
2027
|
+
const token = requireToken();
|
|
2028
|
+
let url = process.env[WEBHOOK_URL_ENV]?.trim() ?? "";
|
|
2029
|
+
if (!url) {
|
|
2030
|
+
if (json || !isInteractive()) {
|
|
2031
|
+
die(
|
|
2032
|
+
`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.`
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
url = await readSecret(" Discord webhook URL (hidden): ").catch(() => "");
|
|
2036
|
+
if (!url) {
|
|
2037
|
+
die(`No webhook URL entered. Paste it at the prompt, or set ${WEBHOOK_URL_ENV} and re-run.`);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
const label = flagString(flags, "label");
|
|
2041
|
+
const res = await api("/cli/discord/webhook", {
|
|
2042
|
+
method: "POST",
|
|
2043
|
+
token,
|
|
2044
|
+
body: JSON.stringify({ url, ...label ? { label } : {} })
|
|
2045
|
+
});
|
|
2046
|
+
const body = await readJson(res);
|
|
2047
|
+
if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
|
|
2048
|
+
if (!res.ok || !body.webhookId) die(body.error ?? `discord webhook failed (${res.status}).`);
|
|
2049
|
+
if (json) return emitResult(body);
|
|
2050
|
+
console.log(
|
|
2051
|
+
` ${okMark()} Connected Discord${body.label ? ` (${terminalText(body.label)})` : ""}.`
|
|
2052
|
+
);
|
|
2053
|
+
console.log(" Turn a form on with `fillo discord enable <form>`.");
|
|
2054
|
+
}
|
|
2055
|
+
function printDestination(body) {
|
|
2056
|
+
if (!body.connected) {
|
|
2057
|
+
console.log(
|
|
2058
|
+
" Discord is not connected to this project. Run `fillo discord connect` (or `fillo discord webhook` to paste a channel URL)."
|
|
2059
|
+
);
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
if (!body.enabled) {
|
|
2063
|
+
console.log(
|
|
2064
|
+
` Discord is connected${body.connectionLabel ? ` (${terminalText(body.connectionLabel)})` : ""} but this form isn't sending. Turn it on with \`fillo discord enable <form>\`.`
|
|
2065
|
+
);
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
console.log(
|
|
2069
|
+
` ${okMark()} Sending to Discord${body.channelLabel ? ` \u2014 ${terminalText(body.channelLabel)}` : ""}`
|
|
2070
|
+
);
|
|
2071
|
+
const fields = body.includedFieldIds ?? [];
|
|
2072
|
+
console.log(
|
|
2073
|
+
` ${dim("Fields:")} ${fields.length > 0 ? terminalText(fields.join(", ")) : "link only"}`
|
|
2074
|
+
);
|
|
2075
|
+
console.log(
|
|
2076
|
+
` ${dim("Early signal:")} ${body.earlySignalLimit ? `first ${body.earlySignalLimit} responses in full (${body.earlySignalDelivered} sent)` : "off"}`
|
|
2077
|
+
);
|
|
2078
|
+
const grant = body.roleGrant;
|
|
2079
|
+
console.log(
|
|
2080
|
+
` ${dim("Role grant:")} ${grant ? `${terminalText(grant.roleId)} in ${terminalText(grant.guildId)}${grant.autoJoin ? " (auto-join)" : ""}` : "off"}`
|
|
2081
|
+
);
|
|
2082
|
+
if (!body.webhookMatches && body.connectionLabel) {
|
|
2083
|
+
console.log(
|
|
2084
|
+
`
|
|
2085
|
+
${dim(
|
|
2086
|
+
`This form posts to its own channel; the project default is ${terminalText(body.connectionLabel)}. Re-point it with \`fillo discord enable <form> --channel <channelId>\`.`
|
|
2087
|
+
)}`
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
const servers = body.servers ?? [];
|
|
2091
|
+
if (servers.length > 0) {
|
|
2092
|
+
console.log("");
|
|
2093
|
+
printTable(
|
|
2094
|
+
["SERVER", "ID"],
|
|
2095
|
+
servers.map((server) => [terminalText(server.name ?? "(name unavailable)"), server.guildId])
|
|
2096
|
+
);
|
|
2097
|
+
console.log(`
|
|
2098
|
+
${dim("Aim a form: fillo discord enable <form> --channel <channelId>")}`);
|
|
2099
|
+
}
|
|
2100
|
+
const channels = body.channels ?? [];
|
|
2101
|
+
if (servers.length === 0 && channels.length > 1) {
|
|
2102
|
+
console.log("");
|
|
2103
|
+
printTable(
|
|
2104
|
+
["CHANNEL", "WEBHOOK ID"],
|
|
2105
|
+
channels.map((channel) => [terminalText(channel.label), channel.webhookId])
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
async function status(form, flags) {
|
|
2110
|
+
if (!form) die("Usage: fillo discord status <form> \u2014 a form id, slug, or push handle.");
|
|
2111
|
+
const body = await fetchDestination(requireToken(), form);
|
|
2112
|
+
if (jsonMode(flags)) return emitResult(body);
|
|
2113
|
+
printDestination(body);
|
|
2114
|
+
}
|
|
2115
|
+
function parseFields(raw) {
|
|
2116
|
+
if (raw.trim().toLowerCase() === "none") return [];
|
|
2117
|
+
return raw.split(",").map((id) => id.trim()).filter(Boolean);
|
|
2118
|
+
}
|
|
2119
|
+
function parseRole(raw) {
|
|
2120
|
+
if (raw.trim().toLowerCase() === "none") return null;
|
|
2121
|
+
const [guildId, roleId, ...extra] = raw.trim().split("/");
|
|
2122
|
+
if (extra.length > 0 || !guildId || !roleId || !SNOWFLAKE.test(guildId) || !SNOWFLAKE.test(roleId)) {
|
|
2123
|
+
die(
|
|
2124
|
+
"--role must be <guildId>/<roleId> (Discord ids), or none to clear it. List them with `fillo discord roles [guildId]`."
|
|
2125
|
+
);
|
|
2126
|
+
}
|
|
2127
|
+
return { guildId, roleId };
|
|
2128
|
+
}
|
|
2129
|
+
async function enable(form, flags) {
|
|
2130
|
+
const json = jsonMode(flags);
|
|
2131
|
+
if (!form)
|
|
2132
|
+
die(
|
|
2133
|
+
"Usage: fillo discord enable <form> [--channel <webhookId>] [--fields a,b,c] [--early-signal 5|10|25|off]"
|
|
2134
|
+
);
|
|
2135
|
+
const token = requireToken();
|
|
2136
|
+
const current = await fetchDestination(token, form);
|
|
2137
|
+
const channels = current.channels ?? [];
|
|
2138
|
+
if ((!current.connected || !current.connectionWebhookId) && channels.length === 0) {
|
|
2139
|
+
die(
|
|
2140
|
+
"Discord isn't connected to this project. Run `fillo discord connect`, or `fillo discord webhook` to paste a channel URL."
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
const rawChannel = flagString(flags, "channel");
|
|
2144
|
+
if (rawChannel !== void 0 && !SNOWFLAKE.test(rawChannel.trim())) {
|
|
2145
|
+
die("--channel must be a Discord channel id (right-click the channel \u2192 Copy Channel ID).");
|
|
2146
|
+
}
|
|
2147
|
+
const targetChannelId = rawChannel?.trim() ?? null;
|
|
2148
|
+
const fallbackWebhookId = current.connectionWebhookId ?? (channels.length === 1 ? channels[0]?.webhookId ?? null : null);
|
|
2149
|
+
if (!current.enabled && !targetChannelId && !fallbackWebhookId) {
|
|
2150
|
+
die(
|
|
2151
|
+
"Pass --channel <channelId> to pick where this form posts. `fillo discord status <form>` lists the connected servers."
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
const patch = {};
|
|
2155
|
+
if (targetChannelId) {
|
|
2156
|
+
patch.enabled = true;
|
|
2157
|
+
patch.channelId = targetChannelId;
|
|
2158
|
+
} else if (!current.enabled && fallbackWebhookId) {
|
|
2159
|
+
patch.enabled = true;
|
|
2160
|
+
patch.webhookId = fallbackWebhookId;
|
|
2161
|
+
}
|
|
2162
|
+
const rawFields = flagString(flags, "fields");
|
|
2163
|
+
if (rawFields !== void 0) patch.includeFieldIds = parseFields(rawFields);
|
|
2164
|
+
const rawEarly = flagString(flags, "early-signal");
|
|
2165
|
+
if (rawEarly !== void 0) {
|
|
2166
|
+
if (!EARLY_SIGNAL_CHOICES.includes(rawEarly)) {
|
|
2167
|
+
die(`--early-signal must be one of: ${EARLY_SIGNAL_CHOICES.join(", ")}`);
|
|
2168
|
+
}
|
|
2169
|
+
patch.earlySignalLimit = rawEarly === "off" ? null : Number(rawEarly);
|
|
2170
|
+
}
|
|
2171
|
+
const rawRole = flagString(flags, "role");
|
|
2172
|
+
const autoJoin = flags["auto-join"] === true;
|
|
2173
|
+
if (rawRole !== void 0) {
|
|
2174
|
+
const grant = parseRole(rawRole);
|
|
2175
|
+
patch.roleGrant = grant ? { ...grant, ...autoJoin ? { autoJoin: true } : {} } : null;
|
|
2176
|
+
} else if (autoJoin) {
|
|
2177
|
+
die(
|
|
2178
|
+
"--auto-join needs --role <guildId>/<roleId> \u2014 it widens that grant, it isn't a setting on its own."
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2181
|
+
if (Object.keys(patch).length === 0) {
|
|
2182
|
+
if (json) return emitResult(current);
|
|
2183
|
+
printDestination(current);
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
if (typeof patch.earlySignalLimit === "number") {
|
|
2187
|
+
notice(
|
|
2188
|
+
json,
|
|
2189
|
+
`Early signal sends every answer to this channel for the first ${patch.earlySignalLimit} responses.`
|
|
2190
|
+
);
|
|
2191
|
+
}
|
|
2192
|
+
if (patch.roleGrant && autoJoin) {
|
|
2193
|
+
notice(
|
|
2194
|
+
json,
|
|
2195
|
+
"Auto-join adds respondents to your Discord server with that role \u2014 Discord asks each person's permission first."
|
|
2196
|
+
);
|
|
2197
|
+
} else if (patch.roleGrant) {
|
|
2198
|
+
notice(
|
|
2199
|
+
json,
|
|
2200
|
+
"Respondents who verify with Discord get that role once their response is accepted."
|
|
2201
|
+
);
|
|
2202
|
+
}
|
|
2203
|
+
const body = await putDestination(token, form, patch);
|
|
2204
|
+
if (json) return emitResult(body);
|
|
2205
|
+
printDestination(body);
|
|
2206
|
+
}
|
|
2207
|
+
async function disable(form, flags) {
|
|
2208
|
+
if (!form) die("Usage: fillo discord disable <form> \u2014 a form id, slug, or push handle.");
|
|
2209
|
+
const token = requireToken();
|
|
2210
|
+
const body = await putDestination(token, form, { enabled: false });
|
|
2211
|
+
if (jsonMode(flags)) return emitResult(body);
|
|
2212
|
+
console.log(
|
|
2213
|
+
` ${okMark()} This form no longer posts to Discord. The workspace's webhook stays connected.`
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
async function roles(guildId, flags) {
|
|
2217
|
+
const json = jsonMode(flags);
|
|
2218
|
+
const token = requireToken();
|
|
2219
|
+
const path = guildId ? `/cli/discord/roles?guildId=${encodeURIComponent(guildId)}` : "/cli/discord/roles";
|
|
2220
|
+
const res = await api(path, { token });
|
|
2221
|
+
if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
|
|
2222
|
+
const body = await readJson(res);
|
|
2223
|
+
if (!res.ok) die(body.error ?? `discord roles failed (${res.status}).`);
|
|
2224
|
+
if (json) return emitResult(body);
|
|
2225
|
+
if (!guildId) {
|
|
2226
|
+
const guilds = body.guilds ?? [];
|
|
2227
|
+
if (guilds.length === 0) {
|
|
2228
|
+
console.log(
|
|
2229
|
+
" No Discord servers yet \u2014 connect a channel webhook first with `fillo discord connect`."
|
|
2230
|
+
);
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
console.log("");
|
|
2234
|
+
printTable(
|
|
2235
|
+
["SERVER", "ID"],
|
|
2236
|
+
guilds.map((guild) => [terminalText(guild.name ?? "(name unavailable)"), guild.id])
|
|
2237
|
+
);
|
|
2238
|
+
if (body.guildsUnavailable === true) {
|
|
2239
|
+
console.log(
|
|
2240
|
+
`
|
|
2241
|
+
${dim("Discord didn't answer, so some names are missing. Ids still work.")}`
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
console.log(`
|
|
2245
|
+
${dim("List a server's roles: fillo discord roles <id>")}`);
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
const list5 = body.roles ?? [];
|
|
2249
|
+
if (list5.length === 0) {
|
|
2250
|
+
console.log(" That server has no roles Fillo could list.");
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
console.log("");
|
|
2254
|
+
printTable(
|
|
2255
|
+
["ROLE", "ID", "GRANTABLE"],
|
|
2256
|
+
list5.map((role) => [terminalText(role.name), role.id, role.grantable ? "yes" : "no"])
|
|
2257
|
+
);
|
|
2258
|
+
console.log(`
|
|
2259
|
+
${dim(`Use one: fillo discord enable <form> --role ${guildId}/<roleId>`)}`);
|
|
2260
|
+
console.log(
|
|
2261
|
+
` ${dim("A role marked no sits above Fillo's own role, or belongs to another integration.")}`
|
|
2262
|
+
);
|
|
2263
|
+
}
|
|
2264
|
+
async function discord(subcommand, args, flags) {
|
|
2265
|
+
if (subcommand === void 0 || subcommand === "help") return discordHelp();
|
|
2266
|
+
if (subcommand === "connect") return connect(flags);
|
|
2267
|
+
if (subcommand === "webhook") return webhook(flags);
|
|
2268
|
+
if (subcommand === "status") return status(args[0], flags);
|
|
2269
|
+
if (subcommand === "enable") return enable(args[0], flags);
|
|
2270
|
+
if (subcommand === "disable") return disable(args[0], flags);
|
|
2271
|
+
if (subcommand === "roles") return roles(args[0], flags);
|
|
2272
|
+
die(
|
|
2273
|
+
`Unknown discord command: ${terminalText(subcommand)} (expected connect, webhook, status, enable, disable, or roles).`
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
function discordHelp() {
|
|
2277
|
+
console.log(`
|
|
2278
|
+
${boldRaw("fillo discord")} \u2014 post responses to a Discord channel, from the terminal
|
|
2279
|
+
|
|
2280
|
+
${boldRaw("Commands")}
|
|
2281
|
+
discord connect Let Discord pick the channel and create the webhook
|
|
2282
|
+
${dimRaw("opens an OAuth URL to approve; needs a Discord app on this deploy")}
|
|
2283
|
+
discord webhook Connect a channel webhook you paste
|
|
2284
|
+
${dimRaw("--label <name> what Fillo shows for this channel")}
|
|
2285
|
+
${dimRaw(`reads the URL from a hidden prompt, or ${WEBHOOK_URL_ENV}`)}
|
|
2286
|
+
discord status <form> What this form sends, to where, with which extras
|
|
2287
|
+
discord enable <form> Start sending \u2014 and set any of the options below
|
|
2288
|
+
${dimRaw("--channel <channelId> which channel this form posts to (bot lane)")}
|
|
2289
|
+
${dimRaw("--fields a,b,c up to three field ids (none = link only)")}
|
|
2290
|
+
${dimRaw("--early-signal 5|10|25 first N responses arrive in full (off to stop)")}
|
|
2291
|
+
${dimRaw("--role <guildId>/<roleId> grant that role on an accepted response")}
|
|
2292
|
+
${dimRaw("--auto-join with --role: add non-members to the server")}
|
|
2293
|
+
discord disable <form> Stop this form posting (the webhook stays connected)
|
|
2294
|
+
discord roles [guildId] Your servers, or one server's roles and what Fillo can grant
|
|
2295
|
+
|
|
2296
|
+
${dimRaw("The webhook URL is the whole credential, so it is NEVER an argument \u2014")}
|
|
2297
|
+
${dimRaw("arguments land in shell history and `ps` output. Paste it at the hidden")}
|
|
2298
|
+
${dimRaw(`prompt, or set ${WEBHOOK_URL_ENV} for an unattended run.`)}
|
|
2299
|
+
${dimRaw("--early-signal and --auto-join widen what leaves Fillo, so each prints a")}
|
|
2300
|
+
${dimRaw("one-line notice saying what was agreed to. Ask the human before setting them.")}
|
|
2301
|
+
${dimRaw("Re-enabling against a different webhook clears early signal \u2014 that consent")}
|
|
2302
|
+
${dimRaw("was given for one channel and is never carried into another.")}
|
|
2303
|
+
${dimRaw("--json prints the raw server response on stdout.")}
|
|
2304
|
+
`);
|
|
2305
|
+
}
|
|
2306
|
+
var discordCommand = {
|
|
2307
|
+
name: "discord",
|
|
2308
|
+
flags: ["label", "channel", "fields", "early-signal", "role", "auto-join"],
|
|
2309
|
+
run: (args, flags) => discord(args[0], args.slice(1), flags),
|
|
2310
|
+
help: discordHelp
|
|
2311
|
+
};
|
|
2312
|
+
|
|
1861
2313
|
// src/commands/forms.ts
|
|
1862
2314
|
import { readFileSync as readFileSync3 } from "fs";
|
|
1863
2315
|
import { isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
|
|
@@ -3258,6 +3710,48 @@ function sortForPicker(countries) {
|
|
|
3258
3710
|
return [...countries].sort((a, b) => collator.compare(a.name, b.name));
|
|
3259
3711
|
}
|
|
3260
3712
|
var byIso = new Map(PHONE_COUNTRIES.map((c) => [c.iso2, c]));
|
|
3713
|
+
var DEFAULT_STRINGS = {
|
|
3714
|
+
back: "Back",
|
|
3715
|
+
next: "Next",
|
|
3716
|
+
submit: "Submit",
|
|
3717
|
+
submitting: "Submitting\u2026",
|
|
3718
|
+
uploading: "Uploading\u2026",
|
|
3719
|
+
optional: " (optional)",
|
|
3720
|
+
other: "Other",
|
|
3721
|
+
otherPrompt: "Other \u2014 please specify",
|
|
3722
|
+
otherPlaceholder: "Your answer",
|
|
3723
|
+
choosePlaceholder: "Choose\u2026",
|
|
3724
|
+
successTitle: "Thanks!",
|
|
3725
|
+
successMessage: "Your response has been recorded.",
|
|
3726
|
+
closed: "This form is no longer accepting responses.",
|
|
3727
|
+
notLive: "This form isn't live yet.",
|
|
3728
|
+
notOpenTitle: "This form isn't open yet",
|
|
3729
|
+
notOpenBody: "The form owner is still setting things up. Please check back soon.",
|
|
3730
|
+
closedTitle: "Responses are closed",
|
|
3731
|
+
submitFailed: "This form can't submit right now. Please try again in a moment.",
|
|
3732
|
+
loadFailedNotFound: "Form not found. Check the link or ask the form owner for help.",
|
|
3733
|
+
loadFailedNetwork: "Couldn't reach the server \u2014 check your connection and try again.",
|
|
3734
|
+
loadFailed: "This form could not be loaded.",
|
|
3735
|
+
renderFailed: "This form could not be rendered.",
|
|
3736
|
+
resumeNotice: "Picked up where you left off.",
|
|
3737
|
+
editNotice: "You're updating your earlier response.",
|
|
3738
|
+
resumeStartOver: "Start over",
|
|
3739
|
+
challengeUnavailable: "The verification check couldn't load. Refresh the page and try again."
|
|
3740
|
+
};
|
|
3741
|
+
var DEFAULT_RESPONDENT_ERROR_STRINGS = {
|
|
3742
|
+
submitFailed: DEFAULT_STRINGS.submitFailed,
|
|
3743
|
+
loadFailedNotFound: DEFAULT_STRINGS.loadFailedNotFound,
|
|
3744
|
+
loadFailedNetwork: DEFAULT_STRINGS.loadFailedNetwork,
|
|
3745
|
+
formUnavailable: "This form is unavailable.",
|
|
3746
|
+
formClosed: DEFAULT_STRINGS.closed,
|
|
3747
|
+
submitRateLimited: "Too many responses are being submitted right now. Wait a moment, then try again.",
|
|
3748
|
+
respondentUnrecognized: "This form couldn't verify who is responding. Ask the form owner for help.",
|
|
3749
|
+
fileUnavailable: "One of the uploaded files is no longer available. Remove it and upload it again.",
|
|
3750
|
+
scopeMissing: "We couldn't tell which entry this response belongs to. Answer the required field and try again.",
|
|
3751
|
+
challengeIncomplete: "Please complete the verification check, then submit.",
|
|
3752
|
+
challengeRetry: "That verification didn't go through. Please complete the check again and resubmit.",
|
|
3753
|
+
reviewAnswers: "We couldn't submit \u2014 please review your answers."
|
|
3754
|
+
};
|
|
3261
3755
|
var fileValueSchema = object({
|
|
3262
3756
|
fileId: string2().check(_minLength(1)),
|
|
3263
3757
|
name: string2(),
|
|
@@ -3322,7 +3816,7 @@ var schemaShape = object({
|
|
|
3322
3816
|
});
|
|
3323
3817
|
var MAX_SCHEMA_VERSION = 1;
|
|
3324
3818
|
var FILLO_SCHEMA_VERSION = 1;
|
|
3325
|
-
var FILLO_SDK_VERSION = true ? "0.
|
|
3819
|
+
var FILLO_SDK_VERSION = true ? "0.20.0" : "0.0.0-dev";
|
|
3326
3820
|
function str(value, max, fallback = "") {
|
|
3327
3821
|
return typeof value === "string" ? value.trim().slice(0, max) : fallback;
|
|
3328
3822
|
}
|
|
@@ -4183,7 +4677,7 @@ async function list(flags) {
|
|
|
4183
4677
|
}
|
|
4184
4678
|
}
|
|
4185
4679
|
var liveDot = () => ansiEnabled() ? "\x1B[32m\u25CF\x1B[0m" : "\u25CF";
|
|
4186
|
-
async function
|
|
4680
|
+
async function status2(handle, flags) {
|
|
4187
4681
|
if (!handle) die("Usage: fillo status <formId|handle>");
|
|
4188
4682
|
const res = await api(`/cli/forms/${encodeURIComponent(handle)}`, { token: requireToken() });
|
|
4189
4683
|
if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
|
|
@@ -4395,6 +4889,7 @@ async function loadSchema(file, allowCode) {
|
|
|
4395
4889
|
die(`Couldn't read ${file}: ${e.message}`);
|
|
4396
4890
|
}
|
|
4397
4891
|
}
|
|
4892
|
+
var PUSH_STORAGE_VALUES = /* @__PURE__ */ new Set(["gdrive", "box", "s3", "r2"]);
|
|
4398
4893
|
function isRecord(value) {
|
|
4399
4894
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4400
4895
|
}
|
|
@@ -4404,7 +4899,19 @@ function toPushItems(value, handle) {
|
|
|
4404
4899
|
return values.map((item, index) => {
|
|
4405
4900
|
if (isRecord(item) && "schema" in item) {
|
|
4406
4901
|
const ownHandle = typeof item.id === "string" ? item.id : void 0;
|
|
4407
|
-
|
|
4902
|
+
if (item.storage !== void 0 && !PUSH_STORAGE_VALUES.has(String(item.storage))) {
|
|
4903
|
+
die(`Item ${index + 1} has an unsupported storage destination.`);
|
|
4904
|
+
}
|
|
4905
|
+
if (item.purpose !== void 0 && item.purpose !== "file_request") {
|
|
4906
|
+
die(`Item ${index + 1} has an unsupported form purpose.`);
|
|
4907
|
+
}
|
|
4908
|
+
return {
|
|
4909
|
+
handle: handle ?? ownHandle,
|
|
4910
|
+
schema: item.schema,
|
|
4911
|
+
theme: item.theme,
|
|
4912
|
+
...typeof item.storage === "string" ? { storage: item.storage } : {},
|
|
4913
|
+
...item.purpose === "file_request" ? { purpose: item.purpose } : {}
|
|
4914
|
+
};
|
|
4408
4915
|
}
|
|
4409
4916
|
if (values.length > 1 && !handle) {
|
|
4410
4917
|
die(`Item ${index + 1} needs an id or --handle.`);
|
|
@@ -4414,9 +4921,9 @@ function toPushItems(value, handle) {
|
|
|
4414
4921
|
}
|
|
4415
4922
|
function printNotices(notices) {
|
|
4416
4923
|
if (!Array.isArray(notices)) return;
|
|
4417
|
-
for (const
|
|
4418
|
-
if (typeof
|
|
4419
|
-
console.log(` ${bold("Note:")} ${terminalText(
|
|
4924
|
+
for (const notice2 of notices) {
|
|
4925
|
+
if (typeof notice2 === "string" && notice2.trim()) {
|
|
4926
|
+
console.log(` ${bold("Note:")} ${terminalText(notice2)}`);
|
|
4420
4927
|
}
|
|
4421
4928
|
}
|
|
4422
4929
|
}
|
|
@@ -4501,7 +5008,9 @@ async function pushWithSync(items, credential, requestedStage, results) {
|
|
|
4501
5008
|
...credential.key ? { key: credential.key } : {},
|
|
4502
5009
|
id: handle,
|
|
4503
5010
|
schema: item.schema,
|
|
4504
|
-
theme: item.theme ?? null
|
|
5011
|
+
theme: item.theme ?? null,
|
|
5012
|
+
...item.storage ? { storage: item.storage } : {},
|
|
5013
|
+
...item.purpose ? { purpose: item.purpose } : {}
|
|
4505
5014
|
})
|
|
4506
5015
|
});
|
|
4507
5016
|
const body = await readJson(res);
|
|
@@ -4566,6 +5075,8 @@ async function push(file, flags) {
|
|
|
4566
5075
|
body: JSON.stringify({
|
|
4567
5076
|
schema: item.schema,
|
|
4568
5077
|
theme: item.theme ?? null,
|
|
5078
|
+
...item.storage ? { storage: item.storage } : {},
|
|
5079
|
+
...item.purpose ? { purpose: item.purpose } : {},
|
|
4569
5080
|
publish: false
|
|
4570
5081
|
})
|
|
4571
5082
|
});
|
|
@@ -4593,6 +5104,8 @@ async function push(file, flags) {
|
|
|
4593
5104
|
schema: item.schema,
|
|
4594
5105
|
theme: item.theme ?? null,
|
|
4595
5106
|
handle: item.handle,
|
|
5107
|
+
...item.storage ? { storage: item.storage } : {},
|
|
5108
|
+
...item.purpose ? { purpose: item.purpose } : {},
|
|
4596
5109
|
publish: true
|
|
4597
5110
|
})
|
|
4598
5111
|
});
|
|
@@ -4608,11 +5121,16 @@ async function push(file, flags) {
|
|
|
4608
5121
|
}
|
|
4609
5122
|
die(body.error ?? `push failed (${res.status}).`);
|
|
4610
5123
|
}
|
|
5124
|
+
const draft = body.status === "draft";
|
|
4611
5125
|
if (results) {
|
|
4612
5126
|
results.push(body);
|
|
4613
|
-
emitProgress({
|
|
5127
|
+
emitProgress({
|
|
5128
|
+
status: "pushed",
|
|
5129
|
+
formId: body.formId,
|
|
5130
|
+
...draft ? { draft: true } : { published: true }
|
|
5131
|
+
});
|
|
4614
5132
|
} else {
|
|
4615
|
-
printPushed(body.formId, body.url, !!body.updated,
|
|
5133
|
+
printPushed(body.formId, body.url, !!body.updated, draft, {
|
|
4616
5134
|
warning: body.warning,
|
|
4617
5135
|
warningUrl: body.warningUrl,
|
|
4618
5136
|
notices: body.notices
|
|
@@ -4643,7 +5161,7 @@ var listCommand = {
|
|
|
4643
5161
|
var statusCommand = {
|
|
4644
5162
|
name: "status",
|
|
4645
5163
|
flags: [],
|
|
4646
|
-
run: (args, flags) =>
|
|
5164
|
+
run: (args, flags) => status2(args[0], flags)
|
|
4647
5165
|
};
|
|
4648
5166
|
var publishCommand = {
|
|
4649
5167
|
name: "publish",
|
|
@@ -4656,6 +5174,61 @@ var testResponseCommand = {
|
|
|
4656
5174
|
run: (args) => testResponse(args[0], args[1])
|
|
4657
5175
|
};
|
|
4658
5176
|
|
|
5177
|
+
// src/commands/branding.ts
|
|
5178
|
+
function printState(body) {
|
|
5179
|
+
const plan = body.plan === "pro" ? "Everything" : "Free";
|
|
5180
|
+
console.log(` Plan: ${plan}`);
|
|
5181
|
+
console.log(` Badge: ${body.poweredBy ? "shown" : "hidden"}`);
|
|
5182
|
+
if (body.canHide === false) {
|
|
5183
|
+
console.log(dim(" Turning it off needs the Everything plan (Settings \u2192 Plan)."));
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
async function branding(subcommand, flags) {
|
|
5187
|
+
if (subcommand === "help") return brandingHelp();
|
|
5188
|
+
if (subcommand !== void 0 && subcommand !== "on" && subcommand !== "off") {
|
|
5189
|
+
die(`Unknown branding command: ${subcommand} (expected on, off, or nothing for status).`);
|
|
5190
|
+
}
|
|
5191
|
+
const token = requireToken();
|
|
5192
|
+
const res = subcommand === void 0 ? await api("/cli/workspace/branding", { token }) : await api("/cli/workspace/branding", {
|
|
5193
|
+
method: "PATCH",
|
|
5194
|
+
token,
|
|
5195
|
+
body: JSON.stringify({ show: subcommand === "on" })
|
|
5196
|
+
});
|
|
5197
|
+
const body = await readJson(res);
|
|
5198
|
+
if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
|
|
5199
|
+
if (!res.ok) die(body.error ?? `branding ${subcommand ?? "status"} failed (${res.status}).`);
|
|
5200
|
+
if (jsonMode(flags)) return emitResult(body);
|
|
5201
|
+
console.log("");
|
|
5202
|
+
if (subcommand !== void 0) {
|
|
5203
|
+
console.log(
|
|
5204
|
+
` ${okMark()} Badge ${body.poweredBy ? "shown" : "hidden"} on Fillo-rendered forms.
|
|
5205
|
+
`
|
|
5206
|
+
);
|
|
5207
|
+
}
|
|
5208
|
+
printState(body);
|
|
5209
|
+
console.log("");
|
|
5210
|
+
}
|
|
5211
|
+
function brandingHelp() {
|
|
5212
|
+
console.log(`
|
|
5213
|
+
${boldRaw("fillo branding")} \u2014 the workspace's "Powered by Fillo" badge
|
|
5214
|
+
|
|
5215
|
+
${boldRaw("Commands")}
|
|
5216
|
+
branding Print the current badge state
|
|
5217
|
+
branding off Hide the badge on Fillo-rendered forms (Everything plan)
|
|
5218
|
+
branding on Show the badge again
|
|
5219
|
+
|
|
5220
|
+
${dimRaw("Hiding needs the Everything plan; a workspace owner or admin selects")}
|
|
5221
|
+
${dimRaw("that in Settings \u2192 Plan. Headless embeds never show a badge.")}
|
|
5222
|
+
${dimRaw("--json prints the raw server response on stdout.")}
|
|
5223
|
+
`);
|
|
5224
|
+
}
|
|
5225
|
+
var brandingCommand = {
|
|
5226
|
+
name: "branding",
|
|
5227
|
+
flags: [],
|
|
5228
|
+
run: (args, flags) => branding(args[0], flags),
|
|
5229
|
+
help: brandingHelp
|
|
5230
|
+
};
|
|
5231
|
+
|
|
4659
5232
|
// src/commands/keys.ts
|
|
4660
5233
|
var PRESETS = ["read", "agent", "full"];
|
|
4661
5234
|
var EXPIRY_CHOICES = ["30d", "90d", "never"];
|
|
@@ -4804,7 +5377,7 @@ async function keys(subcommand, args, flags) {
|
|
|
4804
5377
|
}
|
|
4805
5378
|
function keysHelp() {
|
|
4806
5379
|
console.log(`
|
|
4807
|
-
${boldRaw("fillo keys")} \u2014
|
|
5380
|
+
${boldRaw("fillo keys")} \u2014 project API keys (fsk_) for coding agents and integrations
|
|
4808
5381
|
|
|
4809
5382
|
${boldRaw("Commands")}
|
|
4810
5383
|
keys create --name <name> Mint a key \u2014 the plaintext is shown once, store it
|
|
@@ -5244,73 +5817,16 @@ var settingsCommand = {
|
|
|
5244
5817
|
help: settingsHelp
|
|
5245
5818
|
};
|
|
5246
5819
|
|
|
5247
|
-
// src/
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
const
|
|
5251
|
-
if (
|
|
5252
|
-
|
|
5253
|
-
|
|
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);
|
|
5820
|
+
// src/commands/slack.ts
|
|
5821
|
+
async function slackStartUrl(token) {
|
|
5822
|
+
const res = await api("/cli/whoami", { token });
|
|
5823
|
+
const body = await readJson(res);
|
|
5824
|
+
if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
|
|
5825
|
+
if (!res.ok || typeof body.projectId !== "string" || !body.projectId) {
|
|
5826
|
+
die(body.error ?? "Couldn't resolve the selected Fillo project.");
|
|
5304
5827
|
}
|
|
5305
|
-
|
|
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);
|
|
5828
|
+
return `${API}/api/integrations/slack/start?return=terminal&project=${encodeURIComponent(body.projectId)}`;
|
|
5310
5829
|
}
|
|
5311
|
-
|
|
5312
|
-
// src/commands/slack.ts
|
|
5313
|
-
var SLACK_START_URL = `${API}/api/integrations/slack/start?return=terminal`;
|
|
5314
5830
|
async function refresh(flags) {
|
|
5315
5831
|
const json = jsonMode(flags);
|
|
5316
5832
|
const token = requireToken();
|
|
@@ -5318,8 +5834,9 @@ async function refresh(flags) {
|
|
|
5318
5834
|
const body = await readJson(res);
|
|
5319
5835
|
if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
|
|
5320
5836
|
if (res.status === 409) {
|
|
5837
|
+
const startUrl = await slackStartUrl(token);
|
|
5321
5838
|
die(
|
|
5322
|
-
`${body.error ?? "Reconnect Slack in the browser to refresh channels."} Reconnect at ${
|
|
5839
|
+
`${body.error ?? "Reconnect Slack in the browser to refresh channels."} Reconnect at ${startUrl}`
|
|
5323
5840
|
);
|
|
5324
5841
|
}
|
|
5325
5842
|
if (res.status === 429) {
|
|
@@ -5351,7 +5868,7 @@ function printChannels(channels) {
|
|
|
5351
5868
|
console.log("");
|
|
5352
5869
|
printTable(["NAME", "ID", "VISIBILITY"], rows);
|
|
5353
5870
|
}
|
|
5354
|
-
async function
|
|
5871
|
+
async function status3(flags) {
|
|
5355
5872
|
const json = jsonMode(flags);
|
|
5356
5873
|
const showChannels = flags.channels === true;
|
|
5357
5874
|
const token = requireToken();
|
|
@@ -5376,13 +5893,14 @@ async function status2(flags) {
|
|
|
5376
5893
|
);
|
|
5377
5894
|
}
|
|
5378
5895
|
}
|
|
5379
|
-
async function
|
|
5896
|
+
async function connect2(flags) {
|
|
5380
5897
|
const json = jsonMode(flags);
|
|
5381
5898
|
const token = requireToken();
|
|
5899
|
+
const startUrl = await slackStartUrl(token);
|
|
5382
5900
|
await connectViaBrowser({
|
|
5383
5901
|
json,
|
|
5384
5902
|
what: "Slack",
|
|
5385
|
-
startUrl
|
|
5903
|
+
startUrl,
|
|
5386
5904
|
poll: async () => {
|
|
5387
5905
|
const res = await api("/cli/slack", { token });
|
|
5388
5906
|
if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
|
|
@@ -5401,12 +5919,12 @@ async function connect(flags) {
|
|
|
5401
5919
|
}
|
|
5402
5920
|
async function slack(subcommand, flags) {
|
|
5403
5921
|
if (subcommand === "help") return slackHelp();
|
|
5404
|
-
if (subcommand === "connect") return
|
|
5922
|
+
if (subcommand === "connect") return connect2(flags);
|
|
5405
5923
|
if (subcommand !== void 0 && subcommand !== "status") {
|
|
5406
5924
|
die(`Unknown slack command: ${terminalText(subcommand)} (expected status or connect).`);
|
|
5407
5925
|
}
|
|
5408
5926
|
if (flags.refresh === true) return refresh(flags);
|
|
5409
|
-
return
|
|
5927
|
+
return status3(flags);
|
|
5410
5928
|
}
|
|
5411
5929
|
function slackHelp() {
|
|
5412
5930
|
console.log(`
|
|
@@ -5455,7 +5973,7 @@ async function fetchStatus(token) {
|
|
|
5455
5973
|
if (!res.ok || !body.providers) die(body.error ?? `storage status failed (${res.status}).`);
|
|
5456
5974
|
return body;
|
|
5457
5975
|
}
|
|
5458
|
-
async function
|
|
5976
|
+
async function status4(flags) {
|
|
5459
5977
|
const token = requireToken();
|
|
5460
5978
|
const body = await fetchStatus(token);
|
|
5461
5979
|
if (jsonMode(flags)) return emitResult(body);
|
|
@@ -5622,7 +6140,7 @@ async function connectBrowserProvider(provider, flags) {
|
|
|
5622
6140
|
}
|
|
5623
6141
|
});
|
|
5624
6142
|
}
|
|
5625
|
-
async function
|
|
6143
|
+
async function connect3(providerArg, flags) {
|
|
5626
6144
|
const provider = resolveProviderArg(providerArg);
|
|
5627
6145
|
if (!provider) {
|
|
5628
6146
|
die("Usage: fillo storage connect <s3|drive|box> (r2 and gdrive are accepted aliases).");
|
|
@@ -5645,9 +6163,9 @@ async function disconnect(providerArg, flags) {
|
|
|
5645
6163
|
console.log(` ${okMark()} Disconnected ${PROVIDER_LABEL[provider]} storage.`);
|
|
5646
6164
|
}
|
|
5647
6165
|
async function storage(subcommand, args, flags) {
|
|
5648
|
-
if (subcommand === void 0 || subcommand === "status") return
|
|
6166
|
+
if (subcommand === void 0 || subcommand === "status") return status4(flags);
|
|
5649
6167
|
if (subcommand === "help") return storageHelp();
|
|
5650
|
-
if (subcommand === "connect") return
|
|
6168
|
+
if (subcommand === "connect") return connect3(args[0], flags);
|
|
5651
6169
|
if (subcommand === "disconnect") return disconnect(args[0], flags);
|
|
5652
6170
|
die(
|
|
5653
6171
|
`Unknown storage command: ${terminalText(subcommand)} (expected status, connect, or disconnect).`
|
|
@@ -5682,6 +6200,63 @@ var storageCommand = {
|
|
|
5682
6200
|
help: storageHelp
|
|
5683
6201
|
};
|
|
5684
6202
|
|
|
6203
|
+
// src/commands/tokens.ts
|
|
6204
|
+
var TOOLS = ["zapier", "n8n"];
|
|
6205
|
+
async function createConnector(flags) {
|
|
6206
|
+
const json = jsonMode(flags);
|
|
6207
|
+
const tool = enumFlag(flags, "tool", TOOLS);
|
|
6208
|
+
if (!tool) die(`Usage: fillo tokens create-connector --tool ${TOOLS.join("|")}`);
|
|
6209
|
+
const res = await api("/cli/tokens/connector", {
|
|
6210
|
+
method: "POST",
|
|
6211
|
+
token: requireToken(),
|
|
6212
|
+
body: JSON.stringify({ tool })
|
|
6213
|
+
});
|
|
6214
|
+
const body = await readJson(res);
|
|
6215
|
+
if (res.status === 401) die(body.error ?? "Invalid or missing CLI token \u2014 run `fillo login`");
|
|
6216
|
+
if (!res.ok || !body.token) die(body.error ?? `tokens create-connector failed (${res.status}).`);
|
|
6217
|
+
if (!/^fcli_[A-Za-z0-9._~-]{8,512}$/.test(body.token)) {
|
|
6218
|
+
die("Fillo returned an unexpected token format.");
|
|
6219
|
+
}
|
|
6220
|
+
if (json) return emitResult(body);
|
|
6221
|
+
console.log(
|
|
6222
|
+
`
|
|
6223
|
+
${okMark()} Created a ${bold(terminalText(body.label ?? tool))} connector token`
|
|
6224
|
+
);
|
|
6225
|
+
console.log(`
|
|
6226
|
+
${body.token}
|
|
6227
|
+
`);
|
|
6228
|
+
console.log(` ${bold("Store it now")} \u2014 Fillo cannot show this token again.`);
|
|
6229
|
+
console.log(
|
|
6230
|
+
" Treat it like a password: it can read and change this project's forms and responses."
|
|
6231
|
+
);
|
|
6232
|
+
console.log(" Revoke it any time in Settings \u2192 Connections.\n");
|
|
6233
|
+
}
|
|
6234
|
+
async function tokens(subcommand, flags) {
|
|
6235
|
+
if (subcommand === void 0 || subcommand === "help") return tokensHelp();
|
|
6236
|
+
if (subcommand === "create-connector") return createConnector(flags);
|
|
6237
|
+
die(`Unknown tokens command: ${terminalText(subcommand)} (expected create-connector).`);
|
|
6238
|
+
}
|
|
6239
|
+
function tokensHelp() {
|
|
6240
|
+
console.log(`
|
|
6241
|
+
${boldRaw("fillo tokens")} \u2014 credentials for no-code connectors
|
|
6242
|
+
|
|
6243
|
+
${boldRaw("Commands")}
|
|
6244
|
+
tokens create-connector --tool ${TOOLS.join("|")}
|
|
6245
|
+
Mint the connector's API token \u2014 shown once, store it
|
|
6246
|
+
|
|
6247
|
+
${dimRaw("The token is non-expiring so a live automation doesn't break silently, and")}
|
|
6248
|
+
${dimRaw("stays revocable in Settings \u2192 Connections. For coding agents and your own")}
|
|
6249
|
+
${dimRaw("integrations use `fillo keys create` instead \u2014 those are scoped.")}
|
|
6250
|
+
${dimRaw("--json prints the raw server response on stdout, token included.")}
|
|
6251
|
+
`);
|
|
6252
|
+
}
|
|
6253
|
+
var tokensCommand = {
|
|
6254
|
+
name: "tokens",
|
|
6255
|
+
flags: ["tool"],
|
|
6256
|
+
run: (args, flags) => tokens(args[0], flags),
|
|
6257
|
+
help: tokensHelp
|
|
6258
|
+
};
|
|
6259
|
+
|
|
5685
6260
|
// src/commands/webhooks.ts
|
|
5686
6261
|
var dateTime2 = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
5687
6262
|
async function list3(handle, flags) {
|
|
@@ -5812,6 +6387,115 @@ var webhooksCommand = {
|
|
|
5812
6387
|
help: webhooksHelp
|
|
5813
6388
|
};
|
|
5814
6389
|
|
|
6390
|
+
// src/commands/projects.ts
|
|
6391
|
+
function authFailure(status5) {
|
|
6392
|
+
if (status5 === 401) die("Token invalid \u2014 run `fillo login` again.");
|
|
6393
|
+
}
|
|
6394
|
+
function saveSelectedProject(project) {
|
|
6395
|
+
if (!project.publishableKey.startsWith("pk_")) {
|
|
6396
|
+
die("Fillo returned an invalid publishable key; the selected project was not saved locally.");
|
|
6397
|
+
}
|
|
6398
|
+
const {
|
|
6399
|
+
claimUrl: _claimUrl,
|
|
6400
|
+
email: _email2,
|
|
6401
|
+
name: _name,
|
|
6402
|
+
claimToken: _claimToken,
|
|
6403
|
+
...current
|
|
6404
|
+
} = readConfig();
|
|
6405
|
+
writeConfig({ ...current, pk: project.publishableKey });
|
|
6406
|
+
}
|
|
6407
|
+
async function list4(flags) {
|
|
6408
|
+
const res = await api("/cli/projects", { token: requireToken() });
|
|
6409
|
+
const body = await readJson(res);
|
|
6410
|
+
authFailure(res.status);
|
|
6411
|
+
if (!res.ok || !Array.isArray(body.projects)) {
|
|
6412
|
+
die(body.error ?? `project list failed (${res.status}).`);
|
|
6413
|
+
}
|
|
6414
|
+
if (jsonMode(flags)) return emitResult(body);
|
|
6415
|
+
console.log(`
|
|
6416
|
+
${bold("Projects")}`);
|
|
6417
|
+
printTable(
|
|
6418
|
+
["", "NAME", "SLUG", "ID"],
|
|
6419
|
+
body.projects.map((project) => [
|
|
6420
|
+
project.current ? "*" : "",
|
|
6421
|
+
terminalText(project.name),
|
|
6422
|
+
terminalText(project.slug),
|
|
6423
|
+
terminalText(project.id)
|
|
6424
|
+
])
|
|
6425
|
+
);
|
|
6426
|
+
console.log(`
|
|
6427
|
+
${dim("* selected for this CLI login")}`);
|
|
6428
|
+
console.log(` ${dim("Switch with `fillo project select <id-or-slug>`.")}
|
|
6429
|
+
`);
|
|
6430
|
+
}
|
|
6431
|
+
async function create(name, flags) {
|
|
6432
|
+
if (!name) die('Usage: fillo project create "Project name"');
|
|
6433
|
+
const res = await api("/cli/projects", {
|
|
6434
|
+
method: "POST",
|
|
6435
|
+
token: requireToken(),
|
|
6436
|
+
body: JSON.stringify({ name, source: "cli" })
|
|
6437
|
+
});
|
|
6438
|
+
const body = await readJson(res);
|
|
6439
|
+
authFailure(res.status);
|
|
6440
|
+
if (!res.ok || !body.project || body.selected !== true) {
|
|
6441
|
+
die(body.error ?? `project create failed (${res.status}).`);
|
|
6442
|
+
}
|
|
6443
|
+
saveSelectedProject(body.project);
|
|
6444
|
+
if (jsonMode(flags)) return emitResult(body);
|
|
6445
|
+
console.log(` ${okMark()} Created and selected ${bold(terminalText(body.project.name))}.`);
|
|
6446
|
+
console.log(` ${dim("Forms, keys, origins, identities, and agent access are isolated here.")}`);
|
|
6447
|
+
console.log(` ${dim("Members, billing, storage, and usage totals stay with the workspace.")}`);
|
|
6448
|
+
}
|
|
6449
|
+
async function select(target, flags) {
|
|
6450
|
+
if (!target) die("Usage: fillo project select <id|slug|unique-name>");
|
|
6451
|
+
const res = await api("/cli/projects/select", {
|
|
6452
|
+
method: "POST",
|
|
6453
|
+
token: requireToken(),
|
|
6454
|
+
body: JSON.stringify({ project: target, source: "cli" })
|
|
6455
|
+
});
|
|
6456
|
+
const body = await readJson(res);
|
|
6457
|
+
authFailure(res.status);
|
|
6458
|
+
if (!res.ok || !body.project || body.selected !== true) {
|
|
6459
|
+
die(body.error ?? `project select failed (${res.status}).`);
|
|
6460
|
+
}
|
|
6461
|
+
saveSelectedProject(body.project);
|
|
6462
|
+
if (jsonMode(flags)) return emitResult(body);
|
|
6463
|
+
console.log(` ${okMark()} Selected ${bold(terminalText(body.project.name))}.`);
|
|
6464
|
+
console.log(` ${dim("Future CLI commands use this project until you select another.")}`);
|
|
6465
|
+
}
|
|
6466
|
+
async function projects(subcommand, args, flags) {
|
|
6467
|
+
if (subcommand === void 0 || subcommand === "list" || subcommand === "ls") {
|
|
6468
|
+
return list4(flags);
|
|
6469
|
+
}
|
|
6470
|
+
if (subcommand === "help") return projectHelp();
|
|
6471
|
+
if (subcommand === "create") return create(args.join(" ").trim() || void 0, flags);
|
|
6472
|
+
if (subcommand === "select" || subcommand === "use") {
|
|
6473
|
+
return select(args.join(" ").trim() || void 0, flags);
|
|
6474
|
+
}
|
|
6475
|
+
die(`Unknown project command: ${terminalText(subcommand)} (expected list, create, or select).`);
|
|
6476
|
+
}
|
|
6477
|
+
function projectHelp() {
|
|
6478
|
+
console.log(`
|
|
6479
|
+
${boldRaw("fillo project")} \u2014 create and select isolated sites/apps
|
|
6480
|
+
|
|
6481
|
+
${boldRaw("Commands")}
|
|
6482
|
+
project List projects in the current workspace
|
|
6483
|
+
project create <name> Create and select a project
|
|
6484
|
+
project select <id|slug|name> Select a project for future commands
|
|
6485
|
+
|
|
6486
|
+
${dimRaw("Requires an ordinary `fillo login`. Project-pinned agent handoff")}
|
|
6487
|
+
${dimRaw("tokens cannot list, create, or select sibling projects.")}
|
|
6488
|
+
${dimRaw("--json prints the server response on stdout.")}
|
|
6489
|
+
`);
|
|
6490
|
+
}
|
|
6491
|
+
var projectCommand = {
|
|
6492
|
+
name: "project",
|
|
6493
|
+
aliases: ["projects"],
|
|
6494
|
+
flags: [],
|
|
6495
|
+
run: (args, flags) => projects(args[0], args.slice(1), flags),
|
|
6496
|
+
help: projectHelp
|
|
6497
|
+
};
|
|
6498
|
+
|
|
5815
6499
|
// src/index.ts
|
|
5816
6500
|
var REGISTRY = [
|
|
5817
6501
|
initCommand,
|
|
@@ -5819,6 +6503,7 @@ var REGISTRY = [
|
|
|
5819
6503
|
loginCommand,
|
|
5820
6504
|
logoutCommand,
|
|
5821
6505
|
whoamiCommand,
|
|
6506
|
+
projectCommand,
|
|
5822
6507
|
keysCommand,
|
|
5823
6508
|
pushCommand,
|
|
5824
6509
|
listCommand,
|
|
@@ -5830,8 +6515,11 @@ var REGISTRY = [
|
|
|
5830
6515
|
skillCommand,
|
|
5831
6516
|
storageCommand,
|
|
5832
6517
|
slackCommand,
|
|
6518
|
+
discordCommand,
|
|
6519
|
+
tokensCommand,
|
|
5833
6520
|
webhooksCommand,
|
|
5834
6521
|
settingsCommand,
|
|
6522
|
+
brandingCommand,
|
|
5835
6523
|
membersCommand,
|
|
5836
6524
|
deleteCommand
|
|
5837
6525
|
];
|
|
@@ -5862,15 +6550,17 @@ function help() {
|
|
|
5862
6550
|
${dimRaw("--headless approve a code instead of the local browser handoff")}
|
|
5863
6551
|
${dimRaw("--run <id> --token <token> bind a fresh agent handoff login")}
|
|
5864
6552
|
logout Forget the stored credentials
|
|
5865
|
-
whoami Show the signed-in workspace
|
|
5866
|
-
|
|
6553
|
+
whoami Show the signed-in workspace and selected project
|
|
6554
|
+
project <cmd> Isolated sites/apps: list, create, select
|
|
6555
|
+
${dimRaw("project create <name> \xB7 project select <id|slug|name>")}
|
|
6556
|
+
keys <cmd> Project API keys for agents/CI: create, list, revoke
|
|
5867
6557
|
${dimRaw("keys create --name <n> --preset read|agent|full")}
|
|
5868
6558
|
push <file|-> Create/update a form from JSON (file or stdin)
|
|
5869
6559
|
${dimRaw("--handle <name> idempotent id for re-pushes")}
|
|
5870
6560
|
${dimRaw("--stage stage beside the live form for dashboard review")}
|
|
5871
6561
|
${dimRaw("--draft alias with a handle; legacy one-off draft without")}
|
|
5872
6562
|
${dimRaw("--allow-code allow a .mjs/.js schema (executes the file)")}
|
|
5873
|
-
list List the
|
|
6563
|
+
list List the project's forms and their live URLs
|
|
5874
6564
|
status <form> Show one form's status, live URL, and publish blockers
|
|
5875
6565
|
${dimRaw("<form> is a form id or handle")}
|
|
5876
6566
|
publish <form> Publish staged changes (or a draft form) \u2014 prints the live URL
|
|
@@ -5886,9 +6576,13 @@ function help() {
|
|
|
5886
6576
|
storage <cmd> Inspect/connect upload destinations (status, connect, disconnect)
|
|
5887
6577
|
${dimRaw("storage connect s3 \xB7 connect drive|box \xB7 disconnect <p>")}
|
|
5888
6578
|
slack <cmd> Slack notifications: status, --channels, --refresh, connect
|
|
6579
|
+
discord <cmd> Discord destination: connect, webhook, status, enable, disable, roles
|
|
6580
|
+
${dimRaw("discord enable <form> --fields a,b --early-signal 10 --role <g>/<r>")}
|
|
6581
|
+
tokens <cmd> Connector credentials: create-connector --tool zapier|n8n
|
|
5889
6582
|
webhooks <cmd> A form's signed webhooks: list, add, set, remove
|
|
5890
6583
|
${dimRaw("webhooks add <form> --url <u> shows the signing secret once")}
|
|
5891
6584
|
settings <cmd> A form's operational settings: get, set key=value
|
|
6585
|
+
branding [on|off] The workspace's "Powered by Fillo" badge (off needs Everything)
|
|
5892
6586
|
members <cmd> Workspace members: list, invite <email>, cancel-invite <id>
|
|
5893
6587
|
delete <cmd> Irreversible deletes: delete form <form> \xB7 delete workspace
|
|
5894
6588
|
${dimRaw("typed confirmation required; --confirm for agents, --yes never skips")}
|