@djangocfg/ext-base 1.0.2 → 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.
Files changed (47) hide show
  1. package/README.md +186 -7
  2. package/dist/api.cjs +40 -0
  3. package/dist/api.d.cts +35 -0
  4. package/dist/api.d.ts +35 -0
  5. package/dist/api.js +2 -0
  6. package/dist/auth.cjs +10 -0
  7. package/dist/auth.d.cts +1 -0
  8. package/dist/auth.d.ts +1 -0
  9. package/dist/auth.js +2 -0
  10. package/dist/chunk-3RG5ZIWI.js +8 -0
  11. package/dist/chunk-UXTBBEO5.js +237 -0
  12. package/dist/chunk-VJEGYVBV.js +140 -0
  13. package/dist/cli.mjs +530 -0
  14. package/dist/hooks.cjs +437 -0
  15. package/dist/hooks.d.cts +97 -0
  16. package/dist/hooks.d.ts +97 -0
  17. package/dist/hooks.js +95 -0
  18. package/dist/index.cjs +345 -0
  19. package/dist/index.d.cts +363 -0
  20. package/dist/index.d.ts +363 -0
  21. package/dist/index.js +3 -0
  22. package/package.json +5 -2
  23. package/src/cli/index.ts +470 -35
  24. package/src/context/ExtensionProvider.tsx +67 -4
  25. package/src/extensionConfig.ts +114 -0
  26. package/src/index.ts +3 -0
  27. package/src/metadata.ts +1 -2
  28. package/src/types/context.ts +21 -15
  29. package/src/utils/createExtensionConfig.ts +34 -18
  30. package/templates/extension-template/README.md.template +37 -5
  31. package/templates/extension-template/package.json.template +13 -5
  32. package/templates/extension-template/playground/.gitignore.template +34 -0
  33. package/templates/extension-template/playground/CLAUDE.md +35 -0
  34. package/templates/extension-template/playground/README.md.template +76 -0
  35. package/templates/extension-template/playground/app/globals.css.template +19 -0
  36. package/templates/extension-template/playground/app/layout.tsx.template +30 -0
  37. package/templates/extension-template/playground/app/page.tsx.template +44 -0
  38. package/templates/extension-template/playground/next.config.ts.template +62 -0
  39. package/templates/extension-template/playground/package.json.template +33 -0
  40. package/templates/extension-template/playground/tsconfig.json.template +27 -0
  41. package/templates/extension-template/src/config.ts +1 -2
  42. package/templates/extension-template/src/contexts/__PROVIDER_NAME__Context.tsx +1 -1
  43. package/templates/extension-template/src/contexts/__PROVIDER_NAME__ExtensionProvider.tsx +1 -0
  44. package/templates/extension-template/src/hooks/index.ts +1 -1
  45. package/templates/extension-template/src/index.ts +12 -4
  46. package/templates/extension-template/src/utils/withSmartProvider.tsx +70 -0
  47. package/templates/extension-template/tsup.config.ts +36 -22
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { EXTENSION_CATEGORIES, createExtensionConfig, createExtensionError, createExtensionLogger, extensionConfig, formatErrorMessage, handleExtensionError, isExtensionError } from './chunk-UXTBBEO5.js';
2
+ export { createExtensionAPI, getApiUrl, getSharedAuthStorage, isDevelopment, isProduction, isStaticBuild } from './chunk-VJEGYVBV.js';
3
+ import './chunk-3RG5ZIWI.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ext-base",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Base utilities and common code for DjangoCFG extensions",
5
5
  "keywords": [
6
6
  "django",
@@ -15,7 +15,7 @@
15
15
  "name": "DjangoCFG",
16
16
  "url": "https://djangocfg.com"
17
17
  },
18
- "homepage": "https://djangocfg.com",
18
+ "homepage": "https://hub.djangocfg.com/extensions/djangocfg-ext-base",
19
19
  "repository": {
20
20
  "type": "git",
21
21
  "url": "https://github.com/markolofsen/django-cfg.git",
@@ -64,8 +64,11 @@
64
64
  "build": "tsup",
65
65
  "dev": "tsup --watch",
66
66
  "check": "tsc --noEmit",
67
+ "test": "tsx src/cli/index.ts test",
67
68
  "cli": "tsx src/cli/index.ts",
68
69
  "cli:help": "tsx src/cli/index.ts --help",
70
+ "cli:create": "tsx src/cli/index.ts create",
71
+ "cli:test": "tsx src/cli/index.ts test",
69
72
  "cli:list": "tsx src/cli/index.ts list",
70
73
  "cli:info": "tsx src/cli/index.ts info"
71
74
  },
package/src/cli/index.ts CHANGED
@@ -10,6 +10,7 @@ import prompts from 'prompts';
10
10
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'fs';
11
11
  import { join, dirname } from 'path';
12
12
  import { fileURLToPath } from 'url';
13
+ import { execSync } from 'child_process';
13
14
  import { EXTENSION_CATEGORIES } from '../types/context';
14
15
 
15
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -28,6 +29,7 @@ function getVersion(): string {
28
29
  // CLI Commands
29
30
  const COMMANDS = {
30
31
  create: 'Create a new DjangoCFG extension',
32
+ test: 'Quick create test extension (no prompts)',
31
33
  help: 'Show help',
32
34
  '--help': 'Show help',
33
35
  '-h': 'Show help',
@@ -92,6 +94,80 @@ function printBanner() {
92
94
  console.log();
93
95
  }
94
96
 
97
+ // Check if package exists on npm
98
+ async function checkPackageExists(packageName: string): Promise<boolean> {
99
+ try {
100
+ const encodedName = encodeURIComponent(packageName);
101
+ const response = await fetch(`https://registry.npmjs.org/${encodedName}`);
102
+ return response.status === 200;
103
+ } catch {
104
+ // Network error or other issue - allow creation
105
+ return false;
106
+ }
107
+ }
108
+
109
+ // Check if extension directory already exists (case-insensitive)
110
+ function checkDirectoryExists(packageName: string): { exists: boolean; path?: string } {
111
+ // Generate directory name from package name
112
+ const extDirName = packageName.replace('@', '').replace('/', '-');
113
+
114
+ // Determine base directory
115
+ const isInExtBase = process.cwd().endsWith('ext-base');
116
+ const baseDir = isInExtBase
117
+ ? join(process.cwd(), '..')
118
+ : join(process.cwd(), 'extensions');
119
+
120
+ const extDir = join(baseDir, extDirName);
121
+
122
+ // Check exact match first
123
+ if (existsSync(extDir)) {
124
+ return { exists: true, path: extDir };
125
+ }
126
+
127
+ // Check case-insensitive match (important for macOS)
128
+ try {
129
+ const files = readdirSync(baseDir);
130
+ const lowerCaseName = extDirName.toLowerCase();
131
+
132
+ for (const file of files) {
133
+ if (file.toLowerCase() === lowerCaseName) {
134
+ const fullPath = join(baseDir, file);
135
+ const stats = statSync(fullPath);
136
+ if (stats.isDirectory()) {
137
+ return { exists: true, path: fullPath };
138
+ }
139
+ }
140
+ }
141
+ } catch {
142
+ // If we can't read directory, assume it doesn't exist
143
+ return { exists: false };
144
+ }
145
+
146
+ return { exists: false };
147
+ }
148
+
149
+ // Detect available package manager
150
+ function detectPackageManager(): string | null {
151
+ const managers = ['pnpm', 'yarn', 'npm'];
152
+
153
+ for (const manager of managers) {
154
+ try {
155
+ const result = execSync(`${manager} --version`, {
156
+ encoding: 'utf-8',
157
+ stdio: ['ignore', 'pipe', 'ignore'],
158
+ });
159
+ if (result) {
160
+ return manager;
161
+ }
162
+ } catch {
163
+ // Manager not available, try next
164
+ continue;
165
+ }
166
+ }
167
+
168
+ return null;
169
+ }
170
+
95
171
  // Print help
96
172
  function printHelp() {
97
173
  printBanner();
@@ -118,23 +194,65 @@ async function createExtension() {
118
194
  console.log(chalk.yellow('Create a new DjangoCFG extension'));
119
195
  console.log();
120
196
 
121
- const response = await prompts([
122
- {
197
+ // Step 1: Get and validate package name with npm check
198
+ let packageName = '';
199
+ let packageNameValid = false;
200
+
201
+ while (!packageNameValid) {
202
+ const nameResponse = await prompts({
123
203
  type: 'text',
124
- name: 'name',
125
- message: 'Extension name (e.g., "leads", "payments"):',
204
+ name: 'packageName',
205
+ message: 'Package name (e.g., "@my-org/my-extension" or "my-extension"):',
126
206
  validate: (value: string) => {
127
- if (!value) return 'Extension name is required';
128
- if (!/^[a-z][a-z0-9-]*$/.test(value)) {
129
- return 'Extension name must start with a letter and contain only lowercase letters, numbers, and hyphens';
207
+ if (!value) return 'Package name is required';
208
+ const normalized = value.toLowerCase();
209
+ // Allow @scope/name or just name
210
+ if (!/^(@[a-zA-Z0-9-]+\/)?[a-zA-Z0-9][a-zA-Z0-9-]*$/.test(normalized)) {
211
+ return 'Invalid package name format. Use @scope/name or name';
130
212
  }
131
213
  return true;
132
214
  },
133
- },
215
+ });
216
+
217
+ if (!nameResponse.packageName) {
218
+ consola.info('Extension creation cancelled');
219
+ return;
220
+ }
221
+
222
+ // Always normalize to lowercase (npm requirement)
223
+ packageName = nameResponse.packageName.toLowerCase();
224
+
225
+ // Check if extension directory already exists (case-insensitive)
226
+ const dirCheck = checkDirectoryExists(packageName);
227
+ if (dirCheck.exists) {
228
+ consola.error(`Extension directory already exists: ${chalk.red(dirCheck.path)}`);
229
+ console.log(chalk.gray('Please try a different name.'));
230
+ console.log();
231
+ continue; // Ask again
232
+ }
233
+
234
+ // Check if package already exists on npm
235
+ consola.start(`Checking if ${chalk.cyan(packageName)} is available on npm...`);
236
+ const packageExists = await checkPackageExists(packageName);
237
+
238
+ if (packageExists) {
239
+ consola.error(`Package ${chalk.red(packageName)} already exists on npm!`);
240
+ console.log(chalk.gray('Please try a different name.'));
241
+ console.log();
242
+ continue; // Ask again
243
+ }
244
+
245
+ consola.success('Package name is available!');
246
+ console.log();
247
+ packageNameValid = true;
248
+ }
249
+
250
+ // Step 2: Get remaining details
251
+ const response = await prompts([
134
252
  {
135
253
  type: 'text',
136
254
  name: 'displayName',
137
- message: 'Display name (e.g., "Leads & Forms"):',
255
+ message: 'Display name (e.g., "My Extension"):',
138
256
  validate: (value: string) => value ? true : 'Display name is required',
139
257
  },
140
258
  {
@@ -143,12 +261,6 @@ async function createExtension() {
143
261
  message: 'Description:',
144
262
  validate: (value: string) => value ? true : 'Description is required',
145
263
  },
146
- {
147
- type: 'text',
148
- name: 'icon',
149
- message: 'Lucide icon name (e.g., "Mail", "CreditCard"):',
150
- initial: 'Package',
151
- },
152
264
  {
153
265
  type: 'select',
154
266
  name: 'category',
@@ -157,42 +269,81 @@ async function createExtension() {
157
269
  },
158
270
  ]);
159
271
 
160
- if (!response.name) {
272
+ if (!response.displayName) {
161
273
  consola.info('Extension creation cancelled');
162
274
  return;
163
275
  }
164
276
 
165
- const extName = `ext-${response.name}`;
166
- const extDir = join(process.cwd(), 'extensions', extName);
167
-
168
- // Check if extension already exists
169
- if (existsSync(extDir)) {
170
- consola.error(`Extension already exists: ${extDir}`);
171
- return;
277
+ // Create response with all fields
278
+ const fullResponse = {
279
+ ...response,
280
+ packageName,
281
+ };
282
+
283
+ // Check if we're in djangocfg monorepo (early detection for directory naming)
284
+ const isInDjangoCfgMonorepo = existsSync(join(process.cwd(), '../../pnpm-workspace.yaml')) &&
285
+ existsSync(join(process.cwd(), '../../packages/api/package.json'));
286
+
287
+ // Generate directory name from package name
288
+ // In djangocfg monorepo: @djangocfg/demo123a -> demo123a
289
+ // Outside monorepo: @my-org/my-extension -> my-org-my-extension
290
+ let extDirName: string;
291
+ if (isInDjangoCfgMonorepo && fullResponse.packageName.startsWith('@djangocfg/')) {
292
+ // In djangocfg monorepo, just use the part after the slash
293
+ extDirName = fullResponse.packageName.split('/')[1];
294
+ } else {
295
+ // Outside monorepo or different scope, replace @ and / with -
296
+ extDirName = fullResponse.packageName.replace('@', '').replace('/', '-');
172
297
  }
173
298
 
299
+ // Create extension in parent directory if we're in ext-base
300
+ const isInExtBase = process.cwd().endsWith('ext-base');
301
+ const extDir = isInExtBase
302
+ ? join(process.cwd(), '..', extDirName)
303
+ : join(process.cwd(), 'extensions', extDirName);
304
+
174
305
  console.log();
175
- consola.start(`Creating extension: ${chalk.cyan(`@djangocfg/${extName}`)}`);
306
+ consola.start(`Creating extension: ${chalk.cyan(fullResponse.packageName)}`);
176
307
 
177
308
  try {
178
- const providerName = response.displayName.replace(/[^a-zA-Z]/g, '');
309
+ // Convert displayName to PascalCase for provider/component naming
310
+ // "demo" -> "Demo"
311
+ // "My Extension" -> "MyExtension"
312
+ // "my-extension" -> "MyExtension"
313
+ const providerName = fullResponse.displayName
314
+ .split(/[\s-_]+/) // Split by spaces, hyphens, underscores
315
+ .map((word: string) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
316
+ .join('');
317
+
318
+ // Extract short name from package name (last part after /)
319
+ const shortName = fullResponse.packageName.split('/').pop() || fullResponse.packageName.replace('@', '');
320
+
321
+ // Generate marketplace ID and links for README
322
+ const marketplaceId = fullResponse.packageName.replace('@', '').replace('/', '-');
323
+ const marketplaceLink = `**[📦 View in Marketplace](https://hub.djangocfg.com/extensions/${marketplaceId})** • **[📖 Documentation](https://djangocfg.com)** • **[⭐ GitHub](https://github.com/markolofsen/django-cfg)**`;
179
324
 
180
325
  // Prepare replacements
181
326
  const replacements = {
182
- '__NAME__': response.name,
183
- '__DISPLAY_NAME__': response.displayName,
184
- '__DESCRIPTION__': response.description,
185
- '__ICON__': response.icon,
186
- '__CATEGORY__': response.category,
327
+ '__NAME__': shortName,
328
+ '__PACKAGE_NAME__': fullResponse.packageName,
329
+ '__DISPLAY_NAME__': fullResponse.displayName,
330
+ '__DESCRIPTION__': fullResponse.description,
331
+ '__CATEGORY__': fullResponse.category,
187
332
  '__PROVIDER_NAME__': providerName,
333
+ '__MARKETPLACE_ID__': marketplaceId,
334
+ '__MARKETPLACE_LINK__': marketplaceLink,
188
335
  };
189
336
 
190
337
  // Find template directory
191
338
  const templatePaths = [
192
- // When installed from npm
339
+ // When running from built dist/ (installed from npm)
193
340
  join(__dirname, '../templates/extension-template'),
194
- // Workspace path (for development)
341
+ // When running from src/ during development (tsx)
342
+ join(__dirname, '../../templates/extension-template'),
343
+ // Workspace path (for development from monorepo root)
195
344
  join(process.cwd(), 'extensions', 'ext-base', 'templates', 'extension-template'),
345
+ // When running from ext-base directory
346
+ join(process.cwd(), 'templates', 'extension-template'),
196
347
  ];
197
348
 
198
349
  let templateDir: string | null = null;
@@ -212,17 +363,90 @@ async function createExtension() {
212
363
  copyTemplateRecursive(templateDir, extDir, replacements);
213
364
  consola.success('Extension files created');
214
365
 
366
+ // Replace "latest" with "workspace:*" if in djangocfg monorepo
367
+ if (isInDjangoCfgMonorepo) {
368
+ consola.info('Detected djangocfg monorepo - using workspace:* for @djangocfg packages');
369
+ const pkgJsonPath = join(extDir, 'package.json');
370
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
371
+
372
+ // Replace "latest" with "workspace:*" for @djangocfg packages
373
+ const replaceDeps = (deps?: Record<string, string>) => {
374
+ if (!deps) return;
375
+ for (const [key, value] of Object.entries(deps)) {
376
+ if (key.startsWith('@djangocfg/') && value === 'latest') {
377
+ deps[key] = 'workspace:*';
378
+ }
379
+ }
380
+ };
381
+
382
+ replaceDeps(pkgJson.dependencies);
383
+ replaceDeps(pkgJson.peerDependencies);
384
+ replaceDeps(pkgJson.devDependencies);
385
+
386
+ writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n', 'utf-8');
387
+ }
388
+
215
389
  console.log();
390
+
391
+ // Auto-install dependencies
392
+ const packageManager = detectPackageManager();
393
+ if (packageManager) {
394
+ consola.start(`Installing dependencies with ${chalk.cyan(packageManager)}...`);
395
+ try {
396
+ execSync(`${packageManager} install`, {
397
+ cwd: extDir,
398
+ stdio: 'inherit',
399
+ });
400
+ consola.success('Dependencies installed successfully');
401
+ console.log();
402
+
403
+ // Install playground dependencies
404
+ const playgroundDir = join(extDir, 'playground');
405
+ if (existsSync(playgroundDir)) {
406
+ consola.start(`Installing playground dependencies with ${chalk.cyan(packageManager)}...`);
407
+ try {
408
+ execSync(`${packageManager} install`, {
409
+ cwd: playgroundDir,
410
+ stdio: 'inherit',
411
+ });
412
+ consola.success('Playground dependencies installed successfully');
413
+ console.log();
414
+ } catch (error) {
415
+ consola.warn(`Failed to install playground dependencies. Please run: ${chalk.cyan(`cd ${extDirName}/playground && ${packageManager} install`)}`);
416
+ console.log();
417
+ }
418
+ }
419
+
420
+ // Run type check
421
+ consola.start('Running type check...');
422
+ try {
423
+ execSync(`${packageManager} run check`, {
424
+ cwd: extDir,
425
+ stdio: 'inherit',
426
+ });
427
+ consola.success('Type check passed');
428
+ } catch (error) {
429
+ consola.warn('Type check failed. Please review and fix type errors.');
430
+ }
431
+ } catch (error) {
432
+ consola.warn(`Failed to install dependencies automatically. Please run: ${chalk.cyan(`cd ${extDirName} && ${packageManager} install`)}`);
433
+ }
434
+ console.log();
435
+ } else {
436
+ consola.warn('No package manager found (pnpm, yarn, or npm). Please install dependencies manually.');
437
+ console.log();
438
+ }
439
+
216
440
  consola.success(`Extension created successfully: ${chalk.cyan(extDir)}`);
217
441
  console.log();
218
442
 
219
443
  console.log(chalk.yellow.bold('Next steps:'));
220
444
  console.log();
221
- console.log(chalk.gray('1. Install dependencies:'));
222
- console.log(chalk.cyan(` cd ${extDir} && pnpm install`));
445
+ console.log(chalk.gray('1. Navigate to extension directory:'));
446
+ console.log(chalk.cyan(` cd ${extDirName}`));
223
447
  console.log();
224
448
  console.log(chalk.gray('2. Build the extension:'));
225
- console.log(chalk.cyan(` pnpm build`));
449
+ console.log(chalk.cyan(` ${packageManager || 'pnpm'} build`));
226
450
  console.log();
227
451
  console.log(chalk.gray('3. Add your features and customize:'));
228
452
  console.log(chalk.cyan(` - Edit src/config.ts to add features`));
@@ -237,6 +461,213 @@ async function createExtension() {
237
461
  }
238
462
  }
239
463
 
464
+ // Create test extension (no prompts)
465
+ async function createTestExtension() {
466
+ printBanner();
467
+
468
+ // Clean up old test extensions first
469
+ const isInExtBase = process.cwd().endsWith('ext-base');
470
+ const baseDir = isInExtBase
471
+ ? join(process.cwd(), '..')
472
+ : join(process.cwd(), 'extensions');
473
+
474
+ if (existsSync(baseDir)) {
475
+ const files = readdirSync(baseDir);
476
+ const testExtensions = files.filter(f => f.startsWith('test-'));
477
+
478
+ if (testExtensions.length > 0) {
479
+ consola.info(`Cleaning up ${testExtensions.length} old test extension(s)...`);
480
+ for (const testExt of testExtensions) {
481
+ const testPath = join(baseDir, testExt);
482
+ try {
483
+ execSync(`rm -rf "${testPath}"`, { stdio: 'ignore' });
484
+ consola.success(`Removed ${testExt}`);
485
+ } catch (error) {
486
+ consola.warn(`Failed to remove ${testExt}`);
487
+ }
488
+ }
489
+ console.log();
490
+ }
491
+ }
492
+
493
+ // Generate unique test name
494
+ const timestamp = Date.now().toString(36);
495
+ const packageName = `@djangocfg/test-${timestamp}`;
496
+ const displayName = `Test ${timestamp}`;
497
+ const description = 'Test extension created for development';
498
+ const category = 'utilities';
499
+
500
+ console.log(chalk.yellow('Quick Test Extension Creation'));
501
+ console.log();
502
+ console.log(chalk.cyan(`Package: ${packageName}`));
503
+ console.log(chalk.cyan(`Name: ${displayName}`));
504
+ console.log();
505
+
506
+ // Check if we're in djangocfg monorepo
507
+ const isInDjangoCfgMonorepo = existsSync(join(process.cwd(), '../../pnpm-workspace.yaml')) &&
508
+ existsSync(join(process.cwd(), '../../packages/api/package.json'));
509
+
510
+ // Generate directory name from package name
511
+ const extDirName = packageName.split('/')[1]; // test-xyz
512
+
513
+ // Create extension in parent directory if we're in ext-base (reuse from above)
514
+ const extDir = isInExtBase
515
+ ? join(process.cwd(), '..', extDirName)
516
+ : join(process.cwd(), 'extensions', extDirName);
517
+
518
+ // Check if already exists
519
+ if (existsSync(extDir)) {
520
+ consola.error(`Extension directory already exists: ${chalk.red(extDir)}`);
521
+ process.exit(1);
522
+ }
523
+
524
+ consola.start(`Creating test extension: ${chalk.cyan(packageName)}`);
525
+
526
+ try {
527
+ // Convert displayName to PascalCase for provider/component naming
528
+ const providerName = displayName
529
+ .split(/[\s-_]+/)
530
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
531
+ .join('');
532
+
533
+ // Extract short name from package name
534
+ const shortName = packageName.split('/').pop() || packageName.replace('@', '');
535
+
536
+ // Generate marketplace ID and links for README
537
+ const marketplaceId = packageName.replace('@', '').replace('/', '-');
538
+ const marketplaceLink = `**[📦 View in Marketplace](https://hub.djangocfg.com/extensions/${marketplaceId})** • **[📖 Documentation](https://djangocfg.com)** • **[⭐ GitHub](https://github.com/markolofsen/django-cfg)**`;
539
+
540
+ // Prepare replacements
541
+ const replacements = {
542
+ '__NAME__': shortName,
543
+ '__PACKAGE_NAME__': packageName,
544
+ '__DISPLAY_NAME__': displayName,
545
+ '__DESCRIPTION__': description,
546
+ '__CATEGORY__': category,
547
+ '__PROVIDER_NAME__': providerName,
548
+ '__MARKETPLACE_ID__': marketplaceId,
549
+ '__MARKETPLACE_LINK__': marketplaceLink,
550
+ };
551
+
552
+ // Find template directory
553
+ const templatePaths = [
554
+ join(__dirname, '../templates/extension-template'),
555
+ join(__dirname, '../../templates/extension-template'),
556
+ join(process.cwd(), 'extensions', 'ext-base', 'templates', 'extension-template'),
557
+ join(process.cwd(), 'templates', 'extension-template'),
558
+ ];
559
+
560
+ let templateDir: string | null = null;
561
+ for (const path of templatePaths) {
562
+ if (existsSync(path)) {
563
+ templateDir = path;
564
+ break;
565
+ }
566
+ }
567
+
568
+ if (!templateDir) {
569
+ throw new Error('Extension template not found');
570
+ }
571
+
572
+ // Copy template with replacements
573
+ consola.start('Copying template files...');
574
+ copyTemplateRecursive(templateDir, extDir, replacements);
575
+ consola.success('Extension files created');
576
+
577
+ // Replace "latest" with "workspace:*" if in djangocfg monorepo
578
+ if (isInDjangoCfgMonorepo) {
579
+ consola.info('Detected djangocfg monorepo - using workspace:* for @djangocfg packages');
580
+ const pkgJsonPath = join(extDir, 'package.json');
581
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
582
+
583
+ const replaceDeps = (deps?: Record<string, string>) => {
584
+ if (!deps) return;
585
+ for (const [key, value] of Object.entries(deps)) {
586
+ if (key.startsWith('@djangocfg/') && value === 'latest') {
587
+ deps[key] = 'workspace:*';
588
+ }
589
+ }
590
+ };
591
+
592
+ replaceDeps(pkgJson.dependencies);
593
+ replaceDeps(pkgJson.peerDependencies);
594
+ replaceDeps(pkgJson.devDependencies);
595
+
596
+ // Also update playground package.json
597
+ const playgroundPkgPath = join(extDir, 'playground', 'package.json');
598
+ if (existsSync(playgroundPkgPath)) {
599
+ const playgroundPkg = JSON.parse(readFileSync(playgroundPkgPath, 'utf-8'));
600
+ replaceDeps(playgroundPkg.dependencies);
601
+ replaceDeps(playgroundPkg.devDependencies);
602
+ writeFileSync(playgroundPkgPath, JSON.stringify(playgroundPkg, null, 2) + '\n', 'utf-8');
603
+ }
604
+
605
+ writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n', 'utf-8');
606
+ }
607
+
608
+ console.log();
609
+
610
+ // Auto-install dependencies
611
+ const packageManager = detectPackageManager();
612
+ if (packageManager) {
613
+ consola.start(`Installing dependencies with ${chalk.cyan(packageManager)}...`);
614
+ try {
615
+ execSync(`${packageManager} install`, {
616
+ cwd: extDir,
617
+ stdio: 'inherit',
618
+ });
619
+ consola.success('Dependencies installed successfully');
620
+ console.log();
621
+
622
+ // Install playground dependencies
623
+ const playgroundDir = join(extDir, 'playground');
624
+ if (existsSync(playgroundDir)) {
625
+ consola.start(`Installing playground dependencies with ${chalk.cyan(packageManager)}...`);
626
+ try {
627
+ execSync(`${packageManager} install`, {
628
+ cwd: playgroundDir,
629
+ stdio: 'inherit',
630
+ });
631
+ consola.success('Playground dependencies installed successfully');
632
+ console.log();
633
+ } catch (error) {
634
+ consola.warn(`Failed to install playground dependencies.`);
635
+ console.log();
636
+ }
637
+ }
638
+
639
+ // Run type check
640
+ consola.start('Running type check...');
641
+ try {
642
+ execSync(`${packageManager} run check`, {
643
+ cwd: extDir,
644
+ stdio: 'inherit',
645
+ });
646
+ consola.success('Type check passed');
647
+ } catch (error) {
648
+ consola.warn('Type check failed. Please review and fix type errors.');
649
+ }
650
+ } catch (error) {
651
+ consola.warn(`Failed to install dependencies automatically.`);
652
+ }
653
+ console.log();
654
+ }
655
+
656
+ consola.success(`Test extension created: ${chalk.cyan(extDir)}`);
657
+ console.log();
658
+
659
+ console.log(chalk.yellow.bold('Quick commands:'));
660
+ console.log();
661
+ console.log(chalk.cyan(` cd ${extDirName}`));
662
+ console.log(chalk.cyan(` pnpm build`));
663
+ console.log(chalk.cyan(` pnpm dev:playground`));
664
+ console.log();
665
+ } catch (error) {
666
+ consola.error('Failed to create test extension:', error);
667
+ process.exit(1);
668
+ }
669
+ }
670
+
240
671
  // Main CLI
241
672
  async function main() {
242
673
  const args = process.argv.slice(2);
@@ -254,6 +685,10 @@ async function main() {
254
685
  await createExtension();
255
686
  break;
256
687
 
688
+ case 'test':
689
+ await createTestExtension();
690
+ break;
691
+
257
692
  case 'help':
258
693
  case '--help':
259
694
  case '-h':