@usefillo/cli 0.14.1 → 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 +186 -64
- 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 +1 -1
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
|
@@ -321,9 +321,9 @@ async function startLoopbackServer() {
|
|
|
321
321
|
respond(200, "You can close this tab and return to your terminal.");
|
|
322
322
|
handler?.resolve(code);
|
|
323
323
|
});
|
|
324
|
-
await new Promise((
|
|
324
|
+
await new Promise((resolve5, reject) => {
|
|
325
325
|
server.once("error", reject);
|
|
326
|
-
server.listen(0, "127.0.0.1",
|
|
326
|
+
server.listen(0, "127.0.0.1", resolve5);
|
|
327
327
|
});
|
|
328
328
|
const port = server.address().port;
|
|
329
329
|
const close = () => {
|
|
@@ -332,7 +332,7 @@ async function startLoopbackServer() {
|
|
|
332
332
|
} catch {
|
|
333
333
|
}
|
|
334
334
|
};
|
|
335
|
-
const waitForCode = (state, timeoutMs) => new Promise((
|
|
335
|
+
const waitForCode = (state, timeoutMs) => new Promise((resolve5, reject) => {
|
|
336
336
|
expectedState = state;
|
|
337
337
|
const timer = setTimeout(() => {
|
|
338
338
|
handler = null;
|
|
@@ -343,7 +343,7 @@ async function startLoopbackServer() {
|
|
|
343
343
|
resolve: (code) => {
|
|
344
344
|
clearTimeout(timer);
|
|
345
345
|
close();
|
|
346
|
-
|
|
346
|
+
resolve5(code);
|
|
347
347
|
},
|
|
348
348
|
reject: (err) => {
|
|
349
349
|
clearTimeout(timer);
|
|
@@ -583,23 +583,41 @@ Login failed: ${body.error}`);
|
|
|
583
583
|
}
|
|
584
584
|
die("\nLogin timed out \u2014 run `fillo login` again.");
|
|
585
585
|
}
|
|
586
|
-
async function
|
|
586
|
+
async function fetchWhoami(apiBase = API) {
|
|
587
587
|
const res = await api("/cli/whoami", { token: requireTokenFor(apiBase) }, apiBase);
|
|
588
588
|
if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
|
|
589
589
|
if (!res.ok) die(`whoami failed (${res.status}).`);
|
|
590
590
|
const me = await readJson(res);
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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;
|
|
595
600
|
}
|
|
596
601
|
function printConnected(workspace) {
|
|
597
602
|
console.log(` ${okMark()} Connected to ${bold(workspace)}`);
|
|
598
603
|
}
|
|
599
604
|
async function whoami(flags) {
|
|
600
|
-
const
|
|
601
|
-
if (jsonMode(flags))
|
|
602
|
-
|
|
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
|
+
}
|
|
603
621
|
}
|
|
604
622
|
function logout() {
|
|
605
623
|
const { token, tokenApi, ...rest } = readConfig();
|
|
@@ -624,6 +642,8 @@ var whoamiCommand = {
|
|
|
624
642
|
|
|
625
643
|
// src/commands/init.ts
|
|
626
644
|
import { execFile } from "child_process";
|
|
645
|
+
import { existsSync } from "fs";
|
|
646
|
+
import { basename, dirname, join as join2, resolve } from "path";
|
|
627
647
|
import { promisify } from "util";
|
|
628
648
|
|
|
629
649
|
// src/lib/prompt.ts
|
|
@@ -638,9 +658,9 @@ function promptInterface() {
|
|
|
638
658
|
shared = createInterface({ input: process.stdin, output: process.stdout });
|
|
639
659
|
shared.on("line", (line) => {
|
|
640
660
|
if (pending) {
|
|
641
|
-
const
|
|
661
|
+
const resolve5 = pending;
|
|
642
662
|
pending = null;
|
|
643
|
-
|
|
663
|
+
resolve5({ line });
|
|
644
664
|
} else {
|
|
645
665
|
lineQueue.push(line);
|
|
646
666
|
}
|
|
@@ -653,9 +673,9 @@ function promptInterface() {
|
|
|
653
673
|
sharedClosed = true;
|
|
654
674
|
shared = null;
|
|
655
675
|
if (pending) {
|
|
656
|
-
const
|
|
676
|
+
const resolve5 = pending;
|
|
657
677
|
pending = null;
|
|
658
|
-
|
|
678
|
+
resolve5({ eof: true });
|
|
659
679
|
}
|
|
660
680
|
});
|
|
661
681
|
}
|
|
@@ -677,10 +697,10 @@ function ask(promptText) {
|
|
|
677
697
|
if (!rl) return Promise.resolve({ eof: true });
|
|
678
698
|
rl.setPrompt(promptText);
|
|
679
699
|
rl.prompt();
|
|
680
|
-
return new Promise((
|
|
700
|
+
return new Promise((resolve5) => {
|
|
681
701
|
pending = (answer) => {
|
|
682
702
|
rl.setPrompt("");
|
|
683
|
-
|
|
703
|
+
resolve5(answer);
|
|
684
704
|
};
|
|
685
705
|
});
|
|
686
706
|
}
|
|
@@ -706,7 +726,7 @@ var ETX = String.fromCharCode(3);
|
|
|
706
726
|
var DEL = String.fromCharCode(127);
|
|
707
727
|
var BACKSPACE = "\b";
|
|
708
728
|
function readSecret(promptText) {
|
|
709
|
-
return new Promise((
|
|
729
|
+
return new Promise((resolve5, reject) => {
|
|
710
730
|
const input = process.stdin;
|
|
711
731
|
if (typeof input.setRawMode !== "function") {
|
|
712
732
|
reject(new Error("No interactive terminal to read a secret from."));
|
|
@@ -726,7 +746,7 @@ function readSecret(promptText) {
|
|
|
726
746
|
if (ENTER.has(ch) || ch === EOT) {
|
|
727
747
|
cleanup();
|
|
728
748
|
process.stdout.write("\n");
|
|
729
|
-
|
|
749
|
+
resolve5(value);
|
|
730
750
|
return;
|
|
731
751
|
}
|
|
732
752
|
if (ch === ETX) {
|
|
@@ -781,11 +801,17 @@ async function provisionWorkspace(flags, usageHint, apiBase = API) {
|
|
|
781
801
|
}
|
|
782
802
|
}
|
|
783
803
|
}
|
|
804
|
+
const workspaceName = flagString(flags, "workspace-name") ?? defaultWorkspaceName();
|
|
784
805
|
const res = await api(
|
|
785
806
|
"/workspaces/provision",
|
|
786
807
|
{
|
|
787
808
|
method: "POST",
|
|
788
|
-
body: JSON.stringify({
|
|
809
|
+
body: JSON.stringify({
|
|
810
|
+
email: email3,
|
|
811
|
+
source: "cli",
|
|
812
|
+
...name ? { name } : {},
|
|
813
|
+
...workspaceName ? { workspaceName } : {}
|
|
814
|
+
})
|
|
789
815
|
},
|
|
790
816
|
apiBase
|
|
791
817
|
);
|
|
@@ -800,7 +826,55 @@ async function provisionWorkspace(flags, usageHint, apiBase = API) {
|
|
|
800
826
|
...name ? { name } : {},
|
|
801
827
|
...claimToken ? { claimToken } : {}
|
|
802
828
|
});
|
|
803
|
-
|
|
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;
|
|
804
878
|
}
|
|
805
879
|
function previewLimitLine(limits) {
|
|
806
880
|
if (!limits) return null;
|
|
@@ -828,9 +902,23 @@ function claimCookieValue(res) {
|
|
|
828
902
|
}
|
|
829
903
|
async function init(flags) {
|
|
830
904
|
const json = jsonMode(flags);
|
|
831
|
-
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;
|
|
832
918
|
if (json) {
|
|
833
919
|
return emitResult({
|
|
920
|
+
existing: false,
|
|
921
|
+
workspace: ws.workspace,
|
|
834
922
|
pk: ws.pk,
|
|
835
923
|
email: ws.email,
|
|
836
924
|
name: ws.name || null,
|
|
@@ -839,7 +927,7 @@ async function init(flags) {
|
|
|
839
927
|
}
|
|
840
928
|
console.log(
|
|
841
929
|
`
|
|
842
|
-
${okMark()}
|
|
930
|
+
${okMark()} New workspace ${bold(terminalText(ws.workspace))} ready. A link was sent to ${bold(terminalText(ws.email))}.`
|
|
843
931
|
);
|
|
844
932
|
const preview = previewLimitLine(ws.limits);
|
|
845
933
|
if (preview) console.log(` ${dim(preview)}`);
|
|
@@ -850,7 +938,7 @@ async function init(flags) {
|
|
|
850
938
|
}
|
|
851
939
|
var initCommand = {
|
|
852
940
|
name: "init",
|
|
853
|
-
flags: ["email", "name"],
|
|
941
|
+
flags: ["email", "name", "workspace-name"],
|
|
854
942
|
run: (_args, flags) => init(flags)
|
|
855
943
|
};
|
|
856
944
|
|
|
@@ -858,7 +946,7 @@ var initCommand = {
|
|
|
858
946
|
import { randomUUID } from "crypto";
|
|
859
947
|
import {
|
|
860
948
|
cpSync,
|
|
861
|
-
existsSync,
|
|
949
|
+
existsSync as existsSync2,
|
|
862
950
|
lstatSync,
|
|
863
951
|
mkdirSync as mkdirSync2,
|
|
864
952
|
readdirSync,
|
|
@@ -868,7 +956,7 @@ import {
|
|
|
868
956
|
rmSync
|
|
869
957
|
} from "fs";
|
|
870
958
|
import { homedir as homedir2 } from "os";
|
|
871
|
-
import { dirname, isAbsolute, join as
|
|
959
|
+
import { dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
|
|
872
960
|
import { fileURLToPath } from "url";
|
|
873
961
|
var BUNDLED_SKILL_DIR = fileURLToPath(new URL("./skill/build-with-fillo", import.meta.url));
|
|
874
962
|
var SKILL_AGENT_DIRECTORIES = {
|
|
@@ -892,11 +980,11 @@ function isSkillAgent(value) {
|
|
|
892
980
|
return Object.prototype.hasOwnProperty.call(SKILL_AGENT_DIRECTORIES, value);
|
|
893
981
|
}
|
|
894
982
|
function findProjectRoot(start) {
|
|
895
|
-
let current =
|
|
983
|
+
let current = resolve2(start);
|
|
896
984
|
while (true) {
|
|
897
|
-
if (
|
|
898
|
-
const parent =
|
|
899
|
-
if (parent === current) return
|
|
985
|
+
if (existsSync2(join3(current, ".git"))) return current;
|
|
986
|
+
const parent = dirname2(current);
|
|
987
|
+
if (parent === current) return resolve2(start);
|
|
900
988
|
current = parent;
|
|
901
989
|
}
|
|
902
990
|
}
|
|
@@ -912,7 +1000,7 @@ function parseSkillDirectories(flags) {
|
|
|
912
1000
|
if (parts.length === 0 || parts.some((part) => part === "..")) {
|
|
913
1001
|
die("--dir must name a skill directory inside the selected project or home scope.");
|
|
914
1002
|
}
|
|
915
|
-
return [
|
|
1003
|
+
return [join3(...parts)];
|
|
916
1004
|
}
|
|
917
1005
|
if (!agent2) return [SKILL_AGENT_DIRECTORIES.shared, SKILL_AGENT_DIRECTORIES.claude];
|
|
918
1006
|
if (isSkillAgent(agent2)) return [SKILL_AGENT_DIRECTORIES[agent2]];
|
|
@@ -921,10 +1009,10 @@ function parseSkillDirectories(flags) {
|
|
|
921
1009
|
);
|
|
922
1010
|
}
|
|
923
1011
|
function skillFiles(root, relative2 = "") {
|
|
924
|
-
const directory =
|
|
1012
|
+
const directory = join3(root, relative2);
|
|
925
1013
|
const files = [];
|
|
926
1014
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
927
|
-
const next =
|
|
1015
|
+
const next = join3(relative2, entry.name);
|
|
928
1016
|
if (entry.isDirectory()) files.push(...skillFiles(root, next));
|
|
929
1017
|
else if (entry.isFile()) files.push(next);
|
|
930
1018
|
}
|
|
@@ -936,7 +1024,7 @@ function sameSkillBundle(source, target) {
|
|
|
936
1024
|
const targetFiles = skillFiles(target);
|
|
937
1025
|
if (sourceFiles.length !== targetFiles.length) return false;
|
|
938
1026
|
return sourceFiles.every(
|
|
939
|
-
(file, index) => file === targetFiles[index] && readFileSync2(
|
|
1027
|
+
(file, index) => file === targetFiles[index] && readFileSync2(join3(source, file)).equals(readFileSync2(join3(target, file)))
|
|
940
1028
|
);
|
|
941
1029
|
} catch {
|
|
942
1030
|
return false;
|
|
@@ -954,7 +1042,7 @@ function pathEntryExists(path) {
|
|
|
954
1042
|
var MANAGED_SKILL_MARKER = ".fillo-managed.json";
|
|
955
1043
|
function isCliManagedFilloSkill(target) {
|
|
956
1044
|
try {
|
|
957
|
-
const marker = JSON.parse(readFileSync2(
|
|
1045
|
+
const marker = JSON.parse(readFileSync2(join3(target, MANAGED_SKILL_MARKER), "utf8"));
|
|
958
1046
|
return marker.managedBy === "@usefillo/cli" && marker.skill === "build-with-fillo" && marker.format === 1 && marker.allowAutomaticUpdates === true;
|
|
959
1047
|
} catch {
|
|
960
1048
|
return false;
|
|
@@ -964,7 +1052,7 @@ function assertSafeSkillDestination(scopeRoot, parts) {
|
|
|
964
1052
|
const realScopeRoot = realpathSync(scopeRoot);
|
|
965
1053
|
let current = scopeRoot;
|
|
966
1054
|
for (const part of parts) {
|
|
967
|
-
current =
|
|
1055
|
+
current = join3(current, part);
|
|
968
1056
|
if (!pathEntryExists(current)) return;
|
|
969
1057
|
if (lstatSync(current).isSymbolicLink()) {
|
|
970
1058
|
throw new Error(`Refusing to install through symlinked path ${current}.`);
|
|
@@ -1006,12 +1094,12 @@ function installSkill(flags, opts = {}) {
|
|
|
1006
1094
|
const skillDirectories = parseSkillDirectories(flags);
|
|
1007
1095
|
const global = flags.global === true;
|
|
1008
1096
|
const scopeRoot = global ? homedir2() : findProjectRoot(process.cwd());
|
|
1009
|
-
if (!
|
|
1097
|
+
if (!existsSync2(BUNDLED_SKILL_DIR)) {
|
|
1010
1098
|
die("This CLI package is missing its bundled Fillo skill. Reinstall @usefillo/cli@latest.");
|
|
1011
1099
|
}
|
|
1012
1100
|
const destinations = Array.from(new Set(skillDirectories)).map((skillDirectory) => {
|
|
1013
|
-
const skillsRoot =
|
|
1014
|
-
const target =
|
|
1101
|
+
const skillsRoot = resolve2(scopeRoot, skillDirectory);
|
|
1102
|
+
const target = join3(skillsRoot, "build-with-fillo");
|
|
1015
1103
|
const relativeSkillsRoot = relative(scopeRoot, skillsRoot);
|
|
1016
1104
|
if (relativeSkillsRoot === ".." || relativeSkillsRoot.startsWith(`..${sep}`) || isAbsolute(relativeSkillsRoot)) {
|
|
1017
1105
|
die("The skill destination must stay inside the selected project or home scope.");
|
|
@@ -1037,7 +1125,7 @@ function installSkill(flags, opts = {}) {
|
|
|
1037
1125
|
if (destination.current) continue;
|
|
1038
1126
|
mkdirSync2(destination.skillsRoot, { recursive: true });
|
|
1039
1127
|
assertSafeSkillDestination(scopeRoot, destination.destinationParts.slice(0, 2));
|
|
1040
|
-
const staging =
|
|
1128
|
+
const staging = join3(destination.skillsRoot, `.build-with-fillo.install-${randomUUID()}`);
|
|
1041
1129
|
try {
|
|
1042
1130
|
cpSync(BUNDLED_SKILL_DIR, staging, { recursive: true, errorOnExist: true });
|
|
1043
1131
|
assertSafeSkillDestination(scopeRoot, destination.destinationParts);
|
|
@@ -1056,10 +1144,15 @@ function installSkill(flags, opts = {}) {
|
|
|
1056
1144
|
console.log(`
|
|
1057
1145
|
${okMark()} ${action}.`);
|
|
1058
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
|
+
}
|
|
1059
1152
|
console.log(`
|
|
1060
1153
|
Ask your agent to use ${bold("build-with-fillo")}.`);
|
|
1061
1154
|
console.log(
|
|
1062
|
-
` ${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")}.`)}
|
|
1063
1156
|
`
|
|
1064
1157
|
);
|
|
1065
1158
|
return targets;
|
|
@@ -1085,7 +1178,8 @@ function skillHelp() {
|
|
|
1085
1178
|
${dimRaw("--global install for the current user")}
|
|
1086
1179
|
${dimRaw("--force replace a different existing copy")}
|
|
1087
1180
|
|
|
1088
|
-
${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.")}
|
|
1089
1183
|
`);
|
|
1090
1184
|
}
|
|
1091
1185
|
var skillCommand = {
|
|
@@ -1104,30 +1198,40 @@ async function bootstrapStandalone(flags, apiBase) {
|
|
|
1104
1198
|
);
|
|
1105
1199
|
}
|
|
1106
1200
|
const json = jsonMode(flags);
|
|
1107
|
-
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
|
+
}
|
|
1108
1206
|
const targets = installSkill(flags, { quiet: json });
|
|
1109
1207
|
if (json) {
|
|
1110
|
-
return emitResult(
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
+
);
|
|
1117
1219
|
}
|
|
1118
1220
|
console.log(
|
|
1119
1221
|
`
|
|
1120
|
-
${bold("
|
|
1222
|
+
${bold("Next:")} tell your agent what form to build${identity.existing ? ` in ${bold(terminalText(identity.workspace))}` : ""}, or push one directly:`
|
|
1121
1223
|
);
|
|
1224
|
+
console.log(` ${dim("fillo push form.json --handle my-form")}
|
|
1225
|
+
`);
|
|
1226
|
+
}
|
|
1227
|
+
function printNewWorkspace(ws) {
|
|
1228
|
+
console.log(`
|
|
1229
|
+
${okMark()} New workspace ${bold(terminalText(ws.workspace))} provisioned.`);
|
|
1122
1230
|
console.log(
|
|
1123
1231
|
` ${dim(`A workspace link was sent to ${terminalText(ws.email)} \u2014 claim it anytime with \`fillo claim\`.`)}`
|
|
1124
1232
|
);
|
|
1125
1233
|
const preview = previewLimitLine(ws.limits);
|
|
1126
1234
|
if (preview) console.log(` ${dim(preview)}`);
|
|
1127
|
-
console.log(`
|
|
1128
|
-
${bold("Next:")} tell your agent what form to build, or push one directly:`);
|
|
1129
|
-
console.log(` ${dim("fillo push form.json --handle my-form")}
|
|
1130
|
-
`);
|
|
1131
1235
|
}
|
|
1132
1236
|
var AGENT_ACTIONS = ["claim_required", "storage_required", "publish_required"];
|
|
1133
1237
|
var AGENT_EVENT_STATUSES = [
|
|
@@ -1269,9 +1373,11 @@ function agentHelp() {
|
|
|
1269
1373
|
agent bootstrap Set up a workspace and install the Build with Fillo skill
|
|
1270
1374
|
${dimRaw("--email you@company.com provision a fresh workspace (a TTY confirms your git identity)")}
|
|
1271
1375
|
${dimRaw('--name "Your Name" optional display name')}
|
|
1376
|
+
${dimRaw("--workspace-name <name> name the new workspace (default: this repo/directory)")}
|
|
1272
1377
|
${dimRaw("--api <url> target a different Fillo deployment (default: FILLO_API or https://fillo.so)")}
|
|
1273
1378
|
${dimRaw("--run <id> --token <token> instead: attach to an open browser modal")}
|
|
1274
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.")}
|
|
1275
1381
|
agent connect Connect a coding-agent run to the open browser modal
|
|
1276
1382
|
${dimRaw("--account attach the workspace from `fillo login`")}
|
|
1277
1383
|
agent event Send a short progress update
|
|
@@ -1293,6 +1399,7 @@ var agentCommand = {
|
|
|
1293
1399
|
flags: [
|
|
1294
1400
|
"email",
|
|
1295
1401
|
"name",
|
|
1402
|
+
"workspace-name",
|
|
1296
1403
|
"run",
|
|
1297
1404
|
"token",
|
|
1298
1405
|
"api",
|
|
@@ -1734,7 +1841,7 @@ var deleteCommand = {
|
|
|
1734
1841
|
|
|
1735
1842
|
// src/commands/forms.ts
|
|
1736
1843
|
import { readFileSync as readFileSync3 } from "fs";
|
|
1737
|
-
import { isAbsolute as isAbsolute2, resolve as
|
|
1844
|
+
import { isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
|
|
1738
1845
|
import { pathToFileURL } from "url";
|
|
1739
1846
|
|
|
1740
1847
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
|
|
@@ -4166,7 +4273,7 @@ async function loadResponseData(file) {
|
|
|
4166
4273
|
die(`Couldn't read response data from stdin: ${error.message}`);
|
|
4167
4274
|
}
|
|
4168
4275
|
} else {
|
|
4169
|
-
const abs = isAbsolute2(file) ? file :
|
|
4276
|
+
const abs = isAbsolute2(file) ? file : resolve3(process.cwd(), file);
|
|
4170
4277
|
if (!abs.endsWith(".json")) {
|
|
4171
4278
|
die("Test response data must be a .json file (or - for JSON on stdin).");
|
|
4172
4279
|
}
|
|
@@ -4245,7 +4352,7 @@ async function loadSchema(file, allowCode) {
|
|
|
4245
4352
|
die(`Couldn't read the schema from stdin: ${error.message}`);
|
|
4246
4353
|
}
|
|
4247
4354
|
}
|
|
4248
|
-
const abs = isAbsolute2(file) ? file :
|
|
4355
|
+
const abs = isAbsolute2(file) ? file : resolve3(process.cwd(), file);
|
|
4249
4356
|
if (!abs.endsWith(".json") && !allowCode) {
|
|
4250
4357
|
die(
|
|
4251
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.`
|
|
@@ -4286,7 +4393,7 @@ function toPushItems(value, handle) {
|
|
|
4286
4393
|
return { handle, schema: item };
|
|
4287
4394
|
});
|
|
4288
4395
|
}
|
|
4289
|
-
function printPushed(formId, url, updated, draft = false) {
|
|
4396
|
+
function printPushed(formId, url, updated, draft = false, storage2 = {}) {
|
|
4290
4397
|
const label = draft ? "Created draft" : updated ? "Updated" : "Created";
|
|
4291
4398
|
console.log(`
|
|
4292
4399
|
${okMark()} ${label} ${bold(formId)}`);
|
|
@@ -4295,6 +4402,8 @@ function printPushed(formId, url, updated, draft = false) {
|
|
|
4295
4402
|
` ${bold("Publish:")} fillo publish ${terminalText(formId)} ${dim("(or publish it from the Fillo dashboard)")}`
|
|
4296
4403
|
);
|
|
4297
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)}`);
|
|
4298
4407
|
console.log(` Embed: <FilloForm formId="${terminalText(formId)}" />
|
|
4299
4408
|
`);
|
|
4300
4409
|
console.log(` Verify: [data-fillo-form-id="${terminalText(formId)}"] on the actual app route
|
|
@@ -4437,7 +4546,10 @@ async function push(file, flags) {
|
|
|
4437
4546
|
results.push(body);
|
|
4438
4547
|
emitProgress({ status: "pushed", formId: body.formId, draft: true });
|
|
4439
4548
|
} else {
|
|
4440
|
-
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
|
+
});
|
|
4441
4553
|
}
|
|
4442
4554
|
}
|
|
4443
4555
|
if (results) emitResult({ forms: results });
|
|
@@ -4470,7 +4582,10 @@ async function push(file, flags) {
|
|
|
4470
4582
|
results.push(body);
|
|
4471
4583
|
emitProgress({ status: "pushed", formId: body.formId, published: true });
|
|
4472
4584
|
} else {
|
|
4473
|
-
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
|
+
});
|
|
4474
4589
|
}
|
|
4475
4590
|
}
|
|
4476
4591
|
if (results) emitResult({ forms: results });
|
|
@@ -4786,7 +4901,7 @@ var membersCommand = {
|
|
|
4786
4901
|
// src/commands/responses.ts
|
|
4787
4902
|
import { once } from "events";
|
|
4788
4903
|
import { createWriteStream } from "fs";
|
|
4789
|
-
import { isAbsolute as isAbsolute3, resolve as
|
|
4904
|
+
import { isAbsolute as isAbsolute3, resolve as resolve4 } from "path";
|
|
4790
4905
|
var dateOnly4 = (iso) => iso.slice(0, 10);
|
|
4791
4906
|
var dateTime = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
4792
4907
|
async function dieOnNotFound(res, handle, verb) {
|
|
@@ -4868,7 +4983,7 @@ async function exportCsv(handle, flags) {
|
|
|
4868
4983
|
const body = await readJson(res);
|
|
4869
4984
|
die(body.error ?? `responses export failed (${res.status}).`);
|
|
4870
4985
|
}
|
|
4871
|
-
const path = out ? isAbsolute3(out) ? out :
|
|
4986
|
+
const path = out ? isAbsolute3(out) ? out : resolve4(process.cwd(), out) : void 0;
|
|
4872
4987
|
const file = path ? createWriteStream(path) : void 0;
|
|
4873
4988
|
const target = file ?? process.stdout;
|
|
4874
4989
|
let bytes = 0;
|
|
@@ -5330,6 +5445,13 @@ async function status3(flags) {
|
|
|
5330
5445
|
const target = body.defaultStorageProvider;
|
|
5331
5446
|
const targetLabel = target && target in PROVIDER_LABEL ? PROVIDER_LABEL[target] : target ?? "no durable storage yet";
|
|
5332
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
|
+
}
|
|
5333
5455
|
console.log("");
|
|
5334
5456
|
}
|
|
5335
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
|