@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/README.md +67 -18
- package/SKILL.md +59 -21
- package/bin/fastforms.js +395 -155
- package/lib/fill.js +84 -59
- package/lib/local.js +119 -17
- package/lib/personas.js +100 -23
- package/package.json +1 -1
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
|
|
61
|
-
fastforms
|
|
62
|
-
fastforms
|
|
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
|
|
70
|
-
--business <hint> Business persona name/hint
|
|
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.
|
|
76
|
-
3.
|
|
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
|
-
//
|
|
105
|
+
// Shared persona builder prompts
|
|
82
106
|
// ---------------------------------------------------------------------------
|
|
83
107
|
|
|
84
|
-
async function
|
|
85
|
-
const
|
|
86
|
-
const
|
|
108
|
+
async function promptUser(existing) {
|
|
109
|
+
const ep = existing?.profile || {};
|
|
110
|
+
const u = userTemplate();
|
|
87
111
|
|
|
88
|
-
|
|
112
|
+
const show = (val) => val ? ` [${String(val).slice(0, 40)}]` : "";
|
|
89
113
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
253
|
+
console.log(" --- User persona ---\n");
|
|
254
|
+
const user = await promptUser();
|
|
157
255
|
ensureDir(dir);
|
|
158
|
-
saveUserPersona(dir, user);
|
|
159
|
-
|
|
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
|
|
162
|
-
console.log("
|
|
163
|
-
console.log("
|
|
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
|
-
//
|
|
279
|
+
// add — add a single persona
|
|
170
280
|
// ---------------------------------------------------------------------------
|
|
171
281
|
|
|
172
|
-
async function
|
|
173
|
-
const
|
|
174
|
-
|
|
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
|
|
182
|
-
const existing = dump.personas[0];
|
|
183
|
-
const existingBiz = dump.businessPersonas[0];
|
|
322
|
+
const defaults = loadDefaults(dir);
|
|
184
323
|
|
|
185
|
-
|
|
324
|
+
const users = listPersonaFiles(dir, "user");
|
|
325
|
+
const businesses = listPersonaFiles(dir, "business");
|
|
326
|
+
const forms = listPersonaFiles(dir, "form");
|
|
186
327
|
|
|
187
|
-
|
|
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 (
|
|
209
|
-
console.log("
|
|
210
|
-
for (const
|
|
211
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|