@1dolinski/fastforms 0.1.2 → 0.2.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 CHANGED
@@ -1,25 +1,29 @@
1
1
  # fastforms
2
2
 
3
- Fill any form fast. Manage personas locally, fill forms from your terminal.
3
+ Fill any form fast. Manage multiple personas locally, pick the right one at fill time.
4
4
 
5
5
  ## Quick start
6
6
 
7
7
  ```bash
8
- # 1. Create your personas
8
+ # 1. Create your first user + business persona
9
9
  npx @1dolinski/fastforms init
10
10
 
11
- # 2. Enable remote debugging in Chrome
11
+ # 2. Add more personas
12
+ npx @1dolinski/fastforms add user
13
+ npx @1dolinski/fastforms add business
14
+
15
+ # 3. Enable remote debugging in Chrome
12
16
  # Open chrome://inspect/#remote-debugging and toggle it on
13
17
 
14
- # 3. Fill any form (never submits)
18
+ # 4. Fill any form select which personas to use
15
19
  npx @1dolinski/fastforms fill https://example.com/apply
16
20
  ```
17
21
 
18
22
  ## How it works
19
23
 
20
24
  1. **`fastforms init`** walks you through creating user + business personas interactively
21
- 2. Personas are saved as simple JSON in `.fastforms/user.json` and `.fastforms/business.json`
22
- 3. **`fastforms fill <url>`** connects to Chrome, opens the form, fills it by label matching
25
+ 2. Personas are saved as individual JSON files in `.fastforms/users/` and `.fastforms/businesses/`
26
+ 3. **`fastforms fill <url>`** connects to Chrome, lets you pick personas, fills by label matching
23
27
  4. **Review and submit manually** in Chrome
24
28
 
25
29
  ## Requirements
@@ -31,31 +35,39 @@ npx @1dolinski/fastforms fill https://example.com/apply
31
35
 
32
36
  | Command | Description |
33
37
  |---|---|
34
- | `fastforms init` | Create personas interactively |
35
- | `fastforms fill <url>` | Fill any form |
36
- | `fastforms edit` | Update existing personas |
38
+ | `fastforms init` | Create your first user + business persona |
39
+ | `fastforms add user` | Add another user persona |
40
+ | `fastforms add business` | Add another business persona |
41
+ | `fastforms list` | Show all saved personas |
42
+ | `fastforms fill <url>` | Fill any form (pick from personas) |
43
+ | `fastforms edit` | Edit an existing persona |
44
+ | `fastforms remove` | Remove a persona |
37
45
  | `fastforms personas` | Open web persona manager in Chrome |
38
46
 
39
47
  ### Fill options
40
48
 
41
49
  | Option | Description |
42
50
  |---|---|
51
+ | `--user <hint>` | Pre-select user persona by name |
52
+ | `--business <hint>` | Pre-select business persona by name |
43
53
  | `--web` | Use web app personas instead of local files |
44
54
  | `--dir <path>` | Custom persona directory path |
45
55
  | `--port <port>` | Chrome debug port (auto-detected) |
46
- | `--user <hint>` | User persona hint (web mode) |
47
- | `--business <hint>` | Business persona hint (web mode) |
48
56
 
49
57
  ## `.fastforms/` directory
50
58
 
51
59
  ```
52
60
  .fastforms/
53
- user.json # Your user persona
54
- business.json # Your business persona
55
- dumps/ # Optional: raw text context files
61
+ users/
62
+ chris.json # A user persona
63
+ work-chris.json # Another user persona
64
+ businesses/
65
+ apinow.json # A business persona
66
+ sideproject.json # Another business persona
67
+ defaults.json # Remembers your last selection
56
68
  ```
57
69
 
58
- `user.json` — just fill in what you have:
70
+ Each user JSON file — just fill in what you have:
59
71
 
60
72
  ```json
61
73
  {
@@ -74,7 +86,7 @@ npx @1dolinski/fastforms fill https://example.com/apply
74
86
  }
75
87
  ```
76
88
 
77
- `business.json`:
89
+ Each business JSON file:
78
90
 
79
91
  ```json
80
92
  {
package/SKILL.md CHANGED
@@ -11,25 +11,31 @@ Use this skill when the user says any of:
11
11
  - "fill this form with my persona"
12
12
  - "use fastforms", "fastforms fill"
13
13
  - "set up my personas", "init fastforms"
14
+ - "add a persona", "add another persona"
14
15
 
15
16
  ## What it does
16
17
 
17
18
  `fastforms` is a CLI tool that:
18
19
 
19
- 1. Manages personas locally in a `.fastforms/` directory (simple JSON files)
20
+ 1. Manages multiple personas locally in `.fastforms/users/` and `.fastforms/businesses/` directories
20
21
  2. Connects to Chrome via the DevTools Protocol
21
- 3. Fills any form using label-matching never submits
22
+ 3. Lets you pick which user + business persona to use at fill time
23
+ 4. Fills any form using label-matching — never submits
22
24
 
23
25
  ## Quick start
24
26
 
25
27
  ```bash
26
- # 1. Create your personas interactively
28
+ # 1. Create your first user + business persona
27
29
  npx @1dolinski/fastforms init
28
30
 
29
- # 2. Enable remote debugging in Chrome
31
+ # 2. Add more personas
32
+ npx @1dolinski/fastforms add user
33
+ npx @1dolinski/fastforms add business
34
+
35
+ # 3. Enable remote debugging in Chrome
30
36
  # Open chrome://inspect/#remote-debugging
31
37
 
