@makerkit/cli 1.3.18 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -54
- package/dist/index.js +1590 -703
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1508 -0
- package/dist/mcp.js.map +1 -0
- package/package.json +22 -18
package/dist/index.js
CHANGED
|
@@ -1,765 +1,1652 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/commands/
|
|
4
|
-
import
|
|
3
|
+
// src/commands/new/new.command.ts
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
import prompts from "prompts";
|
|
5
8
|
|
|
6
|
-
// src/utils/
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (!apiKey) {
|
|
11
|
-
throw new Error(
|
|
12
|
-
"OPENAI_API_KEY env variable is not set. Please set it in your .env.local file of your Makerkit workspace."
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
return new OpenAI({ apiKey });
|
|
16
|
-
}
|
|
17
|
-
var openai_client_default = getOpenAIClient;
|
|
9
|
+
// src/utils/create-project.ts
|
|
10
|
+
import { join as join2 } from "path";
|
|
11
|
+
import { execa } from "execa";
|
|
12
|
+
import fs2 from "fs-extra";
|
|
18
13
|
|
|
19
|
-
// src/utils/
|
|
14
|
+
// src/utils/marker-file.ts
|
|
20
15
|
import { join } from "path";
|
|
16
|
+
import fs from "fs-extra";
|
|
21
17
|
|
|
22
|
-
// src/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
AiTextEditor: {
|
|
47
|
-
name: `AI Text Editor`,
|
|
48
|
-
id: `text-editor-v1`,
|
|
49
|
-
branch: `text-editor`,
|
|
50
|
-
description: `Add an AI Text Editor to your site.`,
|
|
51
|
-
folder: `plugins/text-editor`
|
|
52
|
-
},
|
|
53
|
-
AiTextEditorTurbo: {
|
|
54
|
-
name: `AI Text Editor`,
|
|
55
|
-
id: `text-editor`,
|
|
56
|
-
branch: `text-editor`,
|
|
57
|
-
description: `Add an AI Text Editor to your site.`,
|
|
58
|
-
folder: `packages/plugins/text-editor`
|
|
59
|
-
},
|
|
60
|
-
Waitlist: {
|
|
61
|
-
name: `Waitlist`,
|
|
62
|
-
id: `waitlist`,
|
|
63
|
-
branch: `waitlist`,
|
|
64
|
-
description: `Add a waitlist to your site.`,
|
|
65
|
-
folder: `packages/plugins/waitlist`
|
|
66
|
-
},
|
|
67
|
-
FeedbackPopupTurbo: {
|
|
68
|
-
name: `Feedback Popup`,
|
|
69
|
-
id: `feedback`,
|
|
70
|
-
branch: `feedback`,
|
|
71
|
-
description: `Add a feedback popup to your site.`,
|
|
72
|
-
folder: `packages/plugins/feedback`
|
|
73
|
-
},
|
|
74
|
-
AiChatBotTurbo: {
|
|
75
|
-
name: `AI Chatbot`,
|
|
76
|
-
id: `chatbot`,
|
|
77
|
-
branch: `chatbot`,
|
|
78
|
-
description: `Add an AI Chatbot to your site.`,
|
|
79
|
-
folder: `packages/plugins/chatbot`
|
|
80
|
-
},
|
|
81
|
-
Testimonial: {
|
|
82
|
-
name: `Testimonial`,
|
|
83
|
-
id: `testimonial`,
|
|
84
|
-
branch: `testimonial`,
|
|
85
|
-
description: `Add a testimonial to your site.`,
|
|
86
|
-
folder: `packages/plugins/testimonial`
|
|
87
|
-
},
|
|
88
|
-
Roadmap: {
|
|
89
|
-
name: `Roadmap`,
|
|
90
|
-
id: `roadmap`,
|
|
91
|
-
branch: `roadmap`,
|
|
92
|
-
description: `Add a Roadmap to your site.`,
|
|
93
|
-
folder: `packages/plugins/roadmap`
|
|
94
|
-
},
|
|
95
|
-
Kanban: {
|
|
96
|
-
name: `Kanban`,
|
|
97
|
-
id: `kanban`,
|
|
98
|
-
branch: `kanban`,
|
|
99
|
-
description: `Add a Kanban component to your site.`,
|
|
100
|
-
folder: `packages/plugins/kanban`
|
|
101
|
-
},
|
|
102
|
-
GoogleAnalytics: {
|
|
103
|
-
name: `Google Analytics`,
|
|
104
|
-
id: `google-analytics`,
|
|
105
|
-
branch: `google-analytics`,
|
|
106
|
-
description: `Add Google Analytics to your site.`,
|
|
107
|
-
folder: `packages/plugins/analytics/google-analytics`
|
|
108
|
-
},
|
|
109
|
-
Posthog: {
|
|
110
|
-
name: `Posthog`,
|
|
111
|
-
id: `posthog`,
|
|
112
|
-
branch: `posthog`,
|
|
113
|
-
description: `Add Posthog Analytics to your site.`,
|
|
114
|
-
folder: `packages/plugins/analytics/posthog`
|
|
115
|
-
},
|
|
116
|
-
Umami: {
|
|
117
|
-
name: `Umami`,
|
|
118
|
-
id: `umami`,
|
|
119
|
-
branch: `umami`,
|
|
120
|
-
description: `Add Umami Analytics to your site.`,
|
|
121
|
-
folder: `packages/plugins/analytics/umami`
|
|
122
|
-
},
|
|
123
|
-
Signoz: {
|
|
124
|
-
name: `Signoz`,
|
|
125
|
-
id: `signoz`,
|
|
126
|
-
branch: `signoz`,
|
|
127
|
-
description: `Add Signoz Monitoring to your app.`,
|
|
128
|
-
folder: `packages/plugins/signoz`
|
|
129
|
-
},
|
|
130
|
-
Paddle: {
|
|
131
|
-
name: `Paddle`,
|
|
132
|
-
id: `paddle`,
|
|
133
|
-
branch: `paddle`,
|
|
134
|
-
description: `Add Paddle Billing to your app.`,
|
|
135
|
-
folder: `packages/plugins/paddle`
|
|
136
|
-
},
|
|
137
|
-
SupabaseCMS: {
|
|
138
|
-
name: `Supabase CMS`,
|
|
139
|
-
id: `supabase-cms`,
|
|
140
|
-
branch: `supabase-cms`,
|
|
141
|
-
description: `Add Supabase CMS provider to your app.`,
|
|
142
|
-
folder: `packages/plugins/supabase-cms`
|
|
143
|
-
}
|
|
18
|
+
// src/version.ts
|
|
19
|
+
var CLI_VERSION = "2.0.0-beta.1";
|
|
20
|
+
|
|
21
|
+
// src/utils/marker-file.ts
|
|
22
|
+
async function writeMarkerFile(projectPath, variant, kitRepo) {
|
|
23
|
+
const dir = join(projectPath, ".makerkit");
|
|
24
|
+
await fs.ensureDir(dir);
|
|
25
|
+
const config2 = {
|
|
26
|
+
version: 1,
|
|
27
|
+
variant,
|
|
28
|
+
kit_repo: kitRepo,
|
|
29
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
30
|
+
cli_version: CLI_VERSION
|
|
31
|
+
};
|
|
32
|
+
await fs.writeJson(join(dir, "config.json"), config2, { spaces: 2 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/utils/upstream.ts
|
|
36
|
+
import { execaCommand } from "execa";
|
|
37
|
+
var VARIANT_REPO_MAP = {
|
|
38
|
+
"next-supabase": "makerkit/next-supabase-saas-kit-turbo",
|
|
39
|
+
"next-drizzle": "makerkit/next-drizzle-saas-kit-turbo",
|
|
40
|
+
"next-prisma": "makerkit/next-prisma-saas-kit-turbo",
|
|
41
|
+
"react-router-supabase": "makerkit/react-router-supabase-saas-kit-turbo"
|
|
144
42
|
};
|
|
145
|
-
function
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
repository: `git@github.com:makerkit/next-firebase-saas-kit`,
|
|
161
|
-
blogPath: `_posts`,
|
|
162
|
-
localePath: `public/locales`,
|
|
163
|
-
plugins: [
|
|
164
|
-
PluginsModel.CookieBanner.id,
|
|
165
|
-
PluginsModel.AiChatBot.id,
|
|
166
|
-
PluginsModel.FeedbackPopup.id
|
|
167
|
-
]
|
|
168
|
-
},
|
|
169
|
-
NextJsSupabase: {
|
|
170
|
-
name: `Next.js Supabase`,
|
|
171
|
-
id: `next-supabase`,
|
|
172
|
-
localePath: `public/locales`,
|
|
173
|
-
blogPath: `src/content/posts`,
|
|
174
|
-
repository: `git@github.com:makerkit/next-supabase-saas-kit`,
|
|
175
|
-
plugins: [
|
|
176
|
-
PluginsModel.AiChatBot.id,
|
|
177
|
-
PluginsModel.FeedbackPopup.id,
|
|
178
|
-
PluginsModel.CookieBanner.id,
|
|
179
|
-
PluginsModel.AiTextEditor.id
|
|
180
|
-
]
|
|
181
|
-
},
|
|
182
|
-
NextJsSupabaseTurbo: {
|
|
183
|
-
name: `Next.js Supabase Turbo`,
|
|
184
|
-
id: `next-supabase-turbo`,
|
|
185
|
-
localePath: `apps/web/public/locales`,
|
|
186
|
-
blogPath: `apps/web/content/posts`,
|
|
187
|
-
repository: `git@github.com:makerkit/next-supabase-saas-kit-turbo`,
|
|
188
|
-
plugins: [
|
|
189
|
-
PluginsModel.Waitlist.id,
|
|
190
|
-
PluginsModel.Testimonial.id,
|
|
191
|
-
PluginsModel.Roadmap.id,
|
|
192
|
-
PluginsModel.Kanban.id,
|
|
193
|
-
PluginsModel.AiChatBotTurbo.id,
|
|
194
|
-
PluginsModel.FeedbackPopupTurbo.id,
|
|
195
|
-
PluginsModel.AiTextEditorTurbo.id,
|
|
196
|
-
PluginsModel.GoogleAnalytics.id,
|
|
197
|
-
PluginsModel.Posthog.id,
|
|
198
|
-
PluginsModel.Umami.id,
|
|
199
|
-
PluginsModel.Signoz.id,
|
|
200
|
-
PluginsModel.Paddle.id,
|
|
201
|
-
PluginsModel.SupabaseCMS.id
|
|
202
|
-
]
|
|
203
|
-
},
|
|
204
|
-
RemixSupabaseTurbo: {
|
|
205
|
-
name: `Remix Supabase Turbo`,
|
|
206
|
-
id: `remix-supabase-turbo`,
|
|
207
|
-
localePath: `apps/web/public/locales`,
|
|
208
|
-
blogPath: `apps/web/content/posts`,
|
|
209
|
-
repository: `git@github.com:makerkit/remix-supabase-saas-kit-turbo`,
|
|
210
|
-
plugins: [
|
|
211
|
-
PluginsModel.Waitlist.id,
|
|
212
|
-
PluginsModel.Testimonial.id,
|
|
213
|
-
PluginsModel.AiChatBotTurbo.id,
|
|
214
|
-
PluginsModel.FeedbackPopupTurbo.id,
|
|
215
|
-
PluginsModel.AiTextEditorTurbo.id,
|
|
216
|
-
PluginsModel.GoogleAnalytics.id,
|
|
217
|
-
PluginsModel.Posthog.id,
|
|
218
|
-
PluginsModel.Umami.id
|
|
219
|
-
]
|
|
220
|
-
},
|
|
221
|
-
NextJsSupabaseLite: {
|
|
222
|
-
name: `Next.js Supabase Lite`,
|
|
223
|
-
id: `next-supabase-lite`,
|
|
224
|
-
localePath: `public/locales`,
|
|
225
|
-
blogPath: `src/content/posts`,
|
|
226
|
-
repository: `git@github.com:makerkit/next-supabase-saas-kit-lite`,
|
|
227
|
-
plugins: []
|
|
228
|
-
},
|
|
229
|
-
RemixFirebase: {
|
|
230
|
-
name: `Remix Firebase`,
|
|
231
|
-
id: `remix-firebase`,
|
|
232
|
-
localePath: `public/locales`,
|
|
233
|
-
blogPath: "",
|
|
234
|
-
repository: `git@github.com:makerkit/remix-firebase-saas-kit`,
|
|
235
|
-
plugins: []
|
|
236
|
-
},
|
|
237
|
-
RemixSupabase: {
|
|
238
|
-
name: `Remix Supabase`,
|
|
239
|
-
id: `remix-supabase`,
|
|
240
|
-
localePath: `public/locales`,
|
|
241
|
-
blogPath: `app/content/posts`,
|
|
242
|
-
repository: `git@github.com:makerkit/remix-supabase-saas-kit`,
|
|
243
|
-
plugins: [
|
|
244
|
-
PluginsModel.AiChatBot.id,
|
|
245
|
-
PluginsModel.FeedbackPopup.id,
|
|
246
|
-
PluginsModel.CookieBanner.id,
|
|
247
|
-
PluginsModel.AiTextEditor.id
|
|
248
|
-
]
|
|
43
|
+
function sshUrl(repo) {
|
|
44
|
+
return `git@github.com:${repo}`;
|
|
45
|
+
}
|
|
46
|
+
function httpsUrl(repo) {
|
|
47
|
+
return `https://github.com/${repo}`;
|
|
48
|
+
}
|
|
49
|
+
async function hasSshAccess() {
|
|
50
|
+
try {
|
|
51
|
+
await execaCommand("ssh -T git@github.com -o StrictHostKeyChecking=no", {
|
|
52
|
+
timeout: 1e4
|
|
53
|
+
});
|
|
54
|
+
return true;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
const stderr = error instanceof Error && "stderr" in error ? String(error.stderr) : "";
|
|
57
|
+
return stderr.includes("successfully authenticated");
|
|
249
58
|
}
|
|
250
|
-
};
|
|
251
|
-
function getKitById(id) {
|
|
252
|
-
return Object.values(KitsModel).find((kit) => kit.id === id);
|
|
253
59
|
}
|
|
254
|
-
function
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
return kit;
|
|
60
|
+
function getUpstreamUrl(variant, useSsh) {
|
|
61
|
+
const repo = VARIANT_REPO_MAP[variant];
|
|
62
|
+
return useSsh ? sshUrl(repo) : httpsUrl(repo);
|
|
258
63
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
return
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
};
|
|
64
|
+
function isUpstreamUrlValid(url, variant) {
|
|
65
|
+
const normalized = url.replace(/\/+$/, "").replace(/\.git$/, "");
|
|
66
|
+
const repo = VARIANT_REPO_MAP[variant];
|
|
67
|
+
return normalized === sshUrl(repo) || normalized === httpsUrl(repo);
|
|
68
|
+
}
|
|
69
|
+
async function getUpstreamRemoteUrl() {
|
|
70
|
+
try {
|
|
71
|
+
const { stdout } = await execaCommand("git remote get-url upstream");
|
|
72
|
+
return stdout.trim() || void 0;
|
|
73
|
+
} catch {
|
|
74
|
+
return void 0;
|
|
271
75
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
`
|
|
279
|
-
);
|
|
76
|
+
}
|
|
77
|
+
async function setUpstreamRemote(url) {
|
|
78
|
+
const currentUrl = await getUpstreamRemoteUrl();
|
|
79
|
+
if (currentUrl) {
|
|
80
|
+
await execaCommand(`git remote set-url upstream ${url}`);
|
|
81
|
+
} else {
|
|
82
|
+
await execaCommand(`git remote add upstream ${url}`);
|
|
280
83
|
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/utils/create-project.ts
|
|
87
|
+
async function createProject(options) {
|
|
88
|
+
const { variant, name, directory, githubToken } = options;
|
|
89
|
+
const projectPath = join2(directory, name);
|
|
90
|
+
const repo = VARIANT_REPO_MAP[variant];
|
|
91
|
+
if (await fs2.pathExists(projectPath)) {
|
|
285
92
|
throw new Error(
|
|
286
|
-
"
|
|
93
|
+
`Target directory "${projectPath}" already exists. Choose a different name or remove it first.`
|
|
287
94
|
);
|
|
288
95
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
if (deps.includes("turbo")) {
|
|
294
|
-
packageJson = await fs.readJSON(
|
|
295
|
-
join(process.cwd(), "apps/web/package.json")
|
|
96
|
+
if (!await fs2.pathExists(directory)) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Parent directory "${directory}" does not exist.`
|
|
296
99
|
);
|
|
297
|
-
deps = Object.keys(packageJson.dependencies ?? []);
|
|
298
|
-
if (deps.includes("next")) {
|
|
299
|
-
return KitsModel.NextJsSupabaseTurbo;
|
|
300
|
-
}
|
|
301
|
-
if (deps.includes("@remix-run/react")) {
|
|
302
|
-
return KitsModel.RemixSupabaseTurbo;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
if (deps.includes("next") && deps.includes("firebase")) {
|
|
306
|
-
return KitsModel.NextJsFirebase;
|
|
307
|
-
}
|
|
308
|
-
if (deps.includes("next") && deps.includes("@supabase/supabase-js")) {
|
|
309
|
-
return KitsModel.NextJsSupabase;
|
|
310
|
-
}
|
|
311
|
-
if (deps.includes("@remix-run/react") && deps.includes("@supabase/supabase-js")) {
|
|
312
|
-
return KitsModel.RemixSupabase;
|
|
313
100
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// src/commands/i18n/i18n-service.ts
|
|
323
|
-
import chalk2 from "chalk";
|
|
324
|
-
import fs2 from "fs-extra";
|
|
325
|
-
var I18nService = class {
|
|
326
|
-
/**
|
|
327
|
-
* @name verify
|
|
328
|
-
* @description Verifies if the locale files are in sync
|
|
329
|
-
*/
|
|
330
|
-
static async verify(base = "en") {
|
|
331
|
-
const kit = await Workspace.getKitMeta();
|
|
332
|
-
const allLocales = fs2.readdirSync(kit.localePath).filter((locale) => {
|
|
333
|
-
return locale !== base;
|
|
334
|
-
});
|
|
335
|
-
const baseLocaleFolderPath = `${kit.localePath}/${base}`;
|
|
336
|
-
const localeFiles = fs2.readdirSync(baseLocaleFolderPath).filter((file) => {
|
|
337
|
-
return file.endsWith(".json");
|
|
338
|
-
});
|
|
339
|
-
for (const localeFile of localeFiles) {
|
|
340
|
-
for (const locale of allLocales) {
|
|
341
|
-
const baseLocaleFilePath = `${baseLocaleFolderPath}/${localeFile}`;
|
|
342
|
-
const targetLocaleFilePath = `${kit.localePath}/${locale}/${localeFile}`;
|
|
343
|
-
const baseLocaleFile = fs2.readJSONSync(baseLocaleFilePath);
|
|
344
|
-
const file = fs2.readJSONSync(targetLocaleFilePath);
|
|
345
|
-
const missingKeys = collectMissingKeys([baseLocaleFile, file]);
|
|
346
|
-
if (!missingKeys.length) {
|
|
347
|
-
console.log(
|
|
348
|
-
chalk2.green(`Locale ${locale}/${localeFile} is in sync!`)
|
|
349
|
-
);
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
console.log(
|
|
353
|
-
chalk2.yellow(
|
|
354
|
-
`Locale ${locale}/${localeFile} is missing the following keys: ${missingKeys.join(
|
|
355
|
-
", "
|
|
356
|
-
)}`
|
|
357
|
-
)
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
101
|
+
let cloneUrl;
|
|
102
|
+
if (githubToken) {
|
|
103
|
+
cloneUrl = `https://${githubToken}@github.com/${repo}`;
|
|
104
|
+
} else {
|
|
105
|
+
const useSsh = await hasSshAccess();
|
|
106
|
+
cloneUrl = useSsh ? `git@github.com:${repo}` : `https://github.com/${repo}`;
|
|
361
107
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const kit = await Workspace.getKitMeta();
|
|
370
|
-
const client = openai_client_default();
|
|
371
|
-
const targetJsonPath = `${kit.localePath}/${target}`;
|
|
372
|
-
const sourceLocalePath = `${kit.localePath}/${source}`;
|
|
373
|
-
const sourceLocale = await fs2.exists(sourceLocalePath);
|
|
374
|
-
if (!sourceLocale) {
|
|
375
|
-
throw new Error(`Source locale at ${sourceLocalePath} not found`);
|
|
376
|
-
}
|
|
377
|
-
const files = fs2.readdirSync(sourceLocalePath).filter((file) => {
|
|
378
|
-
return file.endsWith(".json");
|
|
379
|
-
});
|
|
380
|
-
console.log(`Found the following files: ${files.join(", ")}`);
|
|
381
|
-
for (const file of files) {
|
|
382
|
-
const data = {};
|
|
383
|
-
const localeFile = fs2.readJSONSync(join2(sourceLocalePath, file));
|
|
384
|
-
console.log(
|
|
385
|
-
chalk2.cyan(`Translating "${file}". This can take a while...`)
|
|
386
|
-
);
|
|
387
|
-
for (const key in localeFile) {
|
|
388
|
-
data[key] = await translateKey(source, target, localeFile[key], client);
|
|
389
|
-
}
|
|
390
|
-
console.log(chalk2.green(`File "${file}" successfully translated!`));
|
|
391
|
-
console.log(chalk2.cyan(`Writing file "${file}" to ${targetJsonPath}`));
|
|
392
|
-
await fs2.exists(targetJsonPath) || await fs2.mkdir(targetJsonPath);
|
|
393
|
-
await fs2.writeJSON(join2(targetJsonPath, file), data, {});
|
|
394
|
-
console.log(chalk2.green(`File "${file}" successfully written!`));
|
|
395
|
-
}
|
|
108
|
+
await execa("git", ["clone", cloneUrl, name], { cwd: directory });
|
|
109
|
+
if (githubToken) {
|
|
110
|
+
await execa(
|
|
111
|
+
"git",
|
|
112
|
+
["remote", "set-url", "origin", `https://github.com/${repo}`],
|
|
113
|
+
{ cwd: projectPath }
|
|
114
|
+
);
|
|
396
115
|
}
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
if (typeof key === "string") {
|
|
400
|
-
return translateString(source, target, key, client);
|
|
401
|
-
}
|
|
402
|
-
const data = {};
|
|
403
|
-
for (const k in key) {
|
|
404
|
-
data[k] = await translateKey(source, target, key[k], client);
|
|
405
|
-
}
|
|
406
|
-
return data;
|
|
407
|
-
}
|
|
408
|
-
async function translateString(source, target, key, client) {
|
|
409
|
-
if (!key.trim() || key.length <= 1) {
|
|
410
|
-
return "";
|
|
411
|
-
}
|
|
412
|
-
const response = await client.chat.completions.create({
|
|
413
|
-
model: "gpt-3.5-turbo",
|
|
414
|
-
max_tokens: target.split(" ").length + 50,
|
|
415
|
-
temperature: 0.3,
|
|
416
|
-
messages: [
|
|
417
|
-
{
|
|
418
|
-
role: "user",
|
|
419
|
-
content: `Translate the text from locale ${source} to ${target}. Text: ${key}. Translation:`
|
|
420
|
-
}
|
|
421
|
-
]
|
|
422
|
-
});
|
|
423
|
-
return response.choices[0].message.content ?? "";
|
|
424
|
-
}
|
|
425
|
-
function collectMissingKeys(objects) {
|
|
426
|
-
const allKeys = [];
|
|
427
|
-
objects.forEach((obj) => {
|
|
428
|
-
Object.keys(obj).forEach((key) => {
|
|
429
|
-
if (!allKeys.includes(key)) {
|
|
430
|
-
allKeys.push(key);
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
});
|
|
434
|
-
const missingKeys = [];
|
|
435
|
-
objects.forEach((obj) => {
|
|
436
|
-
allKeys.forEach((key) => {
|
|
437
|
-
if (!Object.keys(obj).includes(key)) {
|
|
438
|
-
missingKeys.push(key);
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
return missingKeys;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// src/commands/i18n/translate/translate.command.ts
|
|
446
|
-
import chalk3 from "chalk";
|
|
447
|
-
import prompts from "prompts";
|
|
448
|
-
function createTranslateI18nCommand(parentCommand) {
|
|
449
|
-
return parentCommand.command("translate").argument("[source-locale]", "Source Locale").argument("[target-locale]", "Target Locale").description("Translate i18n files from source locale to target locale").action(async (sourceLocale, targetLocale) => {
|
|
450
|
-
const locales = await promptLocales(sourceLocale, targetLocale);
|
|
451
|
-
await I18nService.translate(locales.sourceLocale, locales.targetLocale);
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
async function promptLocales(maybeSource, maybeTarget) {
|
|
455
|
-
const hint = "e.g. en, fr, es, etc.";
|
|
456
|
-
const sourceLocale = maybeSource || (await prompts([
|
|
457
|
-
{
|
|
458
|
-
type: "text",
|
|
459
|
-
name: "locale",
|
|
460
|
-
message: `Enter your ${chalk3.cyan("Source Locale")}. ${hint}`,
|
|
461
|
-
hint
|
|
462
|
-
}
|
|
463
|
-
])).locale;
|
|
464
|
-
const targetLocale = maybeTarget || (await prompts([
|
|
465
|
-
{
|
|
466
|
-
type: "text",
|
|
467
|
-
name: "locale",
|
|
468
|
-
message: `Enter your ${chalk3.cyan("Target Locale")}. ${hint}`,
|
|
469
|
-
hint
|
|
470
|
-
}
|
|
471
|
-
])).locale;
|
|
116
|
+
await execa("pnpm", ["install"], { cwd: projectPath });
|
|
117
|
+
await writeMarkerFile(projectPath, variant, repo);
|
|
472
118
|
return {
|
|
473
|
-
|
|
474
|
-
|
|
119
|
+
success: true,
|
|
120
|
+
projectPath,
|
|
121
|
+
variant,
|
|
122
|
+
kitRepo: repo,
|
|
123
|
+
message: `Project "${name}" created successfully with variant "${variant}".`
|
|
475
124
|
};
|
|
476
125
|
}
|
|
477
126
|
|
|
478
|
-
// src/
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
message: `Enter your ${chalk4.cyan("Github username")}.`,
|
|
522
|
-
validate: (value) => {
|
|
523
|
-
if (value.length < 1) {
|
|
524
|
-
return `Please enter a valid username`;
|
|
525
|
-
}
|
|
526
|
-
return true;
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
]);
|
|
530
|
-
const spinner = ora(`Activating license...`).start();
|
|
531
|
-
const onError = () => {
|
|
532
|
-
spinner.fail(`Failed to activate license`);
|
|
533
|
-
process.exit(1);
|
|
534
|
-
};
|
|
535
|
-
try {
|
|
536
|
-
const response = await activateLicense(schema.parse(params));
|
|
537
|
-
if (!response.ok) {
|
|
538
|
-
return onError();
|
|
539
|
-
}
|
|
540
|
-
spinner.succeed(`License activated successfully`);
|
|
541
|
-
} catch (e) {
|
|
542
|
-
onError();
|
|
543
|
-
}
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
async function activateLicense(params) {
|
|
547
|
-
return fetch(`https://makerkit.dev/api/ls/license/activate`, {
|
|
548
|
-
method: "POST",
|
|
549
|
-
body: JSON.stringify(params)
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// src/commands/license/license.command.ts
|
|
554
|
-
import { Command as Command2 } from "commander";
|
|
555
|
-
var licenseCommand = new Command2().name("license").description("Manage Licenses");
|
|
556
|
-
createActivateLicenseCommand(licenseCommand);
|
|
127
|
+
// src/utils/list-variants.ts
|
|
128
|
+
var VARIANT_CATALOG = [
|
|
129
|
+
{
|
|
130
|
+
id: "next-supabase",
|
|
131
|
+
name: "Next.js + Supabase",
|
|
132
|
+
description: "Full-stack SaaS kit with Next.js App Router and Supabase",
|
|
133
|
+
repo: VARIANT_REPO_MAP["next-supabase"],
|
|
134
|
+
tech: ["Next.js", "Supabase", "Tailwind CSS", "shadcn/ui"],
|
|
135
|
+
database: "PostgreSQL (Supabase)",
|
|
136
|
+
auth: "Supabase Auth",
|
|
137
|
+
status: "stable"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "next-drizzle",
|
|
141
|
+
name: "Next.js + Drizzle",
|
|
142
|
+
description: "Full-stack SaaS kit with Next.js and Drizzle ORM",
|
|
143
|
+
repo: VARIANT_REPO_MAP["next-drizzle"],
|
|
144
|
+
tech: ["Next.js", "Drizzle", "Tailwind CSS", "shadcn/ui"],
|
|
145
|
+
database: "PostgreSQL",
|
|
146
|
+
auth: "Better Auth",
|
|
147
|
+
status: "stable"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "next-prisma",
|
|
151
|
+
name: "Next.js + Prisma",
|
|
152
|
+
description: "Full-stack SaaS kit with Next.js and Prisma ORM",
|
|
153
|
+
repo: VARIANT_REPO_MAP["next-prisma"],
|
|
154
|
+
tech: ["Next.js", "Prisma", "Tailwind CSS", "shadcn/ui"],
|
|
155
|
+
database: "PostgreSQL",
|
|
156
|
+
auth: "Better Auth",
|
|
157
|
+
status: "stable"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "react-router-supabase",
|
|
161
|
+
name: "React Router + Supabase",
|
|
162
|
+
description: "Full-stack SaaS kit with React Router and Supabase",
|
|
163
|
+
repo: VARIANT_REPO_MAP["react-router-supabase"],
|
|
164
|
+
tech: ["React Router", "Supabase", "Tailwind CSS", "shadcn/ui"],
|
|
165
|
+
database: "PostgreSQL (Supabase)",
|
|
166
|
+
auth: "Supabase Auth",
|
|
167
|
+
status: "stable"
|
|
168
|
+
}
|
|
169
|
+
];
|
|
557
170
|
|
|
558
171
|
// src/commands/new/new.command.ts
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
var newCommand = new Command3().name("new").description("Initialize a new Makerkit project").action(async () => {
|
|
566
|
-
const choices = Object.values(KitsModel).map((kit2) => {
|
|
567
|
-
return {
|
|
568
|
-
title: kit2.name,
|
|
569
|
-
value: kit2.id
|
|
570
|
-
};
|
|
571
|
-
});
|
|
572
|
-
const { kit, name: projectName } = await prompts3([
|
|
172
|
+
var newCommand = new Command().name("new").description("Initialize a new Makerkit project").action(async () => {
|
|
173
|
+
const choices = VARIANT_CATALOG.map((v) => ({
|
|
174
|
+
title: v.name,
|
|
175
|
+
value: v.id
|
|
176
|
+
}));
|
|
177
|
+
const { kit, name: projectName } = await prompts([
|
|
573
178
|
{
|
|
574
179
|
type: "select",
|
|
575
180
|
name: "kit",
|
|
576
|
-
message: `Select the ${
|
|
181
|
+
message: `Select the ${chalk.cyan(`SaaS Kit`)} you want to clone`,
|
|
577
182
|
choices
|
|
578
183
|
},
|
|
579
184
|
{
|
|
580
185
|
type: "text",
|
|
581
186
|
name: "name",
|
|
582
|
-
message: `Enter your ${
|
|
187
|
+
message: `Enter your ${chalk.cyan("Project Name")}.`
|
|
583
188
|
}
|
|
584
189
|
]);
|
|
585
|
-
const
|
|
586
|
-
|
|
190
|
+
const selected = VARIANT_CATALOG.find((v) => v.id === kit);
|
|
191
|
+
if (!selected) {
|
|
192
|
+
console.log("Invalid kit selection. Aborting...");
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
const { confirm } = await prompts([
|
|
587
196
|
{
|
|
588
197
|
type: "confirm",
|
|
589
198
|
name: "confirm",
|
|
590
|
-
message: `Are you sure you want to clone ${
|
|
591
|
-
|
|
592
|
-
)} to ${
|
|
199
|
+
message: `Are you sure you want to clone ${chalk.cyan(
|
|
200
|
+
selected.name
|
|
201
|
+
)} to ${chalk.cyan(projectName)}?`
|
|
593
202
|
}
|
|
594
203
|
]);
|
|
595
204
|
if (!confirm) {
|
|
596
205
|
console.log("Aborting...");
|
|
597
206
|
process.exit(0);
|
|
598
207
|
}
|
|
599
|
-
|
|
600
|
-
await installDependencies(projectName);
|
|
601
|
-
});
|
|
602
|
-
async function pullFromGithub(repository, kit, projectName) {
|
|
603
|
-
const spinner = ora2(`Cloning ${kit}...`).start();
|
|
604
|
-
try {
|
|
605
|
-
await execa("git", ["clone", repository, projectName]);
|
|
606
|
-
spinner.succeed(`Cloned ${kit} to ${projectName}...`);
|
|
607
|
-
} catch (e) {
|
|
608
|
-
console.error(e);
|
|
609
|
-
spinner.fail(`Failed to clone ${kit}`);
|
|
610
|
-
return Promise.reject(`Failed to clone ${kit}`);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
async function installDependencies(projectName) {
|
|
614
|
-
const cwd = join3(process.cwd(), projectName);
|
|
615
|
-
const spinner = ora2(`Installing dependencies. Please wait...`).start();
|
|
208
|
+
const spinner = ora(`Cloning ${selected.name}...`).start();
|
|
616
209
|
try {
|
|
617
|
-
await
|
|
618
|
-
|
|
210
|
+
const result = await createProject({
|
|
211
|
+
variant: selected.id,
|
|
212
|
+
name: projectName,
|
|
213
|
+
directory: process.cwd()
|
|
214
|
+
});
|
|
215
|
+
spinner.succeed(`${result.message}`);
|
|
619
216
|
console.log(
|
|
620
|
-
|
|
217
|
+
`You can now get started. Open the project in your IDE and read the README.md file for more information.`
|
|
621
218
|
);
|
|
622
219
|
} catch (e) {
|
|
623
220
|
console.error(e);
|
|
624
|
-
spinner.fail(`Failed to
|
|
625
|
-
|
|
221
|
+
spinner.fail(`Failed to create project`);
|
|
222
|
+
process.exit(1);
|
|
626
223
|
}
|
|
627
|
-
}
|
|
224
|
+
});
|
|
628
225
|
|
|
629
|
-
// src/
|
|
630
|
-
import
|
|
631
|
-
import
|
|
632
|
-
import
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
await
|
|
226
|
+
// src/plugins-model.ts
|
|
227
|
+
import { join as join4 } from "path";
|
|
228
|
+
import fs4 from "fs-extra";
|
|
229
|
+
import invariant from "tiny-invariant";
|
|
230
|
+
|
|
231
|
+
// src/utils/plugins-cache.ts
|
|
232
|
+
import { join as join3 } from "path";
|
|
233
|
+
import { homedir } from "os";
|
|
234
|
+
import fs3 from "fs-extra";
|
|
235
|
+
var CACHE_DIR = join3(homedir(), ".makerkit");
|
|
236
|
+
var CACHE_FILE = join3(CACHE_DIR, "plugins.json");
|
|
237
|
+
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
238
|
+
async function readCache() {
|
|
239
|
+
try {
|
|
240
|
+
if (!await fs3.pathExists(CACHE_FILE)) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
return await fs3.readJson(CACHE_FILE);
|
|
244
|
+
} catch {
|
|
245
|
+
return null;
|
|
644
246
|
}
|
|
645
|
-
}
|
|
646
|
-
async function
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
247
|
+
}
|
|
248
|
+
async function writeCache(plugins) {
|
|
249
|
+
try {
|
|
250
|
+
await fs3.ensureDir(CACHE_DIR);
|
|
251
|
+
await fs3.writeJson(
|
|
252
|
+
CACHE_FILE,
|
|
253
|
+
{ fetchedAt: Date.now(), plugins },
|
|
254
|
+
{ spaces: 2 }
|
|
653
255
|
);
|
|
256
|
+
} catch {
|
|
654
257
|
}
|
|
655
|
-
const repository = buildPluginRepositoryName(kit.repository);
|
|
656
|
-
const verb = action === "add" /* Add */ ? "Installing" : "Updating";
|
|
657
|
-
console.log(
|
|
658
|
-
`${verb} ${chalk6.cyan(plugin.name)} to kit ${chalk6.cyan(
|
|
659
|
-
kit.name
|
|
660
|
-
)} using repo: ${repository} ...`
|
|
661
|
-
);
|
|
662
|
-
return executePluginCommand({
|
|
663
|
-
action,
|
|
664
|
-
repository,
|
|
665
|
-
branch: plugin.branch,
|
|
666
|
-
folder: plugin.folder
|
|
667
|
-
});
|
|
668
258
|
}
|
|
669
|
-
async function
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
};
|
|
678
|
-
});
|
|
679
|
-
const verb = action === "add" /* Add */ ? "install" : "update";
|
|
680
|
-
const { plugin } = await prompts4([
|
|
681
|
-
{
|
|
682
|
-
type: "select",
|
|
683
|
-
name: "plugin",
|
|
684
|
-
message: `Which ${chalk6.cyan("Plugin")} would you like to ${verb}?`,
|
|
685
|
-
choices
|
|
686
|
-
}
|
|
687
|
-
]);
|
|
688
|
-
return plugin;
|
|
689
|
-
}
|
|
690
|
-
function buildPluginRepositoryName(repository) {
|
|
691
|
-
return repository + `-plugins.git`;
|
|
692
|
-
}
|
|
693
|
-
async function executePluginCommand({
|
|
694
|
-
action,
|
|
695
|
-
repository,
|
|
696
|
-
branch,
|
|
697
|
-
folder
|
|
698
|
-
}) {
|
|
699
|
-
const verb = action === "add" /* Add */ ? "Installing" : "Updating";
|
|
700
|
-
const spinner = ora3(`${verb} plugin...`).start();
|
|
701
|
-
const commandString = `git subtree ${action} --prefix ${folder} ${repository} ${branch} --squash`;
|
|
259
|
+
async function fetchPluginRegistry(registryUrl, fallback) {
|
|
260
|
+
if (!registryUrl) {
|
|
261
|
+
return fallback;
|
|
262
|
+
}
|
|
263
|
+
const cached = await readCache();
|
|
264
|
+
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
265
|
+
return cached.plugins;
|
|
266
|
+
}
|
|
702
267
|
try {
|
|
703
|
-
await
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
268
|
+
const response = await fetch(registryUrl);
|
|
269
|
+
if (response.ok) {
|
|
270
|
+
const data = await response.json();
|
|
271
|
+
const plugins = data.plugins;
|
|
272
|
+
await writeCache(plugins);
|
|
273
|
+
return plugins;
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
710
276
|
}
|
|
277
|
+
return cached?.plugins ?? fallback;
|
|
711
278
|
}
|
|
712
279
|
|
|
713
|
-
// src/
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const kits = Object.values(KitsModel);
|
|
725
|
-
console.log(chalk7.white("Makerkit available plugins..."));
|
|
726
|
-
console.log(
|
|
727
|
-
`[${chalk7.green("Plugin Name")} (${chalk7.gray("plugin-id")})]
|
|
728
|
-
`
|
|
729
|
-
);
|
|
730
|
-
for (const kit of kits) {
|
|
731
|
-
console.log(`${chalk7.cyan(kit.name)}`);
|
|
732
|
-
if (!kit.plugins.length) {
|
|
733
|
-
console.log(chalk7.yellow(`- No plugins available`) + "\n");
|
|
734
|
-
continue;
|
|
280
|
+
// src/plugins-model.ts
|
|
281
|
+
var DEFAULT_PLUGINS = {
|
|
282
|
+
feedback: {
|
|
283
|
+
name: "Feedback",
|
|
284
|
+
id: "feedback",
|
|
285
|
+
description: "Add a feedback popup to your site.",
|
|
286
|
+
postInstallMessage: "Run database migrations: pnpm db:migrate",
|
|
287
|
+
variants: {
|
|
288
|
+
"next-supabase": {
|
|
289
|
+
envVars: [],
|
|
290
|
+
path: "packages/plugins/feedback"
|
|
735
291
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
waitlist: {
|
|
295
|
+
name: "Waitlist",
|
|
296
|
+
id: "waitlist",
|
|
297
|
+
description: "Add a waitlist to your site.",
|
|
298
|
+
postInstallMessage: "Run database migrations: pnpm db:migrate",
|
|
299
|
+
variants: {
|
|
300
|
+
"next-supabase": {
|
|
301
|
+
envVars: [],
|
|
302
|
+
path: "packages/plugins/waitlist"
|
|
739
303
|
}
|
|
740
|
-
console.log("");
|
|
741
304
|
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
305
|
+
},
|
|
306
|
+
testimonial: {
|
|
307
|
+
name: "Testimonial",
|
|
308
|
+
id: "testimonial",
|
|
309
|
+
description: "Add a testimonial widget to your site.",
|
|
310
|
+
postInstallMessage: "Run database migrations: pnpm db:migrate",
|
|
311
|
+
variants: {
|
|
312
|
+
"next-supabase": {
|
|
313
|
+
envVars: [],
|
|
314
|
+
path: "packages/plugins/testimonial"
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
roadmap: {
|
|
319
|
+
name: "Roadmap",
|
|
320
|
+
id: "roadmap",
|
|
321
|
+
description: "Add a roadmap to your site.",
|
|
322
|
+
postInstallMessage: "Run database migrations: pnpm db:migrate",
|
|
323
|
+
variants: {
|
|
324
|
+
"next-supabase": {
|
|
325
|
+
envVars: [],
|
|
326
|
+
path: "packages/plugins/roadmap"
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
"google-analytics": {
|
|
331
|
+
name: "Google Analytics",
|
|
332
|
+
id: "google-analytics",
|
|
333
|
+
description: "Add Google Analytics to your site.",
|
|
334
|
+
variants: {
|
|
335
|
+
"next-supabase": {
|
|
336
|
+
envVars: [
|
|
337
|
+
{
|
|
338
|
+
key: "NEXT_PUBLIC_GOOGLE_ANALYTICS_ID",
|
|
339
|
+
description: "Google Analytics Measurement ID"
|
|
340
|
+
}
|
|
341
|
+
],
|
|
342
|
+
path: "packages/plugins/analytics/google-analytics"
|
|
343
|
+
},
|
|
344
|
+
"next-drizzle": {
|
|
345
|
+
envVars: [
|
|
346
|
+
{
|
|
347
|
+
key: "NEXT_PUBLIC_GOOGLE_ANALYTICS_ID",
|
|
348
|
+
description: "Google Analytics Measurement ID"
|
|
349
|
+
}
|
|
350
|
+
],
|
|
351
|
+
path: "packages/plugins/analytics/google-analytics"
|
|
352
|
+
},
|
|
353
|
+
"next-prisma": {
|
|
354
|
+
envVars: [
|
|
355
|
+
{
|
|
356
|
+
key: "NEXT_PUBLIC_GOOGLE_ANALYTICS_ID",
|
|
357
|
+
description: "Google Analytics Measurement ID"
|
|
358
|
+
}
|
|
359
|
+
],
|
|
360
|
+
path: "packages/plugins/analytics/google-analytics"
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
honeybadger: {
|
|
365
|
+
name: "Honeybadger",
|
|
366
|
+
id: "honeybadger",
|
|
367
|
+
description: "Add Honeybadger Error Tracking to your site.",
|
|
368
|
+
variants: {
|
|
369
|
+
"next-supabase": {
|
|
370
|
+
envVars: [
|
|
371
|
+
{
|
|
372
|
+
key: "HONEYBADGER_API_KEY",
|
|
373
|
+
description: "Honeybadger private API key"
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
key: "NEXT_PUBLIC_HONEYBADGER_ENVIRONMENT",
|
|
377
|
+
description: "Honeybadger environment"
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
key: "NEXT_PUBLIC_HONEYBADGER_REVISION",
|
|
381
|
+
description: "Honeybadger log revision"
|
|
382
|
+
}
|
|
383
|
+
],
|
|
384
|
+
path: "packages/plugins/honeybadger"
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
posthog: {
|
|
389
|
+
name: "PostHog",
|
|
390
|
+
id: "posthog",
|
|
391
|
+
description: "Add PostHog Analytics to your site.",
|
|
392
|
+
variants: {
|
|
393
|
+
"next-supabase": {
|
|
394
|
+
envVars: [
|
|
395
|
+
{
|
|
396
|
+
key: "NEXT_PUBLIC_POSTHOG_KEY",
|
|
397
|
+
description: "PostHog project API key"
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
key: "NEXT_PUBLIC_POSTHOG_HOST",
|
|
401
|
+
description: "PostHog host URL",
|
|
402
|
+
defaultValue: "https://app.posthog.com"
|
|
403
|
+
}
|
|
404
|
+
],
|
|
405
|
+
path: "packages/plugins/analytics/posthog"
|
|
406
|
+
},
|
|
407
|
+
"next-drizzle": {
|
|
408
|
+
envVars: [
|
|
409
|
+
{
|
|
410
|
+
key: "NEXT_PUBLIC_POSTHOG_KEY",
|
|
411
|
+
description: "PostHog project API key"
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
key: "NEXT_PUBLIC_POSTHOG_HOST",
|
|
415
|
+
description: "PostHog host URL",
|
|
416
|
+
defaultValue: "https://app.posthog.com"
|
|
417
|
+
}
|
|
418
|
+
],
|
|
419
|
+
path: "packages/plugins/analytics/posthog"
|
|
420
|
+
},
|
|
421
|
+
"next-prisma": {
|
|
422
|
+
envVars: [
|
|
423
|
+
{
|
|
424
|
+
key: "NEXT_PUBLIC_POSTHOG_KEY",
|
|
425
|
+
description: "PostHog project API key"
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
key: "NEXT_PUBLIC_POSTHOG_HOST",
|
|
429
|
+
description: "PostHog host URL",
|
|
430
|
+
defaultValue: "https://app.posthog.com"
|
|
431
|
+
}
|
|
432
|
+
],
|
|
433
|
+
path: "packages/plugins/analytics/posthog"
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
umami: {
|
|
438
|
+
name: "Umami",
|
|
439
|
+
id: "umami",
|
|
440
|
+
description: "Add Umami Analytics to your site.",
|
|
441
|
+
variants: {
|
|
442
|
+
"next-supabase": {
|
|
443
|
+
envVars: [
|
|
444
|
+
{
|
|
445
|
+
key: "NEXT_PUBLIC_UMAMI_WEBSITE_ID",
|
|
446
|
+
description: "Umami website ID"
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
key: "NEXT_PUBLIC_UMAMI_HOST",
|
|
450
|
+
description: "Umami host URL"
|
|
451
|
+
}
|
|
452
|
+
],
|
|
453
|
+
path: "packages/plugins/analytics/umami"
|
|
454
|
+
},
|
|
455
|
+
"next-drizzle": {
|
|
456
|
+
envVars: [
|
|
457
|
+
{
|
|
458
|
+
key: "NEXT_PUBLIC_UMAMI_WEBSITE_ID",
|
|
459
|
+
description: "Umami website ID"
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
key: "NEXT_PUBLIC_UMAMI_HOST",
|
|
463
|
+
description: "Umami host URL"
|
|
464
|
+
}
|
|
465
|
+
],
|
|
466
|
+
path: "packages/plugins/analytics/umami"
|
|
467
|
+
},
|
|
468
|
+
"next-prisma": {
|
|
469
|
+
envVars: [
|
|
470
|
+
{
|
|
471
|
+
key: "NEXT_PUBLIC_UMAMI_WEBSITE_ID",
|
|
472
|
+
description: "Umami website ID"
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
key: "NEXT_PUBLIC_UMAMI_HOST",
|
|
476
|
+
description: "Umami host URL"
|
|
477
|
+
}
|
|
478
|
+
],
|
|
479
|
+
path: "packages/plugins/analytics/umami"
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
signoz: {
|
|
484
|
+
name: "SigNoz",
|
|
485
|
+
id: "signoz",
|
|
486
|
+
description: "Add SigNoz Monitoring to your app.",
|
|
487
|
+
variants: {
|
|
488
|
+
"next-supabase": {
|
|
489
|
+
envVars: [
|
|
490
|
+
{
|
|
491
|
+
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
492
|
+
description: "SigNoz OTLP endpoint URL"
|
|
493
|
+
}
|
|
494
|
+
],
|
|
495
|
+
path: "packages/plugins/signoz"
|
|
496
|
+
},
|
|
497
|
+
"next-drizzle": {
|
|
498
|
+
envVars: [
|
|
499
|
+
{
|
|
500
|
+
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
501
|
+
description: "SigNoz OTLP endpoint URL"
|
|
502
|
+
}
|
|
503
|
+
],
|
|
504
|
+
path: "packages/plugins/signoz"
|
|
505
|
+
},
|
|
506
|
+
"next-prisma": {
|
|
507
|
+
envVars: [
|
|
508
|
+
{
|
|
509
|
+
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
510
|
+
description: "SigNoz OTLP endpoint URL"
|
|
511
|
+
}
|
|
512
|
+
],
|
|
513
|
+
path: "packages/plugins/signoz"
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
paddle: {
|
|
518
|
+
name: "Paddle",
|
|
519
|
+
id: "paddle",
|
|
520
|
+
description: "Add Paddle Billing to your app.",
|
|
521
|
+
variants: {
|
|
522
|
+
"next-supabase": {
|
|
523
|
+
envVars: [
|
|
524
|
+
{
|
|
525
|
+
key: "NEXT_PUBLIC_PADDLE_CLIENT_TOKEN",
|
|
526
|
+
description: "Paddle client-side token"
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
key: "PADDLE_API_KEY",
|
|
530
|
+
description: "Paddle API key"
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
key: "PADDLE_WEBHOOK_SECRET",
|
|
534
|
+
description: "Paddle webhook secret"
|
|
535
|
+
}
|
|
536
|
+
],
|
|
537
|
+
path: "packages/plugins/paddle"
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
"supabase-cms": {
|
|
542
|
+
name: "Supabase CMS",
|
|
543
|
+
id: "supabase-cms",
|
|
544
|
+
description: "Add Supabase CMS provider to your app.",
|
|
545
|
+
postInstallMessage: "Run database migrations: pnpm db:migrate",
|
|
546
|
+
variants: {
|
|
547
|
+
"next-supabase": {
|
|
548
|
+
envVars: [],
|
|
549
|
+
path: "packages/plugins/supabase-cms"
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
"meshes-analytics": {
|
|
554
|
+
name: "Meshes Analytics",
|
|
555
|
+
id: "meshes-analytics",
|
|
556
|
+
description: "Add Meshes Analytics to your app.",
|
|
557
|
+
variants: {
|
|
558
|
+
"next-supabase": {
|
|
559
|
+
envVars: [
|
|
560
|
+
{
|
|
561
|
+
key: "NEXT_PUBLIC_MESHES_PUBLISHABLE_KEY",
|
|
562
|
+
description: "The Meshes publishable key"
|
|
563
|
+
}
|
|
564
|
+
],
|
|
565
|
+
path: "packages/plugins/meshes-analytics"
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
directus: {
|
|
570
|
+
name: "Directus CMS",
|
|
571
|
+
id: "directus-cms",
|
|
572
|
+
description: "Add Directus as your CMS.",
|
|
573
|
+
variants: {
|
|
574
|
+
"next-supabase": {
|
|
575
|
+
envVars: [
|
|
576
|
+
{
|
|
577
|
+
key: "NEXT_PUBLIC_DIRECTUS_URL",
|
|
578
|
+
description: "The Directus URL"
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
key: "DIRECTUS_ACCESS_TOKEN",
|
|
582
|
+
description: "The Directus access token"
|
|
583
|
+
}
|
|
584
|
+
],
|
|
585
|
+
path: "packages/plugins/directus"
|
|
586
|
+
},
|
|
587
|
+
"next-drizzle": {
|
|
588
|
+
envVars: [
|
|
589
|
+
{
|
|
590
|
+
key: "NEXT_PUBLIC_DIRECTUS_URL",
|
|
591
|
+
description: "The Directus URL"
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
key: "DIRECTUS_ACCESS_TOKEN",
|
|
595
|
+
description: "The Directus access token"
|
|
596
|
+
}
|
|
597
|
+
],
|
|
598
|
+
path: "packages/plugins/directus"
|
|
599
|
+
},
|
|
600
|
+
"next-prisma": {
|
|
601
|
+
envVars: [
|
|
602
|
+
{
|
|
603
|
+
key: "NEXT_PUBLIC_DIRECTUS_URL",
|
|
604
|
+
description: "The Directus URL"
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
key: "DIRECTUS_ACCESS_TOKEN",
|
|
608
|
+
description: "The Directus access token"
|
|
609
|
+
}
|
|
610
|
+
],
|
|
611
|
+
path: "packages/plugins/directus"
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
var PluginRegistry = class _PluginRegistry {
|
|
617
|
+
constructor(plugins) {
|
|
618
|
+
this.plugins = plugins;
|
|
619
|
+
}
|
|
620
|
+
static async load() {
|
|
621
|
+
const registryUrl = process.env.MAKERKIT_PLUGINS_REGISTRY_URL;
|
|
622
|
+
const plugins = await fetchPluginRegistry(registryUrl, DEFAULT_PLUGINS);
|
|
623
|
+
return new _PluginRegistry(plugins);
|
|
624
|
+
}
|
|
625
|
+
getPluginById(id) {
|
|
626
|
+
return this.plugins[id];
|
|
627
|
+
}
|
|
628
|
+
getPluginsForVariant(variant) {
|
|
629
|
+
return Object.values(this.plugins).filter((p) => variant in p.variants);
|
|
630
|
+
}
|
|
631
|
+
validatePlugin(pluginId, variant) {
|
|
632
|
+
const plugin = this.getPluginById(pluginId);
|
|
633
|
+
invariant(plugin, `Plugin "${pluginId}" not found`);
|
|
634
|
+
invariant(
|
|
635
|
+
plugin.variants[variant],
|
|
636
|
+
`Plugin "${pluginId}" is not available for the ${variant} variant`
|
|
637
|
+
);
|
|
638
|
+
return plugin;
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
function getEnvVars(plugin, variant) {
|
|
642
|
+
return plugin.variants[variant]?.envVars ?? [];
|
|
643
|
+
}
|
|
644
|
+
function getPath(plugin, variant) {
|
|
645
|
+
return plugin.variants[variant]?.path;
|
|
646
|
+
}
|
|
647
|
+
async function isInstalled(plugin, variant) {
|
|
648
|
+
const pluginPath = getPath(plugin, variant);
|
|
649
|
+
if (!pluginPath) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
const pkgJsonPath = join4(process.cwd(), pluginPath, "package.json");
|
|
653
|
+
if (!await fs4.pathExists(pkgJsonPath)) {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
try {
|
|
657
|
+
const pkg = await fs4.readJson(pkgJsonPath);
|
|
658
|
+
return !!pkg.name && !!pkg.exports;
|
|
659
|
+
} catch {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// src/utils/base-store.ts
|
|
665
|
+
import { dirname, join as join5 } from "path";
|
|
666
|
+
import fs5 from "fs-extra";
|
|
667
|
+
function basesDir(pluginId) {
|
|
668
|
+
return join5(process.cwd(), "node_modules", ".cache", "makerkit", "bases", pluginId);
|
|
669
|
+
}
|
|
670
|
+
async function saveBaseVersions(pluginId, files) {
|
|
671
|
+
const dir = basesDir(pluginId);
|
|
672
|
+
for (const file of files) {
|
|
673
|
+
const targetPath = join5(dir, file.target);
|
|
674
|
+
await fs5.ensureDir(dirname(targetPath));
|
|
675
|
+
await fs5.writeFile(targetPath, file.content);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/utils/git.ts
|
|
680
|
+
import { execaCommand as execaCommand2 } from "execa";
|
|
681
|
+
function getErrorOutput(error) {
|
|
682
|
+
if (error instanceof Error && "stderr" in error) {
|
|
683
|
+
const stderr = String(error.stderr);
|
|
684
|
+
if (stderr) return stderr;
|
|
685
|
+
}
|
|
686
|
+
if (error instanceof Error && "stdout" in error) {
|
|
687
|
+
const stdout = String(error.stdout);
|
|
688
|
+
if (stdout) return stdout;
|
|
689
|
+
}
|
|
690
|
+
return error instanceof Error ? error.message : String(error);
|
|
691
|
+
}
|
|
692
|
+
async function isGitClean() {
|
|
693
|
+
const { stdout } = await execaCommand2("git status --porcelain");
|
|
694
|
+
return stdout.trim() === "";
|
|
695
|
+
}
|
|
696
|
+
async function getGitUsername() {
|
|
697
|
+
try {
|
|
698
|
+
const { stdout } = await execaCommand2("git config --get user.username");
|
|
699
|
+
const username = stdout.trim();
|
|
700
|
+
return username.length > 0 ? username : void 0;
|
|
701
|
+
} catch {
|
|
702
|
+
return void 0;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/utils/install-registry-files.ts
|
|
707
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
708
|
+
import fs6 from "fs-extra";
|
|
709
|
+
async function fetchRegistryItem(variant, pluginId, username) {
|
|
710
|
+
const url = `https://makerkit.dev/r/${variant}/${pluginId}.json?username=${username}`;
|
|
711
|
+
const response = await fetch(url);
|
|
712
|
+
if (!response.ok) {
|
|
713
|
+
throw new Error(
|
|
714
|
+
`Failed to fetch plugin registry for "${pluginId}" (${response.status}): ${response.statusText}`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
const item = await response.json();
|
|
718
|
+
if (!item.files || item.files.length === 0) {
|
|
719
|
+
throw new Error(`Plugin "${pluginId}" has no files in the registry.`);
|
|
720
|
+
}
|
|
721
|
+
return item;
|
|
722
|
+
}
|
|
723
|
+
async function installRegistryFiles(variant, pluginId, username) {
|
|
724
|
+
const item = await fetchRegistryItem(variant, pluginId, username);
|
|
725
|
+
const cwd = process.cwd();
|
|
726
|
+
for (const file of item.files) {
|
|
727
|
+
const targetPath = join6(cwd, file.target);
|
|
728
|
+
await fs6.ensureDir(dirname2(targetPath));
|
|
729
|
+
await fs6.writeFile(targetPath, file.content);
|
|
730
|
+
}
|
|
731
|
+
return item;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/utils/run-codemod.ts
|
|
735
|
+
import { execaCommand as execaCommand3 } from "execa";
|
|
736
|
+
var CODEMOD_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
737
|
+
async function runCodemod(variant, pluginId, options) {
|
|
738
|
+
try {
|
|
739
|
+
const localPath = process.env.MAKERKIT_CODEMODS_PATH;
|
|
740
|
+
const runner = process.env.MAKERKIT_PACKAGE_RUNNER ?? "npx --yes";
|
|
741
|
+
const command = localPath ? `${runner} codemod workflow run --allow-dirty -w ${localPath}/codemods/${variant}/${pluginId}` : `${runner} codemod --allow-dirty @makerkit/${variant}-${pluginId}`;
|
|
742
|
+
const { stdout, stderr } = await execaCommand3(command, {
|
|
743
|
+
stdio: options?.captureOutput ? "pipe" : "inherit",
|
|
744
|
+
timeout: CODEMOD_TIMEOUT_MS
|
|
745
|
+
});
|
|
746
|
+
return {
|
|
747
|
+
success: true,
|
|
748
|
+
output: stdout || stderr || ""
|
|
749
|
+
};
|
|
750
|
+
} catch (error) {
|
|
751
|
+
let message = "Unknown error during codemod";
|
|
752
|
+
if (error instanceof Error) {
|
|
753
|
+
message = error.message;
|
|
754
|
+
if ("stderr" in error && error.stderr) {
|
|
755
|
+
message += `
|
|
756
|
+
${error.stderr}`;
|
|
757
|
+
}
|
|
758
|
+
if ("timedOut" in error && error.timedOut) {
|
|
759
|
+
message = `Codemod timed out after ${CODEMOD_TIMEOUT_MS / 1e3}s (the workflow engine may have stalled after an error)`;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return {
|
|
763
|
+
success: false,
|
|
764
|
+
output: message
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/utils/username-cache.ts
|
|
770
|
+
import { join as join7 } from "path";
|
|
771
|
+
import { tmpdir } from "os";
|
|
772
|
+
import fs7 from "fs-extra";
|
|
773
|
+
import prompts2 from "prompts";
|
|
774
|
+
var USERNAME_FILE = join7(tmpdir(), "makerkit-username");
|
|
775
|
+
function getCachedUsername() {
|
|
776
|
+
try {
|
|
777
|
+
const username = fs7.readFileSync(USERNAME_FILE, "utf-8").trim();
|
|
778
|
+
return username.length > 0 ? username : void 0;
|
|
779
|
+
} catch {
|
|
780
|
+
return void 0;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function cacheUsername(username) {
|
|
784
|
+
fs7.writeFileSync(USERNAME_FILE, username, "utf-8");
|
|
785
|
+
}
|
|
786
|
+
function clearCachedUsername() {
|
|
787
|
+
try {
|
|
788
|
+
fs7.removeSync(USERNAME_FILE);
|
|
789
|
+
} catch {
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
async function promptForUsername() {
|
|
793
|
+
const gitUsername = await getGitUsername();
|
|
794
|
+
const response = await prompts2({
|
|
795
|
+
type: "text",
|
|
796
|
+
name: "githubUsername",
|
|
797
|
+
message: "Enter the GitHub username registered with your Makerkit account",
|
|
798
|
+
initial: gitUsername,
|
|
799
|
+
validate: (value) => {
|
|
800
|
+
return value.trim().length > 0 ? true : "GitHub username is required";
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
const username = response.githubUsername?.trim();
|
|
804
|
+
if (!username) {
|
|
805
|
+
throw new Error("Setup cancelled. GitHub username is required.");
|
|
806
|
+
}
|
|
807
|
+
cacheUsername(username);
|
|
808
|
+
return username;
|
|
809
|
+
}
|
|
810
|
+
async function getOrPromptUsername(providedUsername) {
|
|
811
|
+
if (providedUsername?.trim()) {
|
|
812
|
+
const username = providedUsername.trim();
|
|
813
|
+
cacheUsername(username);
|
|
814
|
+
return username;
|
|
815
|
+
}
|
|
816
|
+
const cached = getCachedUsername();
|
|
817
|
+
if (cached) {
|
|
818
|
+
return cached;
|
|
819
|
+
}
|
|
820
|
+
return promptForUsername();
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// src/utils/workspace.ts
|
|
824
|
+
import { join as join8 } from "path";
|
|
825
|
+
import fs8 from "fs-extra";
|
|
826
|
+
async function readDeps(pkgPath) {
|
|
827
|
+
if (!await fs8.pathExists(pkgPath)) {
|
|
828
|
+
return {};
|
|
829
|
+
}
|
|
830
|
+
const pkg = await fs8.readJson(pkgPath);
|
|
831
|
+
return {
|
|
832
|
+
...pkg.dependencies,
|
|
833
|
+
...pkg.devDependencies
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
async function detectVariant() {
|
|
837
|
+
const cwd = process.cwd();
|
|
838
|
+
const rootPkgPath = join8(cwd, "package.json");
|
|
839
|
+
if (!await fs8.pathExists(rootPkgPath)) {
|
|
840
|
+
throw new Error(
|
|
841
|
+
"No package.json found. Please run this command from a MakerKit project root."
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
const rootDeps = await readDeps(rootPkgPath);
|
|
845
|
+
if (!rootDeps["turbo"]) {
|
|
846
|
+
throw new Error(
|
|
847
|
+
'This does not appear to be a MakerKit Turbo monorepo. The "turbo" dependency was not found in package.json.'
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
const webDeps = await readDeps(join8(cwd, "apps", "web", "package.json"));
|
|
851
|
+
const dbDeps = await readDeps(
|
|
852
|
+
join8(cwd, "packages", "database", "package.json")
|
|
853
|
+
);
|
|
854
|
+
const hasSupabase = !!webDeps["@supabase/supabase-js"];
|
|
855
|
+
const hasReactRouter = !!webDeps["@react-router/node"];
|
|
856
|
+
const hasDrizzle = !!webDeps["drizzle-orm"] || !!dbDeps["drizzle-orm"];
|
|
857
|
+
const hasPrisma = !!webDeps["@prisma/client"] || !!dbDeps["@prisma/client"];
|
|
858
|
+
if (hasReactRouter && hasSupabase) {
|
|
859
|
+
return "react-router-supabase";
|
|
860
|
+
}
|
|
861
|
+
if (hasSupabase) {
|
|
862
|
+
return "next-supabase";
|
|
863
|
+
}
|
|
864
|
+
if (hasDrizzle) {
|
|
865
|
+
return "next-drizzle";
|
|
866
|
+
}
|
|
867
|
+
if (hasPrisma) {
|
|
868
|
+
return "next-prisma";
|
|
869
|
+
}
|
|
870
|
+
throw new Error(
|
|
871
|
+
"Unrecognized MakerKit project. Could not detect variant from dependencies."
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
async function validateProject() {
|
|
875
|
+
const variant = await detectVariant();
|
|
876
|
+
const rootPkg = await fs8.readJson(join8(process.cwd(), "package.json"));
|
|
877
|
+
return {
|
|
878
|
+
variant,
|
|
879
|
+
version: rootPkg.version ?? "unknown"
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/utils/add-plugin.ts
|
|
884
|
+
async function addPlugin(options) {
|
|
885
|
+
if (!options.skipGitCheck) {
|
|
886
|
+
const gitClean = await isGitClean();
|
|
887
|
+
if (!gitClean) {
|
|
888
|
+
return {
|
|
889
|
+
success: false,
|
|
890
|
+
reason: "Git working directory has uncommitted changes. Please commit or stash them before adding a plugin."
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
const { variant } = await validateProject();
|
|
895
|
+
const username = options.githubUsername?.trim() || getCachedUsername();
|
|
896
|
+
if (!username) {
|
|
897
|
+
return {
|
|
898
|
+
success: false,
|
|
899
|
+
reason: "No GitHub username cached and none provided. Call makerkit_init_registry first or pass githubUsername."
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
cacheUsername(username);
|
|
903
|
+
const registry = await PluginRegistry.load();
|
|
904
|
+
const plugin = registry.validatePlugin(options.pluginId, variant);
|
|
905
|
+
if (await isInstalled(plugin, variant)) {
|
|
906
|
+
return {
|
|
907
|
+
success: false,
|
|
908
|
+
reason: `Plugin "${plugin.name}" is already installed.`
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
const item = await installRegistryFiles(variant, options.pluginId, username);
|
|
912
|
+
await saveBaseVersions(options.pluginId, item.files);
|
|
913
|
+
const codemodResult = await runCodemod(variant, options.pluginId, {
|
|
914
|
+
captureOutput: options.captureCodemodOutput ?? true
|
|
915
|
+
});
|
|
916
|
+
const envVars = getEnvVars(plugin, variant);
|
|
917
|
+
return {
|
|
918
|
+
success: true,
|
|
919
|
+
pluginName: plugin.name,
|
|
920
|
+
pluginId: plugin.id,
|
|
921
|
+
variant,
|
|
922
|
+
envVars: envVars.map((e) => ({ key: e.key, description: e.description })),
|
|
923
|
+
postInstallMessage: plugin.postInstallMessage ?? null,
|
|
924
|
+
codemodOutput: codemodResult.output,
|
|
925
|
+
codemodWarning: codemodResult.success ? void 0 : `The automated codemod did not complete successfully. Plugin files were installed but some wiring steps may have failed.
|
|
926
|
+
${codemodResult.output}`
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// src/utils/list-plugins.ts
|
|
931
|
+
async function listPlugins(options) {
|
|
932
|
+
const variant = await detectVariant();
|
|
933
|
+
const registry = await PluginRegistry.load();
|
|
934
|
+
const plugins = registry.getPluginsForVariant(variant);
|
|
935
|
+
const pluginList = await Promise.all(
|
|
936
|
+
plugins.map(async (p) => ({
|
|
937
|
+
id: p.id,
|
|
938
|
+
name: p.name,
|
|
939
|
+
description: p.description,
|
|
940
|
+
installed: await isInstalled(p, variant),
|
|
941
|
+
envVars: getEnvVars(p, variant).map((e) => e.key),
|
|
942
|
+
postInstallMessage: p.postInstallMessage ?? null
|
|
943
|
+
}))
|
|
944
|
+
);
|
|
945
|
+
return { variant, plugins: pluginList };
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// src/commands/plugins/add/add.command.ts
|
|
949
|
+
import chalk2 from "chalk";
|
|
950
|
+
import ora2 from "ora";
|
|
951
|
+
import prompts3 from "prompts";
|
|
952
|
+
function createAddCommand(parentCommand) {
|
|
953
|
+
return parentCommand.command("add").argument("[plugin-id...]", "Plugins to install (e.g. feedback waitlist)").description("Install one or more MakerKit plugins.").action(async (pluginIds) => {
|
|
954
|
+
try {
|
|
955
|
+
const gitClean = await isGitClean();
|
|
956
|
+
if (!gitClean) {
|
|
957
|
+
console.error(
|
|
958
|
+
chalk2.red(
|
|
959
|
+
"Your git working directory has uncommitted changes. Please commit or stash them before adding a plugin."
|
|
960
|
+
)
|
|
961
|
+
);
|
|
962
|
+
process.exit(1);
|
|
963
|
+
}
|
|
964
|
+
if (!pluginIds || pluginIds.length === 0) {
|
|
965
|
+
const pluginsResult = await listPlugins({ projectPath: process.cwd() });
|
|
966
|
+
const available = pluginsResult.plugins.filter((p) => !p.installed);
|
|
967
|
+
if (available.length === 0) {
|
|
968
|
+
console.log(chalk2.green("All plugins are already installed."));
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const response = await prompts3({
|
|
972
|
+
type: "multiselect",
|
|
973
|
+
name: "pluginIds",
|
|
974
|
+
message: "Select plugins to install",
|
|
975
|
+
choices: available.map((p) => ({
|
|
976
|
+
title: `${p.name} ${chalk2.gray(`(${p.id})`)}`,
|
|
977
|
+
value: p.id
|
|
978
|
+
}))
|
|
979
|
+
});
|
|
980
|
+
pluginIds = response.pluginIds ?? [];
|
|
981
|
+
if (pluginIds.length === 0) {
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
let username = await getOrPromptUsername();
|
|
986
|
+
for (const pluginId of pluginIds) {
|
|
987
|
+
console.log(
|
|
988
|
+
`
|
|
989
|
+
Installing ${chalk2.cyan(pluginId)}...
|
|
990
|
+
`
|
|
991
|
+
);
|
|
992
|
+
const filesSpinner = ora2("Installing plugin...").start();
|
|
993
|
+
let result;
|
|
994
|
+
try {
|
|
995
|
+
result = await addPlugin({
|
|
996
|
+
projectPath: process.cwd(),
|
|
997
|
+
pluginId,
|
|
998
|
+
githubUsername: username,
|
|
999
|
+
skipGitCheck: true,
|
|
1000
|
+
captureCodemodOutput: false
|
|
1001
|
+
});
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
filesSpinner.fail("Failed to install plugin.");
|
|
1004
|
+
clearCachedUsername();
|
|
1005
|
+
console.log(
|
|
1006
|
+
chalk2.yellow("\nRetrying with a different username...\n")
|
|
1007
|
+
);
|
|
1008
|
+
username = await getOrPromptUsername();
|
|
1009
|
+
const retrySpinner = ora2("Installing plugin...").start();
|
|
1010
|
+
try {
|
|
1011
|
+
result = await addPlugin({
|
|
1012
|
+
projectPath: process.cwd(),
|
|
1013
|
+
pluginId,
|
|
1014
|
+
githubUsername: username,
|
|
1015
|
+
skipGitCheck: true,
|
|
1016
|
+
captureCodemodOutput: false
|
|
1017
|
+
});
|
|
1018
|
+
} catch (retryError) {
|
|
1019
|
+
retrySpinner.fail("Failed to install plugin.");
|
|
1020
|
+
const message = retryError instanceof Error ? retryError.message : "Unknown error";
|
|
1021
|
+
console.error(chalk2.red(`
|
|
1022
|
+
Error installing "${pluginId}": ${message}`));
|
|
1023
|
+
console.log(
|
|
1024
|
+
chalk2.yellow(
|
|
1025
|
+
"\nTo revert changes, run: git checkout . && git clean -fd"
|
|
1026
|
+
)
|
|
1027
|
+
);
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
}
|
|
1030
|
+
if (result.success) {
|
|
1031
|
+
retrySpinner.succeed("Plugin installed.");
|
|
1032
|
+
} else {
|
|
1033
|
+
retrySpinner.stop();
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (!result.success) {
|
|
1037
|
+
if (result.reason.includes("already installed")) {
|
|
1038
|
+
filesSpinner.warn(result.reason + " Skipping.");
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
filesSpinner.fail("Failed to install plugin.");
|
|
1042
|
+
console.error(chalk2.red(`
|
|
1043
|
+
${result.reason}`));
|
|
1044
|
+
console.log(
|
|
1045
|
+
chalk2.yellow(
|
|
1046
|
+
"\nTo revert changes, run: git checkout . && git clean -fd"
|
|
1047
|
+
)
|
|
1048
|
+
);
|
|
1049
|
+
process.exit(1);
|
|
1050
|
+
}
|
|
1051
|
+
if (result.codemodWarning) {
|
|
1052
|
+
filesSpinner.warn(
|
|
1053
|
+
`${result.pluginName} installed with warnings \u2014 some automated steps may not have completed.`
|
|
1054
|
+
);
|
|
1055
|
+
console.log(chalk2.yellow(`
|
|
1056
|
+
${result.codemodWarning}`));
|
|
1057
|
+
console.log(
|
|
1058
|
+
chalk2.yellow(
|
|
1059
|
+
"\nPlugin files were written successfully. Review the changes with `git diff` and complete any missing steps manually."
|
|
1060
|
+
)
|
|
1061
|
+
);
|
|
1062
|
+
} else if (filesSpinner.isSpinning) {
|
|
1063
|
+
filesSpinner.succeed(`${result.pluginName} installed successfully!`);
|
|
1064
|
+
}
|
|
1065
|
+
if (result.envVars.length > 0) {
|
|
1066
|
+
console.log(chalk2.white("\nEnvironment variables to configure:"));
|
|
1067
|
+
for (const envVar of result.envVars) {
|
|
1068
|
+
console.log(
|
|
1069
|
+
` ${chalk2.cyan(envVar.key)} - ${envVar.description}`
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
if (result.postInstallMessage) {
|
|
1074
|
+
console.log(chalk2.white("\nNext steps:"));
|
|
1075
|
+
console.log(` ${chalk2.cyan(result.postInstallMessage)}`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
console.log("");
|
|
1079
|
+
console.log(chalk2.yellow("Important:"));
|
|
1080
|
+
console.log(
|
|
1081
|
+
chalk2.yellow(
|
|
1082
|
+
" This plugin was installed using an automated migration."
|
|
1083
|
+
)
|
|
1084
|
+
);
|
|
1085
|
+
console.log(
|
|
1086
|
+
chalk2.yellow(
|
|
1087
|
+
" Please review the changes manually and test thoroughly before committing."
|
|
1088
|
+
)
|
|
1089
|
+
);
|
|
1090
|
+
console.log("");
|
|
1091
|
+
console.log(chalk2.white("Tips:"));
|
|
1092
|
+
console.log(
|
|
1093
|
+
` ${chalk2.gray("-")} Run ${chalk2.cyan("git diff")} to review all changes made by the migration.`
|
|
1094
|
+
);
|
|
1095
|
+
console.log(
|
|
1096
|
+
` ${chalk2.gray("-")} Use an AI assistant (e.g. Claude) as a first pass to review the diff for issues.`
|
|
1097
|
+
);
|
|
1098
|
+
console.log(
|
|
1099
|
+
` ${chalk2.gray("-")} Run your test suite and verify the app builds before committing.`
|
|
1100
|
+
);
|
|
1101
|
+
console.log(
|
|
1102
|
+
` ${chalk2.gray("-")} To undo all changes: ${chalk2.cyan("git checkout . && git clean -fd")}`
|
|
1103
|
+
);
|
|
1104
|
+
console.log("");
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1107
|
+
console.error(chalk2.red(`Error: ${message}`));
|
|
1108
|
+
console.log(
|
|
1109
|
+
chalk2.yellow(
|
|
1110
|
+
"\nTo revert changes, run: git checkout . && git clean -fd"
|
|
1111
|
+
)
|
|
1112
|
+
);
|
|
1113
|
+
process.exit(1);
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// src/commands/plugins/diff/diff.command.ts
|
|
1119
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
1120
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1121
|
+
import chalk3 from "chalk";
|
|
1122
|
+
import { execa as execa2 } from "execa";
|
|
1123
|
+
import fs9 from "fs-extra";
|
|
1124
|
+
import ora3 from "ora";
|
|
1125
|
+
import prompts4 from "prompts";
|
|
1126
|
+
function createDiffCommand(parentCommand) {
|
|
1127
|
+
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) => {
|
|
1128
|
+
try {
|
|
1129
|
+
const { variant } = await validateProject();
|
|
1130
|
+
const registry = await PluginRegistry.load();
|
|
1131
|
+
if (!pluginId) {
|
|
1132
|
+
const plugins = registry.getPluginsForVariant(variant);
|
|
1133
|
+
const installed = [];
|
|
1134
|
+
for (const p of plugins) {
|
|
1135
|
+
if (await isInstalled(p, variant)) {
|
|
1136
|
+
installed.push(p);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (installed.length === 0) {
|
|
1140
|
+
console.log(chalk3.yellow("No plugins installed."));
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const response = await prompts4({
|
|
1144
|
+
type: "select",
|
|
1145
|
+
name: "pluginId",
|
|
1146
|
+
message: "Select a plugin to diff",
|
|
1147
|
+
choices: installed.map((p) => ({
|
|
1148
|
+
title: `${p.name} ${chalk3.gray(`(${p.id})`)}`,
|
|
1149
|
+
value: p.id
|
|
1150
|
+
}))
|
|
1151
|
+
});
|
|
1152
|
+
pluginId = response.pluginId;
|
|
1153
|
+
if (!pluginId) {
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
const plugin = registry.validatePlugin(pluginId, variant);
|
|
1158
|
+
if (!await isInstalled(plugin, variant)) {
|
|
1159
|
+
console.error(
|
|
1160
|
+
chalk3.yellow(`Plugin "${plugin.name}" is not installed.`)
|
|
1161
|
+
);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const username = await getOrPromptUsername();
|
|
1165
|
+
const spinner = ora3("Fetching latest plugin files...").start();
|
|
1166
|
+
const item = await fetchRegistryItem(variant, pluginId, username);
|
|
1167
|
+
spinner.succeed("Fetched latest plugin files.");
|
|
1168
|
+
const tempDir = join9(tmpdir2(), `makerkit-diff-${pluginId}`);
|
|
1169
|
+
await fs9.ensureDir(tempDir);
|
|
1170
|
+
try {
|
|
1171
|
+
const cwd = process.cwd();
|
|
1172
|
+
let hasDiff = false;
|
|
1173
|
+
for (const file of item.files) {
|
|
1174
|
+
const localPath = join9(cwd, file.target);
|
|
1175
|
+
const remotePath = join9(tempDir, file.target);
|
|
1176
|
+
await fs9.ensureDir(dirname3(join9(tempDir, file.target)));
|
|
1177
|
+
await fs9.writeFile(remotePath, file.content);
|
|
1178
|
+
if (!await fs9.pathExists(localPath)) {
|
|
1179
|
+
console.log(chalk3.green(`
|
|
1180
|
+
New file: ${file.target}`));
|
|
1181
|
+
hasDiff = true;
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
const localContent = await fs9.readFile(localPath, "utf-8");
|
|
1185
|
+
if (localContent !== file.content) {
|
|
1186
|
+
hasDiff = true;
|
|
1187
|
+
console.log(chalk3.white(`
|
|
1188
|
+
--- ${file.target}`));
|
|
1189
|
+
await execa2("git", ["diff", "--no-index", "--color", "--", localPath, remotePath], {
|
|
1190
|
+
stdio: "inherit",
|
|
1191
|
+
reject: false
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (!hasDiff) {
|
|
1196
|
+
console.log(
|
|
1197
|
+
chalk3.green(`
|
|
1198
|
+
${plugin.name} is up to date.`)
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
} finally {
|
|
1202
|
+
await fs9.remove(tempDir);
|
|
1203
|
+
}
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1206
|
+
console.error(chalk3.red(`Error: ${message}`));
|
|
1207
|
+
process.exit(1);
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// src/utils/init-registry.ts
|
|
1213
|
+
async function initRegistry(options) {
|
|
1214
|
+
const variant = await detectVariant();
|
|
1215
|
+
cacheUsername(options.githubUsername);
|
|
1216
|
+
return {
|
|
1217
|
+
success: true,
|
|
1218
|
+
variant,
|
|
1219
|
+
username: options.githubUsername
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/commands/plugins/init/init.command.ts
|
|
1224
|
+
import chalk4 from "chalk";
|
|
1225
|
+
function createInitCommand(parentCommand) {
|
|
1226
|
+
return parentCommand.command("init").description(
|
|
1227
|
+
"Initialize MakerKit plugin registry in your project (one-time setup)."
|
|
1228
|
+
).action(async () => {
|
|
1229
|
+
try {
|
|
1230
|
+
const username = await getOrPromptUsername();
|
|
1231
|
+
const initResult = await initRegistry({
|
|
1232
|
+
projectPath: process.cwd(),
|
|
1233
|
+
githubUsername: username
|
|
1234
|
+
});
|
|
1235
|
+
console.log(
|
|
1236
|
+
`Detected project variant: ${chalk4.cyan(initResult.variant)}
|
|
1237
|
+
`
|
|
1238
|
+
);
|
|
1239
|
+
console.log(chalk4.green("Registry configured.\n"));
|
|
1240
|
+
const pluginsResult = await listPlugins({ projectPath: process.cwd() });
|
|
1241
|
+
console.log(chalk4.white("Available plugins:\n"));
|
|
1242
|
+
for (const plugin of pluginsResult.plugins) {
|
|
1243
|
+
console.log(
|
|
1244
|
+
` ${chalk4.green(plugin.name)} ${chalk4.gray(`(${plugin.id})`)}`
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
console.log(
|
|
1248
|
+
`
|
|
1249
|
+
Run ${chalk4.cyan("makerkit plugins add <plugin-id>")} to install a plugin.`
|
|
1250
|
+
);
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1253
|
+
console.error(chalk4.red(`Error: ${message}`));
|
|
1254
|
+
process.exit(1);
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// src/commands/plugins/list/list-plugins.command.ts
|
|
1260
|
+
import chalk5 from "chalk";
|
|
1261
|
+
function createListPluginsCommand(parentCommand) {
|
|
1262
|
+
return parentCommand.command("list").description("List available and installed plugins.").action(async () => {
|
|
1263
|
+
const result = await listPlugins({ projectPath: process.cwd() });
|
|
1264
|
+
console.log(
|
|
1265
|
+
chalk5.white(`MakerKit Plugins ${chalk5.gray(`(${result.variant})`)}
|
|
1266
|
+
`)
|
|
1267
|
+
);
|
|
1268
|
+
console.log(
|
|
1269
|
+
` ${chalk5.green("Plugin Name")} ${chalk5.gray("(plugin-id)")} \u2014 Status
|
|
1270
|
+
`
|
|
1271
|
+
);
|
|
1272
|
+
let installedCount = 0;
|
|
1273
|
+
for (const plugin of result.plugins) {
|
|
1274
|
+
if (plugin.installed) {
|
|
1275
|
+
installedCount++;
|
|
1276
|
+
}
|
|
1277
|
+
const status = plugin.installed ? chalk5.green("installed") : chalk5.gray("available");
|
|
1278
|
+
console.log(
|
|
1279
|
+
` ${chalk5.cyan(plugin.name)} ${chalk5.gray(`(${plugin.id})`)} \u2014 ${status}`
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
console.log(
|
|
1283
|
+
`
|
|
1284
|
+
${chalk5.white(`${installedCount} installed`)} / ${chalk5.gray(`${result.plugins.length} available`)}
|
|
1285
|
+
`
|
|
1286
|
+
);
|
|
1287
|
+
if (installedCount === 0) {
|
|
1288
|
+
console.log(
|
|
1289
|
+
`Run ${chalk5.cyan("makerkit plugins init")} to set up the registry, then ${chalk5.cyan("makerkit plugins add <plugin-id>")} to install.`
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/commands/plugins/outdated/outdated.command.ts
|
|
1296
|
+
import { join as join10 } from "path";
|
|
1297
|
+
import chalk6 from "chalk";
|
|
1298
|
+
import fs10 from "fs-extra";
|
|
1299
|
+
import ora4 from "ora";
|
|
1300
|
+
async function isOutdated(plugin, variant, username) {
|
|
1301
|
+
const item = await fetchRegistryItem(variant, plugin.id, username);
|
|
1302
|
+
const cwd = process.cwd();
|
|
1303
|
+
for (const file of item.files) {
|
|
1304
|
+
const localPath = join10(cwd, file.target);
|
|
1305
|
+
if (!await fs10.pathExists(localPath)) {
|
|
1306
|
+
return true;
|
|
1307
|
+
}
|
|
1308
|
+
const localContent = await fs10.readFile(localPath, "utf-8");
|
|
1309
|
+
if (localContent !== file.content) {
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
return false;
|
|
1314
|
+
}
|
|
1315
|
+
function createOutdatedCommand(parentCommand) {
|
|
1316
|
+
return parentCommand.command("outdated").description("Check installed plugins for updates.").action(async () => {
|
|
1317
|
+
try {
|
|
1318
|
+
const { variant } = await validateProject();
|
|
1319
|
+
const registry = await PluginRegistry.load();
|
|
1320
|
+
const plugins = registry.getPluginsForVariant(variant);
|
|
1321
|
+
const installed = [];
|
|
1322
|
+
for (const p of plugins) {
|
|
1323
|
+
if (await isInstalled(p, variant)) {
|
|
1324
|
+
installed.push(p);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
if (installed.length === 0) {
|
|
1328
|
+
console.log(chalk6.yellow("No plugins installed."));
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
const username = await getOrPromptUsername();
|
|
1332
|
+
const spinner = ora4(
|
|
1333
|
+
`Checking ${installed.length} plugin${installed.length > 1 ? "s" : ""} for updates...`
|
|
1334
|
+
).start();
|
|
1335
|
+
const outdated = [];
|
|
1336
|
+
for (const plugin of installed) {
|
|
1337
|
+
try {
|
|
1338
|
+
if (await isOutdated(plugin, variant, username)) {
|
|
1339
|
+
outdated.push(plugin);
|
|
1340
|
+
}
|
|
1341
|
+
} catch {
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
spinner.stop();
|
|
1345
|
+
if (outdated.length === 0) {
|
|
1346
|
+
console.log(chalk6.green("All plugins are up to date."));
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
console.log(
|
|
1350
|
+
chalk6.yellow(
|
|
1351
|
+
`
|
|
1352
|
+
${outdated.length} plugin${outdated.length > 1 ? "s have" : " has"} updates available:
|
|
1353
|
+
`
|
|
1354
|
+
)
|
|
1355
|
+
);
|
|
1356
|
+
for (const plugin of outdated) {
|
|
1357
|
+
const pluginPath = getPath(plugin, variant);
|
|
1358
|
+
console.log(
|
|
1359
|
+
` ${chalk6.cyan(plugin.name)} ${chalk6.gray(`(${plugin.id})`)}${pluginPath ? chalk6.gray(` \u2014 ${pluginPath}`) : ""}`
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
console.log(
|
|
1363
|
+
`
|
|
1364
|
+
Run ${chalk6.cyan("makerkit plugins diff <plugin-id>")} to see what changed.`
|
|
1365
|
+
);
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1368
|
+
console.error(chalk6.red(`Error: ${message}`));
|
|
1369
|
+
process.exit(1);
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// src/commands/plugins/update/update.command.ts
|
|
1375
|
+
import { dirname as dirname4, join as join11 } from "path";
|
|
1376
|
+
import chalk7 from "chalk";
|
|
1377
|
+
import fs11 from "fs-extra";
|
|
1378
|
+
import ora5 from "ora";
|
|
1379
|
+
import prompts5 from "prompts";
|
|
1380
|
+
function createUpdateCommand(parentCommand) {
|
|
1381
|
+
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) => {
|
|
1382
|
+
try {
|
|
1383
|
+
const { variant } = await validateProject();
|
|
1384
|
+
const registry = await PluginRegistry.load();
|
|
1385
|
+
if (!pluginIds || pluginIds.length === 0) {
|
|
1386
|
+
const plugins = registry.getPluginsForVariant(variant);
|
|
1387
|
+
const installed = [];
|
|
1388
|
+
for (const p of plugins) {
|
|
1389
|
+
if (await isInstalled(p, variant)) {
|
|
1390
|
+
installed.push(p);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
if (installed.length === 0) {
|
|
1394
|
+
console.log(chalk7.yellow("No plugins installed."));
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
const response = await prompts5({
|
|
1398
|
+
type: "multiselect",
|
|
1399
|
+
name: "pluginIds",
|
|
1400
|
+
message: "Select plugins to update",
|
|
1401
|
+
choices: installed.map((p) => ({
|
|
1402
|
+
title: `${p.name} ${chalk7.gray(`(${p.id})`)}`,
|
|
1403
|
+
value: p.id
|
|
1404
|
+
}))
|
|
1405
|
+
});
|
|
1406
|
+
pluginIds = response.pluginIds ?? [];
|
|
1407
|
+
if (pluginIds.length === 0) {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const username = await getOrPromptUsername();
|
|
1412
|
+
for (const pluginId of pluginIds) {
|
|
1413
|
+
const plugin = registry.validatePlugin(pluginId, variant);
|
|
1414
|
+
if (!await isInstalled(plugin, variant)) {
|
|
1415
|
+
console.log(
|
|
1416
|
+
chalk7.yellow(
|
|
1417
|
+
`Plugin "${plugin.name}" is not installed. Use "plugins add ${pluginId}" to install it.`
|
|
1418
|
+
)
|
|
1419
|
+
);
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
const spinner = ora5(`Fetching latest ${plugin.name} files...`).start();
|
|
1423
|
+
const item = await fetchRegistryItem(variant, pluginId, username);
|
|
1424
|
+
spinner.succeed(`Fetched latest ${plugin.name} files.`);
|
|
1425
|
+
const cwd = process.cwd();
|
|
1426
|
+
const modifiedFiles = [];
|
|
1427
|
+
for (const file of item.files) {
|
|
1428
|
+
const localPath = join11(cwd, file.target);
|
|
1429
|
+
if (await fs11.pathExists(localPath)) {
|
|
1430
|
+
const localContent = await fs11.readFile(localPath, "utf-8");
|
|
1431
|
+
if (localContent !== file.content) {
|
|
1432
|
+
modifiedFiles.push(file.target);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
if (modifiedFiles.length === 0) {
|
|
1437
|
+
console.log(chalk7.green(`${plugin.name} is already up to date.`));
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
console.log(
|
|
1441
|
+
chalk7.yellow(
|
|
1442
|
+
`
|
|
1443
|
+
${modifiedFiles.length} file${modifiedFiles.length > 1 ? "s" : ""} will be overwritten:`
|
|
1444
|
+
)
|
|
1445
|
+
);
|
|
1446
|
+
for (const file of modifiedFiles) {
|
|
1447
|
+
console.log(` ${chalk7.gray(file)}`);
|
|
1448
|
+
}
|
|
1449
|
+
const { confirm } = await prompts5({
|
|
1450
|
+
type: "confirm",
|
|
1451
|
+
name: "confirm",
|
|
1452
|
+
message: `Update ${plugin.name}? Local changes will be lost.`,
|
|
1453
|
+
initial: false
|
|
1454
|
+
});
|
|
1455
|
+
if (!confirm) {
|
|
1456
|
+
console.log(chalk7.gray(`Skipped ${plugin.name}.`));
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
const writeSpinner = ora5(`Updating ${plugin.name}...`).start();
|
|
1460
|
+
for (const file of item.files) {
|
|
1461
|
+
const targetPath = join11(cwd, file.target);
|
|
1462
|
+
await fs11.ensureDir(dirname4(targetPath));
|
|
1463
|
+
await fs11.writeFile(targetPath, file.content);
|
|
1464
|
+
}
|
|
1465
|
+
await saveBaseVersions(pluginId, item.files);
|
|
1466
|
+
writeSpinner.succeed(`${plugin.name} updated.`);
|
|
1467
|
+
}
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1470
|
+
console.error(chalk7.red(`Error: ${message}`));
|
|
1471
|
+
process.exit(1);
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// src/commands/plugins/plugins.command.ts
|
|
1477
|
+
import { Command as Command2 } from "commander";
|
|
1478
|
+
var pluginsCommand = new Command2().name("plugins").description("Manage MakerKit plugins.");
|
|
1479
|
+
createInitCommand(pluginsCommand);
|
|
1480
|
+
createAddCommand(pluginsCommand);
|
|
1481
|
+
createUpdateCommand(pluginsCommand);
|
|
1482
|
+
createDiffCommand(pluginsCommand);
|
|
1483
|
+
createOutdatedCommand(pluginsCommand);
|
|
1484
|
+
createListPluginsCommand(pluginsCommand);
|
|
1485
|
+
|
|
1486
|
+
// src/utils/project-pull.ts
|
|
1487
|
+
import { join as join12 } from "path";
|
|
1488
|
+
import { execa as execa3, execaCommand as execaCommand4 } from "execa";
|
|
1489
|
+
import fs12 from "fs-extra";
|
|
1490
|
+
async function projectPull(options) {
|
|
1491
|
+
const { variant } = await validateProject();
|
|
1492
|
+
const gitClean = await isGitClean();
|
|
1493
|
+
if (!gitClean) {
|
|
1494
|
+
return {
|
|
1495
|
+
success: false,
|
|
1496
|
+
reason: "Git working directory has uncommitted changes. Please commit or stash them before pulling upstream updates."
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
let currentUrl = await getUpstreamRemoteUrl();
|
|
1500
|
+
if (!currentUrl) {
|
|
1501
|
+
const useSsh = await hasSshAccess();
|
|
1502
|
+
const url = getUpstreamUrl(variant, useSsh);
|
|
1503
|
+
await setUpstreamRemote(url);
|
|
1504
|
+
currentUrl = url;
|
|
1505
|
+
} else if (!isUpstreamUrlValid(currentUrl, variant)) {
|
|
1506
|
+
const useSsh = currentUrl.startsWith("git@");
|
|
1507
|
+
const expectedUrl = getUpstreamUrl(variant, useSsh);
|
|
1508
|
+
return {
|
|
1509
|
+
success: false,
|
|
1510
|
+
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>`
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
await execaCommand4("git fetch upstream");
|
|
1514
|
+
try {
|
|
1515
|
+
const { stdout } = await execaCommand4(
|
|
1516
|
+
"git merge upstream/main --no-edit"
|
|
1517
|
+
);
|
|
1518
|
+
const alreadyUpToDate = stdout.includes("Already up to date");
|
|
1519
|
+
return {
|
|
1520
|
+
success: true,
|
|
1521
|
+
variant,
|
|
1522
|
+
upstreamUrl: currentUrl,
|
|
1523
|
+
alreadyUpToDate,
|
|
1524
|
+
message: alreadyUpToDate ? "Already up to date." : "Successfully merged upstream changes."
|
|
1525
|
+
};
|
|
1526
|
+
} catch (mergeError) {
|
|
1527
|
+
const output = getErrorOutput(mergeError);
|
|
1528
|
+
const isConflict = output.includes("CONFLICT") || output.includes("Automatic merge failed");
|
|
1529
|
+
if (!isConflict) {
|
|
1530
|
+
throw new Error(`Merge failed: ${output}`);
|
|
1531
|
+
}
|
|
1532
|
+
const { stdout: statusOutput } = await execaCommand4(
|
|
1533
|
+
"git diff --name-only --diff-filter=U"
|
|
1534
|
+
);
|
|
1535
|
+
const conflictPaths = statusOutput.trim().split("\n").filter(Boolean);
|
|
1536
|
+
const cwd = process.cwd();
|
|
1537
|
+
const conflicts = await Promise.all(
|
|
1538
|
+
conflictPaths.map(async (filePath) => {
|
|
1539
|
+
let conflicted;
|
|
1540
|
+
try {
|
|
1541
|
+
conflicted = await fs12.readFile(join12(cwd, filePath), "utf-8");
|
|
1542
|
+
} catch {
|
|
1543
|
+
conflicted = void 0;
|
|
1544
|
+
}
|
|
1545
|
+
let base;
|
|
1546
|
+
let ours;
|
|
1547
|
+
let theirs;
|
|
1548
|
+
try {
|
|
1549
|
+
const { stdout: b } = await execa3("git", [
|
|
1550
|
+
"show",
|
|
1551
|
+
`:1:${filePath}`
|
|
1552
|
+
]);
|
|
1553
|
+
base = b;
|
|
1554
|
+
} catch {
|
|
1555
|
+
base = void 0;
|
|
1556
|
+
}
|
|
1557
|
+
try {
|
|
1558
|
+
const { stdout: o } = await execa3("git", [
|
|
1559
|
+
"show",
|
|
1560
|
+
`:2:${filePath}`
|
|
1561
|
+
]);
|
|
1562
|
+
ours = o;
|
|
1563
|
+
} catch {
|
|
1564
|
+
ours = void 0;
|
|
1565
|
+
}
|
|
1566
|
+
try {
|
|
1567
|
+
const { stdout: t } = await execa3("git", [
|
|
1568
|
+
"show",
|
|
1569
|
+
`:3:${filePath}`
|
|
1570
|
+
]);
|
|
1571
|
+
theirs = t;
|
|
1572
|
+
} catch {
|
|
1573
|
+
theirs = void 0;
|
|
1574
|
+
}
|
|
1575
|
+
return { path: filePath, conflicted, base, ours, theirs };
|
|
1576
|
+
})
|
|
1577
|
+
);
|
|
1578
|
+
return {
|
|
1579
|
+
success: false,
|
|
1580
|
+
variant,
|
|
1581
|
+
upstreamUrl: currentUrl,
|
|
1582
|
+
hasConflicts: true,
|
|
1583
|
+
conflictCount: conflicts.length,
|
|
1584
|
+
conflicts,
|
|
1585
|
+
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."
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// src/commands/project/update/update.command.ts
|
|
1591
|
+
import chalk8 from "chalk";
|
|
1592
|
+
import ora6 from "ora";
|
|
1593
|
+
function createProjectUpdateCommand(parentCommand) {
|
|
1594
|
+
return parentCommand.command("update").description(
|
|
1595
|
+
"Pull the latest changes from the upstream MakerKit repository."
|
|
1596
|
+
).action(async () => {
|
|
1597
|
+
try {
|
|
1598
|
+
const spinner = ora6("Pulling latest changes from upstream...").start();
|
|
1599
|
+
const result = await projectPull({ projectPath: process.cwd() });
|
|
1600
|
+
if (result.success) {
|
|
1601
|
+
const displayName = VARIANT_CATALOG.find((v) => v.id === result.variant)?.name ?? result.variant;
|
|
1602
|
+
if (result.alreadyUpToDate) {
|
|
1603
|
+
spinner.succeed(
|
|
1604
|
+
`Already up to date. (${chalk8.cyan(displayName)})`
|
|
1605
|
+
);
|
|
1606
|
+
} else {
|
|
1607
|
+
spinner.succeed(
|
|
1608
|
+
`Successfully pulled latest changes from upstream. (${chalk8.cyan(displayName)})`
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
} else if ("hasConflicts" in result) {
|
|
1612
|
+
spinner.fail("Merge conflicts detected.");
|
|
1613
|
+
console.log(
|
|
1614
|
+
chalk8.yellow(
|
|
1615
|
+
`
|
|
1616
|
+
${result.conflictCount} conflicting file(s):`
|
|
1617
|
+
)
|
|
1618
|
+
);
|
|
1619
|
+
for (const conflict of result.conflicts) {
|
|
1620
|
+
console.log(` ${chalk8.gray("-")} ${conflict.path}`);
|
|
1621
|
+
}
|
|
1622
|
+
console.log(
|
|
1623
|
+
chalk8.yellow("\nPlease resolve them manually:")
|
|
1624
|
+
);
|
|
1625
|
+
console.log(chalk8.gray(" 1. Fix the conflicting files"));
|
|
1626
|
+
console.log(chalk8.gray(" 2. Run: git add ."));
|
|
1627
|
+
console.log(chalk8.gray(" 3. Run: git commit"));
|
|
1628
|
+
process.exit(1);
|
|
1629
|
+
} else {
|
|
1630
|
+
spinner.fail("Failed to pull from upstream.");
|
|
1631
|
+
console.error(chalk8.red(`
|
|
1632
|
+
${result.reason}`));
|
|
1633
|
+
process.exit(1);
|
|
1634
|
+
}
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1637
|
+
console.error(chalk8.red(`Error: ${message}`));
|
|
1638
|
+
process.exit(1);
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// src/commands/project/project.command.ts
|
|
1644
|
+
import { Command as Command3 } from "commander";
|
|
1645
|
+
var projectCommand = new Command3().name("project").description("Manage your MakerKit project.");
|
|
1646
|
+
createProjectUpdateCommand(projectCommand);
|
|
1647
|
+
|
|
1648
|
+
// src/index.ts
|
|
1649
|
+
import { Command as Command4 } from "commander";
|
|
763
1650
|
import { config } from "dotenv";
|
|
764
1651
|
config({
|
|
765
1652
|
path: ".env.local"
|
|
@@ -767,10 +1654,10 @@ config({
|
|
|
767
1654
|
process.on("SIGINT", () => process.exit(0));
|
|
768
1655
|
process.on("SIGTERM", () => process.exit(0));
|
|
769
1656
|
async function main() {
|
|
770
|
-
const program = new
|
|
1657
|
+
const program = new Command4().name("makerkit").description(
|
|
771
1658
|
"Your SaaS Kit companion. Add plugins, manage migrations, and more."
|
|
772
|
-
).version("-v, --version", "display the version number");
|
|
773
|
-
program.addCommand(newCommand).addCommand(pluginsCommand).addCommand(
|
|
1659
|
+
).version(CLI_VERSION, "-v, --version", "display the version number");
|
|
1660
|
+
program.addCommand(newCommand).addCommand(pluginsCommand).addCommand(projectCommand);
|
|
774
1661
|
program.parse();
|
|
775
1662
|
}
|
|
776
1663
|
void main();
|