@usefillo/cli 0.9.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1298,79 +1298,122 @@ var CONTENT_KINDS = ["heading", "paragraph", "divider"];
1298
1298
  function isField(block) {
1299
1299
  return !CONTENT_KINDS.includes(block.kind);
1300
1300
  }
1301
- var C = (iso2, name, dialCode, lengths = [], groups = [], example = "") => ({ iso2, name, dialCode, lengths, groups, example });
1301
+ var regionNames;
1302
+ function regionDisplayNames() {
1303
+ if (regionNames !== void 0) return regionNames ?? void 0;
1304
+ if (typeof Intl === "undefined" || typeof Intl.DisplayNames === "undefined") {
1305
+ regionNames = null;
1306
+ return void 0;
1307
+ }
1308
+ try {
1309
+ regionNames = new Intl.DisplayNames(void 0, { type: "region" });
1310
+ } catch {
1311
+ regionNames = null;
1312
+ }
1313
+ return regionNames ?? void 0;
1314
+ }
1315
+ function regionName(iso2) {
1316
+ try {
1317
+ return regionDisplayNames()?.of(iso2) ?? iso2;
1318
+ } catch {
1319
+ return iso2;
1320
+ }
1321
+ }
1322
+ var C = (iso2, dialCode, lengths = [], groups = [], example = "") => ({ iso2, name: regionName(iso2), dialCode, lengths, groups, example });
1323
+ var CURATED_COUNTRIES = [
1324
+ C("US", "1", [10], [3, 3, 4], "2015550123"),
1325
+ C("CA", "1", [10], [3, 3, 4], "5145550123"),
1326
+ C("GB", "44", [10], [4, 6], "7400123456"),
1327
+ C("AU", "61", [9], [3, 3, 3], "412345678"),
1328
+ C("DE", "49", [10, 11], [4, 7], "15123456789"),
1329
+ C("FR", "33", [9], [1, 2, 2, 2, 2], "612345678"),
1330
+ C("IN", "91", [10], [5, 5], "8123456789"),
1331
+ C("DK", "45", [8], [2, 2, 2, 2], "32123456"),
1332
+ C("SE", "46", [9], [2, 3, 4], "701234567"),
1333
+ C("NO", "47", [8], [3, 2, 3], "40612345"),
1334
+ C("FI", "358", [9, 10], [2, 3, 4], "412345678"),
1335
+ C("NL", "31", [9], [1, 4, 4], "612345678"),
1336
+ C("BE", "32", [9], [3, 2, 2, 2], "470123456"),
1337
+ C("ES", "34", [9], [3, 3, 3], "612345678"),
1338
+ C("IT", "39", [9, 10], [3, 3, 4], "3123456789"),
1339
+ C("PT", "351", [9], [3, 3, 3], "912345678"),
1340
+ C("IE", "353", [9], [2, 3, 4], "851234567"),
1341
+ C("CH", "41", [9], [2, 3, 2, 2], "781234567"),
1342
+ C("AT", "43", [10, 11], [3, 7], "6641234567"),
1343
+ C("PL", "48", [9], [3, 3, 3], "512345678"),
1344
+ C("CZ", "420", [9], [3, 3, 3], "601123456"),
1345
+ C("GR", "30", [10], [3, 3, 4], "6912345678"),
1346
+ C("RO", "40", [9], [3, 3, 3], "712345678"),
1347
+ C("HU", "36", [9], [2, 3, 4], "201234567"),
1348
+ C("UA", "380", [9], [2, 3, 2, 2], "501234567"),
1349
+ C("RU", "7", [10], [3, 3, 2, 2], "9123456789"),
1350
+ C("TR", "90", [10], [3, 3, 2, 2], "5012345678"),
1351
+ C("IL", "972", [9], [2, 3, 4], "501234567"),
1352
+ C("AE", "971", [9], [2, 3, 4], "501234567"),
1353
+ C("SA", "966", [9], [2, 3, 4], "512345678"),
1354
+ C("ZA", "27", [9], [2, 3, 4], "711234567"),
1355
+ C("NG", "234", [10], [3, 3, 4], "8021234567"),
1356
+ C("KE", "254", [9], [3, 3, 3], "712123456"),
1357
+ C("EG", "20", [10], [3, 3, 4], "1001234567"),
1358
+ C("BR", "55", [10, 11], [2, 5, 4], "11961234567"),
1359
+ C("MX", "52", [10], [3, 3, 4], "2221234567"),
1360
+ C("AR", "54", [10], [4, 6], "1123456789"),
1361
+ C("CL", "56", [9], [1, 4, 4], "221234567"),
1362
+ C("CO", "57", [10], [3, 3, 4], "3211234567"),
1363
+ C("PE", "51", [9], [3, 3, 3], "912345678"),
1364
+ C("CN", "86", [11], [3, 4, 4], "13123456789"),
1365
+ C("JP", "81", [10], [2, 4, 4], "9012345678"),
1366
+ C("KR", "82", [9, 10], [2, 4, 4], "1023456789"),
1367
+ C("SG", "65", [8], [4, 4], "81234567"),
1368
+ C("HK", "852", [8], [4, 4], "51234567"),
1369
+ C("TW", "886", [9], [3, 3, 3], "912345678"),
1370
+ C("MY", "60", [9, 10], [2, 3, 4], "123456789"),
1371
+ C("ID", "62", [10, 11], [3, 4, 4], "81234567890"),
1372
+ C("TH", "66", [9], [2, 3, 4], "812345678"),
1373
+ C("PH", "63", [10], [3, 3, 4], "9171234567"),
1374
+ C("VN", "84", [9], [3, 3, 3], "912345678"),
1375
+ C("PK", "92", [10], [3, 3, 4], "3012345678"),
1376
+ C("BD", "880", [10], [2, 4, 4], "1812345678"),
1377
+ C("NZ", "64", [9, 10], [2, 3, 4], "211234567"),
1378
+ C("SK", "421", [9], [3, 3, 3], "912123456"),
1379
+ C("BG", "359", [9], [2, 3, 4], "481234567"),
1380
+ C("HR", "385", [8, 9], [2, 3, 4], "921234567"),
1381
+ C("RS", "381", [8, 9], [2, 3, 4], "601234567"),
1382
+ C("SI", "386", [8], [2, 3, 3], "31234567"),
1383
+ C("LT", "370", [8], [3, 2, 3], "61234567"),
1384
+ C("LV", "371", [8], [2, 3, 3], "21234567"),
1385
+ C("EE", "372", [7, 8], [3, 4], "51234567"),
1386
+ C("IS", "354", [7], [3, 4], "6111234"),
1387
+ C("LU", "352", [9], [3, 3, 3], "628123456"),
1388
+ C("MA", "212", [9], [3, 3, 3], "650123456"),
1389
+ C("CR", "506", [8], [4, 4], "83123456"),
1390
+ C("EC", "593", [9], [2, 3, 4], "991234567"),
1391
+ C("UY", "598", [8], [1, 3, 4], "94231234"),
1392
+ C("QA", "974", [8], [4, 4], "33123456"),
1393
+ C("KW", "965", [8], [4, 4], "50012345")
1394
+ ];
1395
+ var PACKED_DIAL_CODES = "AD376 AF93 AG1 AI1 AL355 AM374 AO244 AS1 AW297 AX358 AZ994 BA387 BB1 BF226 BH973 BI257 BJ229 BL590 BM1 BN673 BO591 BQ599 BS1 BT975 BV47 BW267 BY375 BZ501 CC61 CD243 CF236 CG242 CI225 CK682 CM237 CU53 CV238 CW599 CX61 CY357 DJ253 DM1 DO1 DZ213 EH212 ER291 ET251 FJ679 FK500 FM691 FO298 GA241 GD1 GE995 GF594 GG44 GH233 GI350 GL299 GM220 GN224 GP590 GQ240 GS500 GT502 GU1 GW245 GY592 HN504 HT509 IM44 IO246 IQ964 IR98 JE44 JM1 JO962 KG996 KH855 KI686 KM269 KN1 KP850 KY1 KZ7 LA856 LB961 LC1 LI423 LK94 LR231 LS266 LY218 MC377 MD373 ME382 MF590 MG261 MH692 MK389 ML223 MM95 MN976 MO853 MP1 MQ596 MR222 MS1 MT356 MU230 MV960 MW265 MZ258 NA264 NC687 NE227 NF672 NI505 NP977 NR674 NU683 OM968 PA507 PF689 PG675 PM508 PN64 PR1 PS970 PW680 PY595 RE262 RW250 SB677 SC248 SD249 SH290 SJ47 SL232 SM378 SN221 SO252 SR597 SS211 ST239 SV503 SX1 SY963 SZ268 TC1 TD235 TF262 TG228 TJ992 TK690 TL670 TM993 TN216 TO676 TT1 TV688 TZ255 UG256 UZ998 VA379 VC1 VE58 VG1 VI1 VU678 WF681 WS685 XK383 YE967 YT262 ZM260 ZW263";
1396
+ function parsePackedCountries(packed) {
1397
+ const out = [];
1398
+ for (const token of packed.trim().split(/\s+/)) {
1399
+ const iso2 = token.slice(0, 2);
1400
+ const dialCode = token.slice(2);
1401
+ out.push({ iso2, name: regionName(iso2), dialCode, lengths: [], groups: [], example: "" });
1402
+ }
1403
+ return out;
1404
+ }
1302
1405
  var PHONE_COUNTRIES = [
1303
- C("US", "United States", "1", [10], [3, 3, 4], "2015550123"),
1304
- C("CA", "Canada", "1", [10], [3, 3, 4], "5145550123"),
1305
- C("GB", "United Kingdom", "44", [10], [4, 6], "7400123456"),
1306
- C("AU", "Australia", "61", [9], [3, 3, 3], "412345678"),
1307
- C("DE", "Germany", "49", [10, 11], [4, 7], "15123456789"),
1308
- C("FR", "France", "33", [9], [1, 2, 2, 2, 2], "612345678"),
1309
- C("IN", "India", "91", [10], [5, 5], "8123456789"),
1310
- C("DK", "Denmark", "45", [8], [2, 2, 2, 2], "32123456"),
1311
- C("SE", "Sweden", "46", [9], [2, 3, 4], "701234567"),
1312
- C("NO", "Norway", "47", [8], [3, 2, 3], "40612345"),
1313
- C("FI", "Finland", "358", [9, 10], [2, 3, 4], "412345678"),
1314
- C("NL", "Netherlands", "31", [9], [1, 4, 4], "612345678"),
1315
- C("BE", "Belgium", "32", [9], [3, 2, 2, 2], "470123456"),
1316
- C("ES", "Spain", "34", [9], [3, 3, 3], "612345678"),
1317
- C("IT", "Italy", "39", [9, 10], [3, 3, 4], "3123456789"),
1318
- C("PT", "Portugal", "351", [9], [3, 3, 3], "912345678"),
1319
- C("IE", "Ireland", "353", [9], [2, 3, 4], "851234567"),
1320
- C("CH", "Switzerland", "41", [9], [2, 3, 2, 2], "781234567"),
1321
- C("AT", "Austria", "43", [10, 11], [3, 7], "6641234567"),
1322
- C("PL", "Poland", "48", [9], [3, 3, 3], "512345678"),
1323
- C("CZ", "Czechia", "420", [9], [3, 3, 3], "601123456"),
1324
- C("GR", "Greece", "30", [10], [3, 3, 4], "6912345678"),
1325
- C("RO", "Romania", "40", [9], [3, 3, 3], "712345678"),
1326
- C("HU", "Hungary", "36", [9], [2, 3, 4], "201234567"),
1327
- C("UA", "Ukraine", "380", [9], [2, 3, 2, 2], "501234567"),
1328
- C("RU", "Russia", "7", [10], [3, 3, 2, 2], "9123456789"),
1329
- C("TR", "Turkey", "90", [10], [3, 3, 2, 2], "5012345678"),
1330
- C("IL", "Israel", "972", [9], [2, 3, 4], "501234567"),
1331
- C("AE", "United Arab Emirates", "971", [9], [2, 3, 4], "501234567"),
1332
- C("SA", "Saudi Arabia", "966", [9], [2, 3, 4], "512345678"),
1333
- C("ZA", "South Africa", "27", [9], [2, 3, 4], "711234567"),
1334
- C("NG", "Nigeria", "234", [10], [3, 3, 4], "8021234567"),
1335
- C("KE", "Kenya", "254", [9], [3, 3, 3], "712123456"),
1336
- C("EG", "Egypt", "20", [10], [3, 3, 4], "1001234567"),
1337
- C("BR", "Brazil", "55", [10, 11], [2, 5, 4], "11961234567"),
1338
- C("MX", "Mexico", "52", [10], [3, 3, 4], "2221234567"),
1339
- C("AR", "Argentina", "54", [10], [4, 6], "1123456789"),
1340
- C("CL", "Chile", "56", [9], [1, 4, 4], "221234567"),
1341
- C("CO", "Colombia", "57", [10], [3, 3, 4], "3211234567"),
1342
- C("PE", "Peru", "51", [9], [3, 3, 3], "912345678"),
1343
- C("CN", "China", "86", [11], [3, 4, 4], "13123456789"),
1344
- C("JP", "Japan", "81", [10], [2, 4, 4], "9012345678"),
1345
- C("KR", "South Korea", "82", [9, 10], [2, 4, 4], "1023456789"),
1346
- C("SG", "Singapore", "65", [8], [4, 4], "81234567"),
1347
- C("HK", "Hong Kong", "852", [8], [4, 4], "51234567"),
1348
- C("TW", "Taiwan", "886", [9], [3, 3, 3], "912345678"),
1349
- C("MY", "Malaysia", "60", [9, 10], [2, 3, 4], "123456789"),
1350
- C("ID", "Indonesia", "62", [10, 11], [3, 4, 4], "81234567890"),
1351
- C("TH", "Thailand", "66", [9], [2, 3, 4], "812345678"),
1352
- C("PH", "Philippines", "63", [10], [3, 3, 4], "9171234567"),
1353
- C("VN", "Vietnam", "84", [9], [3, 3, 3], "912345678"),
1354
- C("PK", "Pakistan", "92", [10], [3, 3, 4], "3012345678"),
1355
- C("BD", "Bangladesh", "880", [10], [2, 4, 4], "1812345678"),
1356
- C("NZ", "New Zealand", "64", [9, 10], [2, 3, 4], "211234567"),
1357
- C("SK", "Slovakia", "421", [9], [3, 3, 3], "912123456"),
1358
- C("BG", "Bulgaria", "359", [9], [2, 3, 4], "481234567"),
1359
- C("HR", "Croatia", "385", [8, 9], [2, 3, 4], "921234567"),
1360
- C("RS", "Serbia", "381", [8, 9], [2, 3, 4], "601234567"),
1361
- C("SI", "Slovenia", "386", [8], [2, 3, 3], "31234567"),
1362
- C("LT", "Lithuania", "370", [8], [3, 2, 3], "61234567"),
1363
- C("LV", "Latvia", "371", [8], [2, 3, 3], "21234567"),
1364
- C("EE", "Estonia", "372", [7, 8], [3, 4], "51234567"),
1365
- C("IS", "Iceland", "354", [7], [3, 4], "6111234"),
1366
- C("LU", "Luxembourg", "352", [9], [3, 3, 3], "628123456"),
1367
- C("MA", "Morocco", "212", [9], [3, 3, 3], "650123456"),
1368
- C("CR", "Costa Rica", "506", [8], [4, 4], "83123456"),
1369
- C("EC", "Ecuador", "593", [9], [2, 3, 4], "991234567"),
1370
- C("UY", "Uruguay", "598", [8], [1, 3, 4], "94231234"),
1371
- C("QA", "Qatar", "974", [8], [4, 4], "33123456"),
1372
- C("KW", "Kuwait", "965", [8], [4, 4], "50012345")
1406
+ ...CURATED_COUNTRIES,
1407
+ ...parsePackedCountries(PACKED_DIAL_CODES)
1373
1408
  ];
