@1dolinski/fastforms 0.1.2 → 0.3.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/bin/fastforms.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  connectToChrome,
11
11
  pullPersonas,
12
12
  selectPersonas,
13
+ selectFormPersona,
13
14
  showPersonaDetails,
14
15
  offerSetDefaults,
15
16
  offerOpenPersonaManager,
@@ -21,8 +22,14 @@ import {
21
22
  loadLocalPersonas,
22
23
  saveUserPersona,
23
24
  saveBusinessPersona,
25
+ saveFormPersona,
26
+ deletePersonaFile,
27
+ listPersonaFiles,
28
+ loadDefaults,
29
+ saveDefaults,
24
30
  userTemplate,
25
31
  businessTemplate,
32
+ formTemplate,
26
33
  } from "../lib/local.js";
27
34
 
28
35
  const args = process.argv.slice(2);
@@ -52,215 +59,412 @@ function ask(prompt, fallback = "") {
52
59
  return new Promise((r) => getRL().question(prompt, (a) => r(a.trim() || fallback)));
53
60
  }
54
61
 
62
+ function resolveDir() {
63
+ const dirArg = flag("--dir");
64
+ return dirArg || findFastformsDir() || join(process.cwd(), ".fastforms");
65
+ }
66
+
55
67
  function help() {
56
68
  console.log(`
57
- fastforms — Fill any form fast.
69
+ fastforms — Fill any form fast, with multiple personas.
58
70
 
59
71
  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
72
+ fastforms init Create your first user + business persona
73
+ fastforms add user Add another user persona
74
+ fastforms add business Add another business persona
75
+ fastforms add form Add a form persona (org + purpose + answers)
76
+ fastforms list List all personas
77
+ fastforms edit Edit an existing persona
78
+ fastforms remove Remove a persona
79
+ fastforms fill <url> Fill a form (pick personas interactively)
63
80
  fastforms personas Open web persona manager in Chrome
64
81
  fastforms Show this help
65
82
 
66
83
  Options:
67
84
  --web Use web app personas instead of local .fastforms/
68
85
  --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)
86
+ --user <hint> User persona name/hint to pre-select
87
+ --business <hint> Business persona name/hint to pre-select
88
+ --form <hint> Form persona name/hint to pre-select
71
89
  --port <port> Chrome debug port (auto-detected by default)
72
90
 
91
+ Persona types:
92
+ user — who you are (name, email, bio, skills)
93
+ business — what you're building (company, product, traction)
94
+ form — who's asking and why (org, purpose, form-specific answers)
95
+
73
96
  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
97
+ 1. npx @1dolinski/fastforms init
98
+ 2. npx @1dolinski/fastforms add form # add form-specific context
99
+ 3. Enable remote debugging: chrome://inspect/#remote-debugging
100
+ 4. npx @1dolinski/fastforms fill https://example.com/apply
77
101
  `);
78
102
  }
79
103
 
80
104
  // ---------------------------------------------------------------------------
81
- // init — conversational persona builder
105
+ // Shared persona builder prompts
82
106
  // ---------------------------------------------------------------------------
83
107
 
