mkt-skills 1.0.0 → 1.2.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/LICENSE +21 -0
- package/bin/mkt-skills.mjs +48 -13
- package/lib/api.mjs +22 -3
- package/lib/commands.mjs +159 -5
- package/lib/video.mjs +310 -0
- package/package.json +6 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thrivemattic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/mkt-skills.mjs
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { install, list, search, info, uninstall } from '../lib/commands.mjs';
|
|
3
|
+
import { install, list, search, info, uninstall, sets, installSet, setInfo } from '../lib/commands.mjs';
|
|
4
|
+
import { video } from '../lib/video.mjs';
|
|
4
5
|
|
|
5
|
-
const [,,
|
|
6
|
+
const [,, rawCommand, subCommand, ...args] = process.argv;
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
// Normalize hyphenated commands: "install-set" -> lookup key
|
|
9
|
+
const commandMap = {
|
|
10
|
+
install, list, search, info, uninstall, sets,
|
|
11
|
+
'install-set': installSet,
|
|
12
|
+
'set-info': setInfo,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const command = rawCommand;
|
|
16
|
+
|
|
17
|
+
// Handle "generate video" as a special case
|
|
18
|
+
if (command === 'generate' && subCommand === 'video') {
|
|
19
|
+
try {
|
|
20
|
+
await video(args);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err.name === 'AbortError') {
|
|
23
|
+
console.error('Request timed out. Check your internet connection.');
|
|
24
|
+
} else if (err.cause?.code === 'ENOTFOUND' || err.cause?.code === 'ECONNREFUSED') {
|
|
25
|
+
console.error('Could not connect. Check your internet connection.');
|
|
26
|
+
} else {
|
|
27
|
+
console.error(`Error: ${err.message}`);
|
|
28
|
+
}
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
8
33
|
|
|
9
34
|
if (!command || command === '--help' || command === '-h') {
|
|
10
35
|
showHelp();
|
|
@@ -12,17 +37,17 @@ if (!command || command === '--help' || command === '-h') {
|
|
|
12
37
|
}
|
|
13
38
|
|
|
14
39
|
if (command === '--version' || command === '-v') {
|
|
15
|
-
console.log('mkt-skills 1.
|
|
40
|
+
console.log('mkt-skills 1.2.1');
|
|
16
41
|
process.exit(0);
|
|
17
42
|
}
|
|
18
43
|
|
|
19
|
-
if (!
|
|
44
|
+
if (!commandMap[command]) {
|
|
20
45
|
console.error(`Unknown command: ${command}\nRun "mkt-skills --help" for usage.`);
|
|
21
46
|
process.exit(1);
|
|
22
47
|
}
|
|
23
48
|
|
|
24
49
|
try {
|
|
25
|
-
await
|
|
50
|
+
await commandMap[command](args);
|
|
26
51
|
} catch (err) {
|
|
27
52
|
if (err.name === 'AbortError') {
|
|
28
53
|
console.error('Request timed out. Check your internet connection.');
|
|
@@ -42,21 +67,31 @@ Usage:
|
|
|
42
67
|
npx mkt-skills <command> [args]
|
|
43
68
|
|
|
44
69
|
Commands:
|
|
45
|
-
install <slug>
|
|
46
|
-
list
|
|
47
|
-
search <keyword>
|
|
48
|
-
info <slug>
|
|
49
|
-
uninstall <slug>
|
|
70
|
+
install <slug> Install a skill to ~/.claude/skills/
|
|
71
|
+
list List all available skills
|
|
72
|
+
search <keyword> Search skills by keyword
|
|
73
|
+
info <slug> Show details about a skill
|
|
74
|
+
uninstall <slug> Remove an installed skill
|
|
75
|
+
sets List available skill sets
|
|
76
|
+
install-set <slug> Install all skills in a set
|
|
77
|
+
set-info <slug> Show details about a skill set
|
|
78
|
+
generate video Scaffold a new video production pipeline
|
|
50
79
|
|
|
51
80
|
Options:
|
|
52
|
-
--help, -h
|
|
53
|
-
--version, -v
|
|
81
|
+
--help, -h Show this help
|
|
82
|
+
--version, -v Show version
|
|
83
|
+
--force, -f Overwrite existing skills
|
|
84
|
+
--code CODE Access code to unlock premium skills
|
|
85
|
+
--dry-run, -n Preview without writing files (generate video)
|
|
54
86
|
|
|
55
87
|
Examples:
|
|
56
88
|
npx mkt-skills install seo-content
|
|
57
89
|
npx mkt-skills search "email automation"
|
|
58
90
|
npx mkt-skills list --category=seo
|
|
59
91
|
npx mkt-skills info marketing-audit
|
|
92
|
+
npx mkt-skills install-set thrivemattic-marketing-suite
|
|
93
|
+
npx mkt-skills generate video
|
|
94
|
+
npx mkt-skills generate video my-project --dry-run
|
|
60
95
|
|
|
61
96
|
https://marketingskills.directory
|
|
62
97
|
`);
|
package/lib/api.mjs
CHANGED
|
@@ -8,7 +8,7 @@ async function request(path) {
|
|
|
8
8
|
try {
|
|
9
9
|
const res = await fetch(`${BASE_URL}${path}`, {
|
|
10
10
|
signal: controller.signal,
|
|
11
|
-
headers: { 'User-Agent': 'mkt-skills-cli/1.
|
|
11
|
+
headers: { 'User-Agent': 'mkt-skills-cli/1.1.0' },
|
|
12
12
|
});
|
|
13
13
|
return res;
|
|
14
14
|
} finally {
|
|
@@ -22,8 +22,27 @@ export async function fetchCatalog() {
|
|
|
22
22
|
return res.json();
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export async function
|
|
26
|
-
const res = await request(
|
|
25
|
+
export async function fetchSets() {
|
|
26
|
+
const res = await request('/sets');
|
|
27
|
+
if (!res.ok) throw new Error(`Failed to fetch sets (HTTP ${res.status})`);
|
|
28
|
+
return res.json();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function fetchSet(slug) {
|
|
32
|
+
const res = await request(`/sets/${encodeURIComponent(slug)}`);
|
|
33
|
+
|
|
34
|
+
if (res.status === 404) {
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
throw Object.assign(new Error(data.message), { code: 'NOT_FOUND' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!res.ok) throw new Error(`Failed to fetch set (HTTP ${res.status})`);
|
|
40
|
+
return res.json();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function fetchInstall(slug, code) {
|
|
44
|
+
const path = `/install/${encodeURIComponent(slug)}` + (code ? `?code=${encodeURIComponent(code)}` : '');
|
|
45
|
+
const res = await request(path);
|
|
27
46
|
|
|
28
47
|
if (res.status === 403) {
|
|
29
48
|
const data = await res.json();
|
package/lib/commands.mjs
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { mkdir, rm, access, writeFile } from 'fs/promises';
|
|
2
2
|
import { createInterface } from 'readline';
|
|
3
|
-
import { fetchCatalog, fetchInstall } from './api.mjs';
|
|
3
|
+
import { fetchCatalog, fetchInstall, fetchSets, fetchSet } from './api.mjs';
|
|
4
4
|
import { skillDir, skillFile, installedSlugs } from './paths.mjs';
|
|
5
5
|
import { bold, dim, green, red, yellow, cyan } from './ui.mjs';
|
|
6
6
|
|
|
7
7
|
// ── install ─────────────────────────────────────────────────
|
|
8
8
|
|
|
9
9
|
export async function install(args) {
|
|
10
|
-
const force
|
|
11
|
-
const
|
|
10
|
+
const force = args.includes('--force') || args.includes('-f');
|
|
11
|
+
const codeFlag = args.find(a => a.startsWith('--code=')) || args.find((a, i, arr) => a === '--code' && arr[i + 1]);
|
|
12
|
+
const code = codeFlag?.startsWith('--code=') ? codeFlag.split('=')[1] : (args.includes('--code') ? args[args.indexOf('--code') + 1] : null);
|
|
13
|
+
const slug = args.find(a => !a.startsWith('-') && a !== code);
|
|
12
14
|
|
|
13
15
|
if (!slug) {
|
|
14
|
-
console.error(`Usage: mkt-skills install <slug> [--force]\n`);
|
|
16
|
+
console.error(`Usage: mkt-skills install <slug> [--force] [--code CODE]\n`);
|
|
15
17
|
process.exit(1);
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -30,10 +32,13 @@ export async function install(args) {
|
|
|
30
32
|
|
|
31
33
|
let data;
|
|
32
34
|
try {
|
|
33
|
-
data = await fetchInstall(slug);
|
|
35
|
+
data = await fetchInstall(slug, code);
|
|
34
36
|
} catch (err) {
|
|
35
37
|
if (err.code === 'PREMIUM') {
|
|
36
38
|
console.log(`\n${red('x')} ${bold(`"${slug}"`)} is a premium skill.`);
|
|
39
|
+
if (code) {
|
|
40
|
+
console.log(` ${dim('->')} Access code was invalid or not applicable.`);
|
|
41
|
+
}
|
|
37
42
|
console.log(` ${dim('->')} Purchase at: ${cyan(err.url)}\n`);
|
|
38
43
|
process.exit(1);
|
|
39
44
|
}
|
|
@@ -211,6 +216,155 @@ export async function uninstall(args) {
|
|
|
211
216
|
console.log(` ${dim('->')} Removed ${dir}\n`);
|
|
212
217
|
}
|
|
213
218
|
|
|
219
|
+
// ── sets ───────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
export async function sets(args) {
|
|
222
|
+
const allSets = await fetchSets();
|
|
223
|
+
|
|
224
|
+
if (allSets.length === 0) {
|
|
225
|
+
console.log('\nNo skill sets available yet.\n');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(`\n${bold('Skill Sets')}\n`);
|
|
230
|
+
|
|
231
|
+
const slugW = Math.max(4, ...allSets.map(s => s.slug.length));
|
|
232
|
+
const nameW = Math.max(4, ...allSets.map(s => s.name.length));
|
|
233
|
+
|
|
234
|
+
const header = [
|
|
235
|
+
'SLUG'.padEnd(slugW),
|
|
236
|
+
'NAME'.padEnd(nameW),
|
|
237
|
+
'SKILLS',
|
|
238
|
+
].join(' ');
|
|
239
|
+
|
|
240
|
+
console.log(dim(header));
|
|
241
|
+
console.log(dim('-'.repeat(header.length)));
|
|
242
|
+
|
|
243
|
+
for (const s of allSets) {
|
|
244
|
+
console.log([
|
|
245
|
+
s.slug.padEnd(slugW),
|
|
246
|
+
s.name.padEnd(nameW),
|
|
247
|
+
`${s.count} skills`,
|
|
248
|
+
].join(' '));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log(`\n${dim('Install a set:')} npx mkt-skills install-set <slug>\n`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── install-set ────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
export async function installSet(args) {
|
|
257
|
+
const force = args.includes('--force') || args.includes('-f');
|
|
258
|
+
const codeFlag = args.find(a => a.startsWith('--code=')) || args.find((a, i, arr) => a === '--code' && arr[i + 1]);
|
|
259
|
+
const code = codeFlag?.startsWith('--code=') ? codeFlag.split('=')[1] : (args.includes('--code') ? args[args.indexOf('--code') + 1] : null);
|
|
260
|
+
const slug = args.find(a => !a.startsWith('-') && a !== code);
|
|
261
|
+
|
|
262
|
+
if (!slug) {
|
|
263
|
+
console.error('Usage: mkt-skills install-set <slug> [--force] [--code CODE]\n');
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let set;
|
|
268
|
+
try {
|
|
269
|
+
set = await fetchSet(slug);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
if (err.code === 'NOT_FOUND') {
|
|
272
|
+
console.log(`\n${red('x')} Skill set "${slug}" not found.`);
|
|
273
|
+
console.log(` ${dim('->')} Browse sets: ${cyan('npx mkt-skills sets')}\n`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
throw err;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log(`\n${bold(`Installing "${set.name}"`)} (${set.count} skills)\n`);
|
|
280
|
+
|
|
281
|
+
const installed = new Set(await installedSlugs());
|
|
282
|
+
let ok = 0, skipped = 0, premium = 0;
|
|
283
|
+
|
|
284
|
+
for (const skill of set.skills) {
|
|
285
|
+
const skillSlug = skill.slug;
|
|
286
|
+
|
|
287
|
+
if (skill.price !== 'free' && !code) {
|
|
288
|
+
console.log(` ${dim('-')} ${skillSlug} — ${yellow('premium, skipped')}`);
|
|
289
|
+
premium++;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!force && installed.has(skillSlug)) {
|
|
294
|
+
console.log(` ${dim('-')} ${skillSlug} ${dim('[already installed]')}`);
|
|
295
|
+
skipped++;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const data = await fetchInstall(skillSlug, code);
|
|
301
|
+
await mkdir(skillDir(skillSlug), { recursive: true });
|
|
302
|
+
await writeFile(skillFile(skillSlug), data.content, 'utf-8');
|
|
303
|
+
console.log(` ${green('+')} ${skillSlug} (${data.lines} lines)`);
|
|
304
|
+
ok++;
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (err.code === 'PREMIUM') {
|
|
307
|
+
console.log(` ${dim('-')} ${skillSlug} — ${yellow('premium, skipped')}`);
|
|
308
|
+
premium++;
|
|
309
|
+
} else {
|
|
310
|
+
console.log(` ${red('x')} ${skillSlug} — ${err.message}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const total = set.skills.length;
|
|
316
|
+
const parts = [`${ok} installed`];
|
|
317
|
+
if (skipped) parts.push(`${skipped} already installed`);
|
|
318
|
+
if (premium) parts.push(`${premium} premium skipped`);
|
|
319
|
+
|
|
320
|
+
console.log(`\n${bold(`Done.`)} ${parts.join(', ')} (${total} total)`);
|
|
321
|
+
console.log(` ${dim('->')} ~/.claude/skills/\n`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── set-info ───────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
export async function setInfo(args) {
|
|
327
|
+
const slug = args.find(a => !a.startsWith('-'));
|
|
328
|
+
|
|
329
|
+
if (!slug) {
|
|
330
|
+
console.error('Usage: mkt-skills set-info <slug>\n');
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let set;
|
|
335
|
+
try {
|
|
336
|
+
set = await fetchSet(slug);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
if (err.code === 'NOT_FOUND') {
|
|
339
|
+
console.log(`\n${red('x')} Skill set "${slug}" not found.`);
|
|
340
|
+
console.log(` ${dim('->')} Browse sets: ${cyan('npx mkt-skills sets')}\n`);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const installed = new Set(await installedSlugs());
|
|
347
|
+
|
|
348
|
+
console.log(`\n${bold(set.name)}`);
|
|
349
|
+
console.log(dim('-'.repeat(set.name.length)));
|
|
350
|
+
if (set.description) {
|
|
351
|
+
console.log(set.description);
|
|
352
|
+
}
|
|
353
|
+
console.log(`\n${set.count} skills:\n`);
|
|
354
|
+
|
|
355
|
+
const slugW = Math.max(4, ...set.skills.map(s => s.slug.length));
|
|
356
|
+
const titleW = Math.min(30, Math.max(5, ...set.skills.map(s => s.title.length)));
|
|
357
|
+
|
|
358
|
+
for (const s of set.skills) {
|
|
359
|
+
const price = s.price === 'paid' ? yellow('paid') : green('free');
|
|
360
|
+
const inst = installed.has(s.slug) ? green(' [installed]') : '';
|
|
361
|
+
const title = s.title.length > titleW ? s.title.slice(0, titleW - 1) + '\u2026' : s.title;
|
|
362
|
+
console.log(` ${s.slug.padEnd(slugW)} ${title.padEnd(titleW)} ${String(s.lines).padStart(5)} lines ${price}${inst}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
console.log(`\n${dim('Install:')} npx mkt-skills install-set ${set.slug}\n`);
|
|
366
|
+
}
|
|
367
|
+
|
|
214
368
|
// ── helpers ─────────────────────────────────────────────────
|
|
215
369
|
|
|
216
370
|
function confirm(message) {
|
package/lib/video.mjs
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { createInterface } from "readline";
|
|
2
|
+
import { mkdir, rm, writeFile, readFile } from "fs/promises";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import { bold, dim, green, red, cyan } from "./ui.mjs";
|
|
7
|
+
|
|
8
|
+
const TEMPLATE_REPO = "https://github.com/sandeepkelvadi/tm-videogen-template.git";
|
|
9
|
+
const TEMPLATE_BRANCH = "main";
|
|
10
|
+
const TEMPLATE_DIR = "/tmp/.tm-videogen-template";
|
|
11
|
+
|
|
12
|
+
async function question(query) {
|
|
13
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
14
|
+
return new Promise(resolve => {
|
|
15
|
+
rl.question(query, answer => { rl.close(); resolve(answer); });
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function requiredQuestion(query, example) {
|
|
20
|
+
let answer;
|
|
21
|
+
while (!answer || !answer.trim()) {
|
|
22
|
+
answer = await question(`${dim("?")} ${bold(query)} ${dim(`(${example})`)}: `);
|
|
23
|
+
if (!answer || !answer.trim()) {
|
|
24
|
+
console.log(`${red(" → Required. Please enter a value.")}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return answer.trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function optionalQuestion(query, example, defaultVal) {
|
|
31
|
+
const answer = await question(`${dim("?")} ${bold(query)} ${dim(`(${example}) [default: ${defaultVal}]`)}: `);
|
|
32
|
+
return (answer && answer.trim()) ? answer.trim() : defaultVal;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function confirm(message) {
|
|
36
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
37
|
+
return new Promise(resolve => {
|
|
38
|
+
rl.question(`${message} ${dim("(y/N)")}: `, answer => {
|
|
39
|
+
rl.close();
|
|
40
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isValidHexColor(color) {
|
|
46
|
+
return /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(color);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isValidUrl(url) {
|
|
50
|
+
return /^https?:\/\/.+/.test(url);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function printTree(name, indent = "") {
|
|
54
|
+
console.log(`${indent}${dim("├──")} ${name}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function substitute(content, vars) {
|
|
58
|
+
let result = content;
|
|
59
|
+
for (const [key, val] of Object.entries(vars)) {
|
|
60
|
+
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), val);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function copyDirRecursive(src, dest, vars = {}) {
|
|
66
|
+
await mkdir(dest, { recursive: true });
|
|
67
|
+
const { readdir: read } = await import("fs/promises");
|
|
68
|
+
const entries = await read(src, { withFileTypes: true });
|
|
69
|
+
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
const srcPath = join(src, entry.name);
|
|
72
|
+
const destPath = join(dest, entry.name);
|
|
73
|
+
|
|
74
|
+
if (entry.isDirectory()) {
|
|
75
|
+
await mkdir(destPath, { recursive: true });
|
|
76
|
+
await copyDirRecursive(srcPath, destPath, vars);
|
|
77
|
+
} else {
|
|
78
|
+
let finalContent = await readFile(srcPath, "utf-8");
|
|
79
|
+
|
|
80
|
+
if (/\.(json|ts|tsx|md|sh)$/.test(entry.name)) {
|
|
81
|
+
// Substitute template variables
|
|
82
|
+
finalContent = await substitute(finalContent, vars);
|
|
83
|
+
|
|
84
|
+
// Update package.json name
|
|
85
|
+
if (entry.name === "package.json" && vars.PROJECT_NAME) {
|
|
86
|
+
const pkg = JSON.parse(finalContent);
|
|
87
|
+
pkg.name = vars.PROJECT_NAME;
|
|
88
|
+
finalContent = JSON.stringify(pkg, null, 2);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
93
|
+
await writeFile(destPath, finalContent, "utf-8");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function exec(cmd, cwd) {
|
|
99
|
+
try {
|
|
100
|
+
execSync(cmd, { cwd, stdio: "pipe" });
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function cloneTemplate() {
|
|
108
|
+
if (existsSync(TEMPLATE_DIR)) {
|
|
109
|
+
await rm(TEMPLATE_DIR, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
execSync(`git clone --depth=1 --branch ${TEMPLATE_BRANCH} ${TEMPLATE_REPO} "${TEMPLATE_DIR}"`, { stdio: "pipe" });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function video(args) {
|
|
115
|
+
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
116
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
117
|
+
const skipInstall = args.includes("--skip-install");
|
|
118
|
+
|
|
119
|
+
console.log(`\n${bold("Video Pipeline Generator")} ${dim("v1.1.0")}\n`);
|
|
120
|
+
console.log(dim("All values are required unless marked [default].\n"));
|
|
121
|
+
|
|
122
|
+
// ── Step 1: Project name ──────────────────────────────────
|
|
123
|
+
let projectName = args.find(a => !a.startsWith("-"));
|
|
124
|
+
if (!projectName) {
|
|
125
|
+
projectName = await requiredQuestion("Project name", "e.g. sunshine-academy");
|
|
126
|
+
}
|
|
127
|
+
projectName = projectName.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-");
|
|
128
|
+
|
|
129
|
+
const outputDir = join(process.cwd(), projectName);
|
|
130
|
+
|
|
131
|
+
if (existsSync(outputDir)) {
|
|
132
|
+
if (!force) {
|
|
133
|
+
const overwrite = await confirm(`Directory "${projectName}/" already exists. Overwrite?`);
|
|
134
|
+
if (!overwrite) { console.log("Aborted.\n"); return; }
|
|
135
|
+
}
|
|
136
|
+
await rm(outputDir, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (dryRun) {
|
|
140
|
+
console.log(`${dim("Dry run — no files will be written.\n")}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log(dim("─".repeat(50)));
|
|
144
|
+
|
|
145
|
+
// ── Step 2: Brand setup ──────────────────────────────────
|
|
146
|
+
console.log(`\n${bold("Brand Setup")}\n`);
|
|
147
|
+
|
|
148
|
+
const brandName = await requiredQuestion("Brand name", "e.g. Sunshine Academy");
|
|
149
|
+
const primaryColor = await requiredQuestion("Primary color (hex)", "e.g. #2d5016");
|
|
150
|
+
if (!isValidHexColor(primaryColor)) {
|
|
151
|
+
console.log(`${red(" → Invalid hex color. Use #RRGGBB format (e.g. #2d5016).\n")}`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const accentColor = await requiredQuestion("Accent color (hex)", "e.g. #8bc34a");
|
|
156
|
+
if (!isValidHexColor(accentColor)) {
|
|
157
|
+
console.log(`${red(" → Invalid hex color. Use #RRGGBB format (e.g. #8bc34a).\n")}`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const fontFamily = await optionalQuestion("Font family", "e.g. Poppins", "Poppins");
|
|
162
|
+
const logoPath = await optionalQuestion("Logo file path", "e.g. logo.png", "logo.png");
|
|
163
|
+
const websiteUrl = await requiredQuestion("Website URL", "e.g. https://sunshine.edu");
|
|
164
|
+
if (!isValidUrl(websiteUrl)) {
|
|
165
|
+
console.log(`${red(" → Invalid URL. Must start with http:// or https://.\n")}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Step 3: Campaign copy ─────────────────────────────────
|
|
170
|
+
console.log(`\n${bold("Campaign Copy")}\n`);
|
|
171
|
+
|
|
172
|
+
const headline = await requiredQuestion("Ad headline", "e.g. Admissions Open 2026");
|
|
173
|
+
const ctaText = await requiredQuestion("CTA button text", "e.g. Chat with us");
|
|
174
|
+
const year = await optionalQuestion("Year / season", "e.g. 2026-27", new Date().getFullYear().toString());
|
|
175
|
+
const location = await requiredQuestion("Location", "e.g. HSR Layout, Bangalore");
|
|
176
|
+
|
|
177
|
+
// ── Step 4: Asset source ───────────────────────────────────
|
|
178
|
+
console.log(`\n${bold("Asset Setup")}\n`);
|
|
179
|
+
|
|
180
|
+
const assetSource = await question(
|
|
181
|
+
`${dim("?")} ${bold("Asset source")} ${dim("(local / gdrive / s3) [default: local]")}: `
|
|
182
|
+
);
|
|
183
|
+
const assetSrc = (assetSource && assetSource.trim()) ? assetSource.trim() : "local";
|
|
184
|
+
|
|
185
|
+
console.log(dim("─".repeat(50)));
|
|
186
|
+
|
|
187
|
+
// ── Step 5: Preview tree ─────────────────────────────────
|
|
188
|
+
console.log(`\n${bold("Project structure:")} ${projectName}/\n`);
|
|
189
|
+
printTree("config/");
|
|
190
|
+
printTree(" brand.json");
|
|
191
|
+
printTree(" campaign.json");
|
|
192
|
+
printTree("public/");
|
|
193
|
+
printTree(" logo.png ← add your logo here");
|
|
194
|
+
printTree(" clips/ ← add video clips here");
|
|
195
|
+
printTree(" photos/ ← add photos here");
|
|
196
|
+
printTree(" music/ ← add background music here");
|
|
197
|
+
printTree("src/");
|
|
198
|
+
printTree(" pipeline/");
|
|
199
|
+
printTree(" types/");
|
|
200
|
+
printTree(" brand.ts");
|
|
201
|
+
printTree(" Root.tsx ← add your compositions here");
|
|
202
|
+
printTree(" index.ts");
|
|
203
|
+
printTree("tools/");
|
|
204
|
+
printTree(" ffmpeg_install.sh");
|
|
205
|
+
printTree(" extract_clips.sh");
|
|
206
|
+
printTree("package.json");
|
|
207
|
+
printTree("README.md");
|
|
208
|
+
console.log("");
|
|
209
|
+
|
|
210
|
+
// ── Step 6: Config summary ────────────────────────────────
|
|
211
|
+
console.log(`${bold("Config summary:")}\n`);
|
|
212
|
+
console.log(` ${dim("Brand:")} ${brandName}`);
|
|
213
|
+
console.log(` ${dim("Colors:")} ${primaryColor} / ${accentColor}`);
|
|
214
|
+
console.log(` ${dim("Font:")} ${fontFamily}`);
|
|
215
|
+
console.log(` ${dim("URL:")} ${websiteUrl}`);
|
|
216
|
+
console.log(` ${dim("Headline:")} ${headline}`);
|
|
217
|
+
console.log(` ${dim("CTA:")} ${ctaText}`);
|
|
218
|
+
console.log(` ${dim("Year:")} ${year}`);
|
|
219
|
+
console.log(` ${dim("Location:")} ${location}`);
|
|
220
|
+
console.log("");
|
|
221
|
+
|
|
222
|
+
if (dryRun) {
|
|
223
|
+
console.log(`${green("✓")} Dry run complete. No files created.\n`);
|
|
224
|
+
console.log(`${dim("Run without --dry-run to scaffold the project:")}\n`);
|
|
225
|
+
console.log(` ${cyan(`npx mkt-skills generate video ${projectName}`)}\n`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Step 7: Clone template ─────────────────────────────────
|
|
230
|
+
console.log(`${dim("Cloning template from GitHub...")}`);
|
|
231
|
+
try {
|
|
232
|
+
await cloneTemplate();
|
|
233
|
+
} catch {
|
|
234
|
+
console.log(`\n${red("x")} Failed to clone template. Check your internet connection.\n`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Step 8: Build vars & config ───────────────────────────
|
|
239
|
+
const vars = {
|
|
240
|
+
PROJECT_NAME: projectName,
|
|
241
|
+
BRAND_NAME: brandName,
|
|
242
|
+
PRIMARY_COLOR: primaryColor,
|
|
243
|
+
ACCENT_COLOR: accentColor,
|
|
244
|
+
FONT_FAMILY: fontFamily,
|
|
245
|
+
LOGO_PATH: logoPath,
|
|
246
|
+
WEBSITE_URL: websiteUrl,
|
|
247
|
+
HEADLINE: headline,
|
|
248
|
+
CTA_TEXT: ctaText,
|
|
249
|
+
YEAR: year,
|
|
250
|
+
LOCATION: location,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const brandJson = {
|
|
254
|
+
primaryColor,
|
|
255
|
+
accentColor,
|
|
256
|
+
backgroundColor: "#000000",
|
|
257
|
+
textColor: "#ffffff",
|
|
258
|
+
fontFamily,
|
|
259
|
+
fontWeights: ["400", "600", "700"],
|
|
260
|
+
logoPath,
|
|
261
|
+
websiteUrl,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const campaignJson = {
|
|
265
|
+
headline,
|
|
266
|
+
year,
|
|
267
|
+
location,
|
|
268
|
+
url: websiteUrl.replace(/^https?:\/\//, ""),
|
|
269
|
+
ctaText,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// ── Step 9: Copy template ─────────────────────────────────
|
|
273
|
+
console.log(`${dim("Scaffolding project...")}`);
|
|
274
|
+
await copyDirRecursive(TEMPLATE_DIR, outputDir, vars);
|
|
275
|
+
|
|
276
|
+
// Write brand config (no substitution needed — write direct)
|
|
277
|
+
await writeFile(join(outputDir, "config", "brand.json"), JSON.stringify(brandJson, null, 2), "utf-8");
|
|
278
|
+
await writeFile(join(outputDir, "config", "campaign.json"), JSON.stringify(campaignJson, null, 2), "utf-8");
|
|
279
|
+
|
|
280
|
+
// Cleanup template
|
|
281
|
+
await rm(TEMPLATE_DIR, { recursive: true, force: true });
|
|
282
|
+
|
|
283
|
+
// ── Step 10: Install deps ──────────────────────────────────
|
|
284
|
+
if (!skipInstall) {
|
|
285
|
+
console.log(`${dim("Installing npm dependencies (this may take a minute)...")}`);
|
|
286
|
+
const installed = exec("npm install --prefer-offline --silent 2>&1", outputDir);
|
|
287
|
+
if (!installed) {
|
|
288
|
+
console.log(`${dim(" npm install skipped or failed. Run 'npm install' manually.")}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Step 11: FFmpeg check ─────────────────────────────────
|
|
293
|
+
const hasFfmpeg = exec("which ffmpeg && which ffprobe", outputDir);
|
|
294
|
+
if (!hasFfmpeg) {
|
|
295
|
+
console.log(`\n${dim("FFmpeg not found. Run 'npm run install-ffmpeg' to install it.")}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── Step 12: Done ─────────────────────────────────────────
|
|
299
|
+
console.log(`\n${green("✓")} ${bold("Project created:")} ${projectName}/\n`);
|
|
300
|
+
|
|
301
|
+
console.log(`${bold("Next steps:")}\n`);
|
|
302
|
+
console.log(` ${dim("1.")} ${cyan("cd " + projectName)}`);
|
|
303
|
+
console.log(` ${dim("2.")} Add your logo: ${dim("cp /path/to/logo.png public/logo.png")}`);
|
|
304
|
+
console.log(` ${dim("3.")} Add clips: ${dim("cp /path/to/*.mp4 public/clips/")}`);
|
|
305
|
+
console.log(` ${dim("4.")} Add music: ${dim("cp /path/to/*.mp3 public/music/")}`);
|
|
306
|
+
console.log(` ${dim("5.")} Edit ${dim("src/Root.tsx")} — replace placeholder segments with your content`);
|
|
307
|
+
console.log(` ${dim("6.")} ${cyan("npm run studio")} — preview in browser`);
|
|
308
|
+
console.log(` ${dim("7.")} Render: ${cyan(`npx remotion render src/index.ts <comp-id> out/video.mp4`)}\n`);
|
|
309
|
+
console.log(`${dim("Composition IDs available in Root.tsx: myproject-square, myproject-vertical")}\n`);
|
|
310
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mkt-skills",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Install Claude Code marketing skills from marketingskills.directory",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"mkt-skills": "
|
|
7
|
+
"mkt-skills": "bin/mkt-skills.mjs"
|
|
8
8
|
},
|
|
9
9
|
"engines": {
|
|
10
10
|
"node": ">=18.0.0"
|
|
@@ -22,13 +22,15 @@
|
|
|
22
22
|
"marketing",
|
|
23
23
|
"seo",
|
|
24
24
|
"ai",
|
|
25
|
-
"cli"
|
|
25
|
+
"cli",
|
|
26
|
+
"video",
|
|
27
|
+
"remotion"
|
|
26
28
|
],
|
|
27
29
|
"author": "Thrivemattic",
|
|
28
30
|
"license": "MIT",
|
|
29
31
|
"repository": {
|
|
30
32
|
"type": "git",
|
|
31
|
-
"url": "https://github.com/sandeepkelvadi/mkt-skills-dir"
|
|
33
|
+
"url": "git+https://github.com/sandeepkelvadi/mkt-skills-dir.git"
|
|
32
34
|
},
|
|
33
35
|
"homepage": "https://marketingskills.directory"
|
|
34
36
|
}
|