@navipartner/demo-video 0.4.0 → 0.5.1
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 +128 -109
- package/bin/args.d.mts +39 -30
- package/bin/args.mjs +158 -153
- package/bin/demo-video.mjs +425 -272
- package/bin/encode.mjs +187 -187
- package/bin/init.d.mts +30 -24
- package/bin/init.mjs +520 -447
- package/bin/pick.d.mts +23 -18
- package/bin/pick.mjs +265 -252
- package/bin/pitch.d.mts +17 -0
- package/bin/pitch.mjs +261 -0
- package/dist/{adapter-CWqMoNdw.d.ts → adapter-D_iU2dh2.d.ts} +19 -0
- package/dist/index.d.ts +3 -3
- package/dist/runner/index.d.ts +2 -2
- package/dist/runner/index.js +61 -3
- package/package.json +1 -1
- package/skills/np-demo-video/SKILL.md +364 -348
- package/skills/np-demo-video/linear.md +117 -0
- package/skills/np-demo-video/look.md +158 -158
- package/skills/np-demo-video/reference.md +92 -82
package/bin/pick.d.mts
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import type { StoryEntry } from "./args.mjs";
|
|
2
|
-
|
|
3
|
-
export interface AppMenu {
|
|
4
|
-
dir: string;
|
|
5
|
-
name: string;
|
|
6
|
-
stories: StoryEntry[];
|
|
7
|
-
brands: string[];
|
|
8
|
-
}
|
|
9
|
-
export type Pick =
|
|
10
|
-
| {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
): Pick;
|
|
1
|
+
import type { StoryEntry } from "./args.mjs";
|
|
2
|
+
|
|
3
|
+
export interface AppMenu {
|
|
4
|
+
dir: string;
|
|
5
|
+
name: string;
|
|
6
|
+
stories: StoryEntry[];
|
|
7
|
+
brands: string[];
|
|
8
|
+
}
|
|
9
|
+
export type Pick =
|
|
10
|
+
| {
|
|
11
|
+
app: string;
|
|
12
|
+
story: string;
|
|
13
|
+
brand: string | null;
|
|
14
|
+
menu?: undefined;
|
|
15
|
+
}
|
|
16
|
+
| { menu: true; reason: string; apps?: AppMenu[] };
|
|
17
|
+
export function appMenu(appDir: string): AppMenu;
|
|
18
|
+
export function pick(request: string, apps: AppMenu[]): Pick;
|
|
19
|
+
export function menuMarkdown(apps: AppMenu[], reason: string): string;
|
|
20
|
+
export function pickExact(
|
|
21
|
+
apps: AppMenu[],
|
|
22
|
+
names: { app?: string; story?: string; brand?: string },
|
|
23
|
+
): Pick;
|
package/bin/pick.mjs
CHANGED
|
@@ -1,252 +1,265 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
import { listStories } from "./args.mjs";
|
|
6
|
-
|
|
7
|
-
// `demo-video pick`: turns a `/demo-video …` request into one of the
|
|
8
|
-
// already-made stories and a brand. No AI and nothing written: stories are a
|
|
9
|
-
// developer's to make (locally, with the skill); a request only chooses.
|
|
10
|
-
//
|
|
11
|
-
// /demo-video → the primary story (one app)
|
|
12
|
-
// /demo-video
|
|
13
|
-
// /demo-video
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
...brandNames(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
menu: true,
|
|
154
|
-
reason:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { listStories } from "./args.mjs";
|
|
6
|
+
|
|
7
|
+
// `demo-video pick`: turns a `/demo-video …` request into one of the
|
|
8
|
+
// already-made stories and a brand. No AI and nothing written: stories are a
|
|
9
|
+
// developer's to make (locally, with the skill); a request only chooses.
|
|
10
|
+
//
|
|
11
|
+
// /demo-video → the primary story (one app)
|
|
12
|
+
// /demo-video help → the menu
|
|
13
|
+
// /demo-video checkout in acme branding → story "checkout", brand "acme"
|
|
14
|
+
// /demo-video retail checkout → the app whose folder says retail
|
|
15
|
+
//
|
|
16
|
+
// Anything it can't settle on one story is answered with the menu.
|
|
17
|
+
|
|
18
|
+
const engineDir = path.resolve(
|
|
19
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
20
|
+
"..",
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const STOP = new Set(
|
|
24
|
+
"a an and as at by for from in into make me of on or our please show story the this to video with branding brand marketing demo film render".split(
|
|
25
|
+
" ",
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const words = (text) =>
|
|
30
|
+
(text.toLowerCase().match(/[a-z0-9æøå]+(?:-[a-z0-9æøå]+)*/g) ?? []).flatMap(
|
|
31
|
+
(w) => [w, ...(w.includes("-") ? w.split("-") : [])],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const brandNames = (dir) =>
|
|
35
|
+
fs.existsSync(dir)
|
|
36
|
+
? fs
|
|
37
|
+
.readdirSync(dir)
|
|
38
|
+
.filter((f) => f.endsWith(".json"))
|
|
39
|
+
.map((f) => f.slice(0, -".json".length))
|
|
40
|
+
: [];
|
|
41
|
+
|
|
42
|
+
// The adapter may keep its brands elsewhere (`brands: "../brands"`). Read
|
|
43
|
+
// from demo.config.ts's text: a literal path, as the field is documented.
|
|
44
|
+
const adapterBrandsDir = (appDir) => {
|
|
45
|
+
const config = path.join(appDir, "demo", "demo.config.ts");
|
|
46
|
+
const text = fs.existsSync(config) ? fs.readFileSync(config, "utf8") : "";
|
|
47
|
+
const dir = /\bbrands:\s*["'`]([^"'`]+)["'`]/.exec(text)?.[1] ?? "./brands";
|
|
48
|
+
return path.resolve(appDir, "demo", dir);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** What an app offers: its stories, and its brands plus the engine's. */
|
|
52
|
+
export const appMenu = (appDir) => ({
|
|
53
|
+
dir: appDir,
|
|
54
|
+
name: path.basename(appDir),
|
|
55
|
+
stories: listStories(path.join(appDir, "demo")),
|
|
56
|
+
brands: [
|
|
57
|
+
...new Set([
|
|
58
|
+
...brandNames(adapterBrandsDir(appDir)),
|
|
59
|
+
...brandNames(path.join(engineDir, "brands")),
|
|
60
|
+
]),
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Picks from `apps` (appMenu results). Returns { app, story, brand } (brand
|
|
66
|
+
* null = the adapter's default) or { menu: true, reason }.
|
|
67
|
+
*/
|
|
68
|
+
export const pick = (request, apps) => {
|
|
69
|
+
// A bare /demo-video films the primary story; this asks what there is.
|
|
70
|
+
if (/^\s*(help|menu|list)\s*$/i.test(request)) {
|
|
71
|
+
return { menu: true, reason: "The stories and brands:" };
|
|
72
|
+
}
|
|
73
|
+
return pickStory(request, apps);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const pickStory = (request, apps) => {
|
|
77
|
+
const said = words(request);
|
|
78
|
+
const has = (word) => said.includes(word.toLowerCase());
|
|
79
|
+
|
|
80
|
+
// The brand: any offered brand named in the request.
|
|
81
|
+
const allBrands = [...new Set(apps.flatMap((a) => a.brands))];
|
|
82
|
+
const brands = allBrands.filter(has);
|
|
83
|
+
if (brands.length > 1) {
|
|
84
|
+
return {
|
|
85
|
+
menu: true,
|
|
86
|
+
reason: `Name one brand, not ${brands.join(" and ")}.`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const brand = brands[0] ?? null;
|
|
90
|
+
|
|
91
|
+
// What's left of the request once the brand and filler are gone.
|
|
92
|
+
const content = said.filter(
|
|
93
|
+
(w) => !STOP.has(w) && !brands.includes(w) && !/^\d+$/.test(w),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// The app: named by its folder, else by a part of it no other app's
|
|
97
|
+
// folder has ("retail", not "self"), else the only one.
|
|
98
|
+
let candidates = apps;
|
|
99
|
+
if (apps.length > 1) {
|
|
100
|
+
const exact = apps.filter((a) => has(a.name));
|
|
101
|
+
const partCount = (part) =>
|
|
102
|
+
apps.filter((a) => a.name.split("-").includes(part)).length;
|
|
103
|
+
const named = exact.length
|
|
104
|
+
? exact
|
|
105
|
+
: apps.filter((a) =>
|
|
106
|
+
a.name
|
|
107
|
+
.split("-")
|
|
108
|
+
.some(
|
|
109
|
+
(part) => part.length > 3 && partCount(part) === 1 && has(part),
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
if (named.length === 1) {
|
|
113
|
+
candidates = named;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Score every story: its name (or a part of it) said outright counts most,
|
|
118
|
+
// then words shared with its description.
|
|
119
|
+
// Variants aren't scored: the base story plus the brand finds them.
|
|
120
|
+
const scored = candidates.flatMap((app) =>
|
|
121
|
+
app.stories
|
|
122
|
+
.filter((story) => !story.extends)
|
|
123
|
+
.map((story) => {
|
|
124
|
+
const nameParts = story.name.split("-");
|
|
125
|
+
// "checkout express" says the story checkout-express in full.
|
|
126
|
+
let score = has(story.name) || nameParts.every((p) => has(p)) ? 10 : 0;
|
|
127
|
+
score += nameParts.filter((p) => p.length > 2 && has(p)).length * 3;
|
|
128
|
+
const description = new Set(words(story.description ?? ""));
|
|
129
|
+
score += content.filter(
|
|
130
|
+
(w) => w.length > 2 && description.has(w),
|
|
131
|
+
).length;
|
|
132
|
+
return { app, story, score };
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
const best = Math.max(0, ...scored.map((s) => s.score));
|
|
136
|
+
const top = scored.filter((s) => s.score === best);
|
|
137
|
+
|
|
138
|
+
if (best === 0) {
|
|
139
|
+
// Nothing named a story: a request with no feature gets the primary
|
|
140
|
+
// story ("a marketing video of the retail app"), if that's one app.
|
|
141
|
+
const noFeature = content.every((w) =>
|
|
142
|
+
candidates.some(
|
|
143
|
+
(a) => a.name.includes(w) || a.name.split("-").includes(w),
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
const primaries = candidates
|
|
147
|
+
.map((app) => ({ app, story: app.stories.find((s) => s.primary) }))
|
|
148
|
+
.filter((p) => p.story);
|
|
149
|
+
if (noFeature && candidates.length === 1 && primaries.length === 1) {
|
|
150
|
+
return withBrand(primaries[0].app, primaries[0].story.name, brand);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
menu: true,
|
|
154
|
+
reason:
|
|
155
|
+
candidates.length > 1 && noFeature
|
|
156
|
+
? "Which app?"
|
|
157
|
+
: "No story matches that. Pick one by name:",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (top.length > 1) {
|
|
161
|
+
return {
|
|
162
|
+
menu: true,
|
|
163
|
+
reason: `That matches ${top.map((t) => `\`${t.story.name}\``).join(", ")}. Pick one by name:`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return withBrand(top[0].app, top[0].story.name, brand);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// The brand must be one the chosen app has: another app's brand would fail
|
|
170
|
+
// at render time with a worse message. A story with a variant for that brand
|
|
171
|
+
// (checkout + acme → checkout.acme) films the variant: its words are the
|
|
172
|
+
// customer's.
|
|
173
|
+
const withBrand = (app, story, brand) => {
|
|
174
|
+
if (brand && !app.brands.includes(brand)) {
|
|
175
|
+
return {
|
|
176
|
+
menu: true,
|
|
177
|
+
reason: `No brand \`${brand}\` in ${app.name}. Pick one:`,
|
|
178
|
+
apps: [app],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const variant =
|
|
182
|
+
brand && app.stories.find((s) => s.extends === story && s.brand === brand);
|
|
183
|
+
return { app: app.dir, story: variant ? variant.name : story, brand };
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** The menu as Markdown, for the PR reply. */
|
|
187
|
+
export const menuMarkdown = (apps, reason) => {
|
|
188
|
+
const lines = [reason, ""];
|
|
189
|
+
for (const app of apps) {
|
|
190
|
+
lines.push(`**${app.name}**`);
|
|
191
|
+
if (app.stories.length === 0) {
|
|
192
|
+
lines.push("- no stories yet");
|
|
193
|
+
}
|
|
194
|
+
for (const s of app.stories.filter((x) => !x.extends)) {
|
|
195
|
+
lines.push(
|
|
196
|
+
`- \`${s.name}\`${s.primary ? " (primary)" : ""}${s.description ? `: ${s.description}` : ""}`,
|
|
197
|
+
);
|
|
198
|
+
for (const v of app.stories.filter((x) => x.extends === s.name)) {
|
|
199
|
+
lines.push(` - in \`${v.brand}\` branding: \`${v.name}\``);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
lines.push(`- brands: ${app.brands.map((b) => `\`${b}\``).join(", ")}`, "");
|
|
203
|
+
}
|
|
204
|
+
const example = apps.find((a) => a.stories.some((s) => !s.extends));
|
|
205
|
+
if (example) {
|
|
206
|
+
lines.push(
|
|
207
|
+
`For example: \`/demo-video ${example.stories.find((s) => !s.extends).name}${example.brands.length > 1 ? ` in ${example.brands.find((b) => b !== "navipartner") ?? example.brands[0]} branding` : ""}\``,
|
|
208
|
+
);
|
|
209
|
+
lines.push(
|
|
210
|
+
"",
|
|
211
|
+
"In Linear, ask @Linear instead, also for a customer without a brand here (a pitch draft).",
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return lines.join("\n");
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The form's version (Actions → Run workflow): app, story and brand by name,
|
|
219
|
+
* no guessing. Empty story = the primary story, empty brand = the adapter's
|
|
220
|
+
* default, empty app = the only app. A name that doesn't exist gets the menu,
|
|
221
|
+
* so a typo never films something else. `apps` in a menu result narrows it.
|
|
222
|
+
*/
|
|
223
|
+
export const pickExact = (apps, { app = "", story = "", brand = "" }) => {
|
|
224
|
+
const slashes = (p) => p.split("\\").join("/");
|
|
225
|
+
const wanted = slashes(app).replace(/\/+$/, "");
|
|
226
|
+
const candidates = wanted
|
|
227
|
+
? apps.filter((a) => {
|
|
228
|
+
const dir = slashes(a.dir);
|
|
229
|
+
return (
|
|
230
|
+
a.name === wanted || dir === wanted || dir.endsWith(`/${wanted}`)
|
|
231
|
+
);
|
|
232
|
+
})
|
|
233
|
+
: apps;
|
|
234
|
+
if (candidates.length === 0) {
|
|
235
|
+
return { menu: true, reason: `No app \`${wanted}\`. The apps:` };
|
|
236
|
+
}
|
|
237
|
+
if (candidates.length > 1) {
|
|
238
|
+
return { menu: true, reason: "Which app? Fill in App with one of:" };
|
|
239
|
+
}
|
|
240
|
+
const chosen = candidates[0];
|
|
241
|
+
const found = story
|
|
242
|
+
? chosen.stories.find((s) => s.name === story)
|
|
243
|
+
: chosen.stories.find((s) => s.primary);
|
|
244
|
+
if (!found) {
|
|
245
|
+
return {
|
|
246
|
+
menu: true,
|
|
247
|
+
reason: story
|
|
248
|
+
? `No story \`${story}\` in ${chosen.name}. Pick one:`
|
|
249
|
+
: `${chosen.name} has no primary story. Pick one:`,
|
|
250
|
+
apps: [chosen],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (found.extends) {
|
|
254
|
+
// A variant is made for one brand.
|
|
255
|
+
if (brand && brand !== found.brand) {
|
|
256
|
+
return {
|
|
257
|
+
menu: true,
|
|
258
|
+
reason: `\`${found.name}\` is made for \`${found.brand}\`. Leave Brand empty, or pick \`${found.extends}\`:`,
|
|
259
|
+
apps: [chosen],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return { app: chosen.dir, story: found.name, brand: found.brand };
|
|
263
|
+
}
|
|
264
|
+
return withBrand(chosen, found.name, brand || null);
|
|
265
|
+
};
|
package/bin/pitch.d.mts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function pitchName(customer: string): string;
|
|
2
|
+
export function contrastOnWhite(colour: string): number;
|
|
3
|
+
export function plainText(text: string): string;
|
|
4
|
+
export function imageType(
|
|
5
|
+
bytes: Buffer,
|
|
6
|
+
): ".png" | ".jpg" | ".webp" | ".svg" | null;
|
|
7
|
+
export function buildPitchBrand(
|
|
8
|
+
proposal: unknown,
|
|
9
|
+
outDir: string,
|
|
10
|
+
options?: { fetch?: typeof fetch },
|
|
11
|
+
): Promise<{
|
|
12
|
+
brand: string;
|
|
13
|
+
customer: string;
|
|
14
|
+
website: string;
|
|
15
|
+
sources: string[];
|
|
16
|
+
notes: string;
|
|
17
|
+
}>;
|