@usefillo/cli 0.14.0 → 0.15.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 +4 -3
- package/dist/index.js +279 -104
- package/dist/skill/build-with-fillo/SKILL.md +12 -4
- package/dist/skill/build-with-fillo/references/auth-and-lifecycle.md +12 -4
- package/dist/skill/build-with-fillo/references/operations.md +22 -2
- package/dist/skill/build-with-fillo/references/schema-and-ux.md +8 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -29,9 +29,10 @@ Commands: `init`, `login`, `logout`, `whoami`, `push <file|->`, `list`,
|
|
|
29
29
|
`agent bootstrap`, `agent connect`, `agent event`, and `skill install`. Run
|
|
30
30
|
`npx @usefillo/cli --help` for flags. The canonical skill is
|
|
31
31
|
one portable Agent Skills bundle. The default command installs it in the shared
|
|
32
|
-
`.agents/skills` path and Claude Code's `.claude/skills` path
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
`.agents/skills` path and Claude Code's `.claude/skills` path — both get the
|
|
33
|
+
same files, for cross-agent compatibility rather than duplication. Hosts with
|
|
34
|
+
another location can use `skill install --dir <agent-skill-directory>`, so the
|
|
35
|
+
same bundle works without provider-specific forks. See
|
|
35
36
|
[fillo.so/agents](https://fillo.so/agents) for setup. The API
|
|
36
37
|
commands target
|
|
37
38
|
`https://fillo.so` by default (set `FILLO_API` to override).
|
package/dist/index.js
CHANGED
|
@@ -91,7 +91,9 @@ function die(msg) {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
// src/lib/api.ts
|
|
94
|
-
var
|
|
94
|
+
var DEFAULT_API = "https://fillo.so";
|
|
95
|
+
var apiOverride = process.env.FILLO_API?.trim();
|
|
96
|
+
var API = (apiOverride || DEFAULT_API).replace(/\/$/, "");
|
|
95
97
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
96
98
|
var SYNC_TOKEN_ENV = "FILLO_SYNC_TOKEN";
|
|
97
99
|
function requireTokenFor(apiBase) {
|
|
@@ -113,24 +115,51 @@ function requireTokenFor(apiBase) {
|
|
|
113
115
|
function requireToken() {
|
|
114
116
|
return requireTokenFor(API);
|
|
115
117
|
}
|
|
118
|
+
function networkFailure(apiBase, error) {
|
|
119
|
+
const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
120
|
+
const reason = causeCode(error) ?? (timedOut ? `no response within ${REQUEST_TIMEOUT_MS / 1e3}s` : void 0);
|
|
121
|
+
const hint = apiBase === DEFAULT_API ? "Check your network connection and try again." : `The CLI is pointed there by FILLO_API or --api \u2014 unset the override to use ${DEFAULT_API}, or start that deployment.`;
|
|
122
|
+
return new Error(
|
|
123
|
+
`Couldn't reach ${terminalText(apiBase)}${reason ? ` (${reason})` : ""}. ${hint}`,
|
|
124
|
+
{
|
|
125
|
+
cause: error
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
function causeCode(error, depth = 0) {
|
|
130
|
+
if (!error || typeof error !== "object" || depth > 8) return void 0;
|
|
131
|
+
const { code, errors, cause } = error;
|
|
132
|
+
if (typeof code === "string" && code) return code;
|
|
133
|
+
if (Array.isArray(errors)) {
|
|
134
|
+
for (const nested of errors) {
|
|
135
|
+
const found = causeCode(nested, depth + 1);
|
|
136
|
+
if (found) return found;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return causeCode(cause, depth + 1);
|
|
140
|
+
}
|
|
116
141
|
async function api(path, init2 = {}, apiBase = API) {
|
|
117
142
|
const { token, ...rest } = init2;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
143
|
+
try {
|
|
144
|
+
return await fetch(`${apiBase}/api/v1${path}`, {
|
|
145
|
+
...rest,
|
|
146
|
+
signal: rest.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
147
|
+
headers: {
|
|
148
|
+
"Content-Type": "application/json",
|
|
149
|
+
...token ? { Authorization: `Bearer ${token}` } : {},
|
|
150
|
+
...rest.headers
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
throw networkFailure(apiBase, error);
|
|
155
|
+
}
|
|
127
156
|
}
|
|
128
|
-
async function readJson(res) {
|
|
157
|
+
async function readJson(res, apiBase = API) {
|
|
129
158
|
const text = await res.text();
|
|
130
159
|
try {
|
|
131
160
|
return text ? JSON.parse(text) : {};
|
|
132
161
|
} catch {
|
|
133
|
-
die(`Unexpected non-JSON response from ${
|
|
162
|
+
die(`Unexpected non-JSON response from ${apiBase} (${res.status}).`);
|
|
134
163
|
}
|
|
135
164
|
}
|
|
136
165
|
|
|
@@ -292,9 +321,9 @@ async function startLoopbackServer() {
|
|
|
292
321
|
respond(200, "You can close this tab and return to your terminal.");
|
|
293
322
|
handler?.resolve(code);
|
|
294
323
|
});
|
|
295
|
-
await new Promise((
|
|
324
|
+
await new Promise((resolve5, reject) => {
|
|
296
325
|
server.once("error", reject);
|
|
297
|
-
server.listen(0, "127.0.0.1",
|
|
326
|
+
server.listen(0, "127.0.0.1", resolve5);
|
|
298
327
|
});
|
|
299
328
|
const port = server.address().port;
|
|
300
329
|
const close = () => {
|
|
@@ -303,7 +332,7 @@ async function startLoopbackServer() {
|
|
|
303
332
|
} catch {
|
|
304
333
|
}
|
|
305
334
|
};
|
|
306
|
-
const waitForCode = (state, timeoutMs) => new Promise((
|
|
335
|
+
const waitForCode = (state, timeoutMs) => new Promise((resolve5, reject) => {
|
|
307
336
|
expectedState = state;
|
|
308
337
|
const timer = setTimeout(() => {
|
|
309
338
|
handler = null;
|
|
@@ -314,7 +343,7 @@ async function startLoopbackServer() {
|
|
|
314
343
|
resolve: (code) => {
|
|
315
344
|
clearTimeout(timer);
|
|
316
345
|
close();
|
|
317
|
-
|
|
346
|
+
resolve5(code);
|
|
318
347
|
},
|
|
319
348
|
reject: (err) => {
|
|
320
349
|
clearTimeout(timer);
|
|
@@ -554,23 +583,41 @@ Login failed: ${body.error}`);
|
|
|
554
583
|
}
|
|
555
584
|
die("\nLogin timed out \u2014 run `fillo login` again.");
|
|
556
585
|
}
|
|
557
|
-
async function
|
|
586
|
+
async function fetchWhoami(apiBase = API) {
|
|
558
587
|
const res = await api("/cli/whoami", { token: requireTokenFor(apiBase) }, apiBase);
|
|
559
588
|
if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
|
|
560
589
|
if (!res.ok) die(`whoami failed (${res.status}).`);
|
|
561
590
|
const me = await readJson(res);
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
591
|
+
const workspace = typeof me.workspace === "string" ? terminalText(me.workspace) : "";
|
|
592
|
+
if (!workspace) die("Fillo returned an invalid workspace identity.");
|
|
593
|
+
return {
|
|
594
|
+
workspace,
|
|
595
|
+
canPublishFileFields: typeof me.canPublishFileFields === "boolean" ? me.canPublishFileFields : void 0
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
async function fetchWorkspaceName(apiBase = API) {
|
|
599
|
+
return (await fetchWhoami(apiBase)).workspace;
|
|
566
600
|
}
|
|
567
601
|
function printConnected(workspace) {
|
|
568
602
|
console.log(` ${okMark()} Connected to ${bold(workspace)}`);
|
|
569
603
|
}
|
|
570
604
|
async function whoami(flags) {
|
|
571
|
-
const
|
|
572
|
-
if (jsonMode(flags))
|
|
573
|
-
|
|
605
|
+
const me = await fetchWhoami(API);
|
|
606
|
+
if (jsonMode(flags)) {
|
|
607
|
+
return emitResult({
|
|
608
|
+
connected: true,
|
|
609
|
+
workspace: me.workspace,
|
|
610
|
+
...me.canPublishFileFields === void 0 ? {} : { canPublishFileFields: me.canPublishFileFields }
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
printConnected(me.workspace);
|
|
614
|
+
if (me.canPublishFileFields === true) {
|
|
615
|
+
console.log(` ${dim("File uploads: storage ready \u2014 a file field can publish now.")}`);
|
|
616
|
+
} else if (me.canPublishFileFields === false) {
|
|
617
|
+
console.log(
|
|
618
|
+
` ${dim("File uploads: no destination yet \u2014 connect one (fillo storage connect) before publishing a file field.")}`
|
|
619
|
+
);
|
|
620
|
+
}
|
|
574
621
|
}
|
|
575
622
|
function logout() {
|
|
576
623
|
const { token, tokenApi, ...rest } = readConfig();
|
|
@@ -595,6 +642,8 @@ var whoamiCommand = {
|
|
|
595
642
|
|
|
596
643
|
// src/commands/init.ts
|
|
597
644
|
import { execFile } from "child_process";
|
|
645
|
+
import { existsSync } from "fs";
|
|
646
|
+
import { basename, dirname, join as join2, resolve } from "path";
|
|
598
647
|
import { promisify } from "util";
|
|
599
648
|
|
|
600
649
|
// src/lib/prompt.ts
|
|
@@ -609,9 +658,9 @@ function promptInterface() {
|
|
|
609
658
|
shared = createInterface({ input: process.stdin, output: process.stdout });
|
|
610
659
|
shared.on("line", (line) => {
|
|
611
660
|
if (pending) {
|
|
612
|
-
const
|
|
661
|
+
const resolve5 = pending;
|
|
613
662
|
pending = null;
|
|
614
|
-
|
|
663
|
+
resolve5({ line });
|
|
615
664
|
} else {
|
|
616
665
|
lineQueue.push(line);
|
|
617
666
|
}
|
|
@@ -624,9 +673,9 @@ function promptInterface() {
|
|
|
624
673
|
sharedClosed = true;
|
|
625
674
|
shared = null;
|
|
626
675
|
if (pending) {
|
|
627
|
-
const
|
|
676
|
+
const resolve5 = pending;
|
|
628
677
|
pending = null;
|
|
629
|
-
|
|
678
|
+
resolve5({ eof: true });
|
|
630
679
|
}
|
|
631
680
|
});
|
|
632
681
|
}
|
|
@@ -648,10 +697,10 @@ function ask(promptText) {
|
|
|
648
697
|
if (!rl) return Promise.resolve({ eof: true });
|
|
649
698
|
rl.setPrompt(promptText);
|
|
650
699
|
rl.prompt();
|
|
651
|
-
return new Promise((
|
|
700
|
+
return new Promise((resolve5) => {
|
|
652
701
|
pending = (answer) => {
|
|
653
702
|
rl.setPrompt("");
|
|
654
|
-
|
|
703
|
+
resolve5(answer);
|
|
655
704
|
};
|
|
656
705
|
});
|
|
657
706
|
}
|
|
@@ -677,7 +726,7 @@ var ETX = String.fromCharCode(3);
|
|
|
677
726
|
var DEL = String.fromCharCode(127);
|
|
678
727
|
var BACKSPACE = "\b";
|
|
679
728
|
function readSecret(promptText) {
|
|
680
|
-
return new Promise((
|
|
729
|
+
return new Promise((resolve5, reject) => {
|
|
681
730
|
const input = process.stdin;
|
|
682
731
|
if (typeof input.setRawMode !== "function") {
|
|
683
732
|
reject(new Error("No interactive terminal to read a secret from."));
|
|
@@ -697,7 +746,7 @@ function readSecret(promptText) {
|
|
|
697
746
|
if (ENTER.has(ch) || ch === EOT) {
|
|
698
747
|
cleanup();
|
|
699
748
|
process.stdout.write("\n");
|
|
700
|
-
|
|
749
|
+
resolve5(value);
|
|
701
750
|
return;
|
|
702
751
|
}
|
|
703
752
|
if (ch === ETX) {
|
|
@@ -722,7 +771,7 @@ function readSecret(promptText) {
|
|
|
722
771
|
// src/commands/init.ts
|
|
723
772
|
var EMAIL_REQUIRED = 'Usage: fillo init --email you@company.com [--name "Your Name"] (or run `fillo login` for an existing account).';
|
|
724
773
|
var execFileAsync = promisify(execFile);
|
|
725
|
-
async function provisionWorkspace(flags, usageHint) {
|
|
774
|
+
async function provisionWorkspace(flags, usageHint, apiBase = API) {
|
|
726
775
|
const json = jsonMode(flags);
|
|
727
776
|
let email3 = flagString(flags, "email")?.trim() ?? "";
|
|
728
777
|
let name = flagString(flags, "name")?.trim() ?? "";
|
|
@@ -752,11 +801,21 @@ async function provisionWorkspace(flags, usageHint) {
|
|
|
752
801
|
}
|
|
753
802
|
}
|
|
754
803
|
}
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
804
|
+
const workspaceName = flagString(flags, "workspace-name") ?? defaultWorkspaceName();
|
|
805
|
+
const res = await api(
|
|
806
|
+
"/workspaces/provision",
|
|
807
|
+
{
|
|
808
|
+
method: "POST",
|
|
809
|
+
body: JSON.stringify({
|
|
810
|
+
email: email3,
|
|
811
|
+
source: "cli",
|
|
812
|
+
...name ? { name } : {},
|
|
813
|
+
...workspaceName ? { workspaceName } : {}
|
|
814
|
+
})
|
|
815
|
+
},
|
|
816
|
+
apiBase
|
|
817
|
+
);
|
|
818
|
+
const body = await readJson(res, apiBase);
|
|
760
819
|
if (!res.ok || !body.key) die(body.error ?? `Provisioning failed (${res.status}).`);
|
|
761
820
|
const claimToken = claimCookieValue(res);
|
|
762
821
|
const { claimUrl: _legacyWorkspaceLink, ...current } = readConfig();
|
|
@@ -767,7 +826,55 @@ async function provisionWorkspace(flags, usageHint) {
|
|
|
767
826
|
...name ? { name } : {},
|
|
768
827
|
...claimToken ? { claimToken } : {}
|
|
769
828
|
});
|
|
770
|
-
|
|
829
|
+
const workspace = typeof body.workspaceName === "string" && terminalText(body.workspaceName) || workspaceName || fallbackWorkspaceName(name, email3);
|
|
830
|
+
return { pk: body.key, email: email3, name, workspace, limits: body.limits };
|
|
831
|
+
}
|
|
832
|
+
async function resolveWorkspaceIdentity(flags, usageHint, apiBase = API) {
|
|
833
|
+
const existing = await existingAccountWorkspace(apiBase);
|
|
834
|
+
if (existing) return { existing: true, workspace: existing };
|
|
835
|
+
const ws = await provisionWorkspace(flags, usageHint, apiBase);
|
|
836
|
+
return { existing: false, ...ws };
|
|
837
|
+
}
|
|
838
|
+
async function existingAccountWorkspace(apiBase) {
|
|
839
|
+
const cfg = readConfig();
|
|
840
|
+
if (!cfg.token || cfg.tokenApi?.replace(/\/$/, "") !== apiBase.replace(/\/$/, "")) {
|
|
841
|
+
return void 0;
|
|
842
|
+
}
|
|
843
|
+
const res = await api("/cli/whoami", { token: cfg.token }, apiBase).catch(() => null);
|
|
844
|
+
if (!res?.ok) return void 0;
|
|
845
|
+
const me = await res.json().catch(() => ({}));
|
|
846
|
+
return typeof me.workspace === "string" && terminalText(me.workspace) ? terminalText(me.workspace) : void 0;
|
|
847
|
+
}
|
|
848
|
+
function printExistingWorkspaceGuard(workspace) {
|
|
849
|
+
console.log(
|
|
850
|
+
`
|
|
851
|
+
${danger(`Using your stored Fillo login \u2014 workspace "${terminalText(workspace)}".`)}`
|
|
852
|
+
);
|
|
853
|
+
console.log(
|
|
854
|
+
` ${dim("This is a real, connected workspace, not a fresh preview: forms you push go live in it.")}`
|
|
855
|
+
);
|
|
856
|
+
console.log(
|
|
857
|
+
` ${dim("Want an isolated preview instead? Run `fillo logout`, then re-run this command.")}`
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
function fallbackWorkspaceName(name, email3) {
|
|
861
|
+
const owner = name.split(/\s+/)[0]?.trim() || email3.split("@")[0]?.trim() || "New";
|
|
862
|
+
return `${owner}'s workspace`;
|
|
863
|
+
}
|
|
864
|
+
function defaultWorkspaceName() {
|
|
865
|
+
let current = resolve(process.cwd());
|
|
866
|
+
let root = current;
|
|
867
|
+
while (true) {
|
|
868
|
+
if (existsSync(join2(current, ".git"))) {
|
|
869
|
+
root = current;
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
const parent = dirname(current);
|
|
873
|
+
if (parent === current) break;
|
|
874
|
+
current = parent;
|
|
875
|
+
}
|
|
876
|
+
const cleaned = terminalText(basename(root)).slice(0, 80);
|
|
877
|
+
return cleaned || void 0;
|
|
771
878
|
}
|
|
772
879
|
function previewLimitLine(limits) {
|
|
773
880
|
if (!limits) return null;
|
|
@@ -795,13 +902,32 @@ function claimCookieValue(res) {
|
|
|
795
902
|
}
|
|
796
903
|
async function init(flags) {
|
|
797
904
|
const json = jsonMode(flags);
|
|
798
|
-
const
|
|
905
|
+
const identity = await resolveWorkspaceIdentity(flags, EMAIL_REQUIRED);
|
|
906
|
+
if (identity.existing) {
|
|
907
|
+
if (json) return emitResult({ existing: true, workspace: identity.workspace });
|
|
908
|
+
printExistingWorkspaceGuard(identity.workspace);
|
|
909
|
+
console.log(`
|
|
910
|
+
${bold("Next:")} fillo push form.json --handle my-form`);
|
|
911
|
+
console.log(
|
|
912
|
+
` ${dim(`Pushes publish into ${terminalText(identity.workspace)} \u2014 the workspace above.`)}
|
|
913
|
+
`
|
|
914
|
+
);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const ws = identity;
|
|
799
918
|
if (json) {
|
|
800
|
-
return emitResult({
|
|
919
|
+
return emitResult({
|
|
920
|
+
existing: false,
|
|
921
|
+
workspace: ws.workspace,
|
|
922
|
+
pk: ws.pk,
|
|
923
|
+
email: ws.email,
|
|
924
|
+
name: ws.name || null,
|
|
925
|
+
limits: ws.limits ?? null
|
|
926
|
+
});
|
|
801
927
|
}
|
|
802
928
|
console.log(
|
|
803
929
|
`
|
|
804
|
-
${okMark()}
|
|
930
|
+
${okMark()} New workspace ${bold(terminalText(ws.workspace))} ready. A link was sent to ${bold(terminalText(ws.email))}.`
|
|
805
931
|
);
|
|
806
932
|
const preview = previewLimitLine(ws.limits);
|
|
807
933
|
if (preview) console.log(` ${dim(preview)}`);
|
|
@@ -812,7 +938,7 @@ async function init(flags) {
|
|
|
812
938
|
}
|
|
813
939
|
var initCommand = {
|
|
814
940
|
name: "init",
|
|
815
|
-
flags: ["email", "name"],
|
|
941
|
+
flags: ["email", "name", "workspace-name"],
|
|
816
942
|
run: (_args, flags) => init(flags)
|
|
817
943
|
};
|
|
818
944
|
|
|
@@ -820,7 +946,7 @@ var initCommand = {
|
|
|
820
946
|
import { randomUUID } from "crypto";
|
|
821
947
|
import {
|
|
822
948
|
cpSync,
|
|
823
|
-
existsSync,
|
|
949
|
+
existsSync as existsSync2,
|
|
824
950
|
lstatSync,
|
|
825
951
|
mkdirSync as mkdirSync2,
|
|
826
952
|
readdirSync,
|
|
@@ -830,7 +956,7 @@ import {
|
|
|
830
956
|
rmSync
|
|
831
957
|
} from "fs";
|
|
832
958
|
import { homedir as homedir2 } from "os";
|
|
833
|
-
import { dirname, isAbsolute, join as
|
|
959
|
+
import { dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
|
|
834
960
|
import { fileURLToPath } from "url";
|
|
835
961
|
var BUNDLED_SKILL_DIR = fileURLToPath(new URL("./skill/build-with-fillo", import.meta.url));
|
|
836
962
|
var SKILL_AGENT_DIRECTORIES = {
|
|
@@ -854,11 +980,11 @@ function isSkillAgent(value) {
|
|
|
854
980
|
return Object.prototype.hasOwnProperty.call(SKILL_AGENT_DIRECTORIES, value);
|
|
855
981
|
}
|
|
856
982
|
function findProjectRoot(start) {
|
|
857
|
-
let current =
|
|
983
|
+
let current = resolve2(start);
|
|
858
984
|
while (true) {
|
|
859
|
-
if (
|
|
860
|
-
const parent =
|
|
861
|
-
if (parent === current) return
|
|
985
|
+
if (existsSync2(join3(current, ".git"))) return current;
|
|
986
|
+
const parent = dirname2(current);
|
|
987
|
+
if (parent === current) return resolve2(start);
|
|
862
988
|
current = parent;
|
|
863
989
|
}
|
|
864
990
|
}
|
|
@@ -874,7 +1000,7 @@ function parseSkillDirectories(flags) {
|
|
|
874
1000
|
if (parts.length === 0 || parts.some((part) => part === "..")) {
|
|
875
1001
|
die("--dir must name a skill directory inside the selected project or home scope.");
|
|
876
1002
|
}
|
|
877
|
-
return [
|
|
1003
|
+
return [join3(...parts)];
|
|
878
1004
|
}
|
|
879
1005
|
if (!agent2) return [SKILL_AGENT_DIRECTORIES.shared, SKILL_AGENT_DIRECTORIES.claude];
|
|
880
1006
|
if (isSkillAgent(agent2)) return [SKILL_AGENT_DIRECTORIES[agent2]];
|
|
@@ -883,10 +1009,10 @@ function parseSkillDirectories(flags) {
|
|
|
883
1009
|
);
|
|
884
1010
|
}
|
|
885
1011
|
function skillFiles(root, relative2 = "") {
|
|
886
|
-
const directory =
|
|
1012
|
+
const directory = join3(root, relative2);
|
|
887
1013
|
const files = [];
|
|
888
1014
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
889
|
-
const next =
|
|
1015
|
+
const next = join3(relative2, entry.name);
|
|
890
1016
|
if (entry.isDirectory()) files.push(...skillFiles(root, next));
|
|
891
1017
|
else if (entry.isFile()) files.push(next);
|
|
892
1018
|
}
|
|
@@ -898,7 +1024,7 @@ function sameSkillBundle(source, target) {
|
|
|
898
1024
|
const targetFiles = skillFiles(target);
|
|
899
1025
|
if (sourceFiles.length !== targetFiles.length) return false;
|
|
900
1026
|
return sourceFiles.every(
|
|
901
|
-
(file, index) => file === targetFiles[index] && readFileSync2(
|
|
1027
|
+
(file, index) => file === targetFiles[index] && readFileSync2(join3(source, file)).equals(readFileSync2(join3(target, file)))
|
|
902
1028
|
);
|
|
903
1029
|
} catch {
|
|
904
1030
|
return false;
|
|
@@ -916,7 +1042,7 @@ function pathEntryExists(path) {
|
|
|
916
1042
|
var MANAGED_SKILL_MARKER = ".fillo-managed.json";
|
|
917
1043
|
function isCliManagedFilloSkill(target) {
|
|
918
1044
|
try {
|
|
919
|
-
const marker = JSON.parse(readFileSync2(
|
|
1045
|
+
const marker = JSON.parse(readFileSync2(join3(target, MANAGED_SKILL_MARKER), "utf8"));
|
|
920
1046
|
return marker.managedBy === "@usefillo/cli" && marker.skill === "build-with-fillo" && marker.format === 1 && marker.allowAutomaticUpdates === true;
|
|
921
1047
|
} catch {
|
|
922
1048
|
return false;
|
|
@@ -926,7 +1052,7 @@ function assertSafeSkillDestination(scopeRoot, parts) {
|
|
|
926
1052
|
const realScopeRoot = realpathSync(scopeRoot);
|
|
927
1053
|
let current = scopeRoot;
|
|
928
1054
|
for (const part of parts) {
|
|
929
|
-
current =
|
|
1055
|
+
current = join3(current, part);
|
|
930
1056
|
if (!pathEntryExists(current)) return;
|
|
931
1057
|
if (lstatSync(current).isSymbolicLink()) {
|
|
932
1058
|
throw new Error(`Refusing to install through symlinked path ${current}.`);
|
|
@@ -968,12 +1094,12 @@ function installSkill(flags, opts = {}) {
|
|
|
968
1094
|
const skillDirectories = parseSkillDirectories(flags);
|
|
969
1095
|
const global = flags.global === true;
|
|
970
1096
|
const scopeRoot = global ? homedir2() : findProjectRoot(process.cwd());
|
|
971
|
-
if (!
|
|
1097
|
+
if (!existsSync2(BUNDLED_SKILL_DIR)) {
|
|
972
1098
|
die("This CLI package is missing its bundled Fillo skill. Reinstall @usefillo/cli@latest.");
|
|
973
1099
|
}
|
|
974
1100
|
const destinations = Array.from(new Set(skillDirectories)).map((skillDirectory) => {
|
|
975
|
-
const skillsRoot =
|
|
976
|
-
const target =
|
|
1101
|
+
const skillsRoot = resolve2(scopeRoot, skillDirectory);
|
|
1102
|
+
const target = join3(skillsRoot, "build-with-fillo");
|
|
977
1103
|
const relativeSkillsRoot = relative(scopeRoot, skillsRoot);
|
|
978
1104
|
if (relativeSkillsRoot === ".." || relativeSkillsRoot.startsWith(`..${sep}`) || isAbsolute(relativeSkillsRoot)) {
|
|
979
1105
|
die("The skill destination must stay inside the selected project or home scope.");
|
|
@@ -999,7 +1125,7 @@ function installSkill(flags, opts = {}) {
|
|
|
999
1125
|
if (destination.current) continue;
|
|
1000
1126
|
mkdirSync2(destination.skillsRoot, { recursive: true });
|
|
1001
1127
|
assertSafeSkillDestination(scopeRoot, destination.destinationParts.slice(0, 2));
|
|
1002
|
-
const staging =
|
|
1128
|
+
const staging = join3(destination.skillsRoot, `.build-with-fillo.install-${randomUUID()}`);
|
|
1003
1129
|
try {
|
|
1004
1130
|
cpSync(BUNDLED_SKILL_DIR, staging, { recursive: true, errorOnExist: true });
|
|
1005
1131
|
assertSafeSkillDestination(scopeRoot, destination.destinationParts);
|
|
@@ -1018,10 +1144,15 @@ function installSkill(flags, opts = {}) {
|
|
|
1018
1144
|
console.log(`
|
|
1019
1145
|
${okMark()} ${action}.`);
|
|
1020
1146
|
for (const target of targets) console.log(` ${dim(target)}`);
|
|
1147
|
+
if (targets.length > 1) {
|
|
1148
|
+
console.log(
|
|
1149
|
+
` ${dim("Same bundle in both \u2014 cross-agent compatibility, not duplication.")}`
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1021
1152
|
console.log(`
|
|
1022
1153
|
Ask your agent to use ${bold("build-with-fillo")}.`);
|
|
1023
1154
|
console.log(
|
|
1024
|
-
` ${dim(`If it does not discover skills, point it to ${
|
|
1155
|
+
` ${dim(`If it does not discover skills, point it to ${join3(targets[0], "SKILL.md")}.`)}
|
|
1025
1156
|
`
|
|
1026
1157
|
);
|
|
1027
1158
|
return targets;
|
|
@@ -1047,7 +1178,8 @@ function skillHelp() {
|
|
|
1047
1178
|
${dimRaw("--global install for the current user")}
|
|
1048
1179
|
${dimRaw("--force replace a different existing copy")}
|
|
1049
1180
|
|
|
1050
|
-
${dimRaw("Default: open Agent Skills path (.agents/skills) plus Claude Code (.claude/skills)
|
|
1181
|
+
${dimRaw("Default: open Agent Skills path (.agents/skills) plus Claude Code (.claude/skills) \u2014")}
|
|
1182
|
+
${dimRaw("the same bundle in both, so either agent convention discovers it.")}
|
|
1051
1183
|
`);
|
|
1052
1184
|
}
|
|
1053
1185
|
var skillCommand = {
|
|
@@ -1059,34 +1191,48 @@ var skillCommand = {
|
|
|
1059
1191
|
|
|
1060
1192
|
// src/commands/agent.ts
|
|
1061
1193
|
var BOOTSTRAP_USAGE = 'To set up a workspace I need an email for your recovery link. Re-run: npx @usefillo/cli@latest agent bootstrap --email you@company.com [--name "Your Name"]. (Already have an account? `fillo login`.)';
|
|
1062
|
-
async function bootstrapStandalone(flags) {
|
|
1194
|
+
async function bootstrapStandalone(flags, apiBase) {
|
|
1063
1195
|
if (flags.account === true) {
|
|
1064
|
-
die(
|
|
1196
|
+
die(
|
|
1197
|
+
"--account is for the browser handoff: `fillo agent bootstrap --run <id> --token <token> --account`."
|
|
1198
|
+
);
|
|
1065
1199
|
}
|
|
1066
1200
|
const json = jsonMode(flags);
|
|
1067
|
-
const
|
|
1201
|
+
const identity = await resolveWorkspaceIdentity(flags, BOOTSTRAP_USAGE, apiBase);
|
|
1202
|
+
if (!json) {
|
|
1203
|
+
if (identity.existing) printExistingWorkspaceGuard(identity.workspace);
|
|
1204
|
+
else printNewWorkspace(identity);
|
|
1205
|
+
}
|
|
1068
1206
|
const targets = installSkill(flags, { quiet: json });
|
|
1069
1207
|
if (json) {
|
|
1070
|
-
return emitResult(
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1208
|
+
return emitResult(
|
|
1209
|
+
identity.existing ? { existing: true, workspace: identity.workspace, skill: { installed: true, targets } } : {
|
|
1210
|
+
existing: false,
|
|
1211
|
+
workspace: identity.workspace,
|
|
1212
|
+
pk: identity.pk,
|
|
1213
|
+
email: identity.email,
|
|
1214
|
+
name: identity.name || null,
|
|
1215
|
+
limits: identity.limits ?? null,
|
|
1216
|
+
skill: { installed: true, targets }
|
|
1217
|
+
}
|
|
1218
|
+
);
|
|
1077
1219
|
}
|
|
1078
1220
|
console.log(
|
|
1079
1221
|
`
|
|
1080
|
-
${bold("
|
|
1222
|
+
${bold("Next:")} tell your agent what form to build${identity.existing ? ` in ${bold(terminalText(identity.workspace))}` : ""}, or push one directly:`
|
|
1081
1223
|
);
|
|
1082
|
-
console.log(` ${dim(`A workspace link was sent to ${terminalText(ws.email)} \u2014 claim it anytime with \`fillo claim\`.`)}`);
|
|
1083
|
-
const preview = previewLimitLine(ws.limits);
|
|
1084
|
-
if (preview) console.log(` ${dim(preview)}`);
|
|
1085
|
-
console.log(`
|
|
1086
|
-
${bold("Next:")} tell your agent what form to build, or push one directly:`);
|
|
1087
1224
|
console.log(` ${dim("fillo push form.json --handle my-form")}
|
|
1088
1225
|
`);
|
|
1089
1226
|
}
|
|
1227
|
+
function printNewWorkspace(ws) {
|
|
1228
|
+
console.log(`
|
|
1229
|
+
${okMark()} New workspace ${bold(terminalText(ws.workspace))} provisioned.`);
|
|
1230
|
+
console.log(
|
|
1231
|
+
` ${dim(`A workspace link was sent to ${terminalText(ws.email)} \u2014 claim it anytime with \`fillo claim\`.`)}`
|
|
1232
|
+
);
|
|
1233
|
+
const preview = previewLimitLine(ws.limits);
|
|
1234
|
+
if (preview) console.log(` ${dim(preview)}`);
|
|
1235
|
+
}
|
|
1090
1236
|
var AGENT_ACTIONS = ["claim_required", "storage_required", "publish_required"];
|
|
1091
1237
|
var AGENT_EVENT_STATUSES = [
|
|
1092
1238
|
"created",
|
|
@@ -1112,7 +1258,7 @@ async function agent(subcommand, flags) {
|
|
|
1112
1258
|
die("--account does not take a value.");
|
|
1113
1259
|
}
|
|
1114
1260
|
if (subcommand === "bootstrap") {
|
|
1115
|
-
if (!run || !token) return bootstrapStandalone(flags);
|
|
1261
|
+
if (!run || !token) return bootstrapStandalone(flags, apiBase);
|
|
1116
1262
|
installSkill(flags);
|
|
1117
1263
|
if (flags.account === true) {
|
|
1118
1264
|
await login({ ...flags, json: false }, { continueToAgentRun: true });
|
|
@@ -1176,29 +1322,39 @@ async function connectAgentRun(apiBase, run, token, attachAccount) {
|
|
|
1176
1322
|
);
|
|
1177
1323
|
}
|
|
1178
1324
|
async function postAgentEvent(apiBase, run, token, body) {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1325
|
+
let res;
|
|
1326
|
+
try {
|
|
1327
|
+
res = await fetch(`${apiBase}/api/v1/agent-runs/${encodeURIComponent(run)}/events`, {
|
|
1328
|
+
method: "POST",
|
|
1329
|
+
headers: {
|
|
1330
|
+
Authorization: `Bearer ${token}`,
|
|
1331
|
+
"Content-Type": "application/json"
|
|
1332
|
+
},
|
|
1333
|
+
body: JSON.stringify(body),
|
|
1334
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1335
|
+
});
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
throw networkFailure(apiBase, error);
|
|
1338
|
+
}
|
|
1188
1339
|
const payload = await res.json().catch(() => ({}));
|
|
1189
1340
|
if (!res.ok) die(payload.error ?? `agent progress failed (${res.status}).`);
|
|
1190
1341
|
}
|
|
1191
1342
|
async function attachAgentAccount(apiBase, run, progressToken) {
|
|
1192
1343
|
const accountToken = requireTokenFor(apiBase);
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1344
|
+
let res;
|
|
1345
|
+
try {
|
|
1346
|
+
res = await fetch(`${apiBase}/api/v1/agent-runs/${encodeURIComponent(run)}/account`, {
|
|
1347
|
+
method: "POST",
|
|
1348
|
+
redirect: "error",
|
|
1349
|
+
headers: {
|
|
1350
|
+
Authorization: `Bearer ${accountToken}`,
|
|
1351
|
+
"X-Fillo-Agent-Run-Token": progressToken
|
|
1352
|
+
},
|
|
1353
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1354
|
+
});
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
throw networkFailure(apiBase, error);
|
|
1357
|
+
}
|
|
1202
1358
|
const payload = await res.json().catch(() => ({}));
|
|
1203
1359
|
if (!res.ok) {
|
|
1204
1360
|
const hint = res.status === 401 ? " Run `fillo login` and try again." : "";
|
|
@@ -1217,8 +1373,11 @@ function agentHelp() {
|
|
|
1217
1373
|
agent bootstrap Set up a workspace and install the Build with Fillo skill
|
|
1218
1374
|
${dimRaw("--email you@company.com provision a fresh workspace (a TTY confirms your git identity)")}
|
|
1219
1375
|
${dimRaw('--name "Your Name" optional display name')}
|
|
1376
|
+
${dimRaw("--workspace-name <name> name the new workspace (default: this repo/directory)")}
|
|
1377
|
+
${dimRaw("--api <url> target a different Fillo deployment (default: FILLO_API or https://fillo.so)")}
|
|
1220
1378
|
${dimRaw("--run <id> --token <token> instead: attach to an open browser modal")}
|
|
1221
1379
|
${dimRaw("--account with --run/--token: approve an existing workspace in the browser")}
|
|
1380
|
+
${dimRaw("A stored `fillo login` is reused as-is (and named) \u2014 run `fillo logout` first for an isolated preview.")}
|
|
1222
1381
|
agent connect Connect a coding-agent run to the open browser modal
|
|
1223
1382
|
${dimRaw("--account attach the workspace from `fillo login`")}
|
|
1224
1383
|
agent event Send a short progress update
|
|
@@ -1240,6 +1399,7 @@ var agentCommand = {
|
|
|
1240
1399
|
flags: [
|
|
1241
1400
|
"email",
|
|
1242
1401
|
"name",
|
|
1402
|
+
"workspace-name",
|
|
1243
1403
|
"run",
|
|
1244
1404
|
"token",
|
|
1245
1405
|
"api",
|
|
@@ -1681,7 +1841,7 @@ var deleteCommand = {
|
|
|
1681
1841
|
|
|
1682
1842
|
// src/commands/forms.ts
|
|
1683
1843
|
import { readFileSync as readFileSync3 } from "fs";
|
|
1684
|
-
import { isAbsolute as isAbsolute2, resolve as
|
|
1844
|
+
import { isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
|
|
1685
1845
|
import { pathToFileURL } from "url";
|
|
1686
1846
|
|
|
1687
1847
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
|
|
@@ -4113,7 +4273,7 @@ async function loadResponseData(file) {
|
|
|
4113
4273
|
die(`Couldn't read response data from stdin: ${error.message}`);
|
|
4114
4274
|
}
|
|
4115
4275
|
} else {
|
|
4116
|
-
const abs = isAbsolute2(file) ? file :
|
|
4276
|
+
const abs = isAbsolute2(file) ? file : resolve3(process.cwd(), file);
|
|
4117
4277
|
if (!abs.endsWith(".json")) {
|
|
4118
4278
|
die("Test response data must be a .json file (or - for JSON on stdin).");
|
|
4119
4279
|
}
|
|
@@ -4192,7 +4352,7 @@ async function loadSchema(file, allowCode) {
|
|
|
4192
4352
|
die(`Couldn't read the schema from stdin: ${error.message}`);
|
|
4193
4353
|
}
|
|
4194
4354
|
}
|
|
4195
|
-
const abs = isAbsolute2(file) ? file :
|
|
4355
|
+
const abs = isAbsolute2(file) ? file : resolve3(process.cwd(), file);
|
|
4196
4356
|
if (!abs.endsWith(".json") && !allowCode) {
|
|
4197
4357
|
die(
|
|
4198
4358
|
`${file} is a code module \u2014 pushing it executes the file. Re-run with --allow-code if you trust it, or push a .json schema instead.`
|
|
@@ -4233,7 +4393,7 @@ function toPushItems(value, handle) {
|
|
|
4233
4393
|
return { handle, schema: item };
|
|
4234
4394
|
});
|
|
4235
4395
|
}
|
|
4236
|
-
function printPushed(formId, url, updated, draft = false) {
|
|
4396
|
+
function printPushed(formId, url, updated, draft = false, storage2 = {}) {
|
|
4237
4397
|
const label = draft ? "Created draft" : updated ? "Updated" : "Created";
|
|
4238
4398
|
console.log(`
|
|
4239
4399
|
${okMark()} ${label} ${bold(formId)}`);
|
|
@@ -4242,6 +4402,8 @@ function printPushed(formId, url, updated, draft = false) {
|
|
|
4242
4402
|
` ${bold("Publish:")} fillo publish ${terminalText(formId)} ${dim("(or publish it from the Fillo dashboard)")}`
|
|
4243
4403
|
);
|
|
4244
4404
|
} else if (url) console.log(` Live at ${terminalText(url)}`);
|
|
4405
|
+
if (storage2.warning) console.log(` ${dim(`Before publishing: ${storage2.warning}`)}`);
|
|
4406
|
+
if (storage2.warningUrl) console.log(` Storage settings: ${terminalText(storage2.warningUrl)}`);
|
|
4245
4407
|
console.log(` Embed: <FilloForm formId="${terminalText(formId)}" />
|
|
4246
4408
|
`);
|
|
4247
4409
|
console.log(` Verify: [data-fillo-form-id="${terminalText(formId)}"] on the actual app route
|
|
@@ -4384,7 +4546,10 @@ async function push(file, flags) {
|
|
|
4384
4546
|
results.push(body);
|
|
4385
4547
|
emitProgress({ status: "pushed", formId: body.formId, draft: true });
|
|
4386
4548
|
} else {
|
|
4387
|
-
printPushed(body.formId, body.url, !!body.updated, true
|
|
4549
|
+
printPushed(body.formId, body.url, !!body.updated, true, {
|
|
4550
|
+
warning: body.warning,
|
|
4551
|
+
warningUrl: body.warningUrl
|
|
4552
|
+
});
|
|
4388
4553
|
}
|
|
4389
4554
|
}
|
|
4390
4555
|
if (results) emitResult({ forms: results });
|
|
@@ -4417,7 +4582,10 @@ async function push(file, flags) {
|
|
|
4417
4582
|
results.push(body);
|
|
4418
4583
|
emitProgress({ status: "pushed", formId: body.formId, published: true });
|
|
4419
4584
|
} else {
|
|
4420
|
-
printPushed(body.formId, body.url, !!body.updated
|
|
4585
|
+
printPushed(body.formId, body.url, !!body.updated, false, {
|
|
4586
|
+
warning: body.warning,
|
|
4587
|
+
warningUrl: body.warningUrl
|
|
4588
|
+
});
|
|
4421
4589
|
}
|
|
4422
4590
|
}
|
|
4423
4591
|
if (results) emitResult({ forms: results });
|
|
@@ -4733,7 +4901,7 @@ var membersCommand = {
|
|
|
4733
4901
|
// src/commands/responses.ts
|
|
4734
4902
|
import { once } from "events";
|
|
4735
4903
|
import { createWriteStream } from "fs";
|
|
4736
|
-
import { isAbsolute as isAbsolute3, resolve as
|
|
4904
|
+
import { isAbsolute as isAbsolute3, resolve as resolve4 } from "path";
|
|
4737
4905
|
var dateOnly4 = (iso) => iso.slice(0, 10);
|
|
4738
4906
|
var dateTime = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
4739
4907
|
async function dieOnNotFound(res, handle, verb) {
|
|
@@ -4815,7 +4983,7 @@ async function exportCsv(handle, flags) {
|
|
|
4815
4983
|
const body = await readJson(res);
|
|
4816
4984
|
die(body.error ?? `responses export failed (${res.status}).`);
|
|
4817
4985
|
}
|
|
4818
|
-
const path = out ? isAbsolute3(out) ? out :
|
|
4986
|
+
const path = out ? isAbsolute3(out) ? out : resolve4(process.cwd(), out) : void 0;
|
|
4819
4987
|
const file = path ? createWriteStream(path) : void 0;
|
|
4820
4988
|
const target = file ?? process.stdout;
|
|
4821
4989
|
let bytes = 0;
|
|
@@ -5277,6 +5445,13 @@ async function status3(flags) {
|
|
|
5277
5445
|
const target = body.defaultStorageProvider;
|
|
5278
5446
|
const targetLabel = target && target in PROVIDER_LABEL ? PROVIDER_LABEL[target] : target ?? "no durable storage yet";
|
|
5279
5447
|
console.log(` ${dim("Uploads for storage=null forms resolve to:")} ${targetLabel}`);
|
|
5448
|
+
if (body.canPublishFileFields === true) {
|
|
5449
|
+
console.log(` ${dim("Can publish file fields:")} yes`);
|
|
5450
|
+
} else if (body.canPublishFileFields === false) {
|
|
5451
|
+
console.log(
|
|
5452
|
+
` ${dim("Can publish file fields:")} not yet \u2014 connect or choose a destination first`
|
|
5453
|
+
);
|
|
5454
|
+
}
|
|
5280
5455
|
console.log("");
|
|
5281
5456
|
}
|
|
5282
5457
|
var S3_FIELDS = [
|
|
@@ -17,7 +17,15 @@ Never require a provider-specific agent command.
|
|
|
17
17
|
1. Inspect the host repository. Identify its framework, package manager, target
|
|
18
18
|
route, UI conventions, existing Fillo packages, and any supplied form id,
|
|
19
19
|
key, setup command, or run token.
|
|
20
|
-
2.
|
|
20
|
+
2. If the form may need uploads, check storage readiness before choosing the
|
|
21
|
+
schema, not after a blocked publish. With a CLI login, `fillo whoami` or
|
|
22
|
+
`fillo storage status` (both `--json`) report `canPublishFileFields`:
|
|
23
|
+
`true` means a `file_upload` field can publish now; `false` means a
|
|
24
|
+
destination still needs connecting (a provider can show connected while
|
|
25
|
+
no default destination is resolved). Use that signal to decide on file
|
|
26
|
+
fields — do not defensively drop one just because storage looks unset; ask
|
|
27
|
+
the user to finish connecting it instead.
|
|
28
|
+
3. Establish the form's source of truth:
|
|
21
29
|
- Published form id or slug: render it directly. No client key is required.
|
|
22
30
|
- React-owned schema: use `<Fillo.Form>` or `defineForm()` with
|
|
23
31
|
`@usefillo/react`.
|
|
@@ -32,13 +40,13 @@ Never require a provider-specific agent command.
|
|
|
32
40
|
client. A plain `FormSchema` plus a client is not a code-defined form and
|
|
33
41
|
cannot resolve a target. Use explicit `renderOnly` only for a deliberately
|
|
34
42
|
non-submitting UI preview.
|
|
35
|
-
|
|
43
|
+
4. Ask only for missing product decisions that change the result: purpose,
|
|
36
44
|
placement, required questions or files, conditional behavior, and what
|
|
37
45
|
happens after submit. Infer routine implementation details from the repo.
|
|
38
|
-
|
|
46
|
+
5. If the prompt supplies a handoff command, workspace key, form id, or run
|
|
39
47
|
token, follow that handoff exactly. Do not create a second workspace or save
|
|
40
48
|
a run token.
|
|
41
|
-
|
|
49
|
+
6. Implement the smallest complete form, verify it in the host app, and report
|
|
42
50
|
any remaining dashboard action honestly.
|
|
43
51
|
|
|
44
52
|
## Load only the needed reference
|
|
@@ -42,9 +42,13 @@ Never inspect `~/.fillo/config.json`, expose the account token, or call
|
|
|
42
42
|
|
|
43
43
|
## Claim the workspace from the terminal
|
|
44
44
|
|
|
45
|
-
A capped preview workspace becomes a full account by being claimed.
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
A capped preview workspace becomes a full account by being claimed. The
|
|
46
|
+
preview cap is 10 responses total across every form in the workspace (not per
|
|
47
|
+
form) plus a time window; past either limit, a new submission is rejected
|
|
48
|
+
outright — HTTP 403 with `{error, closed: true}` — never queued or silently
|
|
49
|
+
dropped, and the workspace's email is notified once. Claim it without leaving
|
|
50
|
+
the terminal — this replaces sending the user to a browser to save the
|
|
51
|
+
workspace:
|
|
48
52
|
|
|
49
53
|
```bash
|
|
50
54
|
npx @usefillo/cli@latest claim
|
|
@@ -209,7 +213,11 @@ most one line at the end of the chat summary.
|
|
|
209
213
|
- Add `--json` to a command to get one final JSON object on stdout (progress
|
|
210
214
|
lines go to stderr as JSON). Parse that object instead of scraping human
|
|
211
215
|
output when a step feeds later automation — for example, capture `formId` from
|
|
212
|
-
`push --json` or the key id from `keys list --json`.
|
|
216
|
+
`push --json` or the key id from `keys list --json`. `push --json` returns the
|
|
217
|
+
full lifecycle for each form in one round-trip — `formId`, `slug`, `status`,
|
|
218
|
+
`staged`, `accepting`, `uploadsAvailable`, `canPublishFileFields`, and any
|
|
219
|
+
storage `warning`/`warningUrl` — so a push needs no follow-up `status` call to
|
|
220
|
+
learn whether it staged, published, or is accepting responses.
|
|
213
221
|
- The CLI enters agent mode automatically when stdout is not a TTY, or when
|
|
214
222
|
`FILLO_AGENT=1` is set: no color, no spinners, and it never opens a browser —
|
|
215
223
|
it prints the URL for the user to open instead. Print that URL and stop; the
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Uploads and customer storage
|
|
4
4
|
|
|
5
|
+
Check storage readiness before choosing the schema, not after a blocked
|
|
6
|
+
publish. With a CLI login, `fillo storage status` (or `whoami`) reports
|
|
7
|
+
whether uploads are durable right now. A provider can show connected while no
|
|
8
|
+
default destination is resolved yet — that combination cannot durably store
|
|
9
|
+
uploads, so treat it as "not ready," not as a reason to drop a needed file
|
|
10
|
+
field; ask the user to finish connecting a destination instead.
|
|
11
|
+
|
|
5
12
|
Model a file requirement with a real `file_upload` field and validate its
|
|
6
13
|
limits against the current schema reference:
|
|
7
14
|
|
|
@@ -46,8 +53,21 @@ var. Never put the secret access key in shell history where you can avoid it;
|
|
|
46
53
|
prefer the env var or the hidden prompt. Google Drive and Box connect over
|
|
47
54
|
OAuth: `storage connect drive` (or `box`) prints an approval URL for the user to
|
|
48
55
|
open — print it and let them approve, do not loop. `storage` with no argument
|
|
49
|
-
reports each provider's connection
|
|
50
|
-
`
|
|
56
|
+
reports each provider's connection, the transit window, the destination a
|
|
57
|
+
`storage = null` form resolves to, and a single `canPublishFileFields` boolean.
|
|
58
|
+
This clears the `storage_required` publish blocker for the S3/R2 case without a
|
|
59
|
+
dashboard trip.
|
|
60
|
+
|
|
61
|
+
`canPublishFileFields` is the truthful pre-authoring answer to "can I ship a
|
|
62
|
+
file field right now?" — `true` when a default upload destination resolves
|
|
63
|
+
(an implicit pin, one connected durable provider, or the transit allowance).
|
|
64
|
+
`fillo whoami --json` carries the same boolean, so you can check it at bootstrap
|
|
65
|
+
before designing the form. It is workspace-scoped and deliberately distinct from
|
|
66
|
+
a form's `uploadsAvailable`: several providers connected with none chosen as the
|
|
67
|
+
default reads `canPublishFileFields: false` even though storage IS connected,
|
|
68
|
+
because a `storage = null` file field would still be blocked until one is
|
|
69
|
+
picked. Check this before authoring a `file_upload` field, not after a blocked
|
|
70
|
+
publish.
|
|
51
71
|
|
|
52
72
|
Test with one safe file. Confirm both the response reference and object in the
|
|
53
73
|
connected storage. Treat filenames and file contents as untrusted.
|
|
@@ -10,8 +10,14 @@ eligibility, follow-up, or the work performed after submission.
|
|
|
10
10
|
has a real type. Do not model everything as text.
|
|
11
11
|
- Use single-select for one stored choice and multi-select for several. Keep
|
|
12
12
|
option ids stable even when labels change.
|
|
13
|
-
- Use `file_upload`
|
|
14
|
-
|
|
13
|
+
- Use `file_upload` when the file is genuinely necessary. Before authoring
|
|
14
|
+
the field, read `canPublishFileFields` from `fillo whoami --json` or
|
|
15
|
+
`fillo storage status --json` (see [operations.md](operations.md)): `true`
|
|
16
|
+
means it can publish now. If `false`, connect a destination
|
|
17
|
+
(`fillo storage connect …`) or tell the user — do not defensively drop the
|
|
18
|
+
field; storage is a pre-flight check, not a publish-time surprise. A
|
|
19
|
+
specific form's `uploadsAvailable` is a different, per-form flag and is
|
|
20
|
+
trivially `true` before any file field exists, so it cannot answer this.
|
|
15
21
|
- Put known product context in prefill or a hidden field instead of asking the
|
|
16
22
|
respondent to re-enter it. Treat URL prefill as untrusted input.
|
|
17
23
|
- Split long or conceptually separate flows into pages. Keep short embedded
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usefillo/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Create and publish Fillo forms, and install the Fillo Agent Skill.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsup && node scripts/copy-skill.mjs",
|
|
33
|
-
"test": "node scripts/validate-skill-bundle.mjs && node scripts/test-skill-bundle.mjs && node scripts/test-skill-install.mjs && node scripts/test-agent-account.mjs && node scripts/test-stage-push.mjs && node scripts/test-local-validation.mjs && node scripts/test-loopback.mjs && node scripts/test-init.mjs && node scripts/test-init-tty.mjs && node scripts/test-keys-claim.mjs && node scripts/test-responses.mjs && node scripts/test-workspace-commands.mjs",
|
|
33
|
+
"test": "node scripts/validate-skill-bundle.mjs && node scripts/test-skill-bundle.mjs && node scripts/test-skill-install.mjs && node scripts/test-agent-account.mjs && node scripts/test-stage-push.mjs && node scripts/test-local-validation.mjs && node scripts/test-loopback.mjs && node scripts/test-init.mjs && node scripts/test-api-target.mjs && node scripts/test-init-tty.mjs && node scripts/test-keys-claim.mjs && node scripts/test-responses.mjs && node scripts/test-workspace-commands.mjs",
|
|
34
34
|
"typecheck": "tsc --noEmit"
|
|
35
35
|
}
|
|
36
36
|
}
|