32
- # 3. Fill any form
38
+ # 4. Fill any form — pick from your personas
33
39
  npx @1dolinski/fastforms fill https://example.com/apply
34
40
  ```
35
41
 
@@ -37,20 +43,34 @@ npx @1dolinski/fastforms fill https://example.com/apply
37
43
 
38
44
  ### `npx @1dolinski/fastforms init`
39
45
 
40
- Conversational persona builder. Walks through user + business persona fields, saves to `.fastforms/user.json` and `.fastforms/business.json`.
46
+ Walks through creating a user + business persona. Saves to `.fastforms/users/<name>.json` and `.fastforms/businesses/<name>.json`.
47
+
48
+ ### `npx @1dolinski/fastforms add user|business`
49
+
50
+ Add another user or business persona. Run as many times as you want.
51
+
52
+ ### `npx @1dolinski/fastforms list`
53
+
54
+ Show all saved personas.
41
55
 
42
56
  ### `npx @1dolinski/fastforms fill <url>`
43
57
 
44
- Fills any form. Reads personas from local `.fastforms/` directory by default.
58
+ Fills any form. If you have multiple personas, prompts you to pick which ones to use.
45
59
 
46
60
  Options:
61
+ - `--user <hint>` — pre-select a user persona by name
62
+ - `--business <hint>` — pre-select a business persona by name
47
63
  - `--web` — use web app personas (https://293-fastforms.vercel.app) instead of local files
48
64
  - `--dir <path>` — custom path to persona directory
49
65
  - `--port <port>` — Chrome debug port (auto-detected by default)
50
66
 
51
67
  ### `npx @1dolinski/fastforms edit`
52
68
 
53
- Re-run the persona builder with current values pre-filled.
69
+ Pick a persona to edit interactively.
70
+
71
+ ### `npx @1dolinski/fastforms remove`
72
+
73
+ Pick a persona to delete.
54
74
 
55
75
  ### `npx @1dolinski/fastforms personas`
56
76
 
@@ -63,11 +83,12 @@ Opens the web persona manager in Chrome.
63
83
 
64
84
  ## How it works
65
85
 
66
- 1. Reads personas from `.fastforms/user.json` and `.fastforms/business.json`
67
- 2. Auto-discovers Chrome's debug port from `DevToolsActivePort`
68
- 3. Opens (or reuses) the target form URL tab
69
- 4. Fills using label-matching heuristics. Has site-specific mappings for known forms.
70
- 5. Shows what was filled, what was skipped. **Never submits.**
86
+ 1. Reads all personas from `.fastforms/users/` and `.fastforms/businesses/`
87
+ 2. If multiple, prompts you to pick which user + business persona to use
88
+ 3. Auto-discovers Chrome's debug port from `DevToolsActivePort`
89
+ 4. Opens (or reuses) the target form URL tab
90
+ 5. Fills using label-matching heuristics. Has site-specific mappings for known forms.
91
+ 6. Shows what was filled, what was skipped. **Never submits.**
71
92
 
72
93
  ## Agent instructions
73
94
 
@@ -77,3 +98,11 @@ When the user asks you to fill a form:
77
98
  2. Run `npx @1dolinski/fastforms fill <the-url>`
78
99
  3. If Chrome debugging isn't enabled, tell the user to open `chrome://inspect/#remote-debugging`
79
100
  4. After filling, tell the user to review in Chrome and submit manually
101
+
102
+ When the user wants to add a persona:
103
+
104
+ 1. Run `npx @1dolinski/fastforms add user` or `npx @1dolinski/fastforms add business`
105
+
106
+ When the user wants to see their personas:
107
+
108
+ 1. Run `npx @1dolinski/fastforms list`
package/bin/fastforms.js CHANGED
@@ -21,6 +21,10 @@ import {
21
21
  loadLocalPersonas,
22
22
  saveUserPersona,
23
23
  saveBusinessPersona,
24
+ deletePersonaFile,
25
+ listPersonaFiles,
26
+ loadDefaults,
27
+ saveDefaults,
24
28
  userTemplate,
25
29
  businessTemplate,
26
30
  } from "../lib/local.js";
@@ -52,215 +56,315 @@ function ask(prompt, fallback = "") {
52
56
  return new Promise((r) => getRL().question(prompt, (a) => r(a.trim() || fallback)));
53
57
  }
54
58
 
59
+ function resolveDir() {
60
+ const dirArg = flag("--dir");
61
+ return dirArg || findFastformsDir() || join(process.cwd(), ".fastforms");
62
+ }
63
+
55
64
  function help() {
56
65
  console.log(`
57
- fastforms — Fill any form fast.
66
+ fastforms — Fill any form fast, with multiple personas.
58
67
 
59
68
  Usage:
60
- fastforms init Set up your personas interactively
61
- fastforms fill <url> Fill a form with your personas
62
- fastforms edit Edit your existing personas
69
+ fastforms init Create your first user + business persona
70
+ fastforms add user Add another user persona
71
+ fastforms add business Add another business persona
72
+ fastforms list List all personas
73
+ fastforms edit Edit an existing persona
74
+ fastforms remove Remove a persona
75
+ fastforms fill <url> Fill a form (pick personas interactively)
63
76
  fastforms personas Open web persona manager in Chrome
64
77
  fastforms Show this help
65
78
 
66
79
  Options:
67
80
  --web Use web app personas instead of local .fastforms/
68
81
  --dir <path> Path to .fastforms/ directory (default: auto-detect)
69
- --user <hint> User persona name/hint (web mode)
70
- --business <hint> Business persona name/hint (web mode)
82
+ --user <hint> User persona name/hint to pre-select
83
+ --business <hint> Business persona name/hint to pre-select
71
84
  --port <port> Chrome debug port (auto-detected by default)
72
85
 
73
86
  Quick start:
74
- 1. npx fastforms init
75
- 2. Enable remote debugging: chrome://inspect/#remote-debugging
76
- 3. npx fastforms fill https://example.com/apply
87
+ 1. npx @1dolinski/fastforms init
88
+ 2. npx @1dolinski/fastforms add user # add more personas
89
+ 3. Enable remote debugging: chrome://inspect/#remote-debugging
90
+ 4. npx @1dolinski/fastforms fill https://example.com/apply
77
91
  `);
78
92
  }