84
- async function init() {
85
- const dirArg = flag("--dir");
86
- const dir = dirArg || join(process.cwd(), ".fastforms");
108
+ async function promptUser(existing) {
109
+ const ep = existing?.profile || {};
110
+ const u = userTemplate();
87
111
 
88
- console.log("\n fastforms Let's set up your personas.\n");
112
+ const show = (val) => val ? ` [${String(val).slice(0, 40)}]` : "";
89
113
 
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
- }
114
+ u.name = await ask(` Name (identifier)${show(existing?.name)}: `, existing?.name || "");
115
+ u.fullName = await ask(` Full name${show(ep.fullName)}: `, ep.fullName || "");
116
+ u.email = await ask(` Email${show(ep.email)}: `, ep.email || "");
117
+ u.role = await ask(` Role / title${show(ep.currentRole)}: `, ep.currentRole || "");
118
+ u.location = await ask(` Location${show(ep.location)}: `, ep.location || "");
119
+ u.linkedIn = await ask(` LinkedIn${show(ep.linkedIn)}: `, ep.linkedIn || "");
120
+ u.github = await ask(` GitHub${show(ep.github)}: `, ep.github || "");
121
+ u.bio = await ask(` Short bio${show(ep.bio)}: `, ep.bio || "");
122
+
123
+ const existingFacts = {};
124
+ for (const f of (existing?.customFacts || [])) {
125
+ if (f.enabled !== false && f.key) existingFacts[f.key] = f.value;
96
126
  }
127
+ u.facts = { ...existingFacts };
97
128
 
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");
129
+ if (Object.keys(u.facts).length) {
130
+ console.log("\n Current facts:");
131
+ for (const [k, v] of Object.entries(u.facts)) console.log(` ${k} = ${v}`);
132
+ }
133
+ console.log("\n Add custom facts (key = value). Press Enter to finish.\n");
113
134
  while (true) {
114
135
  const raw = await ask(" fact: ");
115
136
  if (!raw) break;
116
137
  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;
138
+ if (eq === -1) { console.log(" Use format: key = value"); continue; }
139
+ u.facts[raw.slice(0, eq).trim()] = raw.slice(eq + 1).trim();
124
140
  }
125
141
 
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");
142
+ return u;
143
+ }
144
+
145
+ async function promptBusiness(existing) {
146
+ const bp = existing?.profile || {};
147
+ const b = businessTemplate();
148
+
149
+ const show = (val) => val ? ` [${String(val).slice(0, 40)}]` : "";
150
+
151
+ b.name = await ask(` Company / project name${show(existing?.name)}: `, existing?.name || "");
152
+ b.oneLiner = await ask(` One-liner${show(bp.oneLiner)}: `, bp.oneLiner || "");
153
+ b.website = await ask(` Website${show(bp.website)}: `, bp.website || "");
154
+ b.category = await ask(` Category${show(bp.category)}: `, bp.category || "");
155
+ b.location = await ask(` Location${show(bp.location)}: `, bp.location || "");
156
+ b.problem = await ask(` Problem you're solving${show(bp.problem)}: `, bp.problem || "");
157
+ b.solution = await ask(` Your solution${show(bp.solution)}: `, bp.solution || "");
158
+ b.targetUsers = await ask(` Target users${show(bp.targetUsers)}: `, bp.targetUsers || "");
159
+ b.traction = await ask(` Traction${show(bp.traction)}: `, bp.traction || "");
160
+ b.businessModel = await ask(` Business model${show(bp.businessModel)}: `, bp.businessModel || "");
161
+ b.differentiators = await ask(` Differentiators${show(bp.differentiators)}: `, bp.differentiators || "");
162
+
163
+ const existingFacts = {};
164
+ for (const f of (existing?.customFacts || [])) {
165
+ if (f.enabled !== false && f.key) existingFacts[f.key] = f.value;
166
+ }
167
+ b.facts = { ...existingFacts };
168
+
169
+ if (Object.keys(b.facts).length) {
170
+ console.log("\n Current facts:");
171
+ for (const [k, v] of Object.entries(b.facts)) console.log(` ${k} = ${v}`);
172
+ }
173
+ console.log("\n Add business facts (key = value). Press Enter to finish.\n");
143
174
  while (true) {
144
175
  const raw = await ask(" fact: ");
145
176
  if (!raw) break;
146
177
  const eq = raw.indexOf("=");
147
- if (eq === -1) {
148
- console.log(" Use format: key = value");
149
- continue;
178
+ if (eq === -1) { console.log(" Use format: key = value"); continue; }
179
+ b.facts[raw.slice(0, eq).trim()] = raw.slice(eq + 1).trim();
180
+ }
181
+
182
+ return b;
183
+ }
184
+
185
+ async function promptForm(existing) {
186
+ const ep = existing?.profile || {};
187
+ const f = formTemplate();
188
+
189
+ const show = (val) => val ? ` [${String(val).slice(0, 40)}]` : "";
190
+
191
+ f.name = await ask(` Form name (e.g. "Nitro Accelerator")${show(existing?.name)}: `, existing?.name || "");
192
+ f.organization = await ask(` Organization${show(ep.organization)}: `, ep.organization || "");
193
+ f.purpose = await ask(` Purpose (e.g. "crypto accelerator application")${show(ep.purpose)}: `, ep.purpose || "");
194
+
195
+ const existingUrls = existing?.urls || [];
196
+ if (existingUrls.length) {
197
+ console.log(`\n Current URLs: ${existingUrls.join(", ")}`);
198
+ }
199
+ console.log("\n Add URL patterns this form matches (press Enter to finish):\n");
200
+ f.urls = [...existingUrls];
201
+ while (true) {
202
+ const url = await ask(" url: ");
203
+ if (!url) break;
204
+ if (!f.urls.includes(url)) f.urls.push(url);
205
+ }
206
+
207
+ f.notes = await ask(` Notes / context${show(ep.notes)}: `, ep.notes || "");
208
+ f.deadline = await ask(` Deadline${show(ep.deadline)}: `, ep.deadline || "");
209
+ f.requirements = await ask(` Requirements${show(ep.requirements)}: `, ep.requirements || "");
210
+
211
+ const existingFacts = {};
212
+ for (const cf of (existing?.customFacts || [])) {
213
+ if (cf.enabled !== false && cf.key) existingFacts[cf.key] = cf.value;
214
+ }
215
+ f.facts = { ...existingFacts };
216
+
217
+ if (Object.keys(f.facts).length) {
218
+ console.log("\n Current form-specific answers:");
219
+ for (const [k, v] of Object.entries(f.facts)) console.log(` ${k} = ${v}`);
220
+ }
221
+ console.log("\n Add form-specific answers (label hint = answer). Press Enter to finish.");
222
+ console.log(" These override user/business data for matching fields.\n");
223
+ while (true) {
224
+ const raw = await ask(" answer: ");
225
+ if (!raw) break;
226
+ const eq = raw.indexOf("=");
227
+ if (eq === -1) { console.log(" Use format: field hint = answer"); continue; }
228
+ f.facts[raw.slice(0, eq).trim()] = raw.slice(eq + 1).trim();
229
+ }
230
+
231
+ return f;
232
+ }
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // init — first-time setup: one user + one business
236
+ // ---------------------------------------------------------------------------
237
+
238
+ async function init() {
239
+ const dir = resolveDir();
240
+
241
+ console.log("\n fastforms — Let's set up your personas.\n");
242
+
243
+ const existingUsers = existsSync(join(dir, "users")) ? listPersonaFiles(dir, "user") : [];
244
+ if (existingUsers.length) {
245
+ const ans = await ask(` ${existingUsers.length} persona(s) already exist. Add another? [Y/n]: `);
246
+ if (ans.toLowerCase() === "n") {
247
+ console.log(" Use 'fastforms add user|business|form' to add more.\n");
248
+ closeRL();
249
+ return;
150
250
  }
151
- const key = raw.slice(0, eq).trim();
152
- const value = raw.slice(eq + 1).trim();
153
- if (key) biz.facts[key] = value;
154
251
  }
155
252
 
156
- // Save
253
+ console.log(" --- User persona ---\n");
254
+ const user = await promptUser();
157
255
  ensureDir(dir);
158
- saveUserPersona(dir, user);
159
- saveBusinessPersona(dir, biz);
256
+ const userSlug = saveUserPersona(dir, user);
257
+ console.log(`\n Saved users/${userSlug}.json`);
258
+
259
+ console.log("\n --- Business persona ---\n");
260
+ const biz = await promptBusiness();
261
+ const bizSlug = saveBusinessPersona(dir, biz);
262
+ console.log(`\n Saved businesses/${bizSlug}.json`);
263
+
264
+ const addForm = await ask("\n Add a form persona now? (org + purpose + form-specific answers) [y/N]: ");
265
+ if (addForm.toLowerCase() === "y") {
266
+ console.log("\n --- Form persona ---\n");
267
+ const form = await promptForm();
268
+ const formSlug = saveFormPersona(dir, form);
269
+ console.log(`\n Saved forms/${formSlug}.json`);
270
+ }
160
271
 
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");
272
+ console.log(`\n Personas saved to ${dir}/`);
273
+ console.log(" Tip: add form-specific context with 'fastforms add form'");
274
+ console.log(" Next: npx @1dolinski/fastforms fill <url>\n");
165
275
  closeRL();
166
276
  }
167
277
 
168
278
  // ---------------------------------------------------------------------------
169
- // editre-run init with pre-filled values
279
+ // addadd a single persona
170
280
  // ---------------------------------------------------------------------------
171
281
 
172
- async function edit() {
173
- const dirArg = flag("--dir");
174
- const dir = dirArg || findFastformsDir();
282
+ async function addPersona() {
283
+ const type = args[1];
284
+ if (type !== "user" && type !== "business" && type !== "form") {
285
+ console.error(" Usage: fastforms add user|business|form\n");
286
+ process.exit(1);
287
+ }
288
+
289
+ const dir = resolveDir();
290
+ ensureDir(dir);
291
+
292
+ if (type === "user") {
293
+ console.log("\n --- New user persona ---\n");
294
+ const user = await promptUser();
295
+ const slug = saveUserPersona(dir, user);
296
+ console.log(`\n Saved users/${slug}.json to ${dir}/\n`);
297
+ } else if (type === "business") {
298
+ console.log("\n --- New business persona ---\n");
299
+ const biz = await promptBusiness();
300
+ const slug = saveBusinessPersona(dir, biz);
301
+ console.log(`\n Saved businesses/${slug}.json to ${dir}/\n`);
302
+ } else {
303
+ console.log("\n --- New form persona ---\n");
304
+ const form = await promptForm();
305
+ const slug = saveFormPersona(dir, form);
306
+ console.log(`\n Saved forms/${slug}.json to ${dir}/\n`);
307
+ }
308
+ closeRL();
309
+ }
310
+
311
+ // ---------------------------------------------------------------------------
312
+ // list — show all personas
313
+ // ---------------------------------------------------------------------------
175
314
 
315
+ function listAll() {
316
+ const dir = findFastformsDir();
176
317
  if (!dir) {
177
318
  console.error(" No .fastforms/ directory found. Run 'fastforms init' first.\n");
178
319
  process.exit(1);
179
320
  }
180
321
 
181
- const dump = loadLocalPersonas(dir);
182
- const existing = dump.personas[0];
183
- const existingBiz = dump.businessPersonas[0];
322
+ const defaults = loadDefaults(dir);
184
323
 
185
- console.log("\n fastforms Edit your personas. Press Enter to keep current value.\n");
324
+ const users = listPersonaFiles(dir, "user");
325
+ const businesses = listPersonaFiles(dir, "business");
326
+ const forms = listPersonaFiles(dir, "form");
186
327
 
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;
205
- }
206
- user.facts = { ...existingFacts };
328
+ console.log(`\n Personas in ${dir}/\n`);
207
329
 
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}`);
330
+ if (users.length) {
331
+ console.log(" User personas:");
332
+ for (const u of users) {
333
+ const def = defaults.defaultUser === (u.data?.name || u.slug) ? " (default)" : "";
334
+ console.log(` ${u.slug}${def} — ${u.data?.fullName || u.data?.name || "?"} <${u.data?.email || "?"}>`);
212
335
  }
336
+ } else {
337
+ console.log(" No user personas. Run: fastforms add user");
213
338
  }
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();
339
+
340
+ console.log();
341
+
342
+ if (businesses.length) {
343
+ console.log(" Business personas:");
344
+ for (const b of businesses) {
345
+ const def = defaults.defaultBusiness === (b.data?.name || b.slug) ? " (default)" : "";
346
+ console.log(` ${b.slug}${def} — ${b.data?.name || "?"}: ${b.data?.oneLiner || ""}`);
347
+ }
348
+ } else {
349
+ console.log(" No business personas. Run: fastforms add business");
221
350
  }
222
351
 
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;
243
- }
244
- biz.facts = { ...existingBizFacts };
245
-
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}`);
352
+ console.log();
353
+
354
+ if (forms.length) {
355
+ console.log(" Form personas:");
356
+ for (const f of forms) {
357
+ const urls = f.data?.urls?.length ? ` [${f.data.urls.join(", ")}]` : "";
358
+ const purpose = f.data?.purpose ? ` ${f.data.purpose}` : "";
359
+ const facts = f.data?.facts ? Object.keys(f.data.facts).length : 0;
360
+ console.log(` ${f.slug}${urls}${purpose}${facts ? ` (${facts} answers)` : ""}`);
250
361
  }
362
+ } else {
363
+ console.log(" No form personas. Run: fastforms add form");
251
364
  }
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();
365
+
366
+ console.log();
367
+ }
368
+
369
+ // ---------------------------------------------------------------------------
370
+ // edit pick a persona and edit it
371
+ // ---------------------------------------------------------------------------
372
+
373
+ async function edit() {
374
+ const dir = findFastformsDir();
375
+ if (!dir) {
376
+ console.error(" No .fastforms/ directory found. Run 'fastforms init' first.\n");
377
+ process.exit(1);
378
+ }
379
+
380
+ const users = listPersonaFiles(dir, "user");
381
+ const businesses = listPersonaFiles(dir, "business");
382
+ const forms = listPersonaFiles(dir, "form");
383
+ const all = [
384
+ ...users.map((u) => ({ ...u, type: "user", label: `user: ${u.slug} (${u.data?.fullName || u.data?.name || "?"})` })),
385
+ ...businesses.map((b) => ({ ...b, type: "business", label: `biz: ${b.slug} (${b.data?.name || "?"})` })),
386
+ ...forms.map((f) => ({ ...f, type: "form", label: `form: ${f.slug} (${f.data?.organization || f.data?.name || "?"})` })),
387
+ ];
388
+
389
+ if (!all.length) {
390
+ console.error(" No personas found. Run 'fastforms init' first.\n");
391
+ process.exit(1);
259
392
  }
260
393
 
261
- saveUserPersona(dir, user);
262
- saveBusinessPersona(dir, biz);
263
- console.log(`\n Updated ${dir}/\n`);
394
+ console.log("\n Which persona to edit?\n");
395
+ all.forEach((p, i) => console.log(` ${i + 1}. ${p.label}`));
396
+ const ans = await ask(`\n Pick [1-${all.length}]: `);
397
+ const idx = Number(ans) - 1;
398
+ if (idx < 0 || idx >= all.length) { console.log(" Invalid selection.\n"); closeRL(); return; }
399
+
400
+ const picked = all[idx];
401
+ const dump = loadLocalPersonas(dir);
402
+
403
+ if (picked.type === "user") {
404
+ const existing = dump.personas.find((p) => p.name === picked.data?.name) || null;
405
+ console.log(`\n --- Edit user: ${picked.slug} ---\n`);
406
+ const updated = await promptUser(existing);
407
+ if (updated.name !== picked.data?.name) deletePersonaFile(dir, "user", picked.slug);
408
+ const slug = saveUserPersona(dir, updated);
409
+ console.log(`\n Updated users/${slug}.json\n`);
410
+ } else if (picked.type === "business") {
411
+ const existing = dump.businessPersonas.find((p) => p.name === picked.data?.name) || null;
412
+ console.log(`\n --- Edit business: ${picked.slug} ---\n`);
413
+ const updated = await promptBusiness(existing);
414
+ if (updated.name !== picked.data?.name) deletePersonaFile(dir, "business", picked.slug);
415
+ const slug = saveBusinessPersona(dir, updated);
416
+ console.log(`\n Updated businesses/${slug}.json\n`);
417
+ } else {
418
+ const existing = (dump.formPersonas || []).find((p) => p.name === picked.data?.name) || null;
419
+ console.log(`\n --- Edit form: ${picked.slug} ---\n`);
420
+ const updated = await promptForm(existing);
421
+ if (updated.name !== picked.data?.name) deletePersonaFile(dir, "form", picked.slug);
422
+ const slug = saveFormPersona(dir, updated);
423
+ console.log(`\n Updated forms/${slug}.json\n`);
424
+ }
425
+ closeRL();
426
+ }
427
+
428
+ // ---------------------------------------------------------------------------
429
+ // remove — delete a persona
430
+ // ---------------------------------------------------------------------------
431
+
432
+ async function remove() {
433
+ const dir = findFastformsDir();
434
+ if (!dir) {
435
+ console.error(" No .fastforms/ directory found.\n");
436
+ process.exit(1);
437
+ }
438
+
439
+ const users = listPersonaFiles(dir, "user");
440
+ const businesses = listPersonaFiles(dir, "business");
441
+ const forms = listPersonaFiles(dir, "form");
442
+ const all = [
443
+ ...users.map((u) => ({ ...u, type: "user", label: `user: ${u.slug} (${u.data?.fullName || u.data?.name || "?"})` })),
444
+ ...businesses.map((b) => ({ ...b, type: "business", label: `biz: ${b.slug} (${b.data?.name || "?"})` })),
445
+ ...forms.map((f) => ({ ...f, type: "form", label: `form: ${f.slug} (${f.data?.name || "?"})` })),
446
+ ];
447
+
448
+ if (!all.length) {
449
+ console.error(" No personas to remove.\n");
450
+ process.exit(1);
451
+ }
452
+
453
+ console.log("\n Which persona to remove?\n");
454
+ all.forEach((p, i) => console.log(` ${i + 1}. ${p.label}`));
455
+ const ans = await ask(`\n Pick [1-${all.length}]: `);
456
+ const idx = Number(ans) - 1;
457
+ if (idx < 0 || idx >= all.length) { console.log(" Invalid selection.\n"); closeRL(); return; }
458
+
459
+ const picked = all[idx];
460
+ const confirm = await ask(` Delete ${picked.type}/${picked.slug}? [y/N]: `);
461
+ if (confirm.toLowerCase() === "y") {
462
+ deletePersonaFile(dir, picked.type, picked.slug);
463
+ const dirName = picked.type === "user" ? "users" : picked.type === "business" ? "businesses" : "forms";
464
+ console.log(` Removed ${dirName}/${picked.slug}.json\n`);
465
+ } else {
466
+ console.log(" Cancelled.\n");
467
+ }
264
468
  closeRL();
265
469
  }
