@usefillo/cli 0.1.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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +257 -0
  3. package/package.json +31 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fillo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js ADDED
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { isAbsolute, join, resolve } from "path";
7
+ import { spawn } from "child_process";
8
+ import { pathToFileURL } from "url";
9
+ var API = (process.env.FILLO_API ?? "https://fillo.so").replace(/\/$/, "");
10
+ var CONFIG_DIR = join(homedir(), ".fillo");
11
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
12
+ var bold = (s) => `\x1B[1m${s}\x1B[0m`;
13
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
14
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
15
+ function die(msg) {
16
+ console.error(`\x1B[31m\u2717\x1B[0m ${msg}`);
17
+ process.exit(1);
18
+ }
19
+ function readConfig() {
20
+ try {
21
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+ function writeConfig(c) {
27
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
28
+ writeFileSync(CONFIG_PATH, JSON.stringify(c, null, 2), { mode: 384 });
29
+ try {
30
+ chmodSync(CONFIG_PATH, 384);
31
+ } catch {
32
+ }
33
+ }
34
+ function requireToken() {
35
+ const t = readConfig().token;
36
+ if (!t) die("Not logged in. Run `fillo login` first.");
37
+ return t;
38
+ }
39
+ function openBrowser(url) {
40
+ if (!/^https?:\/\//i.test(url)) return;
41
+ try {
42
+ if (process.platform === "win32") {
43
+ spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
44
+ } else {
45
+ const cmd = process.platform === "darwin" ? "open" : "xdg-open";
46
+ spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
47
+ }
48
+ } catch {
49
+ }
50
+ }
51
+ async function api(path, init2 = {}) {
52
+ const { token, ...rest } = init2;
53
+ return fetch(`${API}/api/v1${path}`, {
54
+ ...rest,
55
+ headers: {
56
+ "Content-Type": "application/json",
57
+ ...token ? { Authorization: `Bearer ${token}` } : {},
58
+ ...rest.headers
59
+ }
60
+ });
61
+ }
62
+ async function login() {
63
+ const res = await api("/device/code", { method: "POST" });
64
+ if (!res.ok) die(`Couldn't start login (${res.status}).`);
65
+ const d = await res.json();
66
+ console.log(`
67
+ Your code: ${bold(d.user_code)}`);
68
+ console.log(` Approve it at ${d.verification_uri} \u2014 opening your browser\u2026
69
+ `);
70
+ openBrowser(d.verification_uri_complete);
71
+ const deadline = Date.now() + d.expires_in * 1e3;
72
+ let interval = d.interval * 1e3;
73
+ process.stdout.write(" Waiting for approval");
74
+ while (Date.now() < deadline) {
75
+ await sleep(interval);
76
+ process.stdout.write(".");
77
+ const r = await api("/device/token", {
78
+ method: "POST",
79
+ body: JSON.stringify({ device_code: d.device_code })
80
+ });
81
+ const body = await r.json();
82
+ if (body.access_token) {
83
+ writeConfig({ token: body.access_token });
84
+ console.log("\n");
85
+ await whoami();
86
+ console.log(`
87
+ ${dim("Now run:")} fillo push form.json
88
+ `);
89
+ return;
90
+ }
91
+ if (body.error === "slow_down") interval += 2e3;
92
+ else if (body.error && body.error !== "authorization_pending") die(`
93
+ Login failed: ${body.error}`);
94
+ }
95
+ die("\nLogin timed out \u2014 run `fillo login` again.");
96
+ }
97
+ async function whoami() {
98
+ const res = await api("/cli/whoami", { token: requireToken() });
99
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
100
+ if (!res.ok) die(`whoami failed (${res.status}).`);
101
+ const me = await res.json();
102
+ console.log(` \x1B[32m\u2713\x1B[0m ${bold(me.email)} ${dim("\xB7")} ${me.workspace}`);
103
+ }
104
+ async function list() {
105
+ const res = await api("/cli/forms", { token: requireToken() });
106
+ if (!res.ok) die(`list failed (${res.status}).`);
107
+ const { forms } = await res.json();
108
+ if (!forms.length) return console.log(" No forms yet.");
109
+ for (const f of forms) {
110
+ console.log(` ${f.status === "published" ? "\x1B[32m\u25CF\x1B[0m" : "\u25CB"} ${f.name} ${dim(f.id)}`);
111
+ }
112
+ }
113
+ async function loadSchema(file, allowCode) {
114
+ const abs = isAbsolute(file) ? file : resolve(process.cwd(), file);
115
+ if (!abs.endsWith(".json") && !allowCode) {
116
+ die(
117
+ `${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.`
118
+ );
119
+ }
120
+ try {
121
+ if (abs.endsWith(".json")) return JSON.parse(readFileSync(abs, "utf8"));
122
+ const mod = await import(pathToFileURL(abs).href);
123
+ return mod.default ?? mod.schema ?? mod.form;
124
+ } catch (e) {
125
+ die(`Couldn't read ${file}: ${e.message}`);
126
+ }
127
+ }
128
+ function printPushed(formId, url, updated, staged) {
129
+ console.log(`
130
+ \x1B[32m\u2713\x1B[0m ${updated ? "Updated" : "Created"} ${bold(formId)}`);
131
+ if (staged) console.log(` ${dim("Staged as a draft \u2014 publish it from the dashboard.")}`);
132
+ else if (url) console.log(` Live at ${url}`);
133
+ console.log(` Embed: <FilloForm formId="${formId}" />
134
+ `);
135
+ }
136
+ async function push(file, flags) {
137
+ if (!file) die("Usage: fillo push <form.json> [--handle name] [--draft] [--allow-code]");
138
+ const schema = await loadSchema(file, flags["allow-code"] === true);
139
+ const handle = typeof flags.handle === "string" ? flags.handle : void 0;
140
+ const cfg = readConfig();
141
+ if (cfg.token) {
142
+ const res = await api("/cli/forms", {
143
+ method: "POST",
144
+ token: cfg.token,
145
+ body: JSON.stringify({ schema, handle, publish: !flags.draft })
146
+ });
147
+ const body = await res.json();
148
+ if (!res.ok || !body.formId) die(body.error ?? `push failed (${res.status}).`);
149
+ return printPushed(body.formId, body.url, !!body.updated, !!flags.draft);
150
+ }
151
+ if (cfg.pk) {
152
+ if (!handle) die("Pushing with a provisioned key needs --handle <name>.");
153
+ const res = await api("/forms/sync", {
154
+ method: "POST",
155
+ body: JSON.stringify({ key: cfg.pk, id: handle, schema })
156
+ });
157
+ const body = await res.json();
158
+ if (!res.ok || !body.formId) die(body.error ?? `push failed (${res.status}).`);
159
+ return printPushed(body.formId, body.slug ? `${API}/f/${body.slug}` : void 0, false, !!body.staged);
160
+ }
161
+ die("Not set up yet. Run `fillo init` (no account needed) or `fillo login`.");
162
+ }
163
+ async function init(flags) {
164
+ const email = typeof flags.email === "string" ? flags.email : void 0;
165
+ const res = await api("/workspaces/provision", {
166
+ method: "POST",
167
+ body: JSON.stringify({ email, source: "cli" })
168
+ });
169
+ const body = await res.json();
170
+ if (!res.ok || !body.key) die(body.error ?? `init failed (${res.status}).`);
171
+ writeConfig({ ...readConfig(), pk: body.key, claimUrl: body.claim?.url ?? void 0 });
172
+ console.log(`
173
+ \x1B[32m\u2713\x1B[0m Workspace ready \u2014 no signup needed.`);
174
+ if (body.limits) {
175
+ const days = Math.max(1, Math.round((Date.parse(body.limits.expiresAt) - Date.now()) / 864e5));
176
+ console.log(` ${dim(`Preview: up to ${body.limits.responses} responses for ~${days} days.`)}`);
177
+ }
178
+ console.log(`
179
+ ${bold("Next:")} fillo push form.json --handle my-form`);
180
+ if (body.claim?.url) {
181
+ console.log(`
182
+ ${dim("Keep your responses \u2014 claim it (signup, one click):")}`);
183
+ console.log(` ${body.claim.url}
184
+ `);
185
+ }
186
+ }
187
+ function help() {
188
+ console.log(`
189
+ ${bold("fillo")} \u2014 create Fillo forms from your terminal
190
+
191
+ ${bold("Commands")}
192
+ init Provision a workspace with NO signup \u2014 instant, for first runs
193
+ ${dim("--email <addr> optional: also mail a claim link as backup")}
194
+ login Connect to an existing account (opens the browser)
195
+ logout Forget the stored credentials
196
+ whoami Show the signed-in account + workspace
197
+ push <file> Create/update a form from a JSON schema
198
+ ${dim("--handle <name> idempotent id for re-pushes")}
199
+ ${dim("--draft stage as a draft instead of publishing")}
200
+ ${dim("--allow-code allow a .mjs/.js schema (executes the file)")}
201
+ list List the workspace's forms
202
+
203
+ ${dim(`API: ${API} \xB7 set FILLO_API to override`)}
204
+ `);
205
+ }
206
+ function parseFlags(args) {
207
+ const flags = {};
208
+ const positional = [];
209
+ for (let i = 0; i < args.length; i++) {
210
+ const a = args[i];
211
+ if (!a.startsWith("--")) {
212
+ positional.push(a);
213
+ continue;
214
+ }
215
+ const eq = a.indexOf("=");
216
+ if (eq !== -1) {
217
+ flags[a.slice(2, eq)] = a.slice(eq + 1);
218
+ continue;
219
+ }
220
+ const key = a.slice(2);
221
+ const next = args[i + 1];
222
+ if (next !== void 0 && !next.startsWith("--")) {
223
+ flags[key] = next;
224
+ i++;
225
+ } else flags[key] = true;
226
+ }
227
+ return { positional, flags };
228
+ }
229
+ async function main() {
230
+ const [cmd, ...rest] = process.argv.slice(2);
231
+ const { positional, flags } = parseFlags(rest);
232
+ switch (cmd) {
233
+ case "init":
234
+ return init(flags);
235
+ case "login":
236
+ return login();
237
+ case "logout":
238
+ writeConfig({});
239
+ return console.log(" Logged out.");
240
+ case "whoami":
241
+ return whoami();
242
+ case "push":
243
+ return push(positional[0], flags);
244
+ case "list":
245
+ case "ls":
246
+ return list();
247
+ case void 0:
248
+ case "help":
249
+ case "--help":
250
+ case "-h":
251
+ return help();
252
+ default:
253
+ console.error(`Unknown command: ${cmd}`);
254
+ return help();
255
+ }
256
+ }
257
+ main().catch((e) => die(e instanceof Error ? e.message : String(e)));
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@usefillo/cli",
3
+ "version": "0.1.0",
4
+ "description": "Create and publish Fillo forms from your terminal — auth once, then your coding agent does the rest.",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "forms",
8
+ "cli",
9
+ "fillo"
10
+ ],
11
+ "homepage": "https://fillo.so",
12
+ "type": "module",
13
+ "bin": {
14
+ "fillo": "./dist/index.js"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.10.0",
24
+ "tsup": "^8.4.0",
25
+ "typescript": "^5.8.3"
26
+ },
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "typecheck": "tsc --noEmit"
30
+ }
31
+ }