79
93
 
80
94
  // ---------------------------------------------------------------------------
81
- // init — conversational persona builder
95
+ // Shared persona builder prompts
82
96
  // ---------------------------------------------------------------------------
83
97
 
84
- async function init() {
85
- const dirArg = flag("--dir");
86
- const dir = dirArg || join(process.cwd(), ".fastforms");
98
+ async function promptUser(existing) {
99
+ const ep = existing?.profile || {};
100
+ const u = userTemplate();
87
101
 
88
- console.log("\n fastforms Let's set up your personas.\n");
102
+ const show = (val) => val ? ` [${String(val).slice(0, 40)}]` : "";
89
103
 
90
- if (existsSync(join(dir, "user.json"))) {
91
- const ans = await ask(" .fastforms/ already exists. Overwrite? [y/N]: ");
92
- if (ans.toLowerCase() !== "y") {
93
- console.log(" Use 'fastforms edit' to update existing personas.\n");
94
- return;
95
- }
104
+ u.name = await ask(` Name (identifier)${show(existing?.name)}: `, existing?.name || "");
105
+ u.fullName = await ask(` Full name${show(ep.fullName)}: `, ep.fullName || "");
106
+ u.email = await ask(` Email${show(ep.email)}: `, ep.email || "");
107
+ u.role = await ask(` Role / title${show(ep.currentRole)}: `, ep.currentRole || "");
108
+ u.location = await ask(` Location${show(ep.location)}: `, ep.location || "");
109
+ u.linkedIn = await ask(` LinkedIn${show(ep.linkedIn)}: `, ep.linkedIn || "");
110
+ u.github = await ask(` GitHub${show(ep.github)}: `, ep.github || "");
111
+ u.bio = await ask(` Short bio${show(ep.bio)}: `, ep.bio || "");
112
+
113
+ const existingFacts = {};
114
+ for (const f of (existing?.customFacts || [])) {
115
+ if (f.enabled !== false && f.key) existingFacts[f.key] = f.value;
96
116
  }
117
+ u.facts = { ...existingFacts };
97
118
 
98
- // --- User persona ---
99
- console.log(" --- User persona ---\n");
100
- const user = userTemplate();
101
-
102
- user.name = await ask(" Name (identifier): ");
103
- user.fullName = await ask(" Full name: ");
104
- user.email = await ask(" Email: ");
105
- user.role = await ask(" Role / title: ");
106
- user.location = await ask(" Location: ");
107
- user.linkedIn = await ask(" LinkedIn (optional): ");
108
- user.github = await ask(" GitHub (optional): ");
109
- user.bio = await ask(" Short bio (optional): ");
110
-
111
- // Custom facts
112
- console.log("\n Add custom facts (e.g. 'x handle = @1dolinski'). Press Enter to skip.\n");
119
+ if (Object.keys(u.facts).length) {
120
+ console.log("\n Current facts:");
121
+ for (const [k, v] of Object.entries(u.facts)) console.log(` ${k} = ${v}`);
122
+ }
123
+ console.log("\n Add custom facts (key = value). Press Enter to finish.\n");
113
124
  while (true) {
114
125
  const raw = await ask(" fact: ");
115
126
  if (!raw) break;
116
127
  const eq = raw.indexOf("=");
117
- if (eq === -1) {
118
- console.log(" Use format: key = value");
119
- continue;
120
- }
121
- const key = raw.slice(0, eq).trim();
122
- const value = raw.slice(eq + 1).trim();
123
- if (key) user.facts[key] = value;
128
+ if (eq === -1) { console.log(" Use format: key = value"); continue; }
129
+ u.facts[raw.slice(0, eq).trim()] = raw.slice(eq + 1).trim();
124
130
  }
125
131
 
126
- // --- Business persona ---
127
- console.log("\n --- Business persona ---\n");
128
- const biz = businessTemplate();
129
-
130
- biz.name = await ask(" Company / project name: ");
131
- biz.oneLiner = await ask(" One-liner: ");
132
- biz.website = await ask(" Website (optional): ");
133
- biz.category = await ask(" Category (optional): ");
134
- biz.location = await ask(" Location (optional): ");
135
- biz.problem = await ask(" Problem you're solving (optional): ");
136
- biz.solution = await ask(" Your solution (optional): ");
137
- biz.targetUsers = await ask(" Target users (optional): ");
138
- biz.traction = await ask(" Traction (optional): ");
139
- biz.businessModel = await ask(" Business model (optional): ");
140
- biz.differentiators = await ask(" Differentiators (optional): ");
141
-
142
- console.log("\n Add business facts. Press Enter to skip.\n");
132
+ return u;
133
+ }
134
+
135
+ async function promptBusiness(existing) {
136
+ const bp = existing?.profile || {};
137
+ const b = businessTemplate();
138
+
139
+ const show = (val) => val ? ` [${String(val).slice(0, 40)}]` : "";
140
+
141
+ b.name = await ask(` Company / project name${show(existing?.name)}: `, existing?.name || "");
142
+ b.oneLiner = await ask(` One-liner${show(bp.oneLiner)}: `, bp.oneLiner || "");
143
+ b.website = await ask(` Website${show(bp.website)}: `, bp.website || "");
144
+ b.category = await ask(` Category${show(bp.category)}: `, bp.category || "");
145
+ b.location = await ask(` Location${show(bp.location)}: `, bp.location || "");
146
+ b.problem = await ask(` Problem you're solving${show(bp.problem)}: `, bp.problem || "");
147
+ b.solution = await ask(` Your solution${show(bp.solution)}: `, bp.solution || "");
148
+ b.targetUsers = await ask(` Target users${show(bp.targetUsers)}: `, bp.targetUsers || "");
149
+ b.traction = await ask(` Traction${show(bp.traction)}: `, bp.traction || "");
150
+ b.businessModel = await ask(` Business model${show(bp.businessModel)}: `, bp.businessModel || "");
151
+ b.differentiators = await ask(` Differentiators${show(bp.differentiators)}: `, bp.differentiators || "");
152
+
153
+ const existingFacts = {};
154
+ for (const f of (existing?.customFacts || [])) {
155
+ if (f.enabled !== false && f.key) existingFacts[f.key] = f.value;
156
+ }
157
+ b.facts = { ...existingFacts };
158
+
159
+ if (Object.keys(b.facts).length) {
160
+ console.log("\n Current facts:");
161
+ for (const [k, v] of Object.entries(b.facts)) console.log(` ${k} = ${v}`);
162
+ }
163
+ console.log("\n Add business facts (key = value). Press Enter to finish.\n");
143
164
  while (true) {
144
165
  const raw = await ask(" fact: ");
145
166
  if (!raw) break;
146
167
  const eq = raw.indexOf("=");
147
- if (eq === -1) {
148
- console.log(" Use format: key = value");
149
- continue;
168
+ if (eq === -1) { console.log(" Use format: key = value"); continue; }
169
+ b.facts[raw.slice(0, eq).trim()] = raw.slice(eq + 1).trim();
170
+ }
171
+
172
+ return b;
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // init — first-time setup: one user + one business
177
+ // ---------------------------------------------------------------------------
178
+
179
+ async function init() {
180
+ const dir = resolveDir();
181
+
182
+ console.log("\n fastforms — Let's set up your personas.\n");
183
+
184
+ const existingUsers = existsSync(join(dir, "users")) ? listPersonaFiles(dir, "user") : [];
185
+ if (existingUsers.length) {
186
+ const ans = await ask(` ${existingUsers.length} persona(s) already exist. Add another? [Y/n]: `);
187
+ if (ans.toLowerCase() === "n") {
188
+ console.log(" Use 'fastforms add user' or 'fastforms add business' to add more.\n");
189
+ closeRL();
190
+ return;
150
191
  }
151
- const key = raw.slice(0, eq).trim();
152
- const value = raw.slice(eq + 1).trim();
153
- if (key) biz.facts[key] = value;
154
192
  }
155
193
 
156
- // Save
194
+ console.log(" --- User persona ---\n");
195
+ const user = await promptUser();
157
196
  ensureDir(dir);
158
- saveUserPersona(dir, user);
159
- saveBusinessPersona(dir, biz);
197
+ const userSlug = saveUserPersona(dir, user);
198
+ console.log(`\n Saved users/${userSlug}.json`);
199
+
200
+ console.log("\n --- Business persona ---\n");
201
+ const biz = await promptBusiness();
202
+ const bizSlug = saveBusinessPersona(dir, biz);
203
+ console.log(`\n Saved businesses/${bizSlug}.json`);
160
204
 
161
- console.log(`\n Saved to ${dir}/`);
162
- console.log(" user.json");
163
- console.log(" business.json");
164
- console.log("\n Next: npx fastforms fill <url>\n");
205
+ console.log(`\n Personas saved to ${dir}/`);
206
+ console.log(" Next: npx @1dolinski/fastforms fill <url>\n");
165
207
  closeRL();
166
208
  }
167
209
 
168
210
  // ---------------------------------------------------------------------------
169
- // editre-run init with pre-filled values
211
+ // addadd a single persona
170
212
  // ---------------------------------------------------------------------------
171
213
 
172
- async function edit() {
173
- const dirArg = flag("--dir");
174
- const dir = dirArg || findFastformsDir();
214
+ async function addPersona() {
215
+ const type = args[1];
216
+ if (type !== "user" && type !== "business") {
217
+ console.error(" Usage: fastforms add user|business\n");
218
+ process.exit(1);
219
+ }
175
220
 
221
+ const dir = resolveDir();
222
+ ensureDir(dir);
223
+
224
+ if (type === "user") {
225
+ console.log("\n --- New user persona ---\n");
226
+ const user = await promptUser();
227
+ const slug = saveUserPersona(dir, user);
228
+ console.log(`\n Saved users/${slug}.json to ${dir}/\n`);
229
+ } else {
230
+ console.log("\n --- New business persona ---\n");
231
+ const biz = await promptBusiness();
232
+ const slug = saveBusinessPersona(dir, biz);
233
+ console.log(`\n Saved businesses/${slug}.json to ${dir}/\n`);
234
+ }
235
+ closeRL();
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // list — show all personas
240
+ // ---------------------------------------------------------------------------
241
+
242
+ function listAll() {
243
+ const dir = findFastformsDir();
176
244
  if (!dir) {
177
245
  console.error(" No .fastforms/ directory found. Run 'fastforms init' first.\n");
178
246
  process.exit(1);
179
247
  }
180
248
 
181
- const dump = loadLocalPersonas(dir);
182
- const existing = dump.personas[0];
183
- const existingBiz = dump.businessPersonas[0];
249
+ const defaults = loadDefaults(dir);
184
250
 
185
- console.log("\n fastforms Edit your personas. Press Enter to keep current value.\n");
251
+ const users = listPersonaFiles(dir, "user");
252
+ const businesses = listPersonaFiles(dir, "business");
186
253
 
187
- // --- User ---
188
- console.log(" --- User persona ---\n");
189
- const ep = existing?.profile || {};
190
- const user = userTemplate();
191
-
192
- user.name = await ask(` Name [${existing?.name || ""}]: `, existing?.name || "");
193
- user.fullName = await ask(` Full name [${ep.fullName || ""}]: `, ep.fullName || "");
194
- user.email = await ask(` Email [${ep.email || ""}]: `, ep.email || "");
195
- user.role = await ask(` Role [${ep.currentRole || ""}]: `, ep.currentRole || "");
196
- user.location = await ask(` Location [${ep.location || ""}]: `, ep.location || "");
197
- user.linkedIn = await ask(` LinkedIn [${ep.linkedIn || ""}]: `, ep.linkedIn || "");
198
- user.github = await ask(` GitHub [${ep.github || ""}]: `, ep.github || "");
199
- user.bio = await ask(` Bio [${ep.bio ? ep.bio.slice(0, 40) + "..." : ""}]: `, ep.bio || "");
200
-
201
- // Carry over existing facts
202
- const existingFacts = {};
203
- for (const f of (existing?.customFacts || [])) {
204
- if (f.enabled !== false) existingFacts[f.key] = f.value;
254
+ console.log(`\n Personas in ${dir}/\n`);
255
+
256
+ if (users.length) {
257
+ console.log(" User personas:");
258
+ for (const u of users) {
259
+ const def = defaults.defaultUser === (u.data?.name || u.slug) ? " (default)" : "";
260
+ console.log(` ${u.slug}${def} ${u.data?.fullName || u.data?.name || "?"} <${u.data?.email || "?"}>`);
261
+ }
262
+ } else {
263
+ console.log(" No user personas. Run: fastforms add user");
205
264
  }
206
- user.facts = { ...existingFacts };
207
265
 
208
- if (Object.keys(user.facts).length) {
209
- console.log("\n Current facts:");
210
- for (const [k, v] of Object.entries(user.facts)) {
211
- console.log(` ${k} = ${v}`);
266
+ console.log();
267
+
268
+ if (businesses.length) {
269
+ console.log(" Business personas:");
270
+ for (const b of businesses) {
271
+ const def = defaults.defaultBusiness === (b.data?.name || b.slug) ? " (default)" : "";
272
+ console.log(` ${b.slug}${def} — ${b.data?.name || "?"}: ${b.data?.oneLiner || ""}`);
212
273
  }
274
+ } else {
275
+ console.log(" No business personas. Run: fastforms add business");
213
276
  }
214
- console.log("\n Add/update facts (Enter to finish):\n");
215
- while (true) {
216
- const raw = await ask(" fact: ");
217
- if (!raw) break;
218
- const eq = raw.indexOf("=");
219
- if (eq === -1) { console.log(" Use format: key = value"); continue; }
220
- user.facts[raw.slice(0, eq).trim()] = raw.slice(eq + 1).trim();
277
+
278
+ console.log();
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // edit pick a persona and edit it
283
+ // ---------------------------------------------------------------------------
284
+
285
+ async function edit() {
286
+ const dir = findFastformsDir();
287
+ if (!dir) {
288
+ console.error(" No .fastforms/ directory found. Run 'fastforms init' first.\n");
289
+ process.exit(1);
221
290
  }
222
291
 
223
- // --- Business ---
224
- console.log("\n --- Business persona ---\n");
225
- const bp = existingBiz?.profile || {};
226
- const biz = businessTemplate();
227
-
228
- biz.name = await ask(` Company [${existingBiz?.name || ""}]: `, existingBiz?.name || "");
229
- biz.oneLiner = await ask(` One-liner [${bp.oneLiner || ""}]: `, bp.oneLiner || "");
230
- biz.website = await ask(` Website [${bp.website || ""}]: `, bp.website || "");
231
- biz.category = await ask(` Category [${bp.category || ""}]: `, bp.category || "");
232
- biz.location = await ask(` Location [${bp.location || ""}]: `, bp.location || "");
233
- biz.problem = await ask(` Problem [${bp.problem ? bp.problem.slice(0, 40) + "..." : ""}]: `, bp.problem || "");
234
- biz.solution = await ask(` Solution [${bp.solution ? bp.solution.slice(0, 40) + "..." : ""}]: `, bp.solution || "");
235
- biz.targetUsers = await ask(` Target users [${bp.targetUsers ? bp.targetUsers.slice(0, 40) + "..." : ""}]: `, bp.targetUsers || "");
236
- biz.traction = await ask(` Traction [${bp.traction ? bp.traction.slice(0, 40) + "..." : ""}]: `, bp.traction || "");
237
- biz.businessModel = await ask(` Business model [${bp.businessModel ? bp.businessModel.slice(0, 40) + "..." : ""}]: `, bp.businessModel || "");
238
- biz.differentiators = await ask(` Differentiators [${bp.differentiators ? bp.differentiators.slice(0, 40) + "..." : ""}]: `, bp.differentiators || "");
239
-
240
- const existingBizFacts = {};
241
- for (const f of (existingBiz?.customFacts || [])) {
242
- if (f.enabled !== false) existingBizFacts[f.key] = f.value;
292
+ const users = listPersonaFiles(dir, "user");
293
+ const businesses = listPersonaFiles(dir, "business");
294
+ const all = [
295
+ ...users.map((u) => ({ ...u, type: "user", label: `user: ${u.slug} (${u.data?.fullName || u.data?.name || "?"})` })),
296
+ ...businesses.map((b) => ({ ...b, type: "business", label: `biz: ${b.slug} (${b.data?.name || "?"})` })),
297
+ ];
298
+
299
+ if (!all.length) {
300
+ console.error(" No personas found. Run 'fastforms init' first.\n");
301
+ process.exit(1);
243
302
  }
244
- biz.facts = { ...existingBizFacts };
245
303
 
246
- if (Object.keys(biz.facts).length) {
247
- console.log("\n Current facts:");
248
- for (const [k, v] of Object.entries(biz.facts)) {
249
- console.log(` ${k} = ${v}`);
250
- }
304
+ console.log("\n Which persona to edit?\n");
305
+ all.forEach((p, i) => console.log(` ${i + 1}. ${p.label}`));
306
+ const ans = await ask(`\n Pick [1-${all.length}]: `);
307
+ const idx = Number(ans) - 1;
308
+ if (idx < 0 || idx >= all.length) { console.log(" Invalid selection.\n"); closeRL(); return; }
309
+
310
+ const picked = all[idx];
311
+ const dump = loadLocalPersonas(dir);
312
+
313
+ if (picked.type === "user") {
314
+ const existing = dump.personas.find((p) => p.name === picked.data?.name) || null;
315
+ console.log(`\n --- Edit user: ${picked.slug} ---\n`);
316
+ const updated = await promptUser(existing);
317
+ if (updated.name !== picked.data?.name) deletePersonaFile(dir, "user", picked.slug);
318
+ const slug = saveUserPersona(dir, updated);
319
+ console.log(`\n Updated users/${slug}.json\n`);
320
+ } else {
321
+ const existing = dump.businessPersonas.find((p) => p.name === picked.data?.name) || null;
322
+ console.log(`\n --- Edit business: ${picked.slug} ---\n`);
323
+ const updated = await promptBusiness(existing);
324
+ if (updated.name !== picked.data?.name) deletePersonaFile(dir, "business", picked.slug);
325
+ const slug = saveBusinessPersona(dir, updated);
326
+ console.log(`\n Updated businesses/${slug}.json\n`);
251
327
  }
252
- console.log("\n Add/update facts (Enter to finish):\n");
253
- while (true) {
254
- const raw = await ask(" fact: ");
255
- if (!raw) break;
256
- const eq = raw.indexOf("=");
257
- if (eq === -1) { console.log(" Use format: key = value"); continue; }
258
- biz.facts[raw.slice(0, eq).trim()] = raw.slice(eq + 1).trim();
328
+ closeRL();
329
+ }
330
+
331
+ // ---------------------------------------------------------------------------
332
+ // remove delete a persona
333
+ // ---------------------------------------------------------------------------
334
+
335
+ async function remove() {
336
+ const dir = findFastformsDir();
337
+ if (!dir) {
338
+ console.error(" No .fastforms/ directory found.\n");
339
+ process.exit(1);
259
340
  }
260
341
 
261
- saveUserPersona(dir, user);
262
- saveBusinessPersona(dir, biz);
263
- console.log(`\n Updated ${dir}/\n`);
342
+ const users = listPersonaFiles(dir, "user");
343
+ const businesses = listPersonaFiles(dir, "business");
344
+ const all = [
345
+ ...users.map((u) => ({ ...u, type: "user", label: `user: ${u.slug} (${u.data?.fullName || u.data?.name || "?"})` })),
346
+ ...businesses.map((b) => ({ ...b, type: "business", label: `biz: ${b.slug} (${b.data?.name || "?"})` })),
347
+ ];
348
+
349
+ if (!all.length) {
350
+ console.error(" No personas to remove.\n");
351
+ process.exit(1);
352
+ }
353
+
354
+ console.log("\n Which persona to remove?\n");
355
+ all.forEach((p, i) => console.log(` ${i + 1}. ${p.label}`));
356
+ const ans = await ask(`\n Pick [1-${all.length}]: `);
357
+ const idx = Number(ans) - 1;
358
+ if (idx < 0 || idx >= all.length) { console.log(" Invalid selection.\n"); closeRL(); return; }
359
+
360
+ const picked = all[idx];
361
+ const confirm = await ask(` Delete ${picked.type}/${picked.slug}? [y/N]: `);
362
+ if (confirm.toLowerCase() === "y") {
363
+ deletePersonaFile(dir, picked.type, picked.slug);
364
+ console.log(` Removed ${picked.type}s/${picked.slug}.json\n`);
365
+ } else {
366
+ console.log(" Cancelled.\n");
367
+ }
264
368
  closeRL();
265
369
  }
266
370
 
@@ -285,14 +389,11 @@ async function fill() {
285
389
  let browser;
286
390
 
287
391
  if (hasFlag("--web")) {
288
- // Web app mode
289
392
  console.log(" Pulling personas from web app...");
290
393
  browser = await connectToChrome(port);
291
394
  dump = await pullPersonas(browser);
292
395
  } else {
293
- // Local mode (default)
294
- const dirArg = flag("--dir");
295
- const dir = dirArg || findFastformsDir();
396
+ const dir = findFastformsDir();
296
397
 
297
398
  if (!dir) {
298
399
  console.error(" No .fastforms/ directory found.");
@@ -349,9 +450,16 @@ async function fill() {
349
450
 
350
451
  if (hasFlag("--web")) {
351
452
  await offerSetDefaults(user, biz);
453
+ } else {
454
+ // Save defaults to local dir
455
+ const dir = findFastformsDir();
456
+ if (dir && user && biz) {
457
+ saveDefaults(dir, { defaultUser: user.name, defaultBusiness: biz.name });
458
+ }
352
459
  }
353
460
 
354
461
  browser.disconnect();
462
+ closeRL();
355
463
  }
356
464
 
357
465
  // ---------------------------------------------------------------------------
@@ -382,9 +490,18 @@ switch (command) {
382
490
  case "init":
383
491
  init().catch((e) => { console.error(e.message); process.exit(1); });
384
492
  break;
493
+ case "add":
494
+ addPersona().catch((e) => { console.error(e.message); process.exit(1); });
495
+ break;
496
+ case "list":
497
+ listAll();
498
+ break;
385
499
  case "edit":
386
500
  edit().catch((e) => { console.error(e.message); process.exit(1); });
387
501
  break;
502
+ case "remove":
503
+ remove().catch((e) => { console.error(e.message); process.exit(1); });
504
+ break;
388
505
  case "fill":
389
506
  fill().catch((e) => { console.error(e.message); process.exit(1); });
390
507
  break;
package/lib/local.js CHANGED
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2
- import { join } from "path";
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from "fs";
2
+ import { join, basename } from "path";
3
3
  import { homedir } from "os";
4
4
 
5
5
  export function findFastformsDir() {
@@ -25,10 +25,15 @@ function writeJson(path, data) {
25
25
  writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
26
26
  }
27
27
 
28
- /**
29
- * Normalize flat local format into the shape fill.js expects:
30
- * { name, profile: { ...fields }, customFacts: [...] }
31
- */
28
+ function slugify(name) {
29
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "default";
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Normalize flat local format → shape fill.js expects:
34
+ // { name, profile: { ...fields }, customFacts: [...] }
35
+ // ---------------------------------------------------------------------------
36
+
32
37
  function normalizeUser(raw) {
33
38
  if (!raw) return null;
34
39
  const { name, facts, ...rest } = raw;
@@ -92,29 +97,81 @@ function factsToArray(facts) {
92
97
  }));
93
98
  }
94
99
 
95
- export function loadLocalPersonas(dir) {
96
- const userPath = join(dir, "user.json");
97
- const bizPath = join(dir, "business.json");
100
+ // ---------------------------------------------------------------------------
101
+ // Multi-persona loading: reads users/ and businesses/ subdirs.
102
+ // Falls back to legacy user.json / business.json for backward compat.
103
+ // ---------------------------------------------------------------------------
98
104
 
99
- const rawUser = readJson(userPath);
100
- const rawBiz = readJson(bizPath);
105
+ function loadDir(dir, normalizer) {
106
+ if (!existsSync(dir)) return [];
107
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json")).sort();
108
+ return files.map((f) => normalizer(readJson(join(dir, f)))).filter(Boolean);
109
+ }
101
110
 
102
- const personas = rawUser ? [normalizeUser(rawUser)] : [];
103
- const businessPersonas = rawBiz ? [normalizeBusiness(rawBiz)] : [];
111
+ export function loadLocalPersonas(dir) {
112
+ const usersDir = join(dir, "users");
113
+ const bizDir = join(dir, "businesses");
114
+
115
+ let personas = loadDir(usersDir, normalizeUser);
116
+ let businessPersonas = loadDir(bizDir, normalizeBusiness);
117
+
118
+ // Backward compat: if subdirs are empty, check legacy single files
119
+ if (!personas.length) {
120
+ const legacy = readJson(join(dir, "user.json"));
121
+ if (legacy) personas = [normalizeUser(legacy)];
122
+ }
123
+ if (!businessPersonas.length) {
124
+ const legacy = readJson(join(dir, "business.json"));
125
+ if (legacy) businessPersonas = [normalizeBusiness(legacy)];
126
+ }
104
127
 
105
128
  return { personas, businessPersonas };
106
129
  }
107
130
 
131
+ // ---------------------------------------------------------------------------
132
+ // Save personas into named files under users/ or businesses/
133
+ // ---------------------------------------------------------------------------
134
+
108
135
  export function saveUserPersona(dir, data) {
109
- ensureDir(dir);
110
- writeJson(join(dir, "user.json"), data);
136
+ const subdir = ensureDir(join(dir, "users"));
137
+ const slug = slugify(data.name || "default");
138
+ writeJson(join(subdir, `${slug}.json`), data);
139
+ return slug;
111
140
  }
112
141
 
113
142
  export function saveBusinessPersona(dir, data) {
114
- ensureDir(dir);
115
- writeJson(join(dir, "business.json"), data);
143
+ const subdir = ensureDir(join(dir, "businesses"));
144
+ const slug = slugify(data.name || "default");
145
+ writeJson(join(subdir, `${slug}.json`), data);
146
+ return slug;
116
147
  }
117
148
 
149
+ export function deletePersonaFile(dir, type, slug) {
150
+ const subdir = type === "user" ? "users" : "businesses";
151
+ const path = join(dir, subdir, `${slug}.json`);
152
+ if (existsSync(path)) unlinkSync(path);
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // List raw persona files (for edit/delete commands)
157
+ // ---------------------------------------------------------------------------
158
+
159
+ export function listPersonaFiles(dir, type) {
160
+ const subdir = join(dir, type === "user" ? "users" : "businesses");
161
+ if (!existsSync(subdir)) return [];
162
+ return readdirSync(subdir)
163
+ .filter((f) => f.endsWith(".json"))
164
+ .sort()
165
+ .map((f) => {
166
+ const data = readJson(join(subdir, f));
167
+ return { slug: basename(f, ".json"), file: f, data };
168
+ });
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Defaults
173
+ // ---------------------------------------------------------------------------
174
+
118
175
  export function loadDefaults(dir) {
119
176
  return readJson(join(dir, "defaults.json")) || {};
120
177
  }
@@ -124,6 +181,10 @@ export function saveDefaults(dir, patch) {
124
181
  writeJson(join(dir, "defaults.json"), { ...current, ...patch });
125
182
  }
126
183
 
184
+ // ---------------------------------------------------------------------------
185
+ // Templates
186
+ // ---------------------------------------------------------------------------
187
+
127
188
  export function userTemplate() {
128
189
  return {
129
190
  name: "",
package/lib/personas.js CHANGED
@@ -110,35 +110,64 @@ export function showPersonaDetails(user, biz) {
110
110
  ]);
111
111
  }
112
112
 
113
- export async function selectPersonas(dump, userHint, bizHint) {
114
- const config = loadConfig();
115
- const personas = dump.personas || [];
116
- const bizPersonas = dump.businessPersonas || [];
117
-
118
- // Use defaults if no hint and defaults are saved
119
- const effectiveUserHint = userHint || config.defaultUser || "";
120
- const effectiveBizHint = bizHint || config.defaultBusiness || "";
113
+ // ---------------------------------------------------------------------------
114
+ // Interactive persona selection — supports multiple personas from any source
115
+ // ---------------------------------------------------------------------------
121
116
 
122
- let user = pickPersona(personas, effectiveUserHint, ["name", "profile.fullName"]);
123
- let biz = pickPersona(bizPersonas, effectiveBizHint, ["name", "profile.companyName", "profile.productName"]);
117
+ async function pickFromList(label, list, hint, keys, defaultName) {
118
+ if (!list?.length) return null;
119
+ if (list.length === 1) return list[0];
124
120
 
125
- // Interactive selection if multiple and no effective hint
126
- if (!effectiveUserHint && personas.length > 1) {
127
- console.log("\n User personas:\n");
128
- personas.forEach((p, i) => console.log(` ${i + 1}. ${p.name} (${p.profile?.fullName || "?"})`));
129
- const ans = await ask(`\n Pick user persona [1-${personas.length}]: `);
130
- const idx = Number(ans) - 1;
131
- if (idx >= 0 && idx < personas.length) user = personas[idx];
121
+ // Try hint-based match
122
+ if (hint) {
123
+ const match = pickPersona(list, hint, keys);
124
+ if (match) return match;
132
125
  }
133
126
 
134
- if (!effectiveBizHint && bizPersonas.length > 1) {
135
- console.log("\n Business personas:\n");
136
- bizPersonas.forEach((p, i) => console.log(` ${i + 1}. ${p.name} (${p.profile?.companyName || "?"})`));
137
- const ans = await ask(`\n Pick business persona [1-${bizPersonas.length}]: `);
138
- const idx = Number(ans) - 1;
139
- if (idx >= 0 && idx < bizPersonas.length) biz = bizPersonas[idx];
127
+ // Try default
128
+ if (defaultName) {
129
+ const match = list.find((p) => p.name === defaultName);
130
+ if (match) return match;
140
131
  }
141
132
 
133
+ // Interactive selection
134
+ console.log(`\n ${label}:\n`);
135
+ list.forEach((p, i) => {
136
+ const detail = keys.map((k) => k.split(".").reduce((o, s) => o?.[s], p)).filter(Boolean)[0] || "";
137
+ console.log(` ${i + 1}. ${p.name}${detail ? ` (${detail})` : ""}`);
138
+ });
139
+ const ans = await ask(`\n Pick ${label.toLowerCase()} [1-${list.length}]: `);
140
+ const idx = Number(ans) - 1;
141
+ if (idx >= 0 && idx < list.length) return list[idx];
142
+ return list[0];
143
+ }
144
+
145
+ export async function selectPersonas(dump, userHint, bizHint) {
146
+ const config = loadConfig();
147
+ const personas = dump.personas || [];
148
+ const bizPersonas = dump.businessPersonas || [];
149
+
150
+ const effectiveUserHint = userHint || "";
151
+ const effectiveBizHint = bizHint || "";
152
+ const defaultUser = config.defaultUser || "";
153
+ const defaultBiz = config.defaultBusiness || "";
154
+
155
+ const user = await pickFromList(
156
+ "User personas",
157
+ personas,
158
+ effectiveUserHint,
159
+ ["name", "profile.fullName"],
160
+ defaultUser,
161
+ );
162
+
163
+ const biz = await pickFromList(
164
+ "Business personas",
165
+ bizPersonas,
166
+ effectiveBizHint,
167
+ ["name", "profile.companyName", "profile.productName"],
168
+ defaultBiz,
169
+ );
170
+
142
171
  return { user, biz };
143
172
  }
144
173
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1dolinski/fastforms",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Fill any form fast. Manage personas on the web, fill forms from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {