@kuckit/cli 1.0.0 → 1.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/bin.js +128 -11
- package/dist/discover-module-B4oRIuSK.js +1019 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/discover-module-B7rxN4vq.js +0 -595
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { accessSync, constants, existsSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
//#region src/commands/generate-module.ts
|
|
7
|
+
function toKebabCase(str) {
|
|
8
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
function toPascalCase(str) {
|
|
11
|
+
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
12
|
+
}
|
|
13
|
+
function toCamelCase(str) {
|
|
14
|
+
const pascal = toPascalCase(str);
|
|
15
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
16
|
+
}
|
|
17
|
+
async function generateModule(name, options) {
|
|
18
|
+
const kebabName = toKebabCase(name);
|
|
19
|
+
const pascalName = toPascalCase(name);
|
|
20
|
+
const camelName = toCamelCase(name);
|
|
21
|
+
const moduleDirName = `${kebabName}-module`;
|
|
22
|
+
const packageName = options.org ? `@${options.org}/${moduleDirName}` : `@kuckit/${moduleDirName}`;
|
|
23
|
+
const moduleId = options.org ? `${options.org}.${kebabName}` : `kuckit.${kebabName}`;
|
|
24
|
+
const targetDir = join(process.cwd(), options.dir, moduleDirName);
|
|
25
|
+
console.log(`Creating module: ${packageName}`);
|
|
26
|
+
console.log(` Directory: ${targetDir}`);
|
|
27
|
+
await mkdir(join(targetDir, "src", "domain"), { recursive: true });
|
|
28
|
+
await mkdir(join(targetDir, "src", "ports"), { recursive: true });
|
|
29
|
+
await mkdir(join(targetDir, "src", "adapters"), { recursive: true });
|
|
30
|
+
await mkdir(join(targetDir, "src", "usecases"), { recursive: true });
|
|
31
|
+
await mkdir(join(targetDir, "src", "api"), { recursive: true });
|
|
32
|
+
await mkdir(join(targetDir, "src", "ui"), { recursive: true });
|
|
33
|
+
const packageJson = {
|
|
34
|
+
name: packageName,
|
|
35
|
+
version: "0.1.0",
|
|
36
|
+
private: true,
|
|
37
|
+
description: `${pascalName} module for Kuckit`,
|
|
38
|
+
type: "module",
|
|
39
|
+
main: "src/index.ts",
|
|
40
|
+
types: "src/index.ts",
|
|
41
|
+
exports: {
|
|
42
|
+
".": {
|
|
43
|
+
types: "./src/index.ts",
|
|
44
|
+
default: "./src/index.ts"
|
|
45
|
+
},
|
|
46
|
+
"./client": {
|
|
47
|
+
types: "./src/client-module.ts",
|
|
48
|
+
default: "./src/client-module.ts"
|
|
49
|
+
},
|
|
50
|
+
"./ui": {
|
|
51
|
+
types: "./src/ui/index.ts",
|
|
52
|
+
default: "./src/ui/index.ts"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
peerDependencies: { typescript: "^5" },
|
|
56
|
+
dependencies: {
|
|
57
|
+
"@kuckit/sdk": "^1.0.0",
|
|
58
|
+
"@kuckit/sdk-react": "^1.0.0",
|
|
59
|
+
"@kuckit/api": "^1.0.0",
|
|
60
|
+
"@orpc/server": "^1.10.0",
|
|
61
|
+
"@orpc/zod": "^1.10.0",
|
|
62
|
+
"drizzle-orm": "^0.44.0",
|
|
63
|
+
zod: "^3.23.0",
|
|
64
|
+
react: "^19.0.0",
|
|
65
|
+
"@tanstack/react-query": "^5.0.0"
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
await writeFile(join(targetDir, "package.json"), JSON.stringify(packageJson, null, " ") + "\n");
|
|
69
|
+
await writeFile(join(targetDir, "tsconfig.json"), JSON.stringify({
|
|
70
|
+
extends: "../../tsconfig.base.json",
|
|
71
|
+
compilerOptions: {
|
|
72
|
+
outDir: "dist",
|
|
73
|
+
rootDir: "src"
|
|
74
|
+
},
|
|
75
|
+
include: ["src"]
|
|
76
|
+
}, null, " ") + "\n");
|
|
77
|
+
const entityFile = `import { z } from 'zod'
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* ${pascalName} entity schema
|
|
81
|
+
*/
|
|
82
|
+
export const ${pascalName}Schema = z.object({
|
|
83
|
+
id: z.string(),
|
|
84
|
+
name: z.string().min(1, 'Name is required'),
|
|
85
|
+
description: z.string().optional(),
|
|
86
|
+
createdAt: z.date(),
|
|
87
|
+
updatedAt: z.date(),
|
|
88
|
+
userId: z.string(),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
export type ${pascalName} = z.infer<typeof ${pascalName}Schema>
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create ${camelName} input schema
|
|
95
|
+
*/
|
|
96
|
+
export const Create${pascalName}InputSchema = z.object({
|
|
97
|
+
name: z.string().min(1, 'Name is required'),
|
|
98
|
+
description: z.string().optional(),
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
export type Create${pascalName}Input = z.infer<typeof Create${pascalName}InputSchema>
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Update ${camelName} input schema
|
|
105
|
+
*/
|
|
106
|
+
export const Update${pascalName}InputSchema = z.object({
|
|
107
|
+
id: z.string(),
|
|
108
|
+
name: z.string().min(1, 'Name is required').optional(),
|
|
109
|
+
description: z.string().optional(),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
export type Update${pascalName}Input = z.infer<typeof Update${pascalName}InputSchema>
|
|
113
|
+
`;
|
|
114
|
+
await writeFile(join(targetDir, "src", "domain", `${kebabName}.entity.ts`), entityFile);
|
|
115
|
+
const repositoryPort = `import type { ${pascalName}, Create${pascalName}Input, Update${pascalName}Input } from '../domain/${kebabName}.entity'
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* ${pascalName} repository port interface
|
|
119
|
+
* Defines the contract for ${camelName} persistence operations
|
|
120
|
+
*/
|
|
121
|
+
export interface ${pascalName}Repository {
|
|
122
|
+
findById(id: string): Promise<${pascalName} | null>
|
|
123
|
+
findByUserId(userId: string): Promise<${pascalName}[]>
|
|
124
|
+
create(input: Create${pascalName}Input & { id: string; userId: string }): Promise<${pascalName}>
|
|
125
|
+
update(input: Update${pascalName}Input): Promise<${pascalName} | null>
|
|
126
|
+
delete(id: string): Promise<boolean>
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
await writeFile(join(targetDir, "src", "ports", `${kebabName}.repository.ts`), repositoryPort);
|
|
130
|
+
const drizzleAdapter = `import { pgTable, text, timestamp } from 'drizzle-orm/pg-core'
|
|
131
|
+
import { eq } from 'drizzle-orm'
|
|
132
|
+
import type { ${pascalName}Repository } from '../ports/${kebabName}.repository'
|
|
133
|
+
import type { ${pascalName}, Create${pascalName}Input, Update${pascalName}Input } from '../domain/${kebabName}.entity'
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* ${pascalName}s table schema for Drizzle ORM
|
|
137
|
+
*/
|
|
138
|
+
export const ${camelName}sTable = pgTable('${kebabName}s', {
|
|
139
|
+
id: text('id').primaryKey(),
|
|
140
|
+
name: text('name').notNull(),
|
|
141
|
+
description: text('description'),
|
|
142
|
+
createdAt: timestamp('created_at').notNull().defaultNow(),
|
|
143
|
+
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
|
144
|
+
userId: text('user_id').notNull(),
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Create a Drizzle-based ${camelName} repository
|
|
149
|
+
*/
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
151
|
+
export function make${pascalName}Repository(db: any): ${pascalName}Repository {
|
|
152
|
+
return {
|
|
153
|
+
async findById(id: string): Promise<${pascalName} | null> {
|
|
154
|
+
const results = await db.select().from(${camelName}sTable).where(eq(${camelName}sTable.id, id))
|
|
155
|
+
return results[0] ?? null
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
async findByUserId(userId: string): Promise<${pascalName}[]> {
|
|
159
|
+
return db.select().from(${camelName}sTable).where(eq(${camelName}sTable.userId, userId))
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
async create(input: Create${pascalName}Input & { id: string; userId: string }): Promise<${pascalName}> {
|
|
163
|
+
const now = new Date()
|
|
164
|
+
const ${camelName} = {
|
|
165
|
+
id: input.id,
|
|
166
|
+
name: input.name,
|
|
167
|
+
description: input.description ?? null,
|
|
168
|
+
createdAt: now,
|
|
169
|
+
updatedAt: now,
|
|
170
|
+
userId: input.userId,
|
|
171
|
+
}
|
|
172
|
+
await db.insert(${camelName}sTable).values(${camelName})
|
|
173
|
+
return ${camelName} as ${pascalName}
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
async update(input: Update${pascalName}Input): Promise<${pascalName} | null> {
|
|
177
|
+
const existing = await this.findById(input.id)
|
|
178
|
+
if (!existing) return null
|
|
179
|
+
|
|
180
|
+
const updated = {
|
|
181
|
+
...existing,
|
|
182
|
+
...(input.name !== undefined && { name: input.name }),
|
|
183
|
+
...(input.description !== undefined && { description: input.description }),
|
|
184
|
+
updatedAt: new Date(),
|
|
185
|
+
}
|
|
186
|
+
await db.update(${camelName}sTable).set(updated).where(eq(${camelName}sTable.id, input.id))
|
|
187
|
+
return updated
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
async delete(id: string): Promise<boolean> {
|
|
191
|
+
const result = await db.delete(${camelName}sTable).where(eq(${camelName}sTable.id, id))
|
|
192
|
+
return result.rowCount > 0
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
`;
|
|
197
|
+
await writeFile(join(targetDir, "src", "adapters", `${kebabName}.drizzle.ts`), drizzleAdapter);
|
|
198
|
+
const listUseCase = `import type { ${pascalName}Repository } from '../ports/${kebabName}.repository'
|
|
199
|
+
import type { ${pascalName} } from '../domain/${kebabName}.entity'
|
|
200
|
+
|
|
201
|
+
interface List${pascalName}sDeps {
|
|
202
|
+
${camelName}Repository: ${pascalName}Repository
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* List ${camelName}s use case
|
|
207
|
+
*/
|
|
208
|
+
export function makeList${pascalName}s(deps: List${pascalName}sDeps) {
|
|
209
|
+
return async (userId: string): Promise<${pascalName}[]> => {
|
|
210
|
+
return deps.${camelName}Repository.findByUserId(userId)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
`;
|
|
214
|
+
await writeFile(join(targetDir, "src", "usecases", `list-${kebabName}s.ts`), listUseCase);
|
|
215
|
+
const getUseCase = `import type { ${pascalName}Repository } from '../ports/${kebabName}.repository'
|
|
216
|
+
import type { ${pascalName} } from '../domain/${kebabName}.entity'
|
|
217
|
+
|
|
218
|
+
interface Get${pascalName}Deps {
|
|
219
|
+
${camelName}Repository: ${pascalName}Repository
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get ${camelName} use case
|
|
224
|
+
*/
|
|
225
|
+
export function makeGet${pascalName}(deps: Get${pascalName}Deps) {
|
|
226
|
+
return async (id: string): Promise<${pascalName} | null> => {
|
|
227
|
+
return deps.${camelName}Repository.findById(id)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
`;
|
|
231
|
+
await writeFile(join(targetDir, "src", "usecases", `get-${kebabName}.ts`), getUseCase);
|
|
232
|
+
const createUseCase = `import type { ${pascalName}Repository } from '../ports/${kebabName}.repository'
|
|
233
|
+
import type { ${pascalName}, Create${pascalName}Input } from '../domain/${kebabName}.entity'
|
|
234
|
+
|
|
235
|
+
export interface Create${pascalName}UseCaseInput extends Create${pascalName}Input {
|
|
236
|
+
userId: string
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface Create${pascalName}Deps {
|
|
240
|
+
${camelName}Repository: ${pascalName}Repository
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Create ${camelName} use case
|
|
245
|
+
*/
|
|
246
|
+
export function makeCreate${pascalName}(deps: Create${pascalName}Deps) {
|
|
247
|
+
return async (input: Create${pascalName}UseCaseInput): Promise<${pascalName}> => {
|
|
248
|
+
const id = crypto.randomUUID()
|
|
249
|
+
return deps.${camelName}Repository.create({
|
|
250
|
+
id,
|
|
251
|
+
name: input.name,
|
|
252
|
+
description: input.description,
|
|
253
|
+
userId: input.userId,
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
`;
|
|
258
|
+
await writeFile(join(targetDir, "src", "usecases", `create-${kebabName}.ts`), createUseCase);
|
|
259
|
+
const deleteUseCase = `import type { ${pascalName}Repository } from '../ports/${kebabName}.repository'
|
|
260
|
+
|
|
261
|
+
interface Delete${pascalName}Deps {
|
|
262
|
+
${camelName}Repository: ${pascalName}Repository
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Delete ${camelName} use case
|
|
267
|
+
*/
|
|
268
|
+
export function makeDelete${pascalName}(deps: Delete${pascalName}Deps) {
|
|
269
|
+
return async (id: string): Promise<boolean> => {
|
|
270
|
+
return deps.${camelName}Repository.delete(id)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
`;
|
|
274
|
+
await writeFile(join(targetDir, "src", "usecases", `delete-${kebabName}.ts`), deleteUseCase);
|
|
275
|
+
const routerFile = `import { z } from 'zod'
|
|
276
|
+
import { protectedProcedure } from '@kuckit/api'
|
|
277
|
+
import { Create${pascalName}InputSchema } from '../domain/${kebabName}.entity'
|
|
278
|
+
import type { ${pascalName}Repository } from '../ports/${kebabName}.repository'
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* ${pascalName}s oRPC router
|
|
282
|
+
* Provides CRUD operations for ${camelName}s
|
|
283
|
+
*/
|
|
284
|
+
export const ${camelName}sRouter = {
|
|
285
|
+
list: protectedProcedure.input(z.object({})).handler(async ({ context }) => {
|
|
286
|
+
const userId = context.session?.user?.id
|
|
287
|
+
if (!userId) throw new Error('User not authenticated')
|
|
288
|
+
|
|
289
|
+
const ${camelName}s = await context.di.resolve<${pascalName}Repository>('${camelName}Repository').findByUserId(userId)
|
|
290
|
+
return ${camelName}s
|
|
291
|
+
}),
|
|
292
|
+
|
|
293
|
+
get: protectedProcedure
|
|
294
|
+
.input(z.object({ id: z.string() }))
|
|
295
|
+
.handler(async ({ input, context }) => {
|
|
296
|
+
const ${camelName} = await context.di.resolve<${pascalName}Repository>('${camelName}Repository').findById(input.id)
|
|
297
|
+
return ${camelName}
|
|
298
|
+
}),
|
|
299
|
+
|
|
300
|
+
create: protectedProcedure.input(Create${pascalName}InputSchema).handler(async ({ input, context }) => {
|
|
301
|
+
const userId = context.session?.user?.id
|
|
302
|
+
if (!userId) throw new Error('User not authenticated')
|
|
303
|
+
|
|
304
|
+
const id = crypto.randomUUID()
|
|
305
|
+
const ${camelName} = await context.di.resolve<${pascalName}Repository>('${camelName}Repository').create({
|
|
306
|
+
id,
|
|
307
|
+
name: input.name,
|
|
308
|
+
description: input.description,
|
|
309
|
+
userId,
|
|
310
|
+
})
|
|
311
|
+
return ${camelName}
|
|
312
|
+
}),
|
|
313
|
+
|
|
314
|
+
delete: protectedProcedure
|
|
315
|
+
.input(z.object({ id: z.string() }))
|
|
316
|
+
.handler(async ({ input, context }) => {
|
|
317
|
+
const success = await context.di.resolve<${pascalName}Repository>('${camelName}Repository').delete(input.id)
|
|
318
|
+
return { success }
|
|
319
|
+
}),
|
|
320
|
+
}
|
|
321
|
+
`;
|
|
322
|
+
await writeFile(join(targetDir, "src", "api", `${kebabName}s.router.ts`), routerFile);
|
|
323
|
+
const pageComponent = `import { useState } from 'react'
|
|
324
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
325
|
+
import { useRpc } from '@kuckit/sdk-react'
|
|
326
|
+
|
|
327
|
+
interface ${pascalName} {
|
|
328
|
+
id: string
|
|
329
|
+
name: string
|
|
330
|
+
description?: string
|
|
331
|
+
createdAt: Date
|
|
332
|
+
updatedAt: Date
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
interface ${pascalName}sRpc {
|
|
336
|
+
${camelName}s: {
|
|
337
|
+
list: (input: Record<string, never>) => Promise<${pascalName}[]>
|
|
338
|
+
create: (input: { name: string; description?: string }) => Promise<${pascalName}>
|
|
339
|
+
delete: (input: { id: string }) => Promise<void>
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* ${pascalName}s page component
|
|
345
|
+
* Demonstrates CRUD operations using the ${camelName}s module with useRpc and TanStack Query
|
|
346
|
+
*/
|
|
347
|
+
export function ${pascalName}sPage() {
|
|
348
|
+
const rpc = useRpc<${pascalName}sRpc>()
|
|
349
|
+
const queryClient = useQueryClient()
|
|
350
|
+
const [newName, setNewName] = useState('')
|
|
351
|
+
const [newDescription, setNewDescription] = useState('')
|
|
352
|
+
|
|
353
|
+
const {
|
|
354
|
+
data: ${camelName}s = [],
|
|
355
|
+
isLoading,
|
|
356
|
+
error,
|
|
357
|
+
} = useQuery({
|
|
358
|
+
queryKey: ['${camelName}s'],
|
|
359
|
+
queryFn: () => rpc.${camelName}s.list({}),
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
const createMutation = useMutation({
|
|
363
|
+
mutationFn: (data: { name: string; description?: string }) => rpc.${camelName}s.create(data),
|
|
364
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['${camelName}s'] }),
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
const deleteMutation = useMutation({
|
|
368
|
+
mutationFn: (id: string) => rpc.${camelName}s.delete({ id }),
|
|
369
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['${camelName}s'] }),
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
const create${pascalName} = async (e: React.FormEvent) => {
|
|
373
|
+
e.preventDefault()
|
|
374
|
+
if (!newName.trim()) return
|
|
375
|
+
|
|
376
|
+
createMutation.mutate(
|
|
377
|
+
{ name: newName, description: newDescription || undefined },
|
|
378
|
+
{
|
|
379
|
+
onSuccess: () => {
|
|
380
|
+
setNewName('')
|
|
381
|
+
setNewDescription('')
|
|
382
|
+
},
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const delete${pascalName} = (id: string) => {
|
|
388
|
+
deleteMutation.mutate(id)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (isLoading) {
|
|
392
|
+
return <div style={{ padding: '2rem' }}>Loading ${camelName}s...</div>
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<div style={{ padding: '2rem', fontFamily: 'system-ui', maxWidth: '600px', margin: '0 auto' }}>
|
|
397
|
+
<h1>${pascalName}s</h1>
|
|
398
|
+
|
|
399
|
+
{(error || createMutation.error || deleteMutation.error) && (
|
|
400
|
+
<div style={{ color: 'red', marginBottom: '1rem' }}>
|
|
401
|
+
{error?.message || createMutation.error?.message || deleteMutation.error?.message}
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
|
|
405
|
+
<form onSubmit={create${pascalName}} style={{ marginBottom: '2rem' }}>
|
|
406
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
|
407
|
+
<input
|
|
408
|
+
type="text"
|
|
409
|
+
placeholder="Name"
|
|
410
|
+
value={newName}
|
|
411
|
+
onChange={(e) => setNewName(e.target.value)}
|
|
412
|
+
required
|
|
413
|
+
style={{ padding: '0.5rem', fontSize: '1rem' }}
|
|
414
|
+
/>
|
|
415
|
+
<input
|
|
416
|
+
type="text"
|
|
417
|
+
placeholder="Description (optional)"
|
|
418
|
+
value={newDescription}
|
|
419
|
+
onChange={(e) => setNewDescription(e.target.value)}
|
|
420
|
+
style={{ padding: '0.5rem', fontSize: '1rem' }}
|
|
421
|
+
/>
|
|
422
|
+
<button
|
|
423
|
+
type="submit"
|
|
424
|
+
disabled={createMutation.isPending}
|
|
425
|
+
style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}
|
|
426
|
+
>
|
|
427
|
+
{createMutation.isPending ? 'Adding...' : 'Add ${pascalName}'}
|
|
428
|
+
</button>
|
|
429
|
+
</div>
|
|
430
|
+
</form>
|
|
431
|
+
|
|
432
|
+
<ul style={{ listStyle: 'none', padding: 0 }}>
|
|
433
|
+
{${camelName}s.map((${camelName}) => (
|
|
434
|
+
<li
|
|
435
|
+
key={${camelName}.id}
|
|
436
|
+
style={{
|
|
437
|
+
padding: '1rem',
|
|
438
|
+
border: '1px solid #ccc',
|
|
439
|
+
marginBottom: '0.5rem',
|
|
440
|
+
borderRadius: '4px',
|
|
441
|
+
display: 'flex',
|
|
442
|
+
justifyContent: 'space-between',
|
|
443
|
+
alignItems: 'center',
|
|
444
|
+
}}
|
|
445
|
+
>
|
|
446
|
+
<div>
|
|
447
|
+
<strong>{${camelName}.name}</strong>
|
|
448
|
+
{${camelName}.description && (
|
|
449
|
+
<p style={{ margin: '0.5rem 0 0 0', color: '#666' }}>{${camelName}.description}</p>
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
<button
|
|
453
|
+
onClick={() => delete${pascalName}(${camelName}.id)}
|
|
454
|
+
disabled={deleteMutation.isPending}
|
|
455
|
+
style={{ cursor: 'pointer', color: 'red', background: 'none', border: 'none' }}
|
|
456
|
+
>
|
|
457
|
+
Delete
|
|
458
|
+
</button>
|
|
459
|
+
</li>
|
|
460
|
+
))}
|
|
461
|
+
</ul>
|
|
462
|
+
|
|
463
|
+
{${camelName}s.length === 0 && <p>No ${camelName}s yet. Create one above!</p>}
|
|
464
|
+
</div>
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
`;
|
|
468
|
+
await writeFile(join(targetDir, "src", "ui", `${pascalName}sPage.tsx`), pageComponent);
|
|
469
|
+
const uiIndex = `export { ${pascalName}sPage } from './${pascalName}sPage'
|
|
470
|
+
`;
|
|
471
|
+
await writeFile(join(targetDir, "src", "ui", "index.ts"), uiIndex);
|
|
472
|
+
const serverModule = `import { defineKuckitModule, asFunction, type KuckitModuleContext } from '@kuckit/sdk'
|
|
473
|
+
import { ${camelName}sTable, make${pascalName}Repository } from './adapters/${kebabName}.drizzle'
|
|
474
|
+
import { ${camelName}sRouter } from './api/${kebabName}s.router'
|
|
475
|
+
|
|
476
|
+
export type ${pascalName}ModuleConfig = Record<string, never>
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* ${pascalName} module - Kuckit module demonstrating Clean Architecture pattern
|
|
480
|
+
*
|
|
481
|
+
* This module shows:
|
|
482
|
+
* - Domain entity with Zod validation
|
|
483
|
+
* - Repository port/adapter pattern
|
|
484
|
+
* - Use cases for business logic
|
|
485
|
+
* - oRPC router for API endpoints
|
|
486
|
+
*/
|
|
487
|
+
export const kuckitModule = defineKuckitModule<${pascalName}ModuleConfig>({
|
|
488
|
+
id: '${moduleId}',
|
|
489
|
+
displayName: '${pascalName}',
|
|
490
|
+
description: '${pascalName} module for CRUD operations',
|
|
491
|
+
version: '0.1.0',
|
|
492
|
+
|
|
493
|
+
async register(ctx: KuckitModuleContext<${pascalName}ModuleConfig>) {
|
|
494
|
+
const { container } = ctx
|
|
495
|
+
|
|
496
|
+
// Register schema for migrations
|
|
497
|
+
ctx.registerSchema('${camelName}s', ${camelName}sTable)
|
|
498
|
+
|
|
499
|
+
// Register repository
|
|
500
|
+
container.register({
|
|
501
|
+
${camelName}Repository: asFunction(({ db }) => make${pascalName}Repository(db)).scoped(),
|
|
502
|
+
})
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
registerApi(ctx) {
|
|
506
|
+
ctx.addApiRegistration({
|
|
507
|
+
type: 'rpc-router',
|
|
508
|
+
name: '${camelName}s',
|
|
509
|
+
router: ${camelName}sRouter,
|
|
510
|
+
})
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
async onBootstrap(ctx: KuckitModuleContext<${pascalName}ModuleConfig>) {
|
|
514
|
+
const { container } = ctx
|
|
515
|
+
const logger = container.resolve('logger')
|
|
516
|
+
logger.info('${pascalName} module initialized')
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
async onShutdown(ctx: KuckitModuleContext<${pascalName}ModuleConfig>) {
|
|
520
|
+
const { container } = ctx
|
|
521
|
+
const logger = container.resolve('logger')
|
|
522
|
+
logger.info('${pascalName} module shutting down')
|
|
523
|
+
},
|
|
524
|
+
})
|
|
525
|
+
`;
|
|
526
|
+
await writeFile(join(targetDir, "src", "module.ts"), serverModule);
|
|
527
|
+
const clientModule = `import { defineKuckitClientModule, type KuckitClientModuleContext } from '@kuckit/sdk-react'
|
|
528
|
+
import { ${pascalName}sPage } from './ui/${pascalName}sPage'
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* ${pascalName} client module
|
|
532
|
+
* Registers routes and components for the web app
|
|
533
|
+
*/
|
|
534
|
+
export const kuckitClientModule = defineKuckitClientModule({
|
|
535
|
+
id: '${moduleId}',
|
|
536
|
+
displayName: '${pascalName}',
|
|
537
|
+
version: '0.1.0',
|
|
538
|
+
|
|
539
|
+
register(ctx: KuckitClientModuleContext) {
|
|
540
|
+
// Register the ${camelName}s page component
|
|
541
|
+
ctx.registerComponent('${pascalName}sPage', ${pascalName}sPage)
|
|
542
|
+
|
|
543
|
+
// Add route for ${camelName}s page
|
|
544
|
+
ctx.addRoute({
|
|
545
|
+
id: '${kebabName}s',
|
|
546
|
+
path: '/${kebabName}s',
|
|
547
|
+
component: ${pascalName}sPage,
|
|
548
|
+
meta: {
|
|
549
|
+
title: '${pascalName}s',
|
|
550
|
+
requiresAuth: true,
|
|
551
|
+
},
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
// Add navigation item
|
|
555
|
+
ctx.addNavItem({
|
|
556
|
+
id: '${kebabName}s-nav',
|
|
557
|
+
label: '${pascalName}s',
|
|
558
|
+
path: '/${kebabName}s',
|
|
559
|
+
icon: 'list',
|
|
560
|
+
order: 100,
|
|
561
|
+
})
|
|
562
|
+
},
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
export { ${pascalName}sPage } from './ui/${pascalName}sPage'
|
|
566
|
+
`;
|
|
567
|
+
await writeFile(join(targetDir, "src", "client-module.ts"), clientModule);
|
|
568
|
+
const indexFile = `// Server module
|
|
569
|
+
export { kuckitModule } from './module'
|
|
570
|
+
|
|
571
|
+
// Domain
|
|
572
|
+
export * from './domain/${kebabName}.entity'
|
|
573
|
+
|
|
574
|
+
// Ports
|
|
575
|
+
export type { ${pascalName}Repository } from './ports/${kebabName}.repository'
|
|
576
|
+
|
|
577
|
+
// Adapters
|
|
578
|
+
export { ${camelName}sTable, make${pascalName}Repository } from './adapters/${kebabName}.drizzle'
|
|
579
|
+
|
|
580
|
+
// Use cases
|
|
581
|
+
export { makeList${pascalName}s } from './usecases/list-${kebabName}s'
|
|
582
|
+
export { makeGet${pascalName} } from './usecases/get-${kebabName}'
|
|
583
|
+
export { makeCreate${pascalName} } from './usecases/create-${kebabName}'
|
|
584
|
+
export { makeDelete${pascalName} } from './usecases/delete-${kebabName}'
|
|
585
|
+
|
|
586
|
+
// API
|
|
587
|
+
export { ${camelName}sRouter } from './api/${kebabName}s.router'
|
|
588
|
+
`;
|
|
589
|
+
await writeFile(join(targetDir, "src", "index.ts"), indexFile);
|
|
590
|
+
console.log(`
|
|
591
|
+
✅ Module created successfully!
|
|
592
|
+
|
|
593
|
+
📁 Structure:
|
|
594
|
+
${options.dir}/${moduleDirName}/
|
|
595
|
+
├── src/
|
|
596
|
+
│ ├── domain/
|
|
597
|
+
│ │ └── ${kebabName}.entity.ts # Zod schema for entity
|
|
598
|
+
│ ├── ports/
|
|
599
|
+
│ │ └── ${kebabName}.repository.ts # Repository interface
|
|
600
|
+
│ ├── adapters/
|
|
601
|
+
│ │ └── ${kebabName}.drizzle.ts # Drizzle implementation
|
|
602
|
+
│ ├── usecases/
|
|
603
|
+
│ │ ├── list-${kebabName}s.ts
|
|
604
|
+
│ │ ├── get-${kebabName}.ts
|
|
605
|
+
│ │ ├── create-${kebabName}.ts
|
|
606
|
+
│ │ └── delete-${kebabName}.ts
|
|
607
|
+
│ ├── api/
|
|
608
|
+
│ │ └── ${kebabName}s.router.ts # oRPC router
|
|
609
|
+
│ ├── ui/
|
|
610
|
+
│ │ └── ${pascalName}sPage.tsx # React component
|
|
611
|
+
│ ├── module.ts # Server module
|
|
612
|
+
│ ├── client-module.ts # Client module
|
|
613
|
+
│ └── index.ts
|
|
614
|
+
├── package.json
|
|
615
|
+
└── tsconfig.json
|
|
616
|
+
|
|
617
|
+
📝 Next steps:
|
|
618
|
+
1. Run 'bun install' from the root to link the new package
|
|
619
|
+
2. Add module to apps/server/src/config/modules.ts:
|
|
620
|
+
import { kuckitModule as ${camelName}Module } from '${packageName}'
|
|
621
|
+
export const modules = [..., ${camelName}Module]
|
|
622
|
+
3. Add client module to apps/web/src/modules.client.ts:
|
|
623
|
+
import { kuckitClientModule as ${camelName}Client } from '${packageName}/client'
|
|
624
|
+
export const clientModules = [..., { module: ${camelName}Client }]
|
|
625
|
+
4. Add schema path to packages/db/drizzle.config.ts:
|
|
626
|
+
schema: [..., '../${moduleDirName}/src/adapters/${kebabName}.drizzle.ts']
|
|
627
|
+
5. Run 'bun run db:push' to create the table
|
|
628
|
+
`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
//#endregion
|
|
632
|
+
//#region src/commands/add-module.ts
|
|
633
|
+
const CONFIG_FILES = [
|
|
634
|
+
"kuckit.config.ts",
|
|
635
|
+
"kuckit.config.js",
|
|
636
|
+
"kuckit.config.mjs"
|
|
637
|
+
];
|
|
638
|
+
async function fileExists(path) {
|
|
639
|
+
try {
|
|
640
|
+
await access(path, constants.F_OK);
|
|
641
|
+
return true;
|
|
642
|
+
} catch {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function findConfigFile(cwd) {
|
|
647
|
+
for (const file of CONFIG_FILES) {
|
|
648
|
+
const configPath = join(cwd, file);
|
|
649
|
+
if (existsSync(configPath)) return configPath;
|
|
650
|
+
}
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
function detectPackageManager(cwd) {
|
|
654
|
+
for (const [file, pm] of Object.entries({
|
|
655
|
+
"bun.lock": "bun",
|
|
656
|
+
"bun.lockb": "bun",
|
|
657
|
+
"package-lock.json": "npm",
|
|
658
|
+
"yarn.lock": "yarn",
|
|
659
|
+
"pnpm-lock.yaml": "pnpm"
|
|
660
|
+
})) try {
|
|
661
|
+
accessSync(join(cwd, file), constants.F_OK);
|
|
662
|
+
return pm;
|
|
663
|
+
} catch {}
|
|
664
|
+
return "npm";
|
|
665
|
+
}
|
|
666
|
+
async function readPackageKuckitMetadata(packageName, cwd) {
|
|
667
|
+
const packageJsonPath = join(cwd, "node_modules", packageName, "package.json");
|
|
668
|
+
try {
|
|
669
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
670
|
+
return JSON.parse(content).kuckit ?? null;
|
|
671
|
+
} catch {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const MODULES_MARKER_START = "// KUCKIT_MODULES_START";
|
|
676
|
+
const MODULES_MARKER_END = "// KUCKIT_MODULES_END";
|
|
677
|
+
const CLIENT_MODULES_MARKER_START = "// KUCKIT_CLIENT_MODULES_START";
|
|
678
|
+
const CLIENT_MODULES_MARKER_END = "// KUCKIT_CLIENT_MODULES_END";
|
|
679
|
+
/**
|
|
680
|
+
* Add module to unified kuckit.config.ts
|
|
681
|
+
*/
|
|
682
|
+
async function addToUnifiedConfig(packageName, configPath) {
|
|
683
|
+
let content = await readFile(configPath, "utf-8");
|
|
684
|
+
if (content.includes(`'${packageName}'`) || content.includes(`"${packageName}"`)) {
|
|
685
|
+
console.log(` Module ${packageName} is already in ${configPath}`);
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
const modulesArrayMatch = content.match(/modules:\s*\[/);
|
|
689
|
+
if (!modulesArrayMatch) {
|
|
690
|
+
console.warn(`Warning: Could not find 'modules' array in ${configPath}`);
|
|
691
|
+
console.log(` Please manually add: { package: '${packageName}' }`);
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
const insertPos = modulesArrayMatch.index + modulesArrayMatch[0].length;
|
|
695
|
+
const newEntry = `\n\t\t{ package: '${packageName}' },`;
|
|
696
|
+
content = content.slice(0, insertPos) + newEntry + content.slice(insertPos);
|
|
697
|
+
await writeFile(configPath, content);
|
|
698
|
+
console.log(` Added to unified config: ${configPath}`);
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
async function addToServerModules(packageName, cwd) {
|
|
702
|
+
const modulesPath = join(cwd, "apps", "server", "src", "config", "modules.ts");
|
|
703
|
+
if (!await fileExists(modulesPath)) {
|
|
704
|
+
console.warn(`Warning: Server modules file not found at ${modulesPath}`);
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
let content = await readFile(modulesPath, "utf-8");
|
|
708
|
+
if (content.includes(`package: '${packageName}'`)) {
|
|
709
|
+
console.log(` Module ${packageName} is already in server modules`);
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
if (!content.includes(MODULES_MARKER_START)) {
|
|
713
|
+
if (content.match(/return modules\s*$/)) {
|
|
714
|
+
const insertPos = content.lastIndexOf("return modules");
|
|
715
|
+
const before$1 = content.slice(0, insertPos);
|
|
716
|
+
const after$1 = content.slice(insertPos);
|
|
717
|
+
content = before$1 + `\n\t\t${MODULES_MARKER_START}\n\t\t// Modules installed via 'kuckit add' will be added here\n\t\t${MODULES_MARKER_END}\n\n\t\t` + after$1;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const newEntry = `{ package: '${packageName}' },`;
|
|
721
|
+
const markerEndPos = content.indexOf(MODULES_MARKER_END);
|
|
722
|
+
if (markerEndPos === -1) {
|
|
723
|
+
console.warn(`Warning: Could not find marker in ${modulesPath}`);
|
|
724
|
+
console.log(` Please manually add: ${newEntry}`);
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
const before = content.slice(0, markerEndPos);
|
|
728
|
+
const after = content.slice(markerEndPos);
|
|
729
|
+
content = before + `${newEntry}\n\t\t` + after;
|
|
730
|
+
await writeFile(modulesPath, content);
|
|
731
|
+
console.log(` Added to server modules: ${modulesPath}`);
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
async function addToClientModules(packageName, clientPath, cwd) {
|
|
735
|
+
const clientModulesPath = join(cwd, "apps", "web", "src", "modules.client.ts");
|
|
736
|
+
if (!await fileExists(clientModulesPath)) {
|
|
737
|
+
await writeFile(clientModulesPath, `import type { ClientModuleSpec } from '@kuckit/sdk-react'
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Client module specifications
|
|
741
|
+
*
|
|
742
|
+
* Add your Kuckit client modules here. Modules can be:
|
|
743
|
+
* - Direct references: { module: myModule }
|
|
744
|
+
* - Package imports: { package: '@acme/billing-module/client' }
|
|
745
|
+
*
|
|
746
|
+
* The CLI command \`kuckit add <package>\` will automatically update this file.
|
|
747
|
+
*/
|
|
748
|
+
export const getClientModuleSpecs = (): ClientModuleSpec[] => [
|
|
749
|
+
${CLIENT_MODULES_MARKER_START}
|
|
750
|
+
${CLIENT_MODULES_MARKER_END}
|
|
751
|
+
]
|
|
752
|
+
`);
|
|
753
|
+
console.log(` Created client modules file: ${clientModulesPath}`);
|
|
754
|
+
}
|
|
755
|
+
let content = await readFile(clientModulesPath, "utf-8");
|
|
756
|
+
const clientPackagePath = `${packageName}/${clientPath.replace("./", "")}`;
|
|
757
|
+
if (content.includes(`package: '${clientPackagePath}'`)) {
|
|
758
|
+
console.log(` Client module is already registered`);
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
const newEntry = `{ package: '${clientPackagePath}' },`;
|
|
762
|
+
const markerEndPos = content.indexOf(CLIENT_MODULES_MARKER_END);
|
|
763
|
+
if (markerEndPos === -1) {
|
|
764
|
+
console.warn(`Warning: Could not find marker in ${clientModulesPath}`);
|
|
765
|
+
console.log(` Please manually add: ${newEntry}`);
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
const before = content.slice(0, markerEndPos);
|
|
769
|
+
const after = content.slice(markerEndPos);
|
|
770
|
+
content = before + `${newEntry}\n\t` + after;
|
|
771
|
+
await writeFile(clientModulesPath, content);
|
|
772
|
+
console.log(` Added to client modules: ${clientModulesPath}`);
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
async function addModule(packageName, options) {
|
|
776
|
+
const cwd = process.cwd();
|
|
777
|
+
console.log(`Adding module: ${packageName}`);
|
|
778
|
+
if (!options.skipInstall) {
|
|
779
|
+
const pm = detectPackageManager(cwd);
|
|
780
|
+
const installCmd = pm === "npm" ? `${pm} install ${packageName}` : `${pm} add ${packageName}`;
|
|
781
|
+
console.log(` Installing with ${pm}...`);
|
|
782
|
+
try {
|
|
783
|
+
execSync(installCmd, {
|
|
784
|
+
cwd,
|
|
785
|
+
stdio: "inherit"
|
|
786
|
+
});
|
|
787
|
+
} catch {
|
|
788
|
+
console.error(`Failed to install ${packageName}`);
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
const metadata = await readPackageKuckitMetadata(packageName, cwd);
|
|
793
|
+
if (!metadata) {
|
|
794
|
+
console.error(`\nError: ${packageName} is not a valid Kuckit module.`);
|
|
795
|
+
console.error(`The package.json must contain a "kuckit" field with module metadata.`);
|
|
796
|
+
console.error(`\nExpected structure:`);
|
|
797
|
+
console.error(`{
|
|
798
|
+
"kuckit": {
|
|
799
|
+
"id": "acme.billing",
|
|
800
|
+
"server": ".",
|
|
801
|
+
"client": "./client"
|
|
802
|
+
}
|
|
803
|
+
}`);
|
|
804
|
+
process.exit(1);
|
|
805
|
+
}
|
|
806
|
+
console.log(` Found Kuckit module: ${metadata.id}`);
|
|
807
|
+
const configPath = findConfigFile(cwd);
|
|
808
|
+
if (configPath) await addToUnifiedConfig(packageName, configPath);
|
|
809
|
+
else {
|
|
810
|
+
if (metadata.server) await addToServerModules(packageName, cwd);
|
|
811
|
+
if (metadata.client) await addToClientModules(packageName, metadata.client, cwd);
|
|
812
|
+
}
|
|
813
|
+
console.log(`
|
|
814
|
+
Module ${packageName} added successfully!
|
|
815
|
+
|
|
816
|
+
Run 'bun run dev' to start the server with the new module.
|
|
817
|
+
`);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
//#endregion
|
|
821
|
+
//#region src/lib/sdk-loader.ts
|
|
822
|
+
async function loadTryLoadKuckitConfig() {
|
|
823
|
+
try {
|
|
824
|
+
const sdk = await import("@kuckit/sdk");
|
|
825
|
+
if (!sdk.tryLoadKuckitConfig) throw new Error("Invalid @kuckit/sdk version: tryLoadKuckitConfig not found.");
|
|
826
|
+
return sdk.tryLoadKuckitConfig;
|
|
827
|
+
} catch {
|
|
828
|
+
console.error("Error: This command requires @kuckit/sdk to be installed in your project.");
|
|
829
|
+
console.error("Install it with one of:");
|
|
830
|
+
console.error(" npm install -D @kuckit/sdk");
|
|
831
|
+
console.error(" pnpm add -D @kuckit/sdk");
|
|
832
|
+
console.error(" bun add -d @kuckit/sdk");
|
|
833
|
+
process.exit(1);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
//#endregion
|
|
838
|
+
//#region src/commands/discover-module.ts
|
|
839
|
+
async function readKuckitMetadataFromPath(packageJsonPath) {
|
|
840
|
+
try {
|
|
841
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
842
|
+
return JSON.parse(content).kuckit ?? null;
|
|
843
|
+
} catch {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async function readPackageName(packageJsonPath) {
|
|
848
|
+
try {
|
|
849
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
850
|
+
return JSON.parse(content).name ?? null;
|
|
851
|
+
} catch {
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async function scanWorkspacePackages(cwd) {
|
|
856
|
+
const discovered = [];
|
|
857
|
+
const packagesDir = join(cwd, "packages");
|
|
858
|
+
if (!existsSync(packagesDir)) return discovered;
|
|
859
|
+
try {
|
|
860
|
+
const entries = await readdir(packagesDir, { withFileTypes: true });
|
|
861
|
+
for (const entry of entries) {
|
|
862
|
+
if (!entry.isDirectory()) continue;
|
|
863
|
+
const packageJsonPath = join(packagesDir, entry.name, "package.json");
|
|
864
|
+
const metadata = await readKuckitMetadataFromPath(packageJsonPath);
|
|
865
|
+
if (metadata) {
|
|
866
|
+
const packageName = await readPackageName(packageJsonPath);
|
|
867
|
+
if (packageName) discovered.push({
|
|
868
|
+
package: packageName,
|
|
869
|
+
id: metadata.id,
|
|
870
|
+
server: metadata.server,
|
|
871
|
+
client: metadata.client,
|
|
872
|
+
enabled: false,
|
|
873
|
+
local: true
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
} catch {}
|
|
878
|
+
return discovered;
|
|
879
|
+
}
|
|
880
|
+
async function scanNodeModules(cwd) {
|
|
881
|
+
const nodeModulesPath = join(cwd, "node_modules");
|
|
882
|
+
const discovered = [];
|
|
883
|
+
try {
|
|
884
|
+
const entries = await readdir(nodeModulesPath, { withFileTypes: true });
|
|
885
|
+
for (const entry of entries) {
|
|
886
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
887
|
+
if (entry.name.startsWith(".")) continue;
|
|
888
|
+
if (entry.name.startsWith("@")) {
|
|
889
|
+
const scopePath = join(nodeModulesPath, entry.name);
|
|
890
|
+
try {
|
|
891
|
+
const scopedEntries = await readdir(scopePath, { withFileTypes: true });
|
|
892
|
+
for (const scopedEntry of scopedEntries) {
|
|
893
|
+
if (!scopedEntry.isDirectory() && !scopedEntry.isSymbolicLink()) continue;
|
|
894
|
+
const packageName = `${entry.name}/${scopedEntry.name}`;
|
|
895
|
+
const metadata = await readKuckitMetadataFromPath(join(nodeModulesPath, packageName, "package.json"));
|
|
896
|
+
if (metadata) discovered.push({
|
|
897
|
+
package: packageName,
|
|
898
|
+
id: metadata.id,
|
|
899
|
+
server: metadata.server,
|
|
900
|
+
client: metadata.client,
|
|
901
|
+
enabled: false
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
} catch {}
|
|
905
|
+
} else {
|
|
906
|
+
const metadata = await readKuckitMetadataFromPath(join(nodeModulesPath, entry.name, "package.json"));
|
|
907
|
+
if (metadata) discovered.push({
|
|
908
|
+
package: entry.name,
|
|
909
|
+
id: metadata.id,
|
|
910
|
+
server: metadata.server,
|
|
911
|
+
client: metadata.client,
|
|
912
|
+
enabled: false
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
} catch {}
|
|
917
|
+
return discovered;
|
|
918
|
+
}
|
|
919
|
+
async function addModulesToConfig(packages, configPath) {
|
|
920
|
+
const content = await readFile(configPath, "utf-8");
|
|
921
|
+
let updated = content;
|
|
922
|
+
for (const pkg of packages) {
|
|
923
|
+
if (updated.includes(`'${pkg}'`) || updated.includes(`"${pkg}"`)) continue;
|
|
924
|
+
const modulesArrayMatch = updated.match(/modules:\s*\[/);
|
|
925
|
+
if (!modulesArrayMatch) continue;
|
|
926
|
+
const insertPos = modulesArrayMatch.index + modulesArrayMatch[0].length;
|
|
927
|
+
const newEntry = `\n\t\t{ package: '${pkg}' },`;
|
|
928
|
+
updated = updated.slice(0, insertPos) + newEntry + updated.slice(insertPos);
|
|
929
|
+
}
|
|
930
|
+
if (updated !== content) {
|
|
931
|
+
const { writeFile: writeFile$1 } = await import("node:fs/promises");
|
|
932
|
+
await writeFile$1(configPath, updated);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
async function discoverModules(options) {
|
|
936
|
+
const cwd = process.cwd();
|
|
937
|
+
if (!options.json) console.log("Scanning for Kuckit modules...\n");
|
|
938
|
+
const config = await (await loadTryLoadKuckitConfig())(cwd);
|
|
939
|
+
const configuredPackages = new Set(config?.modules.map((m) => m.package) ?? []);
|
|
940
|
+
const workspaceModules = await scanWorkspacePackages(cwd);
|
|
941
|
+
const nodeModules = await scanNodeModules(cwd);
|
|
942
|
+
const seen = /* @__PURE__ */ new Set();
|
|
943
|
+
const discovered = [];
|
|
944
|
+
for (const mod of workspaceModules) {
|
|
945
|
+
seen.add(mod.package);
|
|
946
|
+
mod.enabled = configuredPackages.has(mod.package);
|
|
947
|
+
discovered.push(mod);
|
|
948
|
+
}
|
|
949
|
+
for (const mod of nodeModules) if (!seen.has(mod.package)) {
|
|
950
|
+
mod.enabled = configuredPackages.has(mod.package);
|
|
951
|
+
discovered.push(mod);
|
|
952
|
+
}
|
|
953
|
+
discovered.sort((a, b) => {
|
|
954
|
+
if (a.enabled !== b.enabled) return a.enabled ? -1 : 1;
|
|
955
|
+
return a.package.localeCompare(b.package);
|
|
956
|
+
});
|
|
957
|
+
const result = {
|
|
958
|
+
discovered,
|
|
959
|
+
enabled: discovered.filter((m) => m.enabled).length,
|
|
960
|
+
unconfigured: discovered.filter((m) => !m.enabled).length
|
|
961
|
+
};
|
|
962
|
+
if (options.json) {
|
|
963
|
+
console.log(JSON.stringify(result, null, 2));
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (discovered.length === 0) {
|
|
967
|
+
console.log("No Kuckit modules found.");
|
|
968
|
+
console.log("\nInstall modules with: kuckit add <package>");
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
console.log(`Found ${discovered.length} Kuckit module${discovered.length === 1 ? "" : "s"}:`);
|
|
972
|
+
for (const mod of discovered) {
|
|
973
|
+
const status = mod.enabled ? "✓" : "○";
|
|
974
|
+
const label = mod.enabled ? "(enabled)" : "(not configured)";
|
|
975
|
+
const localLabel = mod.local ? " [local]" : "";
|
|
976
|
+
console.log(` ${status} ${mod.package}${localLabel} ${label}`);
|
|
977
|
+
}
|
|
978
|
+
const unconfigured = discovered.filter((m) => !m.enabled);
|
|
979
|
+
if (unconfigured.length === 0) {
|
|
980
|
+
console.log("\nAll discovered modules are already configured.");
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (!options.interactive) {
|
|
984
|
+
console.log(`\n${unconfigured.length} module${unconfigured.length === 1 ? "" : "s"} not configured.`);
|
|
985
|
+
console.log("Run without --no-interactive to enable them.");
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (!config?._configPath) {
|
|
989
|
+
console.log("\nNo kuckit.config.ts found. Create one first with:");
|
|
990
|
+
console.log(" npx create-kuckit-app --init");
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
try {
|
|
994
|
+
const { checkbox } = await import("@inquirer/prompts");
|
|
995
|
+
const selected = await checkbox({
|
|
996
|
+
message: "Enable modules:",
|
|
997
|
+
choices: unconfigured.map((m) => ({
|
|
998
|
+
name: m.package,
|
|
999
|
+
value: m.package,
|
|
1000
|
+
checked: true
|
|
1001
|
+
}))
|
|
1002
|
+
});
|
|
1003
|
+
if (selected.length === 0) {
|
|
1004
|
+
console.log("\nNo modules selected.");
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
await addModulesToConfig(selected, config._configPath);
|
|
1008
|
+
console.log(`\nUpdated kuckit.config.ts with ${selected.length} new module${selected.length === 1 ? "" : "s"}.`);
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
if (error.name === "ExitPromptError") {
|
|
1011
|
+
console.log("\nCancelled.");
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
throw error;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
//#endregion
|
|
1019
|
+
export { generateModule as i, loadTryLoadKuckitConfig as n, addModule as r, discoverModules as t };
|