@dench.com/cli 3.5.8-staging.350 → 3.6.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.
@@ -0,0 +1,141 @@
1
+ export const SEARCH_LIST_FLAGS = [
2
+ ["--titles", "titles", "titles"],
3
+ ["--past-titles", "pastTitles", "pastTitles"],
4
+ ["--person-names", "personNames"],
5
+ ["--locations", "locations", "locations"],
6
+ ["--seniorities", "seniorities", "seniorities"],
7
+ ["--past-seniorities", "pastSeniorities", "pastSeniorities"],
8
+ ["--profile-keywords", "profileKeywords", "profileKeywords"],
9
+ ["--company-keywords", "companyKeywords", "companyKeywords"],
10
+ ["--industries", "industries", "industries"],
11
+ ["--functions", "jobFunctions", "functions"],
12
+ ["--sub-functions", "subFunctions", "subFunctions"],
13
+ ["--company-domains", "companyDomains"],
14
+ ["--company-names", "companyNames"],
15
+ ["--past-company-names", "pastCompanyNames"],
16
+ ["--company-hq", "companyHeadquarters", "companyHeadquarters"],
17
+ ["--company-types", "companyTypes", "companyTypes"],
18
+ ["--universities", "universities", "universities"],
19
+ ["--skills", "skills", "skills"],
20
+ ["--languages", "languages", "languages"],
21
+ ["--headcount", "headcountRanges", "headcount"],
22
+ ] as const;
23
+
24
+ const EXCLUDE_ALIASES: Record<string, readonly string[]> = {
25
+ "--company-domains": ["--not-domains", "--exclude-domains"],
26
+ };
27
+
28
+ export function excludeFlagsFor(flag: string): string[] {
29
+ const name = flag.slice(2);
30
+ return [
31
+ `--not-${name}`,
32
+ `--exclude-${name}`,
33
+ ...(EXCLUDE_ALIASES[flag] ?? []),
34
+ ];
35
+ }
36
+
37
+ export const SEARCH_TEXT_FLAGS = [
38
+ ["--keywords", "keywords"],
39
+ ["--company-domain", "companyDomain"],
40
+ ] as const;
41
+
42
+ export const SEARCH_RANGE_FLAGS = [
43
+ ["--years-in-role", "yearsInRole"],
44
+ ["--years-at-company", "yearsAtCompany"],
45
+ ["--days-since-job-change", "daysSinceJobChange"],
46
+ ["--founded-years", "foundedYears"],
47
+ ] as const;
48
+
49
+ export const SEARCH_REQUEST_FLAGS = [
50
+ "--search",
51
+ "--filters",
52
+ "--people-file",
53
+ "--csv",
54
+ "--map",
55
+ "--list",
56
+ "--enrichment",
57
+ "--limit",
58
+ "--page-token",
59
+ "--preview",
60
+ "--exact",
61
+ ] as const;
62
+
63
+ const SEARCH_FLAG_NAMES = new Set<string>([
64
+ ...SEARCH_REQUEST_FLAGS,
65
+ ...SEARCH_LIST_FLAGS.map(([flag]) => flag),
66
+ ...SEARCH_LIST_FLAGS.flatMap(([flag]) => excludeFlagsFor(flag)),
67
+ ...SEARCH_TEXT_FLAGS.map(([flag]) => flag),
68
+ ...SEARCH_RANGE_FLAGS.map(([flag]) => flag),
69
+ ]);
70
+
71
+ export function usesSearchFlags(args: readonly string[]): boolean {
72
+ return args.some((token) => SEARCH_FLAG_NAMES.has(token));
73
+ }
74
+
75
+ export const FILTER_KEY_ALIASES: Record<string, string> = {
76
+ title: "titles",
77
+ jobtitle: "titles",
78
+ jobtitles: "titles",
79
+ role: "titles",
80
+ roles: "titles",
81
+ location: "locations",
82
+ country: "locations",
83
+ countries: "locations",
84
+ city: "locations",
85
+ cities: "locations",
86
+ region: "locations",
87
+ regions: "locations",
88
+ geo: "locations",
89
+ geography: "locations",
90
+ industry: "industries",
91
+ sector: "industries",
92
+ sectors: "industries",
93
+ vertical: "industries",
94
+ verticals: "industries",
95
+ seniority: "seniorities",
96
+ level: "seniorities",
97
+ levels: "seniorities",
98
+ headcount: "headcountRanges",
99
+ headcounts: "headcountRanges",
100
+ headcountrange: "headcountRanges",
101
+ companysize: "headcountRanges",
102
+ companysizes: "headcountRanges",
103
+ employees: "headcountRanges",
104
+ employeecount: "headcountRanges",
105
+ size: "headcountRanges",
106
+ sizes: "headcountRanges",
107
+ function: "jobFunctions",
108
+ functions: "jobFunctions",
109
+ jobfunction: "jobFunctions",
110
+ department: "jobFunctions",
111
+ departments: "jobFunctions",
112
+ subfunction: "subFunctions",
113
+ domain: "companyDomains",
114
+ domains: "companyDomains",
115
+ website: "companyDomains",
116
+ websites: "companyDomains",
117
+ company: "companyNames",
118
+ companies: "companyNames",
119
+ hq: "companyHeadquarters",
120
+ headquarters: "companyHeadquarters",
121
+ companyhq: "companyHeadquarters",
122
+ companylocation: "companyHeadquarters",
123
+ companylocations: "companyHeadquarters",
124
+ companytype: "companyTypes",
125
+ keyword: "keywords",
126
+ skill: "skills",
127
+ university: "universities",
128
+ school: "universities",
129
+ schools: "universities",
130
+ education: "universities",
131
+ language: "languages",
132
+ pasttitle: "pastTitles",
133
+ previoustitles: "pastTitles",
134
+ pastcompanies: "pastCompanyNames",
135
+ previouscompanies: "pastCompanyNames",
136
+ name: "fullName",
137
+ names: "personNames",
138
+ founded: "foundedYears",
139
+ foundedyear: "foundedYears",
140
+ exact: "exactMatch",
141
+ };
@@ -0,0 +1,217 @@
1
+ export const MAX_LINES = 5;
2
+ export const DEFAULT_LINE_CHARS = 200;
3
+ export const LINE_CENTS_PER_PERSON = 3;
4
+
5
+ const KEY_MAX = 40;
6
+ const LINE_FIELDS = [
7
+ "key",
8
+ "label",
9
+ "instructions",
10
+ "prompt",
11
+ "fallback",
12
+ "fallbackFrom",
13
+ "maxChars",
14
+ ] as const;
15
+
16
+ export type LineVariable = {
17
+ key: string;
18
+ label: string;
19
+ prompt: string;
20
+ fallback: string;
21
+ fallbackFrom?: string;
22
+ maxChars?: number;
23
+ };
24
+
25
+ export type LineTemplate = {
26
+ variables: LineVariable[];
27
+ context: { sells: string; offer?: string };
28
+ };
29
+
30
+ export class LinesFileError extends Error {}
31
+
32
+ export function normalizedLineKey(key: string): string {
33
+ return key
34
+ .replace(/^(crm|person):/i, "")
35
+ .toLowerCase()
36
+ .replace(/[^a-z0-9]/g, "");
37
+ }
38
+
39
+ export function keyFromLabel(label: string): string {
40
+ const key = label
41
+ .normalize("NFKD")
42
+ .replace(/\p{M}+/gu, "")
43
+ .split(/[^a-zA-Z0-9]+/)
44
+ .filter(Boolean)
45
+ .map((word, index) =>
46
+ index === 0
47
+ ? word.toLowerCase()
48
+ : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
49
+ )
50
+ .join("")
51
+ .replace(/^[0-9]+/, "")
52
+ .slice(0, KEY_MAX);
53
+ return key || "line";
54
+ }
55
+
56
+ function uniqueKey(label: string, used: Set<string>): string {
57
+ const base = keyFromLabel(label);
58
+ if (!used.has(normalizedLineKey(base))) return base;
59
+ for (let suffix = 2; ; suffix += 1) {
60
+ const tail = String(suffix);
61
+ const key = `${base.slice(0, KEY_MAX - tail.length)}${tail}`;
62
+ if (!used.has(normalizedLineKey(key))) return key;
63
+ }
64
+ }
65
+
66
+ function labelKey(label: string): string {
67
+ return label.trim().replace(/\s+/g, " ").toLowerCase();
68
+ }
69
+
70
+ function text(
71
+ row: Record<string, unknown>,
72
+ field: string,
73
+ where: string,
74
+ ): string | undefined {
75
+ const value = row[field];
76
+ if (value === undefined || value === null) return undefined;
77
+ if (typeof value !== "string")
78
+ throw new LinesFileError(`${where}.${field} must be text.`);
79
+ return value.trim() || undefined;
80
+ }
81
+
82
+ function record(value: unknown): Record<string, unknown> | undefined {
83
+ return value && typeof value === "object" && !Array.isArray(value)
84
+ ? (value as Record<string, unknown>)
85
+ : undefined;
86
+ }
87
+
88
+ export function linesTemplateFromFile(
89
+ parsed: unknown,
90
+ options: {
91
+ existing?: LineTemplate | null;
92
+ suggestedSells?: string;
93
+ source?: string;
94
+ } = {},
95
+ ): LineTemplate {
96
+ const source = options.source ?? "The lines file";
97
+ const root = record(parsed);
98
+ const rows = Array.isArray(parsed)
99
+ ? parsed
100
+ : Array.isArray(root?.lines)
101
+ ? (root?.lines as unknown[])
102
+ : Array.isArray(root?.variables)
103
+ ? (root?.variables as unknown[])
104
+ : null;
105
+ if (!rows)
106
+ throw new LinesFileError(
107
+ `${source} must be {"sells": "...", "lines": [{"label": "...", "instructions": "...", "fallback": "..."}]} or a JSON array of lines.`,
108
+ );
109
+ if (rows.length === 0)
110
+ throw new LinesFileError(
111
+ `${source} has no lines. To turn personalized lines off, run dench campaign personalize clear <campaign>.`,
112
+ );
113
+ if (rows.length > MAX_LINES)
114
+ throw new LinesFileError(
115
+ `${source} has ${rows.length} lines; a campaign can have up to ${MAX_LINES}.`,
116
+ );
117
+ const context = record(root?.context);
118
+ const sells =
119
+ (root ? text(root, "sells", "file") : undefined) ??
120
+ (context ? text(context, "sells", "context") : undefined) ??
121
+ options.existing?.context.sells ??
122
+ options.suggestedSells?.trim();
123
+ if (!sells)
124
+ throw new LinesFileError(
125
+ `${source} needs "sells": one or two sentences on what your company sells, so the writer knows what each line should lead to.`,
126
+ );
127
+ const offer =
128
+ (root ? text(root, "offer", "file") : undefined) ??
129
+ (context ? text(context, "offer", "context") : undefined);
130
+ const existingByLabel = new Map(
131
+ (options.existing?.variables ?? []).map((variable) => [
132
+ labelKey(variable.label),
133
+ variable.key,
134
+ ]),
135
+ );
136
+ const prepared = rows.map((value, index) => {
137
+ const where = `lines[${index}]`;
138
+ const row = record(value);
139
+ if (!row)
140
+ throw new LinesFileError(
141
+ `${where} must be an object like {"label": "Company hook", "instructions": "One sentence about ...", "fallback": "Your team caught my eye."}.`,
142
+ );
143
+ const unknown = Object.keys(row).filter(
144
+ (key) => !(LINE_FIELDS as readonly string[]).includes(key),
145
+ );
146
+ if (unknown.length)
147
+ throw new LinesFileError(
148
+ `${where} has ${unknown.join(", ")}, which a line does not have. Use: label, instructions, fallback, and optionally key, maxChars, fallbackFrom.`,
149
+ );
150
+ const label = text(row, "label", where);
151
+ const prompt =
152
+ text(row, "instructions", where) ?? text(row, "prompt", where);
153
+ const fallback = text(row, "fallback", where);
154
+ if (!label) throw new LinesFileError(`${where} needs a "label".`);
155
+ if (!prompt)
156
+ throw new LinesFileError(
157
+ `${where} ("${label}") needs "instructions": what the writer should say.`,
158
+ );
159
+ if (!fallback)
160
+ throw new LinesFileError(
161
+ `${where} ("${label}") needs a "fallback": the line sent when there is nothing specific to say.`,
162
+ );
163
+ const maxChars = row.maxChars;
164
+ if (
165
+ maxChars !== undefined &&
166
+ (typeof maxChars !== "number" || !Number.isInteger(maxChars))
167
+ )
168
+ throw new LinesFileError(
169
+ `${where} ("${label}"): "maxChars" must be a whole number of characters.`,
170
+ );
171
+ return {
172
+ key: text(row, "key", where),
173
+ label,
174
+ prompt,
175
+ fallback,
176
+ fallbackFrom: text(row, "fallbackFrom", where),
177
+ maxChars: maxChars as number | undefined,
178
+ };
179
+ });
180
+ const seenLabels = new Map<string, number>();
181
+ prepared.forEach((line, index) => {
182
+ const earlier = seenLabels.get(labelKey(line.label));
183
+ if (earlier !== undefined)
184
+ throw new LinesFileError(
185
+ `lines[${index}] has the same label as lines[${earlier}] ("${line.label}"); each line needs its own name.`,
186
+ );
187
+ seenLabels.set(labelKey(line.label), index);
188
+ });
189
+ const used = new Set<string>();
190
+ for (const line of prepared) {
191
+ const kept = line.key ?? existingByLabel.get(labelKey(line.label));
192
+ if (kept) used.add(normalizedLineKey(kept));
193
+ }
194
+ const keyOwners = new Map<string, number>();
195
+ const variables = prepared.map((line, index) => {
196
+ let key = line.key ?? existingByLabel.get(labelKey(line.label));
197
+ if (!key) {
198
+ key = uniqueKey(line.label, used);
199
+ used.add(normalizedLineKey(key));
200
+ }
201
+ const owner = keyOwners.get(normalizedLineKey(key));
202
+ if (owner !== undefined)
203
+ throw new LinesFileError(
204
+ `lines[${index}] and lines[${owner}] both use the key "${key}"; give each line its own key.`,
205
+ );
206
+ keyOwners.set(normalizedLineKey(key), index);
207
+ return {
208
+ key,
209
+ label: line.label,
210
+ prompt: line.prompt,
211
+ fallback: line.fallback,
212
+ ...(line.fallbackFrom ? { fallbackFrom: line.fallbackFrom } : {}),
213
+ ...(line.maxChars !== undefined ? { maxChars: line.maxChars } : {}),
214
+ };
215
+ });
216
+ return { variables, context: { sells, ...(offer ? { offer } : {}) } };
217
+ }