@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/lib/fill.js CHANGED
@@ -2,6 +2,14 @@ import { getCustomFact } from "./personas.js";
2
2
 
3
3
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
4
4
 
5
+ /**
6
+ * Look up a value from the form persona's facts first, then fall back.
7
+ * Form persona facts override everything — they're form-specific answers.
8
+ */
9
+ function formFact(form, key) {
10
+ return getCustomFact(form, key);
11
+ }
12
+
5
13
  export async function fillByLabel(page, label, value, opts = {}) {
6
14
  if (!value) return false;
7
15
  return page.evaluate((lbl, val, ta) => {
@@ -55,82 +63,95 @@ export async function clickCheckbox(page, textHint) {
55
63
  }, textHint);
56
64
  }
57
65
 
58
- export function buildNitroData(user, biz) {
66
+ export function buildNitroData(user, biz, form) {
59
67
  const u = user?.profile || {};
60
68
  const b = biz?.profile || {};
61
69
  const fact = (p, k) => getCustomFact(p, k);
70
+ const ff = (k) => formFact(form, k);
62
71
 
63
72
  return {
64
73
  fields: [
65
- { label: "Company / Project name", value: b.productName || b.companyName || b.name || "" },
66
- { label: "Email", value: u.email || "" },
67
- { label: "One-line description", value: b.oneLiner || "" },
68
- { label: "What are you building", value: b.solution || [b.problem, b.solution].filter(Boolean).join("\n\n"), textarea: true },
69
- { label: "Full Name", value: u.fullName || "" },
70
- { label: "Role in the company", value: u.currentRole || "" },
71
- { label: "X Handle", value: fact(user, "x handle") || fact(user, "twitter") || "" },
72
- { label: "LinkedIn username", value: u.linkedIn || "" },
73
- { label: "Telegram", value: fact(user, "telegram") || "" },
74
- { label: "GitHub username", value: u.github || "" },
75
- { label: "Why are you the right founder", value: [u.bio, u.keySkills ? `Key skills: ${u.keySkills}` : "", u.favoriteProjects ? `Notable projects: ${u.favoriteProjects}` : ""].filter(Boolean).join("\n\n"), textarea: true },
76
- { label: "video content or long form writing", value: u.portfolio || "", textarea: true },
77
- { label: "How did the founders meet", value: fact(user, "founders met") || fact(biz, "founders") || `I'm a solo founder building ${b.productName || b.companyName || "this project"} full-time.`, textarea: true },
78
- { label: "Current location of founders", value: u.location || b.location || "" },
79
- { label: "how do you plan to spend", value: fact(biz, "spend") || fact(biz, "500k") || "", textarea: true },
80
- { label: "What problem are you solving", value: b.problem || "", textarea: true },
81
- { label: "closest comparables", value: b.differentiators || "", textarea: true },
82
- { label: "Product Link", value: b.website || "" },
83
- { label: "What changed in the tech or market", value: fact(biz, "timing") || fact(biz, "why now") || b.rawFacts || "", textarea: true },
84
- { label: "Dune Dashboard", value: "" },
85
- { label: "Google Analytics", value: "", textarea: true },
86
- { label: "target segment", value: b.targetUsers || "", textarea: true },
87
- { label: "wedge into the market", value: b.businessModel || b.differentiators || "", textarea: true },
88
- { label: "traction have you achieved", value: b.traction || "", textarea: true },
89
- { label: "Runway at current burn", value: "12" },
90
- { label: "What attracts you the most about Nitro", value: fact(biz, "nitro") || fact(biz, "accelerator") || `The density of high-signal founders and the NYC residency. ${b.productName || "Our product"} is at a stage where concentrated feedback from experienced operators would dramatically accelerate our trajectory.`, textarea: true },
91
- { label: "Pick one of the mentors", value: fact(biz, "mentor") || "", textarea: true },
92
- { label: "What did you get done last week", value: fact(biz, "last week") || fact(user, "last week") || "", textarea: true },
74
+ { label: "Company / Project name", value: ff("company") || b.productName || b.companyName || b.name || "" },
75
+ { label: "Email", value: ff("email") || u.email || "" },
76
+ { label: "One-line description", value: ff("one-line") || b.oneLiner || "" },
77
+ { label: "What are you building", value: ff("building") || b.solution || [b.problem, b.solution].filter(Boolean).join("\n\n"), textarea: true },
78
+ { label: "Full Name", value: ff("full name") || u.fullName || "" },
79
+ { label: "Role in the company", value: ff("role") || u.currentRole || "" },
80
+ { label: "X Handle", value: ff("x handle") || fact(user, "x handle") || fact(user, "twitter") || "" },
81
+ { label: "LinkedIn username", value: ff("linkedin") || u.linkedIn || "" },
82
+ { label: "Telegram", value: ff("telegram") || fact(user, "telegram") || "" },
83
+ { label: "GitHub username", value: ff("github") || u.github || "" },
84
+ { label: "Why are you the right founder", value: ff("right founder") || [u.bio, u.keySkills ? `Key skills: ${u.keySkills}` : "", u.favoriteProjects ? `Notable projects: ${u.favoriteProjects}` : ""].filter(Boolean).join("\n\n"), textarea: true },
85
+ { label: "video content or long form writing", value: ff("video") || ff("writing") || u.portfolio || "", textarea: true },
86
+ { label: "How did the founders meet", value: ff("founders meet") || ff("founders met") || fact(user, "founders met") || fact(biz, "founders") || `I'm a solo founder building ${b.productName || b.companyName || "this project"} full-time.`, textarea: true },
87
+ { label: "Current location of founders", value: ff("location") || u.location || b.location || "" },
88
+ { label: "how do you plan to spend", value: ff("spend") || ff("500k") || fact(biz, "spend") || fact(biz, "500k") || "", textarea: true },
89
+ { label: "What problem are you solving", value: ff("problem") || b.problem || "", textarea: true },
90
+ { label: "closest comparables", value: ff("comparables") || ff("competitors") || b.differentiators || "", textarea: true },
91
+ { label: "Product Link", value: ff("product link") || b.website || "" },
92
+ { label: "What changed in the tech or market", value: ff("timing") || ff("why now") || fact(biz, "timing") || fact(biz, "why now") || b.rawFacts || "", textarea: true },
93
+ { label: "Dune Dashboard", value: ff("dune") || "" },
94
+ { label: "Google Analytics", value: ff("analytics") || "", textarea: true },
95
+ { label: "target segment", value: ff("target") || b.targetUsers || "", textarea: true },
96
+ { label: "wedge into the market", value: ff("wedge") || ff("market") || b.businessModel || b.differentiators || "", textarea: true },
97
+ { label: "traction have you achieved", value: ff("traction") || b.traction || "", textarea: true },
98
+ { label: "Runway at current burn", value: ff("runway") || "12" },
99
+ { label: "What attracts you the most about Nitro", value: ff("attracts") || ff("nitro") || fact(biz, "nitro") || fact(biz, "accelerator") || `The density of high-signal founders and the NYC residency. ${b.productName || "Our product"} is at a stage where concentrated feedback from experienced operators would dramatically accelerate our trajectory.`, textarea: true },
100
+ { label: "Pick one of the mentors", value: ff("mentor") || fact(biz, "mentor") || "", textarea: true },
101
+ { label: "What did you get done last week", value: ff("last week") || fact(biz, "last week") || fact(user, "last week") || "", textarea: true },
93
102
  ],
94
103
  radios: [
95
- { section: "committed full-time", value: "yes" },
96
- { section: "raised funding before", value: "no" },
97
- { section: "currently fundraising", value: "yes" },
98
- { section: "exclusively in Nitro", value: "yes" },
99
- { section: "attend the full 1-month NYC", value: "yes" },
104
+ { section: "committed full-time", value: ff("full-time") || "yes" },
105
+ { section: "raised funding before", value: ff("raised funding") || "no" },
106
+ { section: "currently fundraising", value: ff("fundraising") || "yes" },
107
+ { section: "exclusively in Nitro", value: ff("exclusively") || "yes" },
108
+ { section: "attend the full 1-month NYC", value: ff("attend") || "yes" },
100
109
  ],
101
110
  checkboxes: ["MVP / demo exists"],
102
111
  };
103
112
  }
104
113
 
105
- export async function genericFill(page, user, biz) {
114
+ export async function genericFill(page, user, biz, form) {
106
115
  const u = user?.profile || {};
107
116
  const b = biz?.profile || {};
108
117
  const fact = (p, k) => getCustomFact(p, k);
118
+ const ff = (k) => formFact(form, k);
109
119
 
110
120
  const fieldMap = [
111
- { hints: ["company", "project name", "organization"], value: b.productName || b.companyName || b.name || "" },
112
- { hints: ["email"], value: u.email || "" },
113
- { hints: ["full name", "your name", "first name"], value: u.fullName || "" },
114
- { hints: ["one-line", "one liner", "tagline", "short description"], value: b.oneLiner || "" },
115
- { hints: ["what are you building", "describe your product", "about your project"], value: b.solution || "", textarea: true },
116
- { hints: ["role", "title", "position"], value: u.currentRole || "" },
117
- { hints: ["x handle", "twitter"], value: fact(user, "x handle") || fact(user, "twitter") || "" },
118
- { hints: ["linkedin"], value: u.linkedIn || "" },
119
- { hints: ["telegram"], value: fact(user, "telegram") || "" },
120
- { hints: ["github"], value: u.github || "" },
121
- { hints: ["website", "url", "product link"], value: b.website || u.portfolio || "" },
122
- { hints: ["phone", "tel"], value: u.phone || "" },
123
- { hints: ["location", "city", "where are you based"], value: u.location || b.location || "" },
124
- { hints: ["bio", "about yourself", "tell us about you"], value: u.bio || "", textarea: true },
125
- { hints: ["problem", "what problem"], value: b.problem || "", textarea: true },
126
- { hints: ["solution", "how does it work"], value: b.solution || "", textarea: true },
127
- { hints: ["traction", "progress", "metrics"], value: b.traction || "", textarea: true },
128
- { hints: ["target", "customer", "user"], value: b.targetUsers || "", textarea: true },
129
- { hints: ["business model", "revenue", "monetiz"], value: b.businessModel || "", textarea: true },
130
- { hints: ["differentiator", "competitive", "unique"], value: b.differentiators || "", textarea: true },
131
- { hints: ["why you", "right founder", "why are you"], value: u.bio || "", textarea: true },
121
+ { hints: ["company", "project name", "organization"], value: ff("company") || b.productName || b.companyName || b.name || "" },
122
+ { hints: ["email"], value: ff("email") || u.email || "" },
123
+ { hints: ["full name", "your name", "first name"], value: ff("full name") || u.fullName || "" },
124
+ { hints: ["one-line", "one liner", "tagline", "short description"], value: ff("one-liner") || b.oneLiner || "" },
125
+ { hints: ["what are you building", "describe your product", "about your project"], value: ff("building") || b.solution || "", textarea: true },
126
+ { hints: ["role", "title", "position"], value: ff("role") || u.currentRole || "" },
127
+ { hints: ["x handle", "twitter"], value: ff("x handle") || fact(user, "x handle") || fact(user, "twitter") || "" },
128
+ { hints: ["linkedin"], value: ff("linkedin") || u.linkedIn || "" },
129
+ { hints: ["telegram"], value: ff("telegram") || fact(user, "telegram") || "" },
130
+ { hints: ["github"], value: ff("github") || u.github || "" },
131
+ { hints: ["website", "url", "product link"], value: ff("website") || b.website || u.portfolio || "" },
132
+ { hints: ["phone", "tel"], value: ff("phone") || u.phone || "" },
133
+ { hints: ["location", "city", "where are you based"], value: ff("location") || u.location || b.location || "" },
134
+ { hints: ["bio", "about yourself", "tell us about you"], value: ff("bio") || u.bio || "", textarea: true },
135
+ { hints: ["problem", "what problem"], value: ff("problem") || b.problem || "", textarea: true },
136
+ { hints: ["solution", "how does it work"], value: ff("solution") || b.solution || "", textarea: true },
137
+ { hints: ["traction", "progress", "metrics"], value: ff("traction") || b.traction || "", textarea: true },
138
+ { hints: ["target", "customer", "user"], value: ff("target") || b.targetUsers || "", textarea: true },
139
+ { hints: ["business model", "revenue", "monetiz"], value: ff("business model") || b.businessModel || "", textarea: true },
140
+ { hints: ["differentiator", "competitive", "unique"], value: ff("differentiator") || b.differentiators || "", textarea: true },
141
+ { hints: ["why you", "right founder", "why are you"], value: ff("right founder") || u.bio || "", textarea: true },
132
142
  ];
133
143
 
144
+ // Append any form persona facts that don't already map to standard fields
145
+ if (form?.customFacts?.length) {
146
+ const usedKeys = new Set(fieldMap.flatMap((f) => f.hints));
147
+ for (const cf of form.customFacts) {
148
+ if (!cf.enabled || !cf.value) continue;
149
+ const keyLower = cf.key.toLowerCase();
150
+ if ([...usedKeys].some((h) => keyLower.includes(h) || h.includes(keyLower))) continue;
151
+ fieldMap.push({ hints: [cf.key], value: cf.value, textarea: cf.value.length > 100 });
152
+ }
153
+ }
154
+
134
155
  let filled = 0, skipped = 0;
135
156
  for (const f of fieldMap) {
136
157
  if (!f.value) { skipped++; continue; }
@@ -146,12 +167,16 @@ export async function genericFill(page, user, biz) {
146
167
  return { filled, skipped };
147
168
  }
148
169
 
149
- export async function fillForm(page, formUrl, user, biz) {
170
+ export async function fillForm(page, formUrl, user, biz, form) {
150
171
  console.log(`\n Filling form...\n`);
172
+ if (form) {
173
+ const fp = form.profile || {};
174
+ if (fp.organization) console.log(` Form: ${fp.organization}${fp.purpose ? ` — ${fp.purpose}` : ""}`);
175
+ }
151
176
  await sleep(2000);
152
177
 
153
178
  if (formUrl.includes("nitroacc.xyz")) {
154
- const data = buildNitroData(user, biz);
179
+ const data = buildNitroData(user, biz, form);
155
180
  let filled = 0, skipped = 0;
156
181
 
157
182
  for (const f of data.fields) {
@@ -180,7 +205,7 @@ export async function fillForm(page, formUrl, user, biz) {
180
205
 
181
206
  console.log(`\n Filled ${filled}, skipped ${skipped} (empty persona fields).`);
182
207
  } else {
183
- const { filled, skipped } = await genericFill(page, user, biz);
208
+ const { filled, skipped } = await genericFill(page, user, biz, form);
184
209
  console.log(`\n Filled ${filled}, skipped ${skipped} (empty or no match).`);
185
210
  }
186
211
 
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;
@@ -82,6 +87,23 @@ function normalizeBusiness(raw) {
82
87
  };
83
88
  }
84
89
 
90
+ function normalizeForm(raw) {
91
+ if (!raw) return null;
92
+ const { name, facts, urls, ...rest } = raw;
93
+ return {
94
+ name: name || "unknown form",
95
+ urls: urls || (rest.url ? [rest.url] : []),
96
+ profile: {
97
+ organization: rest.organization || rest.org || name || "",
98
+ purpose: rest.purpose || "",
99
+ notes: rest.notes || "",
100
+ deadline: rest.deadline || "",
101
+ requirements: rest.requirements || "",
102
+ },
103
+ customFacts: factsToArray(facts),
104
+ };
105
+ }
106
+
85
107
  function factsToArray(facts) {
86
108
  if (!facts || typeof facts !== "object") return [];
87
109
  return Object.entries(facts).map(([key, value], i) => ({
@@ -92,29 +114,92 @@ function factsToArray(facts) {
92
114
  }));
93
115
  }
94
116
 
117
+ // ---------------------------------------------------------------------------
118
+ // Multi-persona loading: reads users/ and businesses/ subdirs.
119
+ // Falls back to legacy user.json / business.json for backward compat.
120
+ // ---------------------------------------------------------------------------
121
+
122
+ function loadDir(dir, normalizer) {
123
+ if (!existsSync(dir)) return [];
124
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json")).sort();
125
+ return files.map((f) => normalizer(readJson(join(dir, f)))).filter(Boolean);
126
+ }
127
+
95
128
  export function loadLocalPersonas(dir) {
96
- const userPath = join(dir, "user.json");
97
- const bizPath = join(dir, "business.json");
129
+ const usersDir = join(dir, "users");
130
+ const bizDir = join(dir, "businesses");
98
131
 
99
- const rawUser = readJson(userPath);
100
- const rawBiz = readJson(bizPath);
132
+ let personas = loadDir(usersDir, normalizeUser);
133
+ let businessPersonas = loadDir(bizDir, normalizeBusiness);
101
134
 
102
- const personas = rawUser ? [normalizeUser(rawUser)] : [];
103
- const businessPersonas = rawBiz ? [normalizeBusiness(rawBiz)] : [];
135
+ // Backward compat: if subdirs are empty, check legacy single files
136
+ if (!personas.length) {
137
+ const legacy = readJson(join(dir, "user.json"));
138
+ if (legacy) personas = [normalizeUser(legacy)];
139
+ }
140
+ if (!businessPersonas.length) {
141
+ const legacy = readJson(join(dir, "business.json"));
142
+ if (legacy) businessPersonas = [normalizeBusiness(legacy)];
143
+ }
104
144
 
105
- return { personas, businessPersonas };
145
+ const formPersonas = loadDir(join(dir, "forms"), normalizeForm);
146
+
147
+ return { personas, businessPersonas, formPersonas };
106
148
  }
107
149
 
150
+ // ---------------------------------------------------------------------------
151
+ // Save personas into named files under users/ or businesses/
152
+ // ---------------------------------------------------------------------------
153
+
108
154
  export function saveUserPersona(dir, data) {
109
- ensureDir(dir);
110
- writeJson(join(dir, "user.json"), data);
155
+ const subdir = ensureDir(join(dir, "users"));
156
+ const slug = slugify(data.name || "default");
157
+ writeJson(join(subdir, `${slug}.json`), data);
158
+ return slug;
111
159
  }
112
160
 
113
161
  export function saveBusinessPersona(dir, data) {
114
- ensureDir(dir);
115
- writeJson(join(dir, "business.json"), data);
162
+ const subdir = ensureDir(join(dir, "businesses"));
163
+ const slug = slugify(data.name || "default");
164
+ writeJson(join(subdir, `${slug}.json`), data);
165
+ return slug;
116
166
  }
117
167
 
168
+ export function saveFormPersona(dir, data) {
169
+ const subdir = ensureDir(join(dir, "forms"));
170
+ const slug = slugify(data.name || "form");
171
+ writeJson(join(subdir, `${slug}.json`), data);
172
+ return slug;
173
+ }
174
+
175
+ const TYPE_DIRS = { user: "users", business: "businesses", form: "forms" };
176
+
177
+ export function deletePersonaFile(dir, type, slug) {
178
+ const subdir = TYPE_DIRS[type] || type;
179
+ const path = join(dir, subdir, `${slug}.json`);
180
+ if (existsSync(path)) unlinkSync(path);
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // List raw persona files (for edit/delete commands)
185
+ // ---------------------------------------------------------------------------
186
+
187
+ export function listPersonaFiles(dir, type) {
188
+ const subdir = join(dir, TYPE_DIRS[type] || type);
189
+ if (!existsSync(subdir)) return [];
190
+ return readdirSync(subdir)
191
+ .filter((f) => f.endsWith(".json"))
192
+ .sort()
193
+ .map((f) => {
194
+ const data = readJson(join(subdir, f));
195
+ return { slug: basename(f, ".json"), file: f, data };
196
+ });
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Defaults
201
+ // ---------------------------------------------------------------------------
202
+
118
203
  export function loadDefaults(dir) {
119
204
  return readJson(join(dir, "defaults.json")) || {};
120
205
  }
@@ -124,6 +209,10 @@ export function saveDefaults(dir, patch) {
124
209
  writeJson(join(dir, "defaults.json"), { ...current, ...patch });
125
210
  }
126
211
 
212
+ // ---------------------------------------------------------------------------
213
+ // Templates
214
+ // ---------------------------------------------------------------------------
215
+
127
216
  export function userTemplate() {
128
217
  return {
129
218
  name: "",
@@ -154,3 +243,16 @@ export function businessTemplate() {
154
243
  facts: {},
155
244
  };
156
245
  }
246
+
247
+ export function formTemplate() {
248
+ return {
249
+ name: "",
250
+ urls: [],
251
+ organization: "",
252
+ purpose: "",
253
+ notes: "",
254
+ deadline: "",
255
+ requirements: "",
256
+ facts: {},
257
+ };
258
+ }
package/lib/personas.js CHANGED
@@ -92,7 +92,7 @@ function printPersonaSummary(label, persona, profileKeys) {
92
92
  if (facts.length) console.log(` Custom facts: ${facts.length}`);
93
93
  }
94
94
 
95
- export function showPersonaDetails(user, biz) {
95
+ export function showPersonaDetails(user, biz, form) {
96
96
  printPersonaSummary("User persona", user, [
97
97
  ["fullName", "Name"],
98
98
  ["email", "Email"],
@@ -108,36 +108,113 @@ export function showPersonaDetails(user, biz) {
108
108
  ["website", "Website"],
109
109
  ["category", "Category"],
110
110
  ]);
111
+ if (form) {
112
+ console.log(`\n Form persona: ${form.name}`);
113
+ const fp = form.profile || {};
114
+ if (fp.organization) console.log(` Organization: ${fp.organization}`);
115
+ if (fp.purpose) console.log(` Purpose: ${fp.purpose}`);
116
+ if (fp.deadline) console.log(` Deadline: ${fp.deadline}`);
117
+ if (fp.notes) console.log(` Notes: ${String(fp.notes).slice(0, 80)}${String(fp.notes).length > 80 ? "..." : ""}`);
118
+ const facts = (form.customFacts || []).filter((f) => f.enabled !== false && f.value);
119
+ if (facts.length) console.log(` Form-specific answers: ${facts.length}`);
120
+ }
111
121
  }
112
122
 
113
- export async function selectPersonas(dump, userHint, bizHint) {
114
- const config = loadConfig();
115
- const personas = dump.personas || [];
116
- const bizPersonas = dump.businessPersonas || [];
123
+ // ---------------------------------------------------------------------------
124
+ // Interactive persona selection — supports multiple personas from any source
125
+ // ---------------------------------------------------------------------------
117
126
 
118
- // Use defaults if no hint and defaults are saved
119
- const effectiveUserHint = userHint || config.defaultUser || "";
120
- const effectiveBizHint = bizHint || config.defaultBusiness || "";
127
+ async function pickFromList(label, list, hint, keys, defaultName) {
128
+ if (!list?.length) return null;
129
+ if (list.length === 1) return list[0];
121
130
 
122
- let user = pickPersona(personas, effectiveUserHint, ["name", "profile.fullName"]);
123
- let biz = pickPersona(bizPersonas, effectiveBizHint, ["name", "profile.companyName", "profile.productName"]);
131
+ // Try hint-based match
132
+ if (hint) {
133
+ const match = pickPersona(list, hint, keys);
134
+ if (match) return match;
135
+ }
124
136
 
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];
137
+ // Try default
138
+ if (defaultName) {
139
+ const match = list.find((p) => p.name === defaultName);
140
+ if (match) return match;
132
141
  }
133
142
 
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];
143
+ // Interactive selection
144
+ console.log(`\n ${label}:\n`);
145
+ list.forEach((p, i) => {
146
+ const detail = keys.map((k) => k.split(".").reduce((o, s) => o?.[s], p)).filter(Boolean)[0] || "";
147
+ console.log(` ${i + 1}. ${p.name}${detail ? ` (${detail})` : ""}`);
148
+ });
149
+ const ans = await ask(`\n Pick ${label.toLowerCase()} [1-${list.length}]: `);
150
+ const idx = Number(ans) - 1;
151
+ if (idx >= 0 && idx < list.length) return list[idx];
152
+ return list[0];
153
+ }
154
+
155
+ export function matchFormByUrl(formPersonas, targetUrl) {
156
+ if (!formPersonas?.length || !targetUrl) return null;
157
+ const lower = targetUrl.toLowerCase();
158
+ for (const fp of formPersonas) {
159
+ for (const u of (fp.urls || [])) {
160
+ if (u && lower.includes(u.toLowerCase())) return fp;
161
+ }
162
+ const orgSlug = (fp.name || "").toLowerCase().replace(/[^a-z0-9]/g, "");
163
+ if (orgSlug && lower.includes(orgSlug)) return fp;
140
164
  }
165
+ return null;
166
+ }
167
+
168
+ export async function selectFormPersona(formPersonas, targetUrl) {
169
+ if (!formPersonas?.length) return null;
170
+
171
+ // Auto-match by URL
172
+ const autoMatch = matchFormByUrl(formPersonas, targetUrl);
173
+ if (autoMatch) {
174
+ console.log(` Auto-matched form persona: ${autoMatch.name}`);
175
+ return autoMatch;
176
+ }
177
+
178
+ if (formPersonas.length === 1) return formPersonas[0];
179
+
180
+ // Interactive
181
+ console.log(`\n Form personas:\n`);
182
+ formPersonas.forEach((f, i) => {
183
+ const org = f.profile?.organization || "";
184
+ console.log(` ${i + 1}. ${f.name}${org ? ` (${org})` : ""}`);
185
+ });
186
+ console.log(` ${formPersonas.length + 1}. (none — skip form persona)`);
187
+ const ans = await ask(`\n Pick form persona [1-${formPersonas.length + 1}]: `);
188
+ const idx = Number(ans) - 1;
189
+ if (idx >= 0 && idx < formPersonas.length) return formPersonas[idx];
190
+ return null;
191
+ }
192
+
193
+ export async function selectPersonas(dump, userHint, bizHint) {
194
+ const config = loadConfig();
195
+ const personas = dump.personas || [];
196
+ const bizPersonas = dump.businessPersonas || [];
197
+
198
+ const effectiveUserHint = userHint || "";
199
+ const effectiveBizHint = bizHint || "";
200
+ const defaultUser = config.defaultUser || "";
201
+ const defaultBiz = config.defaultBusiness || "";
202
+
203
+ const user = await pickFromList(
204
+ "User personas",
205
+ personas,
206
+ effectiveUserHint,
207
+ ["name", "profile.fullName"],
208
+ defaultUser,
209
+ );
210
+
211
+ const biz = await pickFromList(
212
+ "Business personas",
213
+ bizPersonas,
214
+ effectiveBizHint,
215
+ ["name", "profile.companyName", "profile.productName"],
216
+ defaultBiz,
217
+ );
141
218
 
142
219
  return { user, biz };
143
220
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1dolinski/fastforms",
3
- "version": "0.1.2",
3
+ "version": "0.3.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": {