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