1409
+ var PHONE_PICKER_COUNTRIES = sortForPicker(PHONE_COUNTRIES);
1410
+ function sortForPicker(countries) {
1411
+ if (typeof Intl === "undefined" || typeof Intl.Collator === "undefined") {
1412
+ return [...countries].sort((a, b) => a.iso2 < b.iso2 ? -1 : a.iso2 > b.iso2 ? 1 : 0);
1413
+ }
1414
+ const collator = new Intl.Collator(void 0, { sensitivity: "base" });
1415
+ return [...countries].sort((a, b) => collator.compare(a.name, b.name));
1416
+ }
1374
1417
  var byIso = new Map(PHONE_COUNTRIES.map((c) => [c.iso2, c]));
1375
1418
  var fileValueSchema = object({
1376
1419
  fileId: string2().check(_minLength(1)),
@@ -1398,6 +1441,7 @@ var FIELD_KINDS = [
1398
1441
  "date",
1399
1442
  "file_upload",
1400
1443
  "hidden",
1444
+ "calculated",
1401
1445
  "custom"
1402
1446
  ];
1403
1447
  var CONTENT_KINDS2 = ["heading", "paragraph", "divider"];
@@ -1434,7 +1478,7 @@ var schemaShape = object({
1434
1478
  });
1435
1479
  var MAX_SCHEMA_VERSION = 1;
1436
1480
  var FILLO_SCHEMA_VERSION = 1;
1437
- var FILLO_SDK_VERSION = true ? "0.9.0" : "0.0.0-dev";
1481
+ var FILLO_SDK_VERSION = true ? "0.12.0" : "0.0.0-dev";
1438
1482
  function str(value, max, fallback = "") {
1439
1483
  return typeof value === "string" ? value.trim().slice(0, max) : fallback;
1440
1484
  }
@@ -1442,6 +1486,11 @@ function optionalStr(value, max) {
1442
1486
  const s = str(value, max);
1443
1487
  return s === "" ? void 0 : s;
1444
1488
  }
1489
+ function optionalAffix(value, max) {
1490
+ if (typeof value !== "string") return void 0;
1491
+ const s = value.slice(0, max);
1492
+ return s.trim() === "" ? void 0 : s;
1493
+ }
1445
1494
  function bool(value) {
1446
1495
  return typeof value === "boolean" ? value : void 0;
1447
1496
  }
@@ -1489,7 +1538,17 @@ function conditions(value) {
1489
1538
  });
1490
1539
  return normalized.length ? normalized : void 0;
1491
1540
  }
