@makerkit/cli 2.0.2 → 2.0.4

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/dist/index.mjs ADDED
@@ -0,0 +1,1333 @@
1
+ #!/usr/bin/env node
2
+ import chalk from "chalk";
3
+ import { Command } from "commander";
4
+ import ora from "ora";
5
+ import prompts from "prompts";
6
+ import { dirname, join } from "path";
7
+ import { execa, execaCommand } from "execa";
8
+ import fs from "fs-extra";
9
+ import invariant from "tiny-invariant";
10
+ import { homedir, tmpdir } from "os";
11
+ import { config } from "dotenv";
12
+ //#region src/version.ts
13
+ const CLI_VERSION = "2.0.3";
14
+ //#endregion
15
+ //#region src/utils/marker-file.ts
16
+ async function writeMarkerFile(projectPath, variant, kitRepo) {
17
+ const dir = join(projectPath, ".makerkit");
18
+ await fs.ensureDir(dir);
19
+ const config = {
20
+ version: 1,
21
+ variant,
22
+ kit_repo: kitRepo,
23
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
24
+ cli_version: CLI_VERSION
25
+ };
26
+ await fs.writeJson(join(dir, "config.json"), config, { spaces: 2 });
27
+ }
28
+ //#endregion
29
+ //#region src/utils/upstream.ts
30
+ const VARIANT_REPO_MAP = {
31
+ "next-supabase": "makerkit/next-supabase-saas-kit-turbo",
32
+ "next-drizzle": "makerkit/next-drizzle-saas-kit-turbo",
33
+ "next-prisma": "makerkit/next-prisma-saas-kit-turbo",
34
+ "react-router-supabase": "makerkit/react-router-supabase-saas-kit-turbo"
35
+ };
36
+ function sshUrl(repo) {
37
+ return `git@github.com:${repo}`;
38
+ }
39
+ function httpsUrl(repo) {
40
+ return `https://github.com/${repo}`;
41
+ }
42
+ async function hasSshAccess() {
43
+ try {
44
+ await execaCommand("ssh -T git@github.com -o StrictHostKeyChecking=no", { timeout: 1e4 });
45
+ return true;
46
+ } catch (error) {
47
+ return (error instanceof Error && "stderr" in error ? String(error.stderr) : "").includes("successfully authenticated");
48
+ }
49
+ }
50
+ function getUpstreamUrl(variant, useSsh) {
51
+ const repo = VARIANT_REPO_MAP[variant];
52
+ return useSsh ? sshUrl(repo) : httpsUrl(repo);
53
+ }
54
+ /**
55
+ * Check if a remote URL points to the correct repo for the given variant,
56
+ * regardless of whether it uses SSH or HTTPS.
57
+ * Handles trailing `.git` and `/` in URLs.
58
+ */
59
+ function isUpstreamUrlValid(url, variant) {
60
+ const normalized = url.replace(/\/+$/, "").replace(/\.git$/, "");
61
+ const repo = VARIANT_REPO_MAP[variant];
62
+ return normalized === sshUrl(repo) || normalized === httpsUrl(repo);
63
+ }
64
+ async function getUpstreamRemoteUrl() {
65
+ try {
66
+ const { stdout } = await execaCommand("git remote get-url upstream");
67
+ return stdout.trim() || void 0;
68
+ } catch {
69
+ return;
70
+ }
71
+ }
72
+ async function setUpstreamRemote(url) {
73
+ if (await getUpstreamRemoteUrl()) await execaCommand(`git remote set-url upstream ${url}`);
74
+ else await execaCommand(`git remote add upstream ${url}`);
75
+ }
76
+ //#endregion
77
+ //#region src/utils/create-project.ts
78
+ async function createProject(options) {
79
+ const { variant, name, directory, githubToken } = options;
80
+ const projectPath = join(directory, name);
81
+ const repo = VARIANT_REPO_MAP[variant];
82
+ if (await fs.pathExists(projectPath)) throw new Error(`Target directory "${projectPath}" already exists. Choose a different name or remove it first.`);
83
+ if (!await fs.pathExists(directory)) throw new Error(`Parent directory "${directory}" does not exist.`);
84
+ let cloneUrl;
85
+ if (githubToken) cloneUrl = `https://${githubToken}@github.com/${repo}`;
86
+ else cloneUrl = await hasSshAccess() ? `git@github.com:${repo}` : `https://github.com/${repo}`;
87
+ await execa("git", [
88
+ "clone",
89
+ cloneUrl,
90
+ name
91
+ ], { cwd: directory });
92
+ if (githubToken) await execa("git", [
93
+ "remote",
94
+ "set-url",
95
+ "origin",
96
+ `https://github.com/${repo}`
97
+ ], { cwd: projectPath });
98
+ await execa("pnpm", ["install"], { cwd: projectPath });
99
+ await writeMarkerFile(projectPath, variant, repo);
100
+ return {
101
+ success: true,
102
+ projectPath,
103
+ variant,
104
+ kitRepo: repo,
105
+ message: `Project "${name}" created successfully with variant "${variant}".`
106
+ };
107
+ }
108
+ //#endregion
109
+ //#region src/utils/list-variants.ts
110
+ const VARIANT_CATALOG = [
111
+ {
112
+ id: "next-supabase",
113
+ name: "Next.js + Supabase",
114
+ description: "Full-stack SaaS kit with Next.js App Router and Supabase",
115
+ repo: VARIANT_REPO_MAP["next-supabase"],
116
+ tech: [
117
+ "Next.js",
118
+ "Supabase",
119
+ "Tailwind CSS",
120
+ "shadcn/ui"
121
+ ],
122
+ database: "PostgreSQL (Supabase)",
123
+ auth: "Supabase Auth",
124
+ status: "stable"
125
+ },
126
+ {
127
+ id: "next-drizzle",
128
+ name: "Next.js + Drizzle",
129
+ description: "Full-stack SaaS kit with Next.js and Drizzle ORM",
130
+ repo: VARIANT_REPO_MAP["next-drizzle"],
131
+ tech: [
132
+ "Next.js",
133
+ "Drizzle",
134
+ "Tailwind CSS",
135
+ "shadcn/ui"
136
+ ],
137
+ database: "PostgreSQL",
138
+ auth: "Better Auth",
139
+ status: "stable"
140
+ },
141
+ {
142
+ id: "next-prisma",
143
+ name: "Next.js + Prisma",
144
+ description: "Full-stack SaaS kit with Next.js and Prisma ORM",
145
+ repo: VARIANT_REPO_MAP["next-prisma"],
146
+ tech: [
147
+ "Next.js",
148
+ "Prisma",
149
+ "Tailwind CSS",
150
+ "shadcn/ui"
151
+ ],
152
+ database: "PostgreSQL",
153
+ auth: "Better Auth",
154
+ status: "stable"
155
+ },
156
+ {
157
+ id: "react-router-supabase",
158
+ name: "React Router + Supabase",
159
+ description: "Full-stack SaaS kit with React Router and Supabase",
160
+ repo: VARIANT_REPO_MAP["react-router-supabase"],
161
+ tech: [
162
+ "React Router",
163
+ "Supabase",
164
+ "Tailwind CSS",
165
+ "shadcn/ui"
166
+ ],
167
+ database: "PostgreSQL (Supabase)",
168
+ auth: "Supabase Auth",
169
+ status: "stable"
170
+ }
171
+ ];
172
+ //#endregion
173
+ //#region src/commands/new/new.command.ts
174
+ const newCommand = new Command().name("new").description("Initialize a new Makerkit project").action(async () => {
175
+ const choices = VARIANT_CATALOG.map((v) => ({
176
+ title: v.name,
177
+ value: v.id
178
+ }));
179
+ const { kit, name: projectName } = await prompts([{
180
+ type: "select",
181
+ name: "kit",
182
+ message: `Select the ${chalk.cyan(`SaaS Kit`)} you want to clone`,
183
+ choices
184
+ }, {
185
+ type: "text",
186
+ name: "name",
187
+ message: `Enter your ${chalk.cyan("Project Name")}.`
188
+ }]);
189
+ const selected = VARIANT_CATALOG.find((v) => v.id === kit);
190
+ if (!selected) {
191
+ console.log("Invalid kit selection. Aborting...");
192
+ process.exit(1);
193
+ }
194
+ const { confirm } = await prompts([{
195
+ type: "confirm",
196
+ name: "confirm",
197
+ message: `Are you sure you want to clone ${chalk.cyan(selected.name)} to ${chalk.cyan(projectName)}?`
198
+ }]);
199
+ if (!confirm) {
200
+ console.log("Aborting...");
201
+ process.exit(0);
202
+ }
203
+ const spinner = ora(`Cloning ${selected.name}...`).start();
204
+ try {
205
+ const result = await createProject({
206
+ variant: selected.id,
207
+ name: projectName,
208
+ directory: process.cwd()
209
+ });
210
+ spinner.succeed(`${result.message}`);
211
+ console.log(`You can now get started. Open the project in your IDE and read the README.md file for more information.`);
212
+ } catch (e) {
213
+ console.error(e);
214
+ spinner.fail(`Failed to create project`);
215
+ process.exit(1);
216
+ }
217
+ });
218
+ //#endregion
219
+ //#region src/utils/plugins-cache.ts
220
+ const CACHE_DIR = join(homedir(), ".makerkit");
221
+ const CACHE_FILE = join(CACHE_DIR, "plugins.json");
222
+ const CACHE_TTL_MS = 3600 * 1e3;
223
+ async function readCache() {
224
+ try {
225
+ if (!await fs.pathExists(CACHE_FILE)) return null;
226
+ return await fs.readJson(CACHE_FILE);
227
+ } catch {
228
+ return null;
229
+ }
230
+ }
231
+ async function writeCache(plugins) {
232
+ try {
233
+ await fs.ensureDir(CACHE_DIR);
234
+ await fs.writeJson(CACHE_FILE, {
235
+ fetchedAt: Date.now(),
236
+ plugins
237
+ }, { spaces: 2 });
238
+ } catch {}
239
+ }
240
+ async function fetchPluginRegistry(registryUrl, fallback) {
241
+ if (!registryUrl) return fallback;
242
+ const cached = await readCache();
243
+ if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) return cached.plugins;
244
+ try {
245
+ const response = await fetch(registryUrl);
246
+ if (response.ok) {
247
+ const plugins = (await response.json()).plugins;
248
+ await writeCache(plugins);
249
+ return plugins;
250
+ }
251
+ } catch {}
252
+ return cached?.plugins ?? fallback;
253
+ }
254
+ //#endregion
255
+ //#region src/plugins-model.ts
256
+ const DEFAULT_PLUGINS = {
257
+ feedback: {
258
+ name: "Feedback",
259
+ id: "feedback",
260
+ description: "Add a feedback popup to your site.",
261
+ postInstallMessage: "Run database migrations: pnpm db:migrate",
262
+ variants: { "next-supabase": {
263
+ envVars: [],
264
+ path: "packages/plugins/feedback"
265
+ } }
266
+ },
267
+ waitlist: {
268
+ name: "Waitlist",
269
+ id: "waitlist",
270
+ description: "Add a waitlist to your site.",
271
+ postInstallMessage: "Run database migrations: pnpm db:migrate",
272
+ variants: { "next-supabase": {
273
+ envVars: [],
274
+ path: "packages/plugins/waitlist"
275
+ } }
276
+ },
277
+ testimonial: {
278
+ name: "Testimonial",
279
+ id: "testimonial",
280
+ description: "Add a testimonial widget to your site.",
281
+ postInstallMessage: "Run database migrations: pnpm db:migrate",
282
+ variants: { "next-supabase": {
283
+ envVars: [],
284
+ path: "packages/plugins/testimonial"
285
+ } }
286
+ },
287
+ roadmap: {
288
+ name: "Roadmap",
289
+ id: "roadmap",
290
+ description: "Add a roadmap to your site.",
291
+ postInstallMessage: "Run database migrations: pnpm db:migrate",
292
+ variants: { "next-supabase": {
293
+ envVars: [],
294
+ path: "packages/plugins/roadmap"
295
+ } }
296
+ },
297
+ "google-analytics": {
298
+ name: "Google Analytics",
299
+ id: "google-analytics",
300
+ description: "Add Google Analytics to your site.",
301
+ variants: {
302
+ "next-supabase": {
303
+ envVars: [{
304
+ key: "NEXT_PUBLIC_GOOGLE_ANALYTICS_ID",
305
+ description: "Google Analytics Measurement ID"
306
+ }],
307
+ path: "packages/plugins/analytics/google-analytics"
308
+ },
309
+ "next-drizzle": {
310
+ envVars: [{
311
+ key: "NEXT_PUBLIC_GOOGLE_ANALYTICS_ID",
312
+ description: "Google Analytics Measurement ID"
313
+ }],
314
+ path: "packages/plugins/analytics/google-analytics"
315
+ },
316
+ "next-prisma": {
317
+ envVars: [{
318
+ key: "NEXT_PUBLIC_GOOGLE_ANALYTICS_ID",
319
+ description: "Google Analytics Measurement ID"
320
+ }],
321
+ path: "packages/plugins/analytics/google-analytics"
322
+ }
323
+ }
324
+ },
325
+ honeybadger: {
326
+ name: "Honeybadger",
327
+ id: "honeybadger",
328
+ description: "Add Honeybadger Error Tracking to your site.",
329
+ variants: {
330
+ "next-supabase": {
331
+ envVars: [
332
+ {
333
+ key: "HONEYBADGER_API_KEY",
334
+ description: "Honeybadger private API key"
335
+ },
336
+ {
337
+ key: "NEXT_PUBLIC_HONEYBADGER_ENVIRONMENT",
338
+ description: "Honeybadger environment"
339
+ },
340
+ {
341
+ key: "NEXT_PUBLIC_HONEYBADGER_REVISION",
342
+ description: "Honeybadger log revision"
343
+ }
344
+ ],
345
+ path: "packages/plugins/honeybadger"
346
+ },
347
+ "next-drizzle": {
348
+ envVars: [
349
+ {
350
+ key: "HONEYBADGER_API_KEY",
351
+ description: "Honeybadger private API key"
352
+ },
353
+ {
354
+ key: "NEXT_PUBLIC_HONEYBADGER_ENVIRONMENT",
355
+ description: "Honeybadger environment"
356
+ },
357
+ {
358
+ key: "NEXT_PUBLIC_HONEYBADGER_REVISION",
359
+ description: "Honeybadger log revision"
360
+ }
361
+ ],
362
+ path: "packages/plugins/honeybadger"
363
+ },
364
+ "next-prisma": { envVars: [
365
+ {
366
+ key: "HONEYBADGER_API_KEY",
367
+ description: "Honeybadger private API key"
368
+ },
369
+ {
370
+ key: "NEXT_PUBLIC_HONEYBADGER_ENVIRONMENT",
371
+ description: "Honeybadger environment"
372
+ },
373
+ {
374
+ key: "NEXT_PUBLIC_HONEYBADGER_REVISION",
375
+ description: "Honeybadger log revision"
376
+ }
377
+ ] }
378
+ }
379
+ },
380
+ posthog: {
381
+ name: "PostHog",
382
+ id: "posthog",
383
+ description: "Add PostHog Analytics to your site.",
384
+ variants: {
385
+ "next-supabase": {
386
+ envVars: [{
387
+ key: "NEXT_PUBLIC_POSTHOG_KEY",
388
+ description: "PostHog project API key"
389
+ }, {
390
+ key: "NEXT_PUBLIC_POSTHOG_HOST",
391
+ description: "PostHog host URL",
392
+ defaultValue: "https://app.posthog.com"
393
+ }],
394
+ path: "packages/plugins/analytics/posthog"
395
+ },
396
+ "next-drizzle": {
397
+ envVars: [{
398
+ key: "NEXT_PUBLIC_POSTHOG_KEY",
399
+ description: "PostHog project API key"
400
+ }, {
401
+ key: "NEXT_PUBLIC_POSTHOG_HOST",
402
+ description: "PostHog host URL",
403
+ defaultValue: "https://app.posthog.com"
404
+ }],
405
+ path: "packages/plugins/analytics/posthog"
406
+ },
407
+ "next-prisma": {
408
+ envVars: [{
409
+ key: "NEXT_PUBLIC_POSTHOG_KEY",
410
+ description: "PostHog project API key"
411
+ }, {
412
+ key: "NEXT_PUBLIC_POSTHOG_HOST",
413
+ description: "PostHog host URL",
414
+ defaultValue: "https://app.posthog.com"
415
+ }],
416
+ path: "packages/plugins/analytics/posthog"
417
+ }
418
+ }
419
+ },
420
+ umami: {
421
+ name: "Umami",
422
+ id: "umami",
423
+ description: "Add Umami Analytics to your site.",
424
+ variants: {
425
+ "next-supabase": {
426
+ envVars: [{
427
+ key: "NEXT_PUBLIC_UMAMI_WEBSITE_ID",
428
+ description: "Umami website ID"
429
+ }, {
430
+ key: "NEXT_PUBLIC_UMAMI_HOST",
431
+ description: "Umami host URL"
432
+ }],
433
+ path: "packages/plugins/analytics/umami"
434
+ },
435
+ "next-drizzle": {
436
+ envVars: [{
437
+ key: "NEXT_PUBLIC_UMAMI_WEBSITE_ID",
438
+ description: "Umami website ID"
439
+ }, {
440
+ key: "NEXT_PUBLIC_UMAMI_HOST",
441
+ description: "Umami host URL"
442
+ }],
443
+ path: "packages/plugins/analytics/umami"
444
+ },
445
+ "next-prisma": {
446
+ envVars: [{
447
+ key: "NEXT_PUBLIC_UMAMI_WEBSITE_ID",
448
+ description: "Umami website ID"
449
+ }, {
450
+ key: "NEXT_PUBLIC_UMAMI_HOST",
451
+ description: "Umami host URL"
452
+ }],
453
+ path: "packages/plugins/analytics/umami"
454
+ }
455
+ }
456
+ },
457
+ signoz: {
458
+ name: "SigNoz",
459
+ id: "signoz",
460
+ description: "Add SigNoz Monitoring to your app.",
461
+ variants: {
462
+ "next-supabase": {
463
+ envVars: [{
464
+ key: "OTEL_EXPORTER_OTLP_ENDPOINT",
465
+ description: "SigNoz OTLP endpoint URL"
466
+ }],
467
+ path: "packages/plugins/signoz"
468
+ },
469
+ "next-drizzle": {
470
+ envVars: [{
471
+ key: "OTEL_EXPORTER_OTLP_ENDPOINT",
472
+ description: "SigNoz OTLP endpoint URL"
473
+ }],
474
+ path: "packages/plugins/signoz"
475
+ },
476
+ "next-prisma": {
477
+ envVars: [{
478
+ key: "OTEL_EXPORTER_OTLP_ENDPOINT",
479
+ description: "SigNoz OTLP endpoint URL"
480
+ }],
481
+ path: "packages/plugins/signoz"
482
+ }
483
+ }
484
+ },
485
+ paddle: {
486
+ name: "Paddle",
487
+ id: "paddle",
488
+ description: "Add Paddle Billing to your app.",
489
+ variants: { "next-supabase": {
490
+ envVars: [
491
+ {
492
+ key: "NEXT_PUBLIC_PADDLE_CLIENT_TOKEN",
493
+ description: "Paddle client-side token"
494
+ },
495
+ {
496
+ key: "PADDLE_API_KEY",
497
+ description: "Paddle API key"
498
+ },
499
+ {
500
+ key: "PADDLE_WEBHOOK_SECRET",
501
+ description: "Paddle webhook secret"
502
+ }
503
+ ],
504
+ path: "packages/plugins/paddle"
505
+ } }
506
+ },
507
+ "supabase-cms": {
508
+ name: "Supabase CMS",
509
+ id: "supabase-cms",
510
+ description: "Add Supabase CMS provider to your app.",
511
+ postInstallMessage: "Run database migrations: pnpm db:migrate",
512
+ variants: { "next-supabase": {
513
+ envVars: [],
514
+ path: "packages/plugins/supabase-cms"
515
+ } }
516
+ },
517
+ "meshes-analytics": {
518
+ name: "Meshes Analytics",
519
+ id: "meshes-analytics",
520
+ description: "Add Meshes Analytics to your app.",
521
+ variants: { "next-supabase": {
522
+ envVars: [{
523
+ key: "NEXT_PUBLIC_MESHES_PUBLISHABLE_KEY",
524
+ description: "The Meshes publishable key"
525
+ }],
526
+ path: "packages/plugins/meshes-analytics"
527
+ } }
528
+ },
529
+ directus: {
530
+ name: "Directus CMS",
531
+ id: "directus",
532
+ description: "Add Directus as your CMS.",
533
+ variants: {
534
+ "next-supabase": {
535
+ envVars: [{
536
+ key: "NEXT_PUBLIC_DIRECTUS_URL",
537
+ description: "The Directus URL"
538
+ }, {
539
+ key: "DIRECTUS_ACCESS_TOKEN",
540
+ description: "The Directus access token"
541
+ }],
542
+ path: "packages/plugins/directus"
543
+ },
544
+ "next-drizzle": {
545
+ envVars: [{
546
+ key: "NEXT_PUBLIC_DIRECTUS_URL",
547
+ description: "The Directus URL"
548
+ }, {
549
+ key: "DIRECTUS_ACCESS_TOKEN",
550
+ description: "The Directus access token"
551
+ }],
552
+ path: "packages/plugins/directus"
553
+ },
554
+ "next-prisma": {
555
+ envVars: [{
556
+ key: "NEXT_PUBLIC_DIRECTUS_URL",
557
+ description: "The Directus URL"
558
+ }, {
559
+ key: "DIRECTUS_ACCESS_TOKEN",
560
+ description: "The Directus access token"
561
+ }],
562
+ path: "packages/plugins/directus"
563
+ }
564
+ }
565
+ }
566
+ };
567
+ var PluginRegistry = class PluginRegistry {
568
+ constructor(plugins) {
569
+ this.plugins = plugins;
570
+ }
571
+ static async load() {
572
+ const registryUrl = process.env.MAKERKIT_PLUGINS_REGISTRY_URL;
573
+ return new PluginRegistry(await fetchPluginRegistry(registryUrl, DEFAULT_PLUGINS));
574
+ }
575
+ getPluginById(id) {
576
+ return this.plugins[id];
577
+ }
578
+ getPluginsForVariant(variant) {
579
+ return Object.values(this.plugins).filter((p) => variant in p.variants);
580
+ }
581
+ validatePlugin(pluginId, variant) {
582
+ const plugin = this.getPluginById(pluginId);
583
+ invariant(plugin, `Plugin "${pluginId}" not found`);
584
+ invariant(plugin.variants[variant], `Plugin "${pluginId}" is not available for the ${variant} variant`);
585
+ return plugin;
586
+ }
587
+ };
588
+ function getEnvVars(plugin, variant) {
589
+ return plugin.variants[variant]?.envVars ?? [];
590
+ }
591
+ function getPath(plugin, variant) {
592
+ return plugin.variants[variant]?.path;
593
+ }
594
+ async function isInstalled(plugin, variant) {
595
+ const pluginPath = getPath(plugin, variant);
596
+ if (!pluginPath) return false;
597
+ const pkgJsonPath = join(process.cwd(), pluginPath, "package.json");
598
+ if (!await fs.pathExists(pkgJsonPath)) return false;
599
+ try {
600
+ const pkg = await fs.readJson(pkgJsonPath);
601
+ return !!pkg.name && !!pkg.exports;
602
+ } catch {
603
+ return false;
604
+ }
605
+ }
606
+ //#endregion
607
+ //#region src/utils/base-store.ts
608
+ function basesDir(pluginId) {
609
+ return join(process.cwd(), "node_modules", ".cache", "makerkit", "bases", pluginId);
610
+ }
611
+ async function saveBaseVersions(pluginId, files) {
612
+ const dir = basesDir(pluginId);
613
+ for (const file of files) {
614
+ const targetPath = join(dir, file.target);
615
+ await fs.ensureDir(dirname(targetPath));
616
+ await fs.writeFile(targetPath, file.content);
617
+ }
618
+ }
619
+ //#endregion
620
+ //#region src/utils/git.ts
621
+ function getErrorOutput(error) {
622
+ if (error instanceof Error && "stderr" in error) {
623
+ const stderr = String(error.stderr);
624
+ if (stderr) return stderr;
625
+ }
626
+ if (error instanceof Error && "stdout" in error) {
627
+ const stdout = String(error.stdout);
628
+ if (stdout) return stdout;
629
+ }
630
+ return error instanceof Error ? error.message : String(error);
631
+ }
632
+ async function isGitClean() {
633
+ const { stdout } = await execaCommand("git status --porcelain");
634
+ return stdout.trim() === "";
635
+ }
636
+ async function getGitUsername() {
637
+ try {
638
+ const { stdout } = await execaCommand("git config --get user.username");
639
+ const username = stdout.trim();
640
+ return username.length > 0 ? username : void 0;
641
+ } catch {
642
+ return;
643
+ }
644
+ }
645
+ //#endregion
646
+ //#region src/utils/install-registry-files.ts
647
+ async function fetchRegistryItem(variant, pluginId, username, majorVersion) {
648
+ const url = `https://makerkit.dev/r/${variant}${majorVersion != null ? `/v${majorVersion}` : ""}/${pluginId}.json?username=${username}`;
649
+ const response = await fetch(url);
650
+ if (!response.ok) throw new Error(`Failed to fetch plugin registry for "${pluginId}" (${response.status}): ${response.statusText}`);
651
+ const item = await response.json();
652
+ if (!item.files || item.files.length === 0) throw new Error(`Plugin "${pluginId}" has no files in the registry.`);
653
+ return item;
654
+ }
655
+ async function installRegistryFiles(variant, pluginId, username, majorVersion) {
656
+ const item = await fetchRegistryItem(variant, pluginId, username, majorVersion);
657
+ const cwd = process.cwd();
658
+ for (const file of item.files) {
659
+ const targetPath = join(cwd, file.target);
660
+ await fs.ensureDir(dirname(targetPath));
661
+ await fs.writeFile(targetPath, file.content);
662
+ }
663
+ return item;
664
+ }
665
+ //#endregion
666
+ //#region src/utils/run-codemod.ts
667
+ const CODEMOD_TIMEOUT_MS = 300 * 1e3;
668
+ async function runCodemod(variant, pluginId, codemodVersion, options) {
669
+ try {
670
+ const localPath = process.env.MAKERKIT_CODEMODS_PATH;
671
+ const runner = process.env.MAKERKIT_PACKAGE_RUNNER ?? "npx --yes";
672
+ const versionTag = codemodVersion ? `@${codemodVersion}` : "";
673
+ const { stdout, stderr } = await execaCommand(localPath ? `${runner} codemod workflow run --allow-dirty -w ${localPath}/codemods/${variant}/${pluginId}` : `${runner} codemod --allow-dirty @makerkit/${variant}-${pluginId}${versionTag}`, {
674
+ stdio: options?.captureOutput ? "pipe" : "inherit",
675
+ timeout: CODEMOD_TIMEOUT_MS
676
+ });
677
+ return {
678
+ success: true,
679
+ output: stdout || stderr || ""
680
+ };
681
+ } catch (error) {
682
+ let message = "Unknown error during codemod";
683
+ if (error instanceof Error) {
684
+ message = error.message;
685
+ if ("stderr" in error && error.stderr) message += `\n${error.stderr}`;
686
+ if ("timedOut" in error && error.timedOut) message = `Codemod timed out after ${CODEMOD_TIMEOUT_MS / 1e3}s (the workflow engine may have stalled after an error)`;
687
+ }
688
+ return {
689
+ success: false,
690
+ output: message
691
+ };
692
+ }
693
+ }
694
+ //#endregion
695
+ //#region src/utils/username-cache.ts
696
+ const USERNAME_FILE = join(tmpdir(), "makerkit-username");
697
+ function getCachedUsername() {
698
+ try {
699
+ const username = fs.readFileSync(USERNAME_FILE, "utf-8").trim();
700
+ return username.length > 0 ? username : void 0;
701
+ } catch {
702
+ return;
703
+ }
704
+ }
705
+ function cacheUsername(username) {
706
+ fs.writeFileSync(USERNAME_FILE, username, "utf-8");
707
+ }
708
+ function clearCachedUsername() {
709
+ try {
710
+ fs.removeSync(USERNAME_FILE);
711
+ } catch {}
712
+ }
713
+ async function promptForUsername() {
714
+ const username = (await prompts({
715
+ type: "text",
716
+ name: "githubUsername",
717
+ message: "Enter the GitHub username registered with your Makerkit account",
718
+ initial: await getGitUsername(),
719
+ validate: (value) => {
720
+ return value.trim().length > 0 ? true : "GitHub username is required";
721
+ }
722
+ })).githubUsername?.trim();
723
+ if (!username) throw new Error("Setup cancelled. GitHub username is required.");
724
+ cacheUsername(username);
725
+ return username;
726
+ }
727
+ async function getOrPromptUsername(providedUsername) {
728
+ if (providedUsername?.trim()) {
729
+ const username = providedUsername.trim();
730
+ cacheUsername(username);
731
+ return username;
732
+ }
733
+ const cached = getCachedUsername();
734
+ if (cached) return cached;
735
+ return promptForUsername();
736
+ }
737
+ //#endregion
738
+ //#region src/utils/workspace.ts
739
+ async function readDeps(pkgPath) {
740
+ if (!await fs.pathExists(pkgPath)) return {};
741
+ const pkg = await fs.readJson(pkgPath);
742
+ return {
743
+ ...pkg.dependencies,
744
+ ...pkg.devDependencies
745
+ };
746
+ }
747
+ async function detectVariant() {
748
+ const cwd = process.cwd();
749
+ const rootPkgPath = join(cwd, "package.json");
750
+ if (!await fs.pathExists(rootPkgPath)) throw new Error("No package.json found. Please run this command from a MakerKit project root.");
751
+ if (!(await readDeps(rootPkgPath))["turbo"]) throw new Error("This does not appear to be a MakerKit Turbo monorepo. The \"turbo\" dependency was not found in package.json.");
752
+ const webDeps = await readDeps(join(cwd, "apps", "web", "package.json"));
753
+ const dbDeps = await readDeps(join(cwd, "packages", "database", "package.json"));
754
+ const hasSupabase = !!webDeps["@supabase/supabase-js"];
755
+ const hasReactRouter = !!webDeps["@react-router/node"];
756
+ const hasDrizzle = !!webDeps["drizzle-orm"] || !!dbDeps["drizzle-orm"];
757
+ const hasPrisma = !!webDeps["@prisma/client"] || !!dbDeps["@prisma/client"];
758
+ if (hasReactRouter && hasSupabase) return "react-router-supabase";
759
+ if (hasSupabase) return "next-supabase";
760
+ if (hasDrizzle) return "next-drizzle";
761
+ if (hasPrisma) return "next-prisma";
762
+ throw new Error("Unrecognized MakerKit project. Could not detect variant from dependencies.");
763
+ }
764
+ async function validateProject() {
765
+ const variant = await detectVariant();
766
+ const version = (await fs.readJson(join(process.cwd(), "package.json"))).version ?? "unknown";
767
+ const major = parseInt(version.split(".")[0], 10);
768
+ return {
769
+ variant,
770
+ version,
771
+ majorVersion: isNaN(major) ? void 0 : major
772
+ };
773
+ }
774
+ //#endregion
775
+ //#region src/utils/add-plugin.ts
776
+ async function addPlugin(options) {
777
+ if (!options.skipGitCheck) {
778
+ if (!await isGitClean()) return {
779
+ success: false,
780
+ reason: "Git working directory has uncommitted changes. Please commit or stash them before adding a plugin."
781
+ };
782
+ }
783
+ const { variant, majorVersion } = await validateProject();
784
+ const username = options.githubUsername?.trim() || getCachedUsername();
785
+ if (!username) return {
786
+ success: false,
787
+ reason: "No GitHub username cached and none provided. Call makerkit_init_registry first or pass githubUsername."
788
+ };
789
+ cacheUsername(username);
790
+ const plugin = (await PluginRegistry.load()).validatePlugin(options.pluginId, variant);
791
+ if (await isInstalled(plugin, variant)) return {
792
+ success: false,
793
+ reason: `Plugin "${plugin.name}" is already installed.`
794
+ };
795
+ const item = await installRegistryFiles(variant, options.pluginId, username, majorVersion);
796
+ await saveBaseVersions(options.pluginId, item.files);
797
+ const codemodResult = await runCodemod(variant, options.pluginId, item.codemodVersion, { captureOutput: options.captureCodemodOutput ?? true });
798
+ const envVars = getEnvVars(plugin, variant);
799
+ return {
800
+ success: true,
801
+ pluginName: plugin.name,
802
+ pluginId: plugin.id,
803
+ variant,
804
+ envVars: envVars.map((e) => ({
805
+ key: e.key,
806
+ description: e.description
807
+ })),
808
+ postInstallMessage: plugin.postInstallMessage ?? null,
809
+ codemodOutput: codemodResult.output,
810
+ codemodWarning: codemodResult.success ? void 0 : `The automated codemod did not complete successfully. Plugin files were installed but some wiring steps may have failed.\n${codemodResult.output}`
811
+ };
812
+ }
813
+ //#endregion
814
+ //#region src/utils/list-plugins.ts
815
+ async function listPlugins(options) {
816
+ const variant = await detectVariant();
817
+ const plugins = (await PluginRegistry.load()).getPluginsForVariant(variant);
818
+ return {
819
+ variant,
820
+ plugins: await Promise.all(plugins.map(async (p) => ({
821
+ id: p.id,
822
+ name: p.name,
823
+ description: p.description,
824
+ installed: await isInstalled(p, variant),
825
+ envVars: getEnvVars(p, variant).map((e) => e.key),
826
+ postInstallMessage: p.postInstallMessage ?? null
827
+ })))
828
+ };
829
+ }
830
+ //#endregion
831
+ //#region src/commands/plugins/add/add.command.ts
832
+ function createAddCommand(parentCommand) {
833
+ return parentCommand.command("add").argument("[plugin-id...]", "Plugins to install (e.g. feedback waitlist)").description("Install one or more MakerKit plugins.").action(async (pluginIds) => {
834
+ try {
835
+ if (!await isGitClean()) {
836
+ console.error(chalk.red("Your git working directory has uncommitted changes. Please commit or stash them before adding a plugin."));
837
+ process.exit(1);
838
+ }
839
+ if (!pluginIds || pluginIds.length === 0) {
840
+ const available = (await listPlugins({ projectPath: process.cwd() })).plugins.filter((p) => !p.installed);
841
+ if (available.length === 0) {
842
+ console.log(chalk.green("All plugins are already installed."));
843
+ return;
844
+ }
845
+ pluginIds = (await prompts({
846
+ type: "multiselect",
847
+ name: "pluginIds",
848
+ message: "Select plugins to install",
849
+ choices: available.map((p) => ({
850
+ title: `${p.name} ${chalk.gray(`(${p.id})`)}`,
851
+ value: p.id
852
+ }))
853
+ })).pluginIds ?? [];
854
+ if (pluginIds.length === 0) return;
855
+ }
856
+ let username = await getOrPromptUsername();
857
+ for (const pluginId of pluginIds) {
858
+ console.log(`\nInstalling ${chalk.cyan(pluginId)}...\n`);
859
+ const filesSpinner = ora("Installing plugin...").start();
860
+ let result;
861
+ try {
862
+ result = await addPlugin({
863
+ projectPath: process.cwd(),
864
+ pluginId,
865
+ githubUsername: username,
866
+ skipGitCheck: true,
867
+ captureCodemodOutput: false
868
+ });
869
+ } catch (error) {
870
+ filesSpinner.fail("Failed to install plugin.");
871
+ clearCachedUsername();
872
+ console.log(chalk.yellow("\nRetrying with a different username...\n"));
873
+ username = await getOrPromptUsername();
874
+ const retrySpinner = ora("Installing plugin...").start();
875
+ try {
876
+ result = await addPlugin({
877
+ projectPath: process.cwd(),
878
+ pluginId,
879
+ githubUsername: username,
880
+ skipGitCheck: true,
881
+ captureCodemodOutput: false
882
+ });
883
+ } catch (retryError) {
884
+ retrySpinner.fail("Failed to install plugin.");
885
+ const message = retryError instanceof Error ? retryError.message : "Unknown error";
886
+ console.error(chalk.red(`\nError installing "${pluginId}": ${message}`));
887
+ console.log(chalk.yellow("\nTo revert changes, run: git checkout . && git clean -fd"));
888
+ process.exit(1);
889
+ }
890
+ if (result.success) retrySpinner.succeed("Plugin installed.");
891
+ else retrySpinner.stop();
892
+ }
893
+ if (!result.success) {
894
+ if (result.reason.includes("already installed")) {
895
+ filesSpinner.warn(result.reason + " Skipping.");
896
+ continue;
897
+ }
898
+ filesSpinner.fail("Failed to install plugin.");
899
+ console.error(chalk.red(`\n${result.reason}`));
900
+ console.log(chalk.yellow("\nTo revert changes, run: git checkout . && git clean -fd"));
901
+ process.exit(1);
902
+ }
903
+ if (result.codemodWarning) {
904
+ filesSpinner.warn(`${result.pluginName} installed with warnings — some automated steps may not have completed.`);
905
+ console.log(chalk.yellow(`\n${result.codemodWarning}`));
906
+ console.log(chalk.yellow("\nPlugin files were written successfully. Review the changes with `git diff` and complete any missing steps manually."));
907
+ } else if (filesSpinner.isSpinning) filesSpinner.succeed(`${result.pluginName} installed successfully!`);
908
+ if (result.envVars.length > 0) {
909
+ console.log(chalk.white("\nEnvironment variables to configure:"));
910
+ for (const envVar of result.envVars) console.log(` ${chalk.cyan(envVar.key)} - ${envVar.description}`);
911
+ }
912
+ if (result.postInstallMessage) {
913
+ console.log(chalk.white("\nNext steps:"));
914
+ console.log(` ${chalk.cyan(result.postInstallMessage)}`);
915
+ }
916
+ }
917
+ console.log("");
918
+ console.log(chalk.yellow("Important:"));
919
+ console.log(chalk.yellow(" This plugin was installed using an automated migration."));
920
+ console.log(chalk.yellow(" Please review the changes manually and test thoroughly before committing."));
921
+ console.log("");
922
+ console.log(chalk.white("Tips:"));
923
+ console.log(` ${chalk.gray("-")} Run ${chalk.cyan("git diff")} to review all changes made by the migration.`);
924
+ console.log(` ${chalk.gray("-")} Use an AI assistant (e.g. Claude) as a first pass to review the diff for issues.`);
925
+ console.log(` ${chalk.gray("-")} Run your test suite and verify the app builds before committing.`);
926
+ console.log(` ${chalk.gray("-")} To undo all changes: ${chalk.cyan("git checkout . && git clean -fd")}`);
927
+ console.log("");
928
+ } catch (error) {
929
+ const message = error instanceof Error ? error.message : "Unknown error";
930
+ console.error(chalk.red(`Error: ${message}`));
931
+ console.log(chalk.yellow("\nTo revert changes, run: git checkout . && git clean -fd"));
932
+ process.exit(1);
933
+ }
934
+ });
935
+ }
936
+ //#endregion
937
+ //#region src/commands/plugins/diff/diff.command.ts
938
+ function createDiffCommand(parentCommand) {
939
+ return parentCommand.command("diff").argument("[plugin-id]", "Plugin to diff (e.g. feedback, waitlist)").description("Show differences between installed plugin files and the latest registry version.").action(async (pluginId) => {
940
+ try {
941
+ const { variant, majorVersion } = await validateProject();
942
+ const registry = await PluginRegistry.load();
943
+ if (!pluginId) {
944
+ const plugins = registry.getPluginsForVariant(variant);
945
+ const installed = [];
946
+ for (const p of plugins) if (await isInstalled(p, variant)) installed.push(p);
947
+ if (installed.length === 0) {
948
+ console.log(chalk.yellow("No plugins installed."));
949
+ return;
950
+ }
951
+ pluginId = (await prompts({
952
+ type: "select",
953
+ name: "pluginId",
954
+ message: "Select a plugin to diff",
955
+ choices: installed.map((p) => ({
956
+ title: `${p.name} ${chalk.gray(`(${p.id})`)}`,
957
+ value: p.id
958
+ }))
959
+ })).pluginId;
960
+ if (!pluginId) return;
961
+ }
962
+ const plugin = registry.validatePlugin(pluginId, variant);
963
+ if (!await isInstalled(plugin, variant)) {
964
+ console.error(chalk.yellow(`Plugin "${plugin.name}" is not installed.`));
965
+ return;
966
+ }
967
+ const username = await getOrPromptUsername();
968
+ const spinner = ora("Fetching latest plugin files...").start();
969
+ const item = await fetchRegistryItem(variant, pluginId, username, majorVersion);
970
+ spinner.succeed("Fetched latest plugin files.");
971
+ const tempDir = join(tmpdir(), `makerkit-diff-${pluginId}`);
972
+ await fs.ensureDir(tempDir);
973
+ try {
974
+ const cwd = process.cwd();
975
+ let hasDiff = false;
976
+ for (const file of item.files) {
977
+ const localPath = join(cwd, file.target);
978
+ const remotePath = join(tempDir, file.target);
979
+ await fs.ensureDir(dirname(join(tempDir, file.target)));
980
+ await fs.writeFile(remotePath, file.content);
981
+ if (!await fs.pathExists(localPath)) {
982
+ console.log(chalk.green(`\nNew file: ${file.target}`));
983
+ hasDiff = true;
984
+ continue;
985
+ }
986
+ if (await fs.readFile(localPath, "utf-8") !== file.content) {
987
+ hasDiff = true;
988
+ console.log(chalk.white(`\n--- ${file.target}`));
989
+ await execa("git", [
990
+ "diff",
991
+ "--no-index",
992
+ "--color",
993
+ "--",
994
+ localPath,
995
+ remotePath
996
+ ], {
997
+ stdio: "inherit",
998
+ reject: false
999
+ });
1000
+ }
1001
+ }
1002
+ if (!hasDiff) console.log(chalk.green(`\n${plugin.name} is up to date.`));
1003
+ } finally {
1004
+ await fs.remove(tempDir);
1005
+ }
1006
+ } catch (error) {
1007
+ const message = error instanceof Error ? error.message : "Unknown error";
1008
+ console.error(chalk.red(`Error: ${message}`));
1009
+ process.exit(1);
1010
+ }
1011
+ });
1012
+ }
1013
+ //#endregion
1014
+ //#region src/utils/init-registry.ts
1015
+ async function initRegistry(options) {
1016
+ const variant = await detectVariant();
1017
+ cacheUsername(options.githubUsername);
1018
+ return {
1019
+ success: true,
1020
+ variant,
1021
+ username: options.githubUsername
1022
+ };
1023
+ }
1024
+ //#endregion
1025
+ //#region src/commands/plugins/init/init.command.ts
1026
+ function createInitCommand(parentCommand) {
1027
+ return parentCommand.command("init").description("Initialize MakerKit plugin registry in your project (one-time setup).").action(async () => {
1028
+ try {
1029
+ const username = await getOrPromptUsername();
1030
+ const initResult = await initRegistry({
1031
+ projectPath: process.cwd(),
1032
+ githubUsername: username
1033
+ });
1034
+ console.log(`Detected project variant: ${chalk.cyan(initResult.variant)}\n`);
1035
+ console.log(chalk.green("Registry configured.\n"));
1036
+ const pluginsResult = await listPlugins({ projectPath: process.cwd() });
1037
+ console.log(chalk.white("Available plugins:\n"));
1038
+ for (const plugin of pluginsResult.plugins) console.log(` ${chalk.green(plugin.name)} ${chalk.gray(`(${plugin.id})`)}`);
1039
+ console.log(`\nRun ${chalk.cyan("makerkit plugins add <plugin-id>")} to install a plugin.`);
1040
+ } catch (error) {
1041
+ const message = error instanceof Error ? error.message : "Unknown error";
1042
+ console.error(chalk.red(`Error: ${message}`));
1043
+ process.exit(1);
1044
+ }
1045
+ });
1046
+ }
1047
+ //#endregion
1048
+ //#region src/commands/plugins/list/list-plugins.command.ts
1049
+ function createListPluginsCommand(parentCommand) {
1050
+ return parentCommand.command("list").description("List available and installed plugins.").action(async () => {
1051
+ const result = await listPlugins({ projectPath: process.cwd() });
1052
+ console.log(chalk.white(`MakerKit Plugins ${chalk.gray(`(${result.variant})`)}\n`));
1053
+ console.log(` ${chalk.green("Plugin Name")} ${chalk.gray("(plugin-id)")} — Status\n`);
1054
+ let installedCount = 0;
1055
+ for (const plugin of result.plugins) {
1056
+ if (plugin.installed) installedCount++;
1057
+ const status = plugin.installed ? chalk.green("installed") : chalk.gray("available");
1058
+ console.log(` ${chalk.cyan(plugin.name)} ${chalk.gray(`(${plugin.id})`)} — ${status}`);
1059
+ }
1060
+ console.log(`\n ${chalk.white(`${installedCount} installed`)} / ${chalk.gray(`${result.plugins.length} available`)}\n`);
1061
+ if (installedCount === 0) console.log(`Run ${chalk.cyan("makerkit plugins init")} to set up the registry, then ${chalk.cyan("makerkit plugins add <plugin-id>")} to install.`);
1062
+ });
1063
+ }
1064
+ //#endregion
1065
+ //#region src/commands/plugins/outdated/outdated.command.ts
1066
+ async function isOutdated(plugin, variant, username) {
1067
+ const item = await fetchRegistryItem(variant, plugin.id, username);
1068
+ const cwd = process.cwd();
1069
+ for (const file of item.files) {
1070
+ const localPath = join(cwd, file.target);
1071
+ if (!await fs.pathExists(localPath)) return true;
1072
+ if (await fs.readFile(localPath, "utf-8") !== file.content) return true;
1073
+ }
1074
+ return false;
1075
+ }
1076
+ function createOutdatedCommand(parentCommand) {
1077
+ return parentCommand.command("outdated").description("Check installed plugins for updates.").action(async () => {
1078
+ try {
1079
+ const { variant } = await validateProject();
1080
+ const plugins = (await PluginRegistry.load()).getPluginsForVariant(variant);
1081
+ const installed = [];
1082
+ for (const p of plugins) if (await isInstalled(p, variant)) installed.push(p);
1083
+ if (installed.length === 0) {
1084
+ console.log(chalk.yellow("No plugins installed."));
1085
+ return;
1086
+ }
1087
+ const username = await getOrPromptUsername();
1088
+ const spinner = ora(`Checking ${installed.length} plugin${installed.length > 1 ? "s" : ""} for updates...`).start();
1089
+ const outdated = [];
1090
+ for (const plugin of installed) try {
1091
+ if (await isOutdated(plugin, variant, username)) outdated.push(plugin);
1092
+ } catch {}
1093
+ spinner.stop();
1094
+ if (outdated.length === 0) {
1095
+ console.log(chalk.green("All plugins are up to date."));
1096
+ return;
1097
+ }
1098
+ console.log(chalk.yellow(`\n${outdated.length} plugin${outdated.length > 1 ? "s have" : " has"} updates available:\n`));
1099
+ for (const plugin of outdated) {
1100
+ const pluginPath = getPath(plugin, variant);
1101
+ console.log(` ${chalk.cyan(plugin.name)} ${chalk.gray(`(${plugin.id})`)}${pluginPath ? chalk.gray(` — ${pluginPath}`) : ""}`);
1102
+ }
1103
+ console.log(`\nRun ${chalk.cyan("makerkit plugins diff <plugin-id>")} to see what changed.`);
1104
+ } catch (error) {
1105
+ const message = error instanceof Error ? error.message : "Unknown error";
1106
+ console.error(chalk.red(`Error: ${message}`));
1107
+ process.exit(1);
1108
+ }
1109
+ });
1110
+ }
1111
+ //#endregion
1112
+ //#region src/commands/plugins/update/update.command.ts
1113
+ function createUpdateCommand(parentCommand) {
1114
+ return parentCommand.command("update").argument("[plugin-id...]", "Plugins to update (e.g. feedback waitlist)").description("Update installed plugins to the latest registry version.").action(async (pluginIds) => {
1115
+ try {
1116
+ const { variant, majorVersion } = await validateProject();
1117
+ const registry = await PluginRegistry.load();
1118
+ if (!pluginIds || pluginIds.length === 0) {
1119
+ const plugins = registry.getPluginsForVariant(variant);
1120
+ const installed = [];
1121
+ for (const p of plugins) if (await isInstalled(p, variant)) installed.push(p);
1122
+ if (installed.length === 0) {
1123
+ console.log(chalk.yellow("No plugins installed."));
1124
+ return;
1125
+ }
1126
+ pluginIds = (await prompts({
1127
+ type: "multiselect",
1128
+ name: "pluginIds",
1129
+ message: "Select plugins to update",
1130
+ choices: installed.map((p) => ({
1131
+ title: `${p.name} ${chalk.gray(`(${p.id})`)}`,
1132
+ value: p.id
1133
+ }))
1134
+ })).pluginIds ?? [];
1135
+ if (pluginIds.length === 0) return;
1136
+ }
1137
+ const username = await getOrPromptUsername();
1138
+ for (const pluginId of pluginIds) {
1139
+ const plugin = registry.validatePlugin(pluginId, variant);
1140
+ if (!await isInstalled(plugin, variant)) {
1141
+ console.log(chalk.yellow(`Plugin "${plugin.name}" is not installed. Use "plugins add ${pluginId}" to install it.`));
1142
+ continue;
1143
+ }
1144
+ const spinner = ora(`Fetching latest ${plugin.name} files...`).start();
1145
+ const item = await fetchRegistryItem(variant, pluginId, username, majorVersion);
1146
+ spinner.succeed(`Fetched latest ${plugin.name} files.`);
1147
+ const cwd = process.cwd();
1148
+ const modifiedFiles = [];
1149
+ for (const file of item.files) {
1150
+ const localPath = join(cwd, file.target);
1151
+ if (await fs.pathExists(localPath)) {
1152
+ if (await fs.readFile(localPath, "utf-8") !== file.content) modifiedFiles.push(file.target);
1153
+ }
1154
+ }
1155
+ if (modifiedFiles.length === 0) {
1156
+ console.log(chalk.green(`${plugin.name} is already up to date.`));
1157
+ continue;
1158
+ }
1159
+ console.log(chalk.yellow(`\n${modifiedFiles.length} file${modifiedFiles.length > 1 ? "s" : ""} will be overwritten:`));
1160
+ for (const file of modifiedFiles) console.log(` ${chalk.gray(file)}`);
1161
+ const { confirm } = await prompts({
1162
+ type: "confirm",
1163
+ name: "confirm",
1164
+ message: `Update ${plugin.name}? Local changes will be lost.`,
1165
+ initial: false
1166
+ });
1167
+ if (!confirm) {
1168
+ console.log(chalk.gray(`Skipped ${plugin.name}.`));
1169
+ continue;
1170
+ }
1171
+ const writeSpinner = ora(`Updating ${plugin.name}...`).start();
1172
+ for (const file of item.files) {
1173
+ const targetPath = join(cwd, file.target);
1174
+ await fs.ensureDir(dirname(targetPath));
1175
+ await fs.writeFile(targetPath, file.content);
1176
+ }
1177
+ await saveBaseVersions(pluginId, item.files);
1178
+ writeSpinner.succeed(`${plugin.name} updated.`);
1179
+ }
1180
+ } catch (error) {
1181
+ const message = error instanceof Error ? error.message : "Unknown error";
1182
+ console.error(chalk.red(`Error: ${message}`));
1183
+ process.exit(1);
1184
+ }
1185
+ });
1186
+ }
1187
+ //#endregion
1188
+ //#region src/commands/plugins/plugins.command.ts
1189
+ const pluginsCommand = new Command().name("plugins").description("Manage MakerKit plugins.");
1190
+ createInitCommand(pluginsCommand);
1191
+ createAddCommand(pluginsCommand);
1192
+ createUpdateCommand(pluginsCommand);
1193
+ createDiffCommand(pluginsCommand);
1194
+ createOutdatedCommand(pluginsCommand);
1195
+ createListPluginsCommand(pluginsCommand);
1196
+ //#endregion
1197
+ //#region src/utils/project-pull.ts
1198
+ async function projectPull(options) {
1199
+ const { variant } = await validateProject();
1200
+ if (!await isGitClean()) return {
1201
+ success: false,
1202
+ reason: "Git working directory has uncommitted changes. Please commit or stash them before pulling upstream updates."
1203
+ };
1204
+ let currentUrl = await getUpstreamRemoteUrl();
1205
+ if (!currentUrl) {
1206
+ const url = getUpstreamUrl(variant, await hasSshAccess());
1207
+ await setUpstreamRemote(url);
1208
+ currentUrl = url;
1209
+ } else if (!isUpstreamUrlValid(currentUrl, variant)) {
1210
+ const expectedUrl = getUpstreamUrl(variant, currentUrl.startsWith("git@"));
1211
+ return {
1212
+ success: false,
1213
+ reason: `Upstream remote points to "${currentUrl}" but expected "${expectedUrl}" for variant "${variant}". Please ask the user whether to update the upstream URL, then run: git remote set-url upstream <correct-url>`
1214
+ };
1215
+ }
1216
+ await execaCommand("git fetch upstream");
1217
+ try {
1218
+ const { stdout } = await execaCommand("git merge upstream/main --no-edit");
1219
+ const alreadyUpToDate = stdout.includes("Already up to date");
1220
+ return {
1221
+ success: true,
1222
+ variant,
1223
+ upstreamUrl: currentUrl,
1224
+ alreadyUpToDate,
1225
+ message: alreadyUpToDate ? "Already up to date." : "Successfully merged upstream changes."
1226
+ };
1227
+ } catch (mergeError) {
1228
+ const output = getErrorOutput(mergeError);
1229
+ if (!(output.includes("CONFLICT") || output.includes("Automatic merge failed"))) throw new Error(`Merge failed: ${output}`);
1230
+ const { stdout: statusOutput } = await execaCommand("git diff --name-only --diff-filter=U");
1231
+ const conflictPaths = statusOutput.trim().split("\n").filter(Boolean);
1232
+ const cwd = process.cwd();
1233
+ const conflicts = await Promise.all(conflictPaths.map(async (filePath) => {
1234
+ let conflicted;
1235
+ try {
1236
+ conflicted = await fs.readFile(join(cwd, filePath), "utf-8");
1237
+ } catch {
1238
+ conflicted = void 0;
1239
+ }
1240
+ let base;
1241
+ let ours;
1242
+ let theirs;
1243
+ try {
1244
+ const { stdout: b } = await execa("git", ["show", `:1:${filePath}`]);
1245
+ base = b;
1246
+ } catch {
1247
+ base = void 0;
1248
+ }
1249
+ try {
1250
+ const { stdout: o } = await execa("git", ["show", `:2:${filePath}`]);
1251
+ ours = o;
1252
+ } catch {
1253
+ ours = void 0;
1254
+ }
1255
+ try {
1256
+ const { stdout: t } = await execa("git", ["show", `:3:${filePath}`]);
1257
+ theirs = t;
1258
+ } catch {
1259
+ theirs = void 0;
1260
+ }
1261
+ return {
1262
+ path: filePath,
1263
+ conflicted,
1264
+ base,
1265
+ ours,
1266
+ theirs
1267
+ };
1268
+ }));
1269
+ return {
1270
+ success: false,
1271
+ variant,
1272
+ upstreamUrl: currentUrl,
1273
+ hasConflicts: true,
1274
+ conflictCount: conflicts.length,
1275
+ conflicts,
1276
+ instructions: "Merge conflicts detected. For each conflict: review base, ours (local), and theirs (upstream) versions. Produce resolved content and call makerkit_project_resolve_conflicts. Ask the user for guidance when the intent behind local changes is unclear."
1277
+ };
1278
+ }
1279
+ }
1280
+ //#endregion
1281
+ //#region src/commands/project/update/update.command.ts
1282
+ function createProjectUpdateCommand(parentCommand) {
1283
+ return parentCommand.command("update").description("Pull the latest changes from the upstream MakerKit repository.").action(async () => {
1284
+ try {
1285
+ const spinner = ora("Pulling latest changes from upstream...").start();
1286
+ const result = await projectPull({ projectPath: process.cwd() });
1287
+ if (result.success) {
1288
+ const displayName = VARIANT_CATALOG.find((v) => v.id === result.variant)?.name ?? result.variant;
1289
+ if (result.alreadyUpToDate) spinner.succeed(`Already up to date. (${chalk.cyan(displayName)})`);
1290
+ else spinner.succeed(`Successfully pulled latest changes from upstream. (${chalk.cyan(displayName)})`);
1291
+ } else if ("hasConflicts" in result) {
1292
+ spinner.fail("Merge conflicts detected.");
1293
+ console.log(chalk.yellow(`\n${result.conflictCount} conflicting file(s):`));
1294
+ for (const conflict of result.conflicts) console.log(` ${chalk.gray("-")} ${conflict.path}`);
1295
+ console.log(chalk.yellow("\nPlease resolve them manually:"));
1296
+ console.log(chalk.gray(" 1. Fix the conflicting files"));
1297
+ console.log(chalk.gray(" 2. Run: git add ."));
1298
+ console.log(chalk.gray(" 3. Run: git commit"));
1299
+ process.exit(1);
1300
+ } else {
1301
+ spinner.fail("Failed to pull from upstream.");
1302
+ console.error(chalk.red(`\n${result.reason}`));
1303
+ process.exit(1);
1304
+ }
1305
+ } catch (error) {
1306
+ const message = error instanceof Error ? error.message : "Unknown error";
1307
+ console.error(chalk.red(`Error: ${message}`));
1308
+ process.exit(1);
1309
+ }
1310
+ });
1311
+ }
1312
+ //#endregion
1313
+ //#region src/commands/project/project.command.ts
1314
+ const projectCommand = new Command().name("project").description("Manage your MakerKit project.");
1315
+ createProjectUpdateCommand(projectCommand);
1316
+ //#endregion
1317
+ //#region src/index.ts
1318
+ config({
1319
+ path: ".env.local",
1320
+ quiet: true
1321
+ });
1322
+ process.on("SIGINT", () => process.exit(0));
1323
+ process.on("SIGTERM", () => process.exit(0));
1324
+ async function main() {
1325
+ const program = new Command().name("makerkit").description("Your SaaS Kit companion. Add plugins, manage migrations, and more.").version(CLI_VERSION, "-v, --version", "display the version number");
1326
+ program.addCommand(newCommand).addCommand(pluginsCommand).addCommand(projectCommand);
1327
+ program.parse();
1328
+ }
1329
+ main();
1330
+ //#endregion
1331
+ export {};
1332
+
1333
+ //# sourceMappingURL=index.mjs.map