266
470
 
@@ -285,14 +489,11 @@ async function fill() {
285
489
  let browser;
286
490
 
287
491
  if (hasFlag("--web")) {
288
- // Web app mode
289
492
  console.log(" Pulling personas from web app...");
290
493
  browser = await connectToChrome(port);
291
494
  dump = await pullPersonas(browser);
292
495
  } else {
293
- // Local mode (default)
294
- const dirArg = flag("--dir");
295
- const dir = dirArg || findFastformsDir();
496
+ const dir = findFastformsDir();
296
497
 
297
498
  if (!dir) {
298
499
  console.error(" No .fastforms/ directory found.");
@@ -307,7 +508,13 @@ async function fill() {
307
508
 
308
509
  const personas = dump.personas || [];
309
510
  const bizPersonas = dump.businessPersonas || [];
310
- console.log(` Found ${personas.length} user persona(s), ${bizPersonas.length} business persona(s).`);
511
+ const formPersonas = dump.formPersonas || [];
512
+ const counts = [
513
+ `${personas.length} user`,
514
+ `${bizPersonas.length} business`,
515
+ `${formPersonas.length} form`,
516
+ ].join(", ");
517
+ console.log(` Found ${counts} persona(s).`);
311
518
 
312
519
  if (!personas.length && !bizPersonas.length) {
313
520
  console.error("\n No personas found.");
@@ -322,15 +529,29 @@ async function fill() {
322
529
 
323
530
  const userHint = flag("--user");
324
531
  const bizHint = flag("--business");
532
+ const formHint = flag("--form");
325
533
  const { user, biz } = await selectPersonas(dump, userHint, bizHint);
326
534
 
535
+ // Form persona: auto-match by URL, then hint, then interactive
536
+ let form = null;
537
+ if (formPersonas.length) {
538
+ if (formHint) {
539
+ form = formPersonas.find((f) =>
540
+ f.name.toLowerCase().includes(formHint.toLowerCase())
541
+ ) || null;
542
+ }
543
+ if (!form) {
544
+ form = await selectFormPersona(formPersonas, formUrl);
545
+ }
546
+ }
547
+
327
548
  if (!user && !biz) {
328
549
  console.error("\n No matching personas.");
329
550
  browser.disconnect();
330
551
  process.exit(1);
331
552
  }
332
553
 
333
- showPersonaDetails(user, biz);
554
+ showPersonaDetails(user, biz, form);
334
555
 
335
556
  const pages = await browser.pages();
336
557
  const host = new URL(formUrl).host;
@@ -345,13 +566,23 @@ async function fill() {
345
566
  console.log(`\n Opened ${formUrl}`);
346
567
  }
347
568
 
348
- await fillForm(page, formUrl, user, biz);
569
+ await fillForm(page, formUrl, user, biz, form);
349
570
 
350
571
  if (hasFlag("--web")) {
351
572
  await offerSetDefaults(user, biz);
573
+ } else {
574
+ const dir = findFastformsDir();
575
+ if (dir) {
576
+ const patch = {};
577
+ if (user) patch.defaultUser = user.name;
578
+ if (biz) patch.defaultBusiness = biz.name;
579
+ if (form) patch.defaultForm = form.name;
580
+ saveDefaults(dir, patch);
581
+ }
352
582
  }
353
583
 
354
584
  browser.disconnect();
585
+ closeRL();
355
586
  }
356
587
 
357
588
  // ---------------------------------------------------------------------------
@@ -382,9 +613,18 @@ switch (command) {
382
613
  case "init":
383
614
  init().catch((e) => { console.error(e.message); process.exit(1); });
384
615
  break;
616
+ case "add":
617
+ addPersona().catch((e) => { console.error(e.message); process.exit(1); });
618
+ break;
619
+ case "list":
620
+ listAll();
621
+ break;
385
622
  case "edit":
386
623
  edit().catch((e) => { console.error(e.message); process.exit(1); });
387
624
  break;
625
+ case "remove":
626
+ remove().catch((e) => { console.error(e.message); process.exit(1); });
627
+ break;
388
628
  case "fill":
389
629
  fill().catch((e) => { console.error(e.message); process.exit(1); });
390
630
  break;