1492
- function jumpRules(value, pageIds, sourceId, allowedFieldIds) {
1541
+ function jumpDepsAllowed(fieldId, calcRefs, allowed, seen) {
1542
+ const refs = calcRefs.get(fieldId);
1543
+ if (refs === void 0) return allowed.has(fieldId);
1544
+ if (seen.has(fieldId)) return true;
1545
+ seen.add(fieldId);
1546
+ for (const ref of refs) {
1547
+ if (!jumpDepsAllowed(ref, calcRefs, allowed, seen)) return false;
1548
+ }
1549
+ return true;
1550
+ }
1551
+ function jumpRules(value, pageIds, sourceId, allowedFieldIds, calcRefs) {
1493
1552
  if (!Array.isArray(value)) return void 0;
1494
1553
  const rules = value.slice(0, 50).flatMap((item) => {
1495
1554
  if (!item || typeof item !== "object") return [];
@@ -1501,11 +1560,187 @@ function jumpRules(value, pageIds, sourceId, allowedFieldIds) {
1501
1560
  if (rawWhen !== void 0 && !Array.isArray(rawWhen)) return [];
1502
1561
  const when2 = conditions(rawWhen);
1503
1562
  if (Array.isArray(rawWhen) && rawWhen.length > 0 && when2 === void 0) return [];
1504
- if (when2 && when2.some((c) => !allowedFieldIds.has(c.fieldId))) return [];
1563
+ if (when2 && when2.some((c) => !jumpDepsAllowed(c.fieldId, calcRefs, allowedFieldIds, /* @__PURE__ */ new Set())))
1564
+ return [];
1505
1565
  return [{ when: when2 ?? [], to }];
1506
1566
  });
1507
1567
  return rules.length ? rules : void 0;
1508
1568
  }
1569
+ var CALC_OPERAND_KINDS = ["number", "rating", "linear_scale", "calculated"];
1570
+ function normalizeCalc(value, depth = 0) {
1571
+ if (depth > 32) return null;
1572
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1573
+ const rec = value;
1574
+ switch (rec.op) {
1575
+ case "value": {
1576
+ const fieldId = str(rec.fieldId, 128);
1577
+ return fieldId ? { op: "value", fieldId } : null;
1578
+ }
1579
+ case "const": {
1580
+ const n = finite(rec.value);
1581
+ return n === void 0 ? null : { op: "const", value: n };
1582
+ }
1583
+ case "add":
1584
+ case "mul":
1585
+ case "min":
1586
+ case "max": {
1587
+ if (!Array.isArray(rec.args) || rec.args.length < 1 || rec.args.length > 50) return null;
1588
+ const args = [];
1589
+ for (const raw of rec.args) {
1590
+ const arg = normalizeCalc(raw, depth + 1);
1591
+ if (!arg) return null;
1592
+ args.push(arg);
1593
+ }
1594
+ return { op: rec.op, args };
1595
+ }
1596
+ case "sub":
1597
+ case "div": {
1598
+ const left = normalizeCalc(rec.left, depth + 1);
1599
+ const right = normalizeCalc(rec.right, depth + 1);
1600
+ return left && right ? { op: rec.op, left, right } : null;
1601
+ }
1602
+ case "round": {
1603
+ const arg = normalizeCalc(rec.arg, depth + 1);
1604
+ if (!arg) return null;
1605
+ return cleanObject({
1606
+ op: "round",
1607
+ arg,
1608
+ decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0)
1609
+ });
1610
+ }
1611
+ case "if": {
1612
+ if (!Array.isArray(rec.when)) return null;
1613
+ const when2 = conditions(rec.when) ?? [];
1614
+ if (when2.length !== rec.when.length) return null;
1615
+ const then = normalizeCalc(rec.then, depth + 1);
1616
+ const els = normalizeCalc(rec.else, depth + 1);
1617
+ return then && els ? { op: "if", when: when2, then, else: els } : null;
1618
+ }
1619
+ default:
1620
+ return null;
1621
+ }
1622
+ }
1623
+ function collectCalcRefs(expr, valueRefs, whenRefs) {
1624
+ switch (expr.op) {
1625
+ case "value":
1626
+ valueRefs.add(expr.fieldId);
1627
+ return;
1628
+ case "const":
1629
+ return;
1630
+ case "add":
1631
+ case "mul":
1632
+ case "min":
1633
+ case "max":
1634
+ for (const arg of expr.args) collectCalcRefs(arg, valueRefs, whenRefs);
1635
+ return;
1636
+ case "sub":
1637
+ case "div":
1638
+ collectCalcRefs(expr.left, valueRefs, whenRefs);
1639
+ collectCalcRefs(expr.right, valueRefs, whenRefs);
1640
+ return;
1641
+ case "round":
1642
+ collectCalcRefs(expr.arg, valueRefs, whenRefs);
1643
+ return;
1644
+ case "if":
1645
+ for (const cond of expr.when) whenRefs.add(cond.fieldId);
1646
+ collectCalcRefs(expr.then, valueRefs, whenRefs);
1647
+ collectCalcRefs(expr.else, valueRefs, whenRefs);
1648
+ return;
1649
+ }
1650
+ }
1651
+ function validateCalculatedFields(pages) {
1652
+ const kinds = /* @__PURE__ */ new Map();
1653
+ for (const page of pages) {
1654
+ for (const block of page.blocks) if (isField(block)) kinds.set(block.id, block.kind);
1655
+ }
1656
+ const deps = /* @__PURE__ */ new Map();
1657
+ for (const page of pages) {
1658
+ for (const block of page.blocks) {
1659
+ if (block.kind !== "calculated") continue;
1660
+ const calc = normalizeCalc(block.calc);
1661
+ if (!calc) {
1662
+ return `Field ${block.id} has a malformed calc expression \u2014 build it from value/const/add/sub/mul/div/min/max/round/if nodes`;
1663
+ }
1664
+ block.calc = calc;
1665
+ const valueRefs = /* @__PURE__ */ new Set();
1666
+ const whenRefs = /* @__PURE__ */ new Set();
1667
+ collectCalcRefs(calc, valueRefs, whenRefs);
1668
+ for (const ref of valueRefs) {
1669
+ const kind = kinds.get(ref);
1670
+ if (kind === void 0) {
1671
+ return `Calculated field ${block.id} references a missing field: ${ref} \u2014 remove the reference or restore that field`;
1672
+ }
1673
+ if (!CALC_OPERAND_KINDS.includes(kind)) {
1674
+ return `Calculated field ${block.id} references non-numeric field ${ref} (${kind}) \u2014 calculations can read number, rating, linear_scale, and calculated fields only`;
1675
+ }
1676
+ }
1677
+ deps.set(
1678
+ block.id,
1679
+ [...valueRefs, ...whenRefs].filter((ref) => kinds.get(ref) === "calculated")
1680
+ );
1681
+ }
1682
+ }
1683
+ const state = /* @__PURE__ */ new Map();
1684
+ const visit = (id, path) => {
1685
+ const mark = state.get(id);
1686
+ if (mark === "done") return null;
1687
+ if (mark === "visiting") {
1688
+ const chain = [...path.slice(path.indexOf(id)), id].join(" \u2192 ");
1689
+ return `Calculated field ${id} depends on its own result (${chain}) \u2014 break the cycle so the value can be computed`;
1690
+ }
1691
+ state.set(id, "visiting");
1692
+ for (const dep of deps.get(id) ?? []) {
1693
+ const error = visit(dep, [...path, id]);
1694
+ if (error) return error;
1695
+ }
1696
+ state.set(id, "done");
1697
+ return null;
1698
+ };
1699
+ for (const id of deps.keys()) {
1700
+ const error = visit(id, []);
1701
+ if (error) return error;
1702
+ }
1703
+ return null;
1704
+ }
1705
+ var MAX_CALC_FIELDS = 100;
1706
+ var MAX_CALC_TOTAL_NODES = 5e3;
1707
+ function countCalcNodes(expr) {
1708
+ switch (expr.op) {
1709
+ case "value":
1710
+ case "const":
1711
+ return 1;
1712
+ case "add":
1713
+ case "mul":
1714
+ case "min":
1715
+ case "max":
1716
+ return 1 + expr.args.reduce((n, arg) => n + countCalcNodes(arg), 0);
1717
+ case "sub":
1718
+ case "div":
1719
+ return 1 + countCalcNodes(expr.left) + countCalcNodes(expr.right);
1720
+ case "round":
1721
+ return 1 + countCalcNodes(expr.arg);
1722
+ case "if":
1723
+ return 1 + countCalcNodes(expr.then) + countCalcNodes(expr.else);
1724
+ }
1725
+ }
1726
+ function calcComplexityError(pages) {
1727
+ let calcFields = 0;
1728
+ let totalNodes = 0;
1729
+ for (const page of pages) {
1730
+ for (const block of page.blocks) {
1731
+ if (block.kind !== "calculated") continue;
1732
+ calcFields += 1;
1733
+ totalNodes += countCalcNodes(block.calc);
1734
+ }
1735
+ }
1736
+ if (calcFields > MAX_CALC_FIELDS) {
1737
+ return `Form has ${calcFields} calculated fields \u2014 the maximum is ${MAX_CALC_FIELDS}; remove some or consolidate them into fewer expressions`;
1738
+ }
1739
+ if (totalNodes > MAX_CALC_TOTAL_NODES) {
1740
+ return `Calculated expressions total ${totalNodes} operations \u2014 the maximum is ${MAX_CALC_TOTAL_NODES} across the form; simplify or split up the calculations`;
1741
+ }
1742
+ return null;
1743
+ }
1509
1744
  function baseBlock(rec) {
1510
1745
  return {
1511
1746
  id: str(rec.id, 128),
@@ -1617,7 +1852,20 @@ function normalizeBlock(input) {
1617
1852
  maxLength: finite(rec.maxLength) === void 0 ? void 0 : int(rec.maxLength, 1, 2e4, 2e4)
1618
1853
  });
1619
1854
  case "number":
1620
- return cleanObject({ ...baseField(rec, kind), min: finite(rec.min), max: finite(rec.max) });
1855
+ return cleanObject({
1856
+ ...baseField(rec, kind),
1857
+ min: finite(rec.min),
1858
+ max: finite(rec.max),
1859
+ // Display-only (decision 1 in the number-formatting contract) —
1860
+ // mirrors the calculated field's decimals/prefix/suffix carry-through
1861
+ // exactly, plus notation which calculated doesn't have.
1862
+ decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0),
1863
+ prefix: optionalAffix(rec.prefix, 100),
1864
+ suffix: optionalAffix(rec.suffix, 100),
1865
+ // The only supported values; anything else drops silently rather than
1866
+ // erroring (an old/garbage value is just "no grouping").
1867
+ notation: rec.notation === "grouped" ? "grouped" : rec.notation === "grouped-comma" ? "grouped-comma" : rec.notation === "grouped-dot" ? "grouped-dot" : void 0
1868
+ });
1621
1869
  case "select":
1622
1870
  case "multi_select":
1623
1871
  case "dropdown": {
@@ -1681,6 +1929,21 @@ function normalizeBlock(input) {
1681
1929
  paramName: optionalStr(rec.paramName, 128),
1682
1930
  defaultValue: optionalStr(rec.defaultValue, 1e3)
1683
1931
  });
1932
+ case "calculated":
1933
+ return cleanObject({
1934
+ ...baseField(rec, kind),
1935
+ // A derived value isn't answerable, so it can't be required (the
1936
+ // hidden-field precedent) — a required one could never be satisfied.
1937
+ required: false,
1938
+ // Kept raw here on purpose: the calc AST is normalized and structurally
1939
+ // validated in normalizeFormSchema once the full field universe is
1940
+ // known (missing refs, operand kinds, cycles are HARD errors there — a
1941
+ // calculated field must never be silently dropped or degraded).
1942
+ calc: rec.calc,
1943
+ decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0),
1944
+ prefix: optionalAffix(rec.prefix, 100),
1945
+ suffix: optionalAffix(rec.suffix, 100)
1946
+ });
1684
1947
  case "file_upload":
1685
1948
  return cleanObject({
1686
1949
  ...baseField(rec, kind),
@@ -1719,6 +1982,22 @@ function normalizeFormSchema(input) {
1719
1982
  return { id: str(page.id, 128), title: optionalStr(page.title, 500), blocks, next: page.next };
1720
1983
  });
1721
1984
  const targetIds = new Set(rebuilt.map((p) => p.id));
1985
+ const calcRefs = /* @__PURE__ */ new Map();
1986
+ for (const page of rebuilt) {
1987
+ for (const block of page.blocks) {
1988
+ if (block.kind !== "calculated") continue;
1989
+ const refs = /* @__PURE__ */ new Set();
1990
+ const calc = normalizeCalc(block.calc);
1991
+ if (calc) {
1992
+ const valueRefs = /* @__PURE__ */ new Set();
1993
+ const whenRefs = /* @__PURE__ */ new Set();
1994
+ collectCalcRefs(calc, valueRefs, whenRefs);
1995
+ for (const ref of valueRefs) refs.add(ref);
1996
+ for (const ref of whenRefs) refs.add(ref);
1997
+ }
1998
+ calcRefs.set(block.id, refs);
1999
+ }
2000
+ }
1722
2001
  const priorFieldIds = /* @__PURE__ */ new Set();
1723
2002
  const pages = rebuilt.map((page) => {
1724
2003
  for (const block of page.blocks) {
@@ -1728,7 +2007,7 @@ function normalizeFormSchema(input) {
1728
2007
  id: page.id,
1729
2008
  title: page.title,
1730
2009
  blocks: page.blocks,
1731
- next: jumpRules(page.next, targetIds, page.id, new Set(priorFieldIds))
2010
+ next: jumpRules(page.next, targetIds, page.id, new Set(priorFieldIds), calcRefs)
1732
2011
  });
1733
2012
  });
1734
2013
  const pageIds = /* @__PURE__ */ new Set();
@@ -1763,6 +2042,10 @@ function normalizeFormSchema(input) {
1763
2042
  }
1764
2043
  }
1765
2044
  }
2045
+ const calcError = validateCalculatedFields(pages);
2046
+ if (calcError) return { ok: false, error: calcError };
2047
+ const calcComplexity = calcComplexityError(pages);
2048
+ if (calcComplexity) return { ok: false, error: calcComplexity };
1766
2049
  if (pages.every((page) => page.blocks.length === 0)) {
1767
2050
  return { ok: false, error: "Schema must contain at least one valid block" };
1768
2051
  }
@@ -1788,6 +2071,7 @@ var MASK = 256 - 256 % ALPHABET.length;
1788
2071
  var S3_PART_TIMEOUT_MS = 5 * 6e4;
1789
2072
  var UPLOAD_CONTROL_TIMEOUT_MS = 10 * 6e4;
1790
2073
  var CLIENT_HEADER = { "X-Fillo-Client": `@usefillo/core@${FILLO_SDK_VERSION}` };
2074
+ var DARK_LUMINANCE_THRESHOLD = Math.sqrt(1.05 * 0.05) - 0.05;
1791
2075
  var FilloJsxError = class extends Error {
1792
2076
  constructor(code, message) {
1793
2077
  super(`[fillo] ${message} \u2014 https://fillo.so/docs/troubleshooting#jsx-${code.toLowerCase()}`);
@@ -1818,7 +2102,7 @@ var JSX_BLOCK_SPECS = [
1818
2102
  field("Email", "email", ["maxLength"]),
1819
2103
  field("Url", "url", ["maxLength"]),
1820
2104
  field("Phone", "phone", ["defaultCountry"]),
1821
- field("Number", "number", ["min", "max"]),
2105
+ field("Number", "number", ["min", "max", "decimals", "prefix", "suffix", "notation"]),
1822
2106
  field("Select", "select", CHOICE_EXTRA, { optionChildren: true }),
1823
2107
  field("MultiSelect", "multi_select", CHOICE_EXTRA, { optionChildren: true }),
1824
2108
  field("Dropdown", "dropdown", CHOICE_EXTRA, { optionChildren: true }),
@@ -1831,6 +2115,7 @@ var JSX_BLOCK_SPECS = [
1831
2115
  field("Date", "date", []),
1832
2116
  field("FileUpload", "file_upload", ["maxFiles", "maxFileSizeMb", "accept"]),
1833
2117
  field("Hidden", "hidden", ["paramName", "defaultValue"]),
2118
+ field("Calculated", "calculated", ["calc", "decimals", "prefix", "suffix"]),
1834
2119
  field("Custom", "custom", ["component", "config"]),
1835
2120
  content("Heading", "heading", true),
1836
2121
  content("Paragraph", "paragraph", true),
@@ -1927,6 +2212,7 @@ function openBrowser(url) {
1927
2212
  } catch {
1928
2213
  return;
1929
2214
  }
2215
+ if (process.env.CI === "true") return;
1930
2216
  try {
1931
2217
  if (process.platform === "win32") {
1932
2218
  const child = spawn("cmd", ["/c", "start", "", safeUrl], { stdio: "ignore", detached: true });
@@ -1963,7 +2249,7 @@ async function readJson(res) {
1963
2249
  die(`Unexpected non-JSON response from ${API} (${res.status}).`);
1964
2250
  }
1965
2251
  }
1966
- async function login(flags) {
2252
+ async function login(flags, options2 = {}) {
1967
2253
  const apiBase = flagString(flags, "api")?.replace(/\/$/, "") ?? API;
1968
2254
  const run = flagString(flags, "run");
1969
2255
  const progressToken = flagString(flags, "token");
@@ -2012,7 +2298,9 @@ Login failed (${r.status}).`);
2012
2298
  console.log("\n");
2013
2299
  await whoami(apiBase);
2014
2300
  console.log(
2015
- run ? `
2301
+ options2.continueToAgentRun ? `
2302
+ ${dim("Workspace approved. Connecting this setup...")}
2303
+ ` : run ? `
2016
2304
  ${dim("Return to the Fillo setup prompt and run its next command.")}
2017
2305
  ` : `
2018
2306
  ${dim("Now run:")} fillo push form.json
@@ -2021,7 +2309,8 @@ Login failed (${r.status}).`);
2021
2309
  return;
2022
2310
  }
2023
2311
  if (body.error === "slow_down") interval += 2e3;
2024
- else if (body.error && body.error !== "authorization_pending") die(`
2312
+ else if (body.error && body.error !== "authorization_pending")
2313
+ die(`
2025
2314
  Login failed: ${body.error}`);
2026
2315
  }
2027
2316
  die("\nLogin timed out \u2014 run `fillo login` again.");
@@ -2043,10 +2332,174 @@ async function list() {
2043
2332
  if (!forms.length) return console.log(" No forms yet.");
2044
2333
  for (const f of forms) {
2045
2334
  console.log(
2046
- ` ${f.status === "published" ? "\x1B[32m\u25CF\x1B[0m" : "\u25CB"} ${terminalText(f.name)} ${dim(f.id)}`
2335
+ ` ${f.status === "published" ? "\x1B[32m\u25CF\x1B[0m" : "\u25CB"} ${terminalText(f.name)} ${dim(f.id)} ${dim(f.url)}`
2047
2336
  );
2048
2337
  }
2049
2338
  }
2339
+ async function status(handle) {
2340
+ if (!handle) die("Usage: fillo status <formId|handle>");
2341
+ const res = await api(`/cli/forms/${encodeURIComponent(handle)}`, { token: requireToken() });
2342
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
2343
+ if (res.status === 404) {
2344
+ try {
2345
+ JSON.parse(await res.text());
2346
+ } catch {
2347
+ die(
2348
+ "This Fillo server does not support `fillo status` yet. Update the deployment, or check the form in the dashboard."
2349
+ );
2350
+ }
2351
+ die(`No form matches "${handle}" in this workspace. Run \`fillo list\` to see its forms.`);
2352
+ }
2353
+ const body = await readJson(res);
2354
+ if (!res.ok || !body.form) die(body.error ?? `status failed (${res.status}).`);
2355
+ const form = body.form;
2356
+ console.log(
2357
+ ` ${form.status === "published" ? "\x1B[32m\u25CF\x1B[0m" : "\u25CB"} ${terminalText(form.name)} ${dim(form.id)}`
2358
+ );
2359
+ console.log(` Status: ${bold(form.staged ? "staged" : form.status)}`);
2360
+ if (form.status === "published") console.log(` Live at ${terminalText(form.url)}`);
2361
+ else console.log(` ${dim(`Publishes to ${form.url}`)}`);
2362
+ if (form.staged) {
2363
+ console.log(` ${dim("Staged changes are waiting for review in the Fillo dashboard.")}`);
2364
+ }
2365
+ if (form.warning) {
2366
+ const pending = form.status !== "published" || form.staged === true;
2367
+ console.log(` ${dim(pending ? `Before publishing: ${form.warning}` : form.warning)}`);
2368
+ }
2369
+ if (form.warningUrl) console.log(` Storage settings: ${terminalText(form.warningUrl)}`);
2370
+ }
2371
+ async function publish(handle, flags) {
2372
+ if (!handle) die("Usage: fillo publish <formId|handle> [--allow-breaking]");
2373
+ const res = await api(`/cli/forms/${encodeURIComponent(handle)}/publish`, {
2374
+ method: "POST",
2375
+ token: requireToken(),
2376
+ body: JSON.stringify(flags["allow-breaking"] === true ? { allowBreaking: true } : {})
2377
+ });
2378
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
2379
+ if (res.status === 404) {
2380
+ try {
2381
+ JSON.parse(await res.text());
2382
+ } catch {
2383
+ die(
2384
+ "This Fillo server does not support `fillo publish` yet. Update the deployment, or publish the form from the dashboard."
2385
+ );
2386
+ }
2387
+ die(`No form matches "${handle}" in this workspace. Run \`fillo list\` to see its forms.`);
2388
+ }
2389
+ const body = await readJson(res);
2390
+ if (res.status === 409 && body.code === "breaking_changes") {
2391
+ const fields = Array.isArray(body.breakingFields) ? body.breakingFields.filter((f) => typeof f === "string") : [];
2392
+ console.error(
2393
+ "\x1B[31m\u2717\x1B[0m Not published \u2014 the staged changes remove or re-type fields that existing responses answered."
2394
+ );
2395
+ if (fields.length) console.error(` Fields: ${terminalText(fields.join(", "))}`);
2396
+ console.error(
2397
+ ` ${dim("Recorded answers are kept, but the live form, grid, and exports stop showing these fields.")}`
2398
+ );
2399
+ console.error(" Re-run with --allow-breaking to publish anyway.");
2400
+ process.exit(1);
2401
+ }
2402
+ if (res.status === 409 && typeof body.warningUrl === "string" && body.warningUrl) {
2403
+ console.error(
2404
+ `\x1B[31m\u2717\x1B[0m ${terminalText(body.error ?? `publish failed (${res.status}).`)}`
2405
+ );
2406
+ console.error(` Storage settings: ${terminalText(body.warningUrl)}`);
2407
+ process.exit(1);
2408
+ }
2409
+ if (!res.ok || !body.form) die(body.error ?? `publish failed (${res.status}).`);
2410
+ const form = body.form;
2411
+ if (body.changed === false) {
2412
+ console.log(
2413
+ `
2414
+ \x1B[32m\u2713\x1B[0m ${bold(terminalText(form.name))} is already live \u2014 nothing staged to publish.`
2415
+ );
2416
+ } else {
2417
+ console.log(`
2418
+ \x1B[32m\u2713\x1B[0m Published ${bold(terminalText(form.name))} ${dim(form.id)}`);
2419
+ }
2420
+ console.log(` Live at ${terminalText(form.url)}
2421
+ `);
2422
+ }
2423
+ async function loadResponseData(file) {
2424
+ let value;
2425
+ if (file === "-") {
2426
+ try {
2427
+ const chunks = [];
2428
+ let bytes = 0;
2429
+ for await (const chunk of process.stdin) {
2430
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
2431
+ bytes += buffer.byteLength;
2432
+ if (bytes > MAX_STDIN_SCHEMA_BYTES) {
2433
+ die("The response data on stdin is too large (maximum 1 MB).");
2434
+ }
2435
+ chunks.push(buffer);
2436
+ }
2437
+ const input = Buffer.concat(chunks).toString("utf8");
2438
+ if (!input.trim()) {
2439
+ die("stdin is empty - pipe one JSON answer object to `fillo test-response <form> -`.");
2440
+ }
2441
+ value = JSON.parse(input);
2442
+ } catch (error) {
2443
+ die(`Couldn't read response data from stdin: ${error.message}`);
2444
+ }
2445
+ } else {
2446
+ const abs = isAbsolute(file) ? file : resolve(process.cwd(), file);
2447
+ if (!abs.endsWith(".json")) {
2448
+ die("Test response data must be a .json file (or - for JSON on stdin).");
2449
+ }
2450
+ try {
2451
+ value = JSON.parse(readFileSync(abs, "utf8"));
2452
+ } catch (error) {
2453
+ die(`Couldn't read ${file}: ${error.message}`);
2454
+ }
2455
+ }
2456
+ if (!isRecord(value)) {
2457
+ die("Test response data must be one JSON object keyed by field id.");
2458
+ }
2459
+ return value;
2460
+ }
2461
+ async function testResponse(handle, file) {
2462
+ if (!handle || !file) {
2463
+ die("Usage: fillo test-response <formId|handle> <answers.json|->");
2464
+ }
2465
+ const token = requireToken();
2466
+ const data = await loadResponseData(file);
2467
+ const res = await api(`/cli/forms/${encodeURIComponent(handle)}/test-response`, {
2468
+ method: "POST",
2469
+ token,
2470
+ body: JSON.stringify({ data })
2471
+ });
2472
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
2473
+ if (res.status === 404) {
2474
+ try {
2475
+ JSON.parse(await res.text());
2476
+ } catch {
2477
+ die(
2478
+ "This Fillo server does not support `fillo test-response` yet. Update the deployment, then retry."
2479
+ );
2480
+ }
2481
+ die(`No form matches "${handle}" in this workspace. Run \`fillo list\` to see its forms.`);
2482
+ }
2483
+ const body = await readJson(res);
2484
+ if (res.status === 422 && body.errors && typeof body.errors === "object") {
2485
+ console.error("\x1B[31m\u2717\x1B[0m Test response failed server validation.");
2486
+ for (const [field2, message] of Object.entries(body.errors)) {
2487
+ console.error(` ${terminalText(field2)}: ${terminalText(message)}`);
2488
+ }
2489
+ process.exit(1);
2490
+ }
2491
+ if (!res.ok || !body.id || body.preview !== true) {
2492
+ die(body.error ?? `test response failed (${res.status}).`);
2493
+ }
2494
+ console.log(
2495
+ `
2496
+ \x1B[32m\u2713\x1B[0m Test response passed the ${bold(body.schema ?? "current")} schema ${dim(body.id)}`
2497
+ );
2498
+ console.log(
2499
+ ` ${dim("Preview only \u2014 excluded from responses, limits, delivery, and analytics; auto-deletes after 7 days.")}
2500
+ `
2501
+ );
2502
+ }
2050
2503
  async function loadSchema(file, allowCode) {
2051
2504
  if (file === "-") {
2052
2505
  try {
@@ -2117,6 +2570,8 @@ function printPushed(formId, url, updated, draft = false) {
2117
2570
  if (draft) console.log(` ${dim("Review and publish it from the Fillo dashboard.")}`);
2118
2571
  else if (url) console.log(` Live at ${terminalText(url)}`);
2119
2572
  console.log(` Embed: <FilloForm formId="${terminalText(formId)}" />
2573
+ `);
2574
+ console.log(` Verify: [data-fillo-form-id="${terminalText(formId)}"] on the actual app route
2120
2575
  `);
2121
2576
  }
2122
2577
  function syncError(response, body, credential) {
@@ -2160,7 +2615,10 @@ function printSynced(body, requestedStage) {
2160
2615
  console.log(` Live at ${terminalText(`${API}/f/${body.slug}`)}`);
2161
2616
  }
2162
2617
  if (body.warning) console.log(` ${dim(`Before publishing: ${body.warning}`)}`);
2618
+ if (body.warningUrl) console.log(` Storage settings: ${terminalText(body.warningUrl)}`);
2163
2619
  console.log(` Embed: <FilloForm formId="${terminalText(formId)}" />
2620
+ `);
2621
+ console.log(` Verify: [data-fillo-form-id="${terminalText(formId)}"] on the actual app route
2164
2622
  `);
2165
2623
  }
2166
2624
  async function pushWithSync(items, credential, requestedStage) {
@@ -2206,7 +2664,9 @@ async function push(file, flags) {
2206
2664
  );
2207
2665
  }
2208
2666
  if (!syncToken && !accountToken && stage) {
2209
- die(`To stage changes, run \`fillo login\` or set ${SYNC_TOKEN_ENV} from Settings > Developers.`);
2667
+ die(
2668
+ `To stage changes, run \`fillo login\` or set ${SYNC_TOKEN_ENV} from Settings > Developers.`
2669
+ );
2210
2670
  }
2211
2671
  const handle = typeof flags.handle === "string" ? flags.handle : void 0;
2212
2672
  const items = toPushItems(await loadSchema(file, flags["allow-code"] === true), handle);
@@ -2249,7 +2709,16 @@ async function push(file, flags) {
2249
2709
  })
2250
2710
  });
2251
2711
  const body = await readJson(res);
2252
- if (!res.ok || !body.formId) die(body.error ?? `push failed (${res.status}).`);
2712
+ if (!res.ok || !body.formId) {
2713
+ if (res.status === 409 && typeof body.warningUrl === "string" && body.warningUrl) {
2714
+ console.error(
2715
+ `\x1B[31m\u2717\x1B[0m ${terminalText(body.error ?? `push failed (${res.status}).`)}`
2716
+ );
2717
+ console.error(` Storage settings: ${terminalText(body.warningUrl)}`);
2718
+ process.exit(1);
2719
+ }
2720
+ die(body.error ?? `push failed (${res.status}).`);
2721
+ }
2253
2722
  printPushed(body.formId, body.url, !!body.updated);
2254
2723
  }
2255
2724
  return;
@@ -2263,7 +2732,9 @@ async function push(file, flags) {
2263
2732
  async function init(flags) {
2264
2733
  const email3 = typeof flags.email === "string" ? flags.email.trim() : "";
2265
2734
  if (!email3) {
2266
- die("Usage: fillo init --email you@company.com (or run `fillo login` for an existing account).");
2735
+ die(
2736
+ "Usage: fillo init --email you@company.com (or run `fillo login` for an existing account)."
2737
+ );
2267
2738
  }
2268
2739
  const res = await api("/workspaces/provision", {
2269
2740
  method: "POST",
@@ -2273,13 +2744,17 @@ async function init(flags) {
2273
2744
  if (!res.ok || !body.key) die(body.error ?? `init failed (${res.status}).`);
2274
2745
  const { claimUrl: _legacyWorkspaceLink, ...current } = readConfig();
2275
2746
  writeConfig({ ...current, pk: body.key });
2276
- console.log(`
2277
- \x1B[32m\u2713\x1B[0m Workspace ready. A workspace link was sent to ${bold(terminalText(email3))}.`);
2747
+ console.log(
2748
+ `
2749
+ \x1B[32m\u2713\x1B[0m Workspace ready. A workspace link was sent to ${bold(terminalText(email3))}.`
2750
+ );
2278
2751
  if (body.limits) {
2279
2752
  const expiry = Date.parse(body.limits.expiresAt);
2280
2753
  if (Number.isFinite(expiry)) {
2281
2754
  const days = Math.max(1, Math.round((expiry - Date.now()) / 864e5));
2282
- console.log(` ${dim(`Preview: up to ${body.limits.responses} responses for ~${days} days.`)}`);
2755
+ console.log(
2756
+ ` ${dim(`Preview: up to ${body.limits.responses} responses for ~${days} days.`)}`
2757
+ );
2283
2758
  }
2284
2759
  }
2285
2760
  console.log(`
@@ -2288,6 +2763,18 @@ async function init(flags) {
2288
2763
  `);
2289
2764
  }
2290
2765
  var AGENT_ACTIONS = ["claim_required", "storage_required", "publish_required"];
2766
+ var AGENT_EVENT_STATUSES = [
2767
+ "created",
2768
+ "connected",
2769
+ "asking",
2770
+ "planning",
2771
+ "installing",
2772
+ "editing",
2773
+ "checking",
2774
+ "needs_action",
2775
+ "done",
2776
+ "error"
2777
+ ];
2291
2778
  var FORM_STATUSES = ["draft", "published"];
2292
2779
  function enumFlag(flags, key, allowed) {
2293
2780
  const value = flags[key];
@@ -2311,46 +2798,40 @@ async function agent(subcommand, flags) {
2311
2798
  return agentHelp();
2312
2799
  }
2313
2800
  if (!run || !token) {
2314
- die("Usage: fillo agent <connect|event> --run <id> --token <token> [--api <url>]");
2801
+ die("Usage: fillo agent <bootstrap|connect|event> --run <id> --token <token> [--api <url>]");
2315
2802
  }
2316
2803
  if (flags.account !== void 0 && flags.account !== true) {
2317
2804
  die("--account does not take a value.");
2318
2805
  }
2319
- if (subcommand === "connect") {
2320
- const account = flags.account === true ? await attachAgentAccount(apiBase, run, token) : void 0;
2321
- await postAgentEvent(apiBase, run, token, {
2322
- status: "connected",
2323
- message: "Agent connected. Reading the app now."
2324
- });
2325
- console.log(` \x1B[32m\u2713\x1B[0m Live progress connected.`);
2326
- if (account) {
2327
- console.log(` \x1B[32m\u2713\x1B[0m ${bold(account.workspace)}`);
2328
- console.log(` ${dim("Publishable key:")} ${account.publishableKey}`);
2806
+ if (subcommand === "bootstrap") {
2807
+ installSkill(flags);
2808
+ if (flags.account === true) {
2809
+ await login(flags, { continueToAgentRun: true });
2329
2810
  }
2330
- console.log(` ${dim("Report next steps with:")}`);
2331
- console.log(
2332
- ` fillo agent event --api ${terminalText(apiBase)} --run ${terminalText(run)} --token <same-token> --status editing --message "Editing the form screen"`
2333
- );
2334
- return;
2811
+ return connectAgentRun(apiBase, run, token, flags.account === true);
2812
+ }
2813
+ if (subcommand === "connect") {
2814
+ return connectAgentRun(apiBase, run, token, flags.account === true);
2335
2815
  }
2336
2816
  if (flags.account === true) {
2337
- die("--account can only be used with `fillo agent connect`.");
2817
+ die("--account can only be used with `fillo agent bootstrap` or `fillo agent connect`.");
2338
2818
  }
2339
2819
  if (subcommand === "event") {
2340
- const status = flagString(flags, "status");
2820
+ const status2 = enumFlag(flags, "status", AGENT_EVENT_STATUSES);
2341
2821
  const message = flagString(flags, "message");
2342
- if (!status) die("Usage: fillo agent event --status <status> --message <short update>");
2822
+ if (!status2)
2823
+ die('Usage: fillo agent event --status <status> --message "<what the human does next>"');
2343
2824
  const action = enumFlag(flags, "action", AGENT_ACTIONS);
2344
2825
  const formStatus = enumFlag(flags, "form-status", FORM_STATUSES);
2345
2826
  const formId = optionalStringFlag(flags, "form-id");
2346
- if ((status === "done" || status === "needs_action") && !formId) {
2347
- die(`--form-id is required when --status is ${status}.`);
2827
+ if ((status2 === "done" || status2 === "needs_action") && !formId) {
2828
+ die(`--form-id is required when --status is ${status2}.`);
2348
2829
  }
2349
- if (status === "needs_action" && !action) {
2830
+ if (status2 === "needs_action" && !action) {
2350
2831
  die("--action is required when --status is needs_action.");
2351
2832
  }
2352
2833
  await postAgentEvent(apiBase, run, token, {
2353
- status,
2834
+ status: status2,
2354
2835
  message,
2355
2836
  appUrl: flagString(flags, "app-url"),
2356
2837
  action,
@@ -2363,6 +2844,25 @@ async function agent(subcommand, flags) {
2363
2844
  }
2364
2845
  die(`Unknown agent command: ${subcommand}`);
2365
2846
  }
2847
+ async function connectAgentRun(apiBase, run, token, attachAccount) {
2848
+ const account = attachAccount ? await attachAgentAccount(apiBase, run, token) : void 0;
2849
+ await postAgentEvent(apiBase, run, token, {
2850
+ status: "connected",
2851
+ message: "Agent connected. Reading the app now."
2852
+ });
2853
+ console.log(` \x1B[32m\u2713\x1B[0m Live progress connected.`);
2854
+ if (account) {
2855
+ console.log(` \x1B[32m\u2713\x1B[0m ${bold(account.workspace)}`);
2856
+ console.log(` ${dim("Publishable key:")} ${account.publishableKey}`);
2857
+ }
2858
+ console.log(` ${dim("Report next steps with:")}`);
2859
+ console.log(
2860
+ ` fillo agent event --api ${terminalText(apiBase)} --run ${terminalText(run)} --token <same-token> --status editing --message "Editing the form screen"`
2861
+ );
2862
+ console.log(
2863
+ ` ${dim("For needs_action or done, lead --message with what the human does next \u2014 max 180 chars, longer is cut off.")}`
2864
+ );
2865
+ }
2366
2866
  var SKILL_AGENT_DIRECTORIES = {
2367
2867
  shared: ".agents/skills",
2368
2868
  universal: ".agents/skills",
@@ -2446,9 +2946,7 @@ function pathEntryExists(path) {
2446
2946
  var MANAGED_SKILL_MARKER = ".fillo-managed.json";
2447
2947
  function isCliManagedFilloSkill(target) {
2448
2948
  try {
2449
- const marker = JSON.parse(
2450
- readFileSync(join(target, MANAGED_SKILL_MARKER), "utf8")
2451
- );
2949
+ const marker = JSON.parse(readFileSync(join(target, MANAGED_SKILL_MARKER), "utf8"));
2452
2950
  return marker.managedBy === "@usefillo/cli" && marker.skill === "build-with-fillo" && marker.format === 1 && marker.allowAutomaticUpdates === true;
2453
2951
  } catch {
2454
2952
  return false;
@@ -2503,19 +3001,14 @@ function installSkill(flags) {
2503
3001
  if (!existsSync(BUNDLED_SKILL_DIR)) {
2504
3002
  die("This CLI package is missing its bundled Fillo skill. Reinstall @usefillo/cli@latest.");
2505
3003
  }
2506
- const destinations = Array.from(
2507
- new Set(skillDirectories)
2508
- ).map((skillDirectory) => {
3004
+ const destinations = Array.from(new Set(skillDirectories)).map((skillDirectory) => {
2509
3005
  const skillsRoot = resolve(scopeRoot, skillDirectory);
2510
3006
  const target = join(skillsRoot, "build-with-fillo");
2511
3007
  const relativeSkillsRoot = relative(scopeRoot, skillsRoot);
2512
3008
  if (relativeSkillsRoot === ".." || relativeSkillsRoot.startsWith(`..${sep}`) || isAbsolute(relativeSkillsRoot)) {
2513
3009
  die("The skill destination must stay inside the selected project or home scope.");
2514
3010
  }
2515
- const destinationParts = [
2516
- ...relativeSkillsRoot.split(sep).filter(Boolean),
2517
- "build-with-fillo"
2518
- ];
3011
+ const destinationParts = [...relativeSkillsRoot.split(sep).filter(Boolean), "build-with-fillo"];
2519
3012
  assertSafeSkillDestination(scopeRoot, destinationParts);
2520
3013
  const targetExists = pathEntryExists(target);
2521
3014
  const current = targetExists && sameSkillBundle(BUNDLED_SKILL_DIR, target);
@@ -2536,10 +3029,7 @@ function installSkill(flags) {
2536
3029
  if (destination.current) continue;
2537
3030
  mkdirSync(destination.skillsRoot, { recursive: true });
2538
3031
  assertSafeSkillDestination(scopeRoot, destination.destinationParts.slice(0, 2));
2539
- const staging = join(
2540
- destination.skillsRoot,
2541
- `.build-with-fillo.install-${randomUUID()}`
2542
- );
3032
+ const staging = join(destination.skillsRoot, `.build-with-fillo.install-${randomUUID()}`);
2543
3033
  try {
2544
3034
  cpSync(BUNDLED_SKILL_DIR, staging, { recursive: true, errorOnExist: true });
2545
3035
  assertSafeSkillDestination(scopeRoot, destination.destinationParts);
@@ -2585,18 +3075,15 @@ async function postAgentEvent(apiBase, run, token, body) {
2585
3075
  }
2586
3076
  async function attachAgentAccount(apiBase, run, progressToken) {
2587
3077
  const accountToken = requireTokenFor(apiBase);
2588
- const res = await fetch(
2589
- `${apiBase}/api/v1/agent-runs/${encodeURIComponent(run)}/account`,
2590
- {
2591
- method: "POST",
2592
- redirect: "error",
2593
- headers: {
2594
- Authorization: `Bearer ${accountToken}`,
2595
- "X-Fillo-Agent-Run-Token": progressToken
2596
- },
2597
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
2598
- }
2599
- );
3078
+ const res = await fetch(`${apiBase}/api/v1/agent-runs/${encodeURIComponent(run)}/account`, {
3079
+ method: "POST",
3080
+ redirect: "error",
3081
+ headers: {
3082
+ Authorization: `Bearer ${accountToken}`,
3083
+ "X-Fillo-Agent-Run-Token": progressToken
3084
+ },
3085
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
3086
+ });
2600
3087
  const payload = await res.json().catch(() => ({}));
2601
3088
  if (!res.ok) {
2602
3089
  const hint = res.status === 401 ? " Run `fillo login` and try again." : "";
@@ -2612,15 +3099,22 @@ function agentHelp() {
2612
3099
  ${bold("fillo agent")} \u2014 report live progress back to a Fillo prompt modal
2613
3100
 
2614
3101
  ${bold("Commands")}
3102
+ agent bootstrap Install the skill and connect this coding-agent run
3103
+ ${dim("--account approve and attach an existing workspace in the browser")}
2615
3104
  agent connect Connect a coding-agent run to the open browser modal
2616
3105
  ${dim("--account attach the workspace from `fillo login`")}
2617
3106
  agent event Send a short progress update
2618
- ${dim("--status <created|connected|asking|planning|installing|editing|checking|needs_action|done|error>")}
2619
- ${dim('--message "Short update"')}
3107
+ ${dim(`--status <${AGENT_EVENT_STATUSES.join("|")}>`)}
3108
+ ${dim('--message "What the human does next" (max 180 chars)')}
2620
3109
  ${dim("--form-id <id> --form-status <draft|published> --form-name <name>")}
2621
3110
  ${dim("--action <claim_required|storage_required|publish_required>")}
2622
3111
  ${dim("--app-url <localhost-url>")}
2623
3112
  ${dim("--run <id> --token <token> --api <url>")}
3113
+
3114
+ ${dim("For --status needs_action or done, lead the message with what the human does")}
3115
+ ${dim('next, in one or two sentences, plus the form URL or form id \u2014 e.g. "Connect')}
3116
+ ${dim('storage in Fillo, publish the form, then submit one test response".')}
3117
+ ${dim("Max 180 characters \u2014 longer is cut off. Never list changed files in the message.")}
2624
3118
  `);
2625
3119
  }
2626
3120
  function skillHelp() {
@@ -2661,8 +3155,14 @@ function help() {
2661
3155
  ${dim("--stage stage beside the live form for dashboard review")}
2662
3156
  ${dim("--draft alias with a handle; legacy one-off draft without")}
2663
3157
  ${dim("--allow-code allow a .mjs/.js schema (executes the file)")}
2664
- list List the workspace's forms
2665
- agent <cmd> Report live progress to a Fillo prompt modal
3158
+ list List the workspace's forms and their live URLs
3159
+ status <form> Show one form's status, live URL, and publish blockers
3160
+ ${dim("<form> is a form id or handle")}
3161
+ publish <form> Publish staged changes (or a draft form) \u2014 prints the live URL
3162
+ ${dim("--allow-breaking confirm removing/re-typing fields responses answered")}
3163
+ test-response <form> <file|->
3164
+ Validate answers against staged changes without real delivery
3165
+ agent <cmd> Prepare an agent run and report live progress
2666
3166
  skill install Install the Build with Fillo Agent Skill
2667
3167
 
2668
3168
  ${dim(`API: ${API} \xB7 set FILLO_API to override`)}
@@ -2671,6 +3171,7 @@ function help() {
2671
3171
  }
2672
3172
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
2673
3173
  "account",
3174
+ "allow-breaking",
2674
3175
  "allow-code",
2675
3176
  "draft",
2676
3177
  "stage",
@@ -2691,6 +3192,9 @@ var FLAGS_BY_COMMAND = {
2691
3192
  push: ["handle", "stage", "draft", "allow-code"],
2692
3193
  list: [],
2693
3194
  ls: [],
3195
+ status: [],
3196
+ publish: ["allow-breaking"],
3197
+ "test-response": [],
2694
3198
  agent: [
2695
3199
  "run",
2696
3200
  "token",
@@ -2709,10 +3213,7 @@ var FLAGS_BY_COMMAND = {
2709
3213
  version: []
2710
3214
  };
2711
3215
  function validateFlags(command, flags) {
2712
- const allowed = /* @__PURE__ */ new Set([
2713
- ...GLOBAL_FLAGS,
2714
- ...command ? FLAGS_BY_COMMAND[command] ?? [] : []
2715
- ]);
3216
+ const allowed = /* @__PURE__ */ new Set([...GLOBAL_FLAGS, ...command ? FLAGS_BY_COMMAND[command] ?? [] : []]);
2716
3217
  for (const [key, value] of Object.entries(flags)) {
2717
3218
  if (!allowed.has(key)) die(`Unknown flag for ${command ?? "fillo"}: --${key}`);
2718
3219
  if (BOOLEAN_FLAGS.has(key)) {
@@ -2790,6 +3291,12 @@ async function main() {
2790
3291
  case "list":
2791
3292
  case "ls":
2792
3293
  return list();
3294
+ case "status":
3295
+ return status(positional[0]);
3296
+ case "publish":
3297
+ return publish(positional[0], flags);
3298
+ case "test-response":
3299
+ return testResponse(positional[0], positional[1]);
2793
3300
  case "agent":
2794
3301
  return agent(positional[0], flags);
2795
3302
  case "skill":