create-minions-bundle 1.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.
Files changed (79) hide show
  1. package/README.md +16 -0
  2. package/package.json +41 -0
  3. package/src/bundle-codegen.js +259 -0
  4. package/src/generator.js +107 -0
  5. package/src/github.js +110 -0
  6. package/src/index.js +144 -0
  7. package/src/manual.js +171 -0
  8. package/src/prompts.js +128 -0
  9. package/src/template.js +62 -0
  10. package/templates/apps/blog/astro.config.mjs +25 -0
  11. package/templates/apps/blog/netlify.toml +3 -0
  12. package/templates/apps/blog/package.json +34 -0
  13. package/templates/apps/blog/public/favicon.svg +4 -0
  14. package/templates/apps/blog/src/layouts/BaseLayout.astro +39 -0
  15. package/templates/apps/blog/src/pages/index.astro +18 -0
  16. package/templates/apps/blog/src/pages/posts/welcome.md +23 -0
  17. package/templates/apps/blog/src/styles/global.css.template +6 -0
  18. package/templates/apps/blog/tsconfig.json +11 -0
  19. package/templates/apps/docs/astro.config.mjs +42 -0
  20. package/templates/apps/docs/netlify.toml +4 -0
  21. package/templates/apps/docs/package.json +21 -0
  22. package/templates/apps/docs/src/content/docs/api/python.md +24 -0
  23. package/templates/apps/docs/src/content/docs/api/typescript.md +24 -0
  24. package/templates/apps/docs/src/content/docs/getting-started/installation.md +27 -0
  25. package/templates/apps/docs/src/content/docs/getting-started/introduction.md +22 -0
  26. package/templates/apps/docs/src/content/docs/getting-started/quick-start.md +28 -0
  27. package/templates/apps/docs/src/content/docs/index.mdx.template +24 -0
  28. package/templates/apps/docs/src/styles/custom.css.template +14 -0
  29. package/templates/apps/docs/tsconfig.json +3 -0
  30. package/templates/apps/web/index.html +17 -0
  31. package/templates/apps/web/netlify.toml +6 -0
  32. package/templates/apps/web/package.json +34 -0
  33. package/templates/apps/web/postcss.config.js +5 -0
  34. package/templates/apps/web/public/favicon.svg +4 -0
  35. package/templates/apps/web/src/App.css +111 -0
  36. package/templates/apps/web/src/App.tsx +46 -0
  37. package/templates/apps/web/src/index.css.template +12 -0
  38. package/templates/apps/web/src/main.tsx +10 -0
  39. package/templates/apps/web/tsconfig.json +36 -0
  40. package/templates/apps/web/tsconfig.node.json +13 -0
  41. package/templates/apps/web/vite.config.ts +21 -0
  42. package/templates/github/CODE_OF_CONDUCT.md +5 -0
  43. package/templates/github/CONTRIBUTING.md +49 -0
  44. package/templates/github/FUNDING.yml +2 -0
  45. package/templates/github/FUNDING.yml.template +1 -0
  46. package/templates/github/ISSUE_TEMPLATE/bug_report.md +20 -0
  47. package/templates/github/ISSUE_TEMPLATE/feature_request.md +18 -0
  48. package/templates/github/workflows/ci.yml +65 -0
  49. package/templates/github/workflows/publish.yml +81 -0
  50. package/templates/github/workflows/release.yml +16 -0
  51. package/templates/packages/cli/README.md +19 -0
  52. package/templates/packages/cli/package.json +38 -0
  53. package/templates/packages/cli/src/index.ts +353 -0
  54. package/templates/packages/cli/tsconfig.json +23 -0
  55. package/templates/packages/python/README.md +21 -0
  56. package/templates/packages/python/__pythonModule__/__init__.py +24 -0
  57. package/templates/packages/python/__pythonModule__/schemas.py.template +8 -0
  58. package/templates/packages/python/pyproject.toml +34 -0
  59. package/templates/packages/python/tests/test_basic.py +20 -0
  60. package/templates/packages/sdk/README.md.template +25 -0
  61. package/templates/packages/sdk/package.json.template +43 -0
  62. package/templates/packages/sdk/src/__tests__/bundle.test.ts.template +18 -0
  63. package/templates/packages/sdk/src/bundle.ts.template +6 -0
  64. package/templates/packages/sdk/src/index.ts.template +13 -0
  65. package/templates/packages/sdk/src/relations.ts.template +6 -0
  66. package/templates/packages/sdk/src/views.ts.template +6 -0
  67. package/templates/packages/sdk/tsconfig.json +26 -0
  68. package/templates/root/.release-please-manifest.json +3 -0
  69. package/templates/root/CHANGELOG.md +15 -0
  70. package/templates/root/CHANGELOG.md.template +10 -0
  71. package/templates/root/LICENSE.template +21 -0
  72. package/templates/root/README.md.template +42 -0
  73. package/templates/root/SECURITY.md +35 -0
  74. package/templates/root/SECURITY.md.template +11 -0
  75. package/templates/root/SKILLS.md.template +10 -0
  76. package/templates/root/package.json.template +28 -0
  77. package/templates/root/pnpm-workspace.yaml +4 -0
  78. package/templates/root/release-please-config.json +61 -0
  79. package/templates/root/tsconfig.json +31 -0
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # create-minions-bundle
2
+
3
+ CLI tool to scaffold Minions ecosystem **bundles**.
4
+ Bundles are curated assemblies of MinionTypes (schemas), relations, and views tailored for a specific domain.
5
+
6
+ Unlike standard toolboxes, bundles do not have CLI or Python SDKs and use a simpler single-package structure.
7
+
8
+ ## Usage
9
+
10
+ \`\`\`bash
11
+ # Run with a TOML file
12
+ node src/index.js path/to/my-bundle.toml
13
+
14
+ # Test without writing files
15
+ node src/index.js path/to/my-bundle.toml --dry-run
16
+ \`\`\`
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "create-minions-bundle",
3
+ "version": "1.0.0",
4
+ "description": "Scaffold curated workflow bundles of MinionTypes, relations, and views",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Mehdi Nabhani <mehdi@the-mehdi.com> (https://the-mehdi.com)",
8
+ "bin": {
9
+ "create-minions-bundle": "./src/index.js"
10
+ },
11
+ "main": "./src/index.js",
12
+ "files": [
13
+ "src",
14
+ "templates",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "minions",
19
+ "scaffold",
20
+ "cli",
21
+ "create-app",
22
+ "bundle",
23
+ "generator"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/mxn2020/create-minions-bundle"
28
+ },
29
+ "dependencies": {
30
+ "chalk": "^5.4.1",
31
+ "commander": "^14.0.3",
32
+ "dotenv": "^17.3.1",
33
+ "fs-extra": "^11.3.0",
34
+ "inquirer": "^13.2.5",
35
+ "ora": "^8.2.0",
36
+ "smol-toml": "^1.6.0"
37
+ },
38
+ "engines": {
39
+ "node": ">=20"
40
+ }
41
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Contains the logic to generate TypeScript and Markdown code corresponding
3
+ * to the types, relations, views, and skills defined in the bundle config.
4
+ */
5
+
6
+ export function generateBundleCode(config) {
7
+ const { finalCode, importsMap } = generateTypesCode(config.types, config.projectSlug);
8
+ let bundleTypesCode = finalCode;
9
+ let relationsCode = generateRelationsCode(config.relations);
10
+ let viewsCode = generateViewsCode(config.views);
11
+ let skillsCode = generateSkillsCode(config.skills, config.types);
12
+ let pythonSchemas = generatePythonSchemas(config.types);
13
+ let bundleTestsCode = generateTypeScriptTests(config.types);
14
+
15
+ // Collect dependencies
16
+ const dependenciesObj = {};
17
+ for (const source of importsMap.keys()) {
18
+ dependenciesObj[source] = "latest"; // Use latest for external dependencies
19
+ }
20
+ const dependenciesJson = Object.keys(dependenciesObj).length > 0
21
+ ? ",\n " + JSON.stringify(dependenciesObj, null, 4).slice(1, -1).trim()
22
+ : "";
23
+
24
+ return {
25
+ bundleTypesCode,
26
+ relationsCode,
27
+ viewsCode,
28
+ skillsCode,
29
+ dependenciesJson,
30
+ pythonSchemas,
31
+ bundleTestsCode,
32
+ };
33
+ }
34
+
35
+ function generateTypesCode(typesDef, projectSlug) {
36
+ let importsMap = new Map(); // e.g. '@minions-tasks/sdk' => ['taskType', 'taskListType']
37
+
38
+ if (!typesDef || Object.keys(typesDef).length === 0) {
39
+ return { finalCode: `export const bundleTypes: MinionType[] = [];\n`, importsMap };
40
+ }
41
+ let inlineTypes = [];
42
+ let exportedTypeNames = [];
43
+
44
+ for (const [slug, def] of Object.entries(typesDef)) {
45
+ if (def.source && def.import) {
46
+ // It's a referenced type
47
+ if (!importsMap.has(def.source)) {
48
+ importsMap.set(def.source, new Set());
49
+ }
50
+ importsMap.get(def.source).add(def.import);
51
+ exportedTypeNames.push({ name: def.import, isInline: false });
52
+ } else {
53
+ // It's an inline newly defined type
54
+ const name = slug.charAt(0).toUpperCase() + slug.slice(1).replace(/[-_]/g, ' ');
55
+ const tsVarName = `${slug.replace(/[-_]/g, '')}Type`;
56
+ const description = def.description || `Bundle type for ${name}`;
57
+ const icon = def.icon || '📦';
58
+
59
+ let code = `export const ${tsVarName}: MinionType = {\n`;
60
+ code += ` id: 'bundle-${projectSlug}-${slug}',\n`;
61
+ code += ` name: '${name}',\n`;
62
+ code += ` slug: '${slug}',\n`;
63
+ code += ` description: '${description.replace(/'/g, "\\'")}',\n`;
64
+ code += ` icon: '${icon}',\n`;
65
+
66
+ if (def.extends) {
67
+ // Not formally supported by MinionType but good for comments/future
68
+ code += ` // extends: '${def.extends}',\n`;
69
+ }
70
+
71
+ code += ` schema: [\n`;
72
+ for (const [fieldName, fieldType] of Object.entries(def.fields || {})) {
73
+ let finalType = fieldType === 'boolean' ? 'boolean' :
74
+ fieldType === 'number' ? 'number' :
75
+ fieldType === 'select' ? 'select' :
76
+ fieldType === 'date' || fieldType === 'datetime' ? 'date' : 'string';
77
+
78
+ code += ` { name: '${fieldName}', type: '${finalType}', label: '${fieldName}' },\n`;
79
+ }
80
+ code += ` ],\n};\n`;
81
+
82
+ inlineTypes.push(code);
83
+ exportedTypeNames.push({ name: tsVarName, isInline: true });
84
+ }
85
+ }
86
+
87
+ let finalCode = `import type { MinionType } from 'minions-sdk';\n\n`;
88
+
89
+ // Add imports
90
+ for (const [source, namedImports] of importsMap.entries()) {
91
+ finalCode += `import { ${Array.from(namedImports).join(', ')} } from '${source}';\n`;
92
+ }
93
+
94
+ finalCode += `\n// --- Inline Bundle Types ---\n\n`;
95
+ finalCode += inlineTypes.join('\n');
96
+
97
+ finalCode += `\n// --- Bundle Export ---\n\n`;
98
+ finalCode += `export const bundleTypes: MinionType[] = [\n`;
99
+ for (const t of exportedTypeNames) {
100
+ finalCode += ` ${t.name},\n`;
101
+ }
102
+ finalCode += `];\n`;
103
+
104
+ return { finalCode, importsMap };
105
+ }
106
+
107
+ function generateRelationsCode(relationsDef) {
108
+ if (!relationsDef || !relationsDef.items || relationsDef.items.length === 0) {
109
+ return `export const bundleRelations = [];\n`;
110
+ }
111
+
112
+ let code = `export const bundleRelations = [\n`;
113
+ for (const rel of relationsDef.items) {
114
+ code += ` { from: '${rel.from}', relation: '${rel.relation}', to: '${rel.to}' },\n`;
115
+ }
116
+ code += `];\n`;
117
+ return code;
118
+ }
119
+
120
+ function generateViewsCode(viewsDef) {
121
+ if (!viewsDef || Object.keys(viewsDef).length === 0) {
122
+ return `export const bundleViews = {};\n`;
123
+ }
124
+
125
+ let code = `export const bundleViews = {\n`;
126
+ for (const [viewName, def] of Object.entries(viewsDef)) {
127
+ code += ` ${viewName}: {\n`;
128
+ if (def.description) code += ` description: '${def.description.replace(/'/g, "\\'")}',\n`;
129
+ if (def.type) code += ` type: '${def.type}',\n`;
130
+ if (def.filter) {
131
+ code += ` filter: ${JSON.stringify(def.filter, null, 6).replace(/\\n/g, '\\n').trim()},\n`;
132
+ }
133
+ if (def.aggregate) {
134
+ code += ` aggregate: ${JSON.stringify(def.aggregate, null, 6).replace(/\\n/g, '\\n').trim()},\n`;
135
+ }
136
+ code += ` },\n`;
137
+ }
138
+ code += `};\n`;
139
+ return code;
140
+ }
141
+
142
+ function generateSkillsCode(skillsDef, typesDef) {
143
+ if (!skillsDef) {
144
+ return `No skills defined.`;
145
+ }
146
+
147
+ let code = '';
148
+ if (skillsDef.context) {
149
+ code += `## Your Context\n\n${skillsDef.context}\n\n`;
150
+ }
151
+
152
+ const typeNames = Object.keys(typesDef || {});
153
+ if (typeNames.length > 0) {
154
+ code += `Your MinionTypes are: ${typeNames.join(', ')}.\n\n`;
155
+ }
156
+
157
+ if (skillsDef.rules && skillsDef.rules.length > 0) {
158
+ code += `## Hard Rules\n\n`;
159
+ for (const rule of skillsDef.rules) {
160
+ code += `- ${rule}\n`;
161
+ }
162
+ code += `\n`;
163
+ }
164
+
165
+ if (skillsDef.items && skillsDef.items.length > 0) {
166
+ for (const skill of skillsDef.items) {
167
+ code += `## Skill: ${skill.name}\n`;
168
+ for (let i = 0; i < (skill.steps || []).length; i++) {
169
+ code += `${i + 1}. ${skill.steps[i]}\n`;
170
+ }
171
+ code += `\n`;
172
+ }
173
+ }
174
+
175
+ return code.trim();
176
+ }
177
+
178
+ function generatePythonSchemas(typesDef) {
179
+ if (!typesDef || Object.keys(typesDef).length === 0) {
180
+ return '# No custom types defined.\nBUNDLE_TYPES = []\n';
181
+ }
182
+
183
+ const typeFieldMap = {
184
+ string: 'str',
185
+ number: 'float',
186
+ boolean: 'bool',
187
+ date: 'str',
188
+ datetime: 'str',
189
+ select: 'str',
190
+ };
191
+
192
+ let code = '';
193
+ const classNames = [];
194
+
195
+ for (const [slug, def] of Object.entries(typesDef)) {
196
+ if (def.source && def.import) continue; // Skip imported types
197
+
198
+ const className = slug.charAt(0).toUpperCase() + slug.slice(1).replace(/[-_](\w)/g, (_, c) => c.toUpperCase());
199
+ const description = def.description || `Bundle type for ${className}`;
200
+ classNames.push(className);
201
+
202
+ code += `\nclass ${className}(MinionType):\n`;
203
+ code += ` """${description}"""\n`;
204
+ code += ` slug = "${slug}"\n`;
205
+ code += ` icon = "${def.icon || '📦'}"\n\n`;
206
+
207
+ if (def.fields && Object.keys(def.fields).length > 0) {
208
+ code += ` fields = [\n`;
209
+ for (const [fieldName, fieldType] of Object.entries(def.fields)) {
210
+ const pyType = typeFieldMap[fieldType] || 'str';
211
+ code += ` FieldDefinition(name="${fieldName}", type="${pyType}", label="${fieldName}"),\n`;
212
+ }
213
+ code += ` ]\n`;
214
+ } else {
215
+ code += ` fields = []\n`;
216
+ }
217
+
218
+ code += `\n`;
219
+ }
220
+
221
+ code += `\nBUNDLE_TYPES = [${classNames.join(', ')}]\n`;
222
+
223
+ return code.trim();
224
+ }
225
+
226
+ function generateTypeScriptTests(typesDef) {
227
+ if (!typesDef || Object.keys(typesDef).length === 0) {
228
+ return ' // No custom bundle types defined.\n';
229
+ }
230
+
231
+ let testCode = '';
232
+
233
+ for (const [slug, def] of Object.entries(typesDef)) {
234
+ if (def.source && def.import) continue; // Skip imported types
235
+
236
+ const name = slug.charAt(0).toUpperCase() + slug.slice(1).replace(/[-_]/g, ' ');
237
+ const fields = def.fields || {};
238
+ const fieldNames = Object.keys(fields);
239
+ const tsVarName = `${slug.replace(/[-_]/g, '')}Type`;
240
+
241
+ testCode += ` it('should define the ${tsVarName} schema correctly', () => {\n`;
242
+ testCode += ` const type = bundleTypes.find(t => t.slug === '${slug}');\n`;
243
+ testCode += ` expect(type).toBeDefined();\n`;
244
+ testCode += ` expect(type?.name).toBe('${name}');\n`;
245
+ testCode += ` expect(type?.schema.length).toBe(${fieldNames.length});\n`;
246
+
247
+ if (fieldNames.length > 0) {
248
+ testCode += `\n`;
249
+ testCode += ` const fieldNames = type?.schema.map(f => f.name);\n`;
250
+ for (const fieldName of fieldNames) {
251
+ testCode += ` expect(fieldNames).toContain('${fieldName}');\n`;
252
+ }
253
+ }
254
+
255
+ testCode += ` });\n\n`;
256
+ }
257
+
258
+ return testCode.trimEnd();
259
+ }
@@ -0,0 +1,107 @@
1
+ import { readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, existsSync } from 'fs';
2
+ import { join, dirname, relative } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { render, buildVariables } from './template.js';
5
+ import { generateManual } from './manual.js';
6
+ import chalk from 'chalk';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const TEMPLATES_DIR = join(__dirname, '..', 'templates');
11
+
12
+ /**
13
+ * Generate a project from templates.
14
+ * @param {object} config - Project configuration
15
+ * @returns {{ outputDir: string, filesCreated: number, dirsCreated: number }}
16
+ */
17
+ export async function generateProject(config) {
18
+ const outputDir = join(process.cwd(), config.projectName);
19
+ const variables = buildVariables(config);
20
+
21
+ if (config.dryRun) {
22
+ console.log(chalk.yellow('\n ── DRY RUN ──\n'));
23
+ const files = collectTemplateFiles(TEMPLATES_DIR);
24
+ for (const file of files) {
25
+ const relPath = relative(TEMPLATES_DIR, file);
26
+ const outputPath = resolveOutputPath(relPath, variables);
27
+ console.log(` ${chalk.dim('create')} ${outputPath}`);
28
+ }
29
+ console.log(`\n ${chalk.dim(`${files.length} files would be created`)}\n`);
30
+ return { outputDir, filesCreated: 0, dirsCreated: 0 };
31
+ }
32
+
33
+ if (existsSync(outputDir)) {
34
+ throw new Error(`Directory "${config.projectName}" already exists`);
35
+ }
36
+
37
+ let filesCreated = 0;
38
+ let dirsCreated = 0;
39
+
40
+ // Walk templates and render each file
41
+ const files = collectTemplateFiles(TEMPLATES_DIR);
42
+
43
+ for (const templateFile of files) {
44
+ const relPath = relative(TEMPLATES_DIR, templateFile);
45
+ const outputPath = resolveOutputPath(relPath, variables);
46
+ const fullOutputPath = join(outputDir, outputPath);
47
+
48
+ // Create parent directories
49
+ const parentDir = dirname(fullOutputPath);
50
+ if (!existsSync(parentDir)) {
51
+ mkdirSync(parentDir, { recursive: true });
52
+ dirsCreated++;
53
+ }
54
+
55
+ // Read template and render
56
+ const content = readFileSync(templateFile, 'utf-8');
57
+ const rendered = render(content, variables);
58
+
59
+ writeFileSync(fullOutputPath, rendered, 'utf-8');
60
+ filesCreated++;
61
+ }
62
+
63
+ // Generate MANUAL.md
64
+ const manualContent = generateManual(config);
65
+ const manualPath = join(outputDir, 'MANUAL.md');
66
+ writeFileSync(manualPath, manualContent, 'utf-8');
67
+ filesCreated++;
68
+
69
+ return { outputDir, filesCreated, dirsCreated };
70
+ }
71
+
72
+ /**
73
+ * Recursively collect all files in a directory.
74
+ */
75
+ function collectTemplateFiles(dir) {
76
+ const results = [];
77
+ const entries = readdirSync(dir);
78
+
79
+ for (const entry of entries) {
80
+ const fullPath = join(dir, entry);
81
+ const stat = statSync(fullPath);
82
+ if (stat.isDirectory()) {
83
+ results.push(...collectTemplateFiles(fullPath));
84
+ } else {
85
+ results.push(fullPath);
86
+ }
87
+ }
88
+
89
+ return results;
90
+ }
91
+
92
+ function resolveOutputPath(relPath, variables) {
93
+ // Replace __pythonModule__ with actual python module name
94
+ let outputPath = relPath.replace(/__pythonModule__/g, variables.pythonModule);
95
+
96
+ // Map template directory prefixes to output paths
97
+ if (outputPath.startsWith('root/')) {
98
+ outputPath = outputPath.replace(/^root\//, '');
99
+ } else if (outputPath.startsWith('github/')) {
100
+ outputPath = outputPath.replace(/^github\//, '.github/');
101
+ }
102
+
103
+ // Remove .template extension if present
104
+ outputPath = outputPath.replace(/\.template$/, '');
105
+
106
+ return outputPath;
107
+ }
package/src/github.js ADDED
@@ -0,0 +1,110 @@
1
+ import { execSync } from 'child_process';
2
+ import chalk from 'chalk';
3
+
4
+ /**
5
+ * Setup GitHub repository using the `gh` CLI.
6
+ * @param {object} config - Project configuration
7
+ */
8
+ export async function setupGitHub(config) {
9
+ const repoName = `${config.githubOrg}/${config.projectName}`;
10
+ const projectDir = config.projectName;
11
+
12
+ // Check if gh CLI is available
13
+ try {
14
+ execSync('gh --version', { stdio: 'ignore' });
15
+ } catch {
16
+ throw new Error(
17
+ 'GitHub CLI (gh) not found. Install it from https://cli.github.com/ or run without --github'
18
+ );
19
+ }
20
+
21
+ // Check if authenticated
22
+ try {
23
+ execSync('gh auth status', { stdio: 'ignore' });
24
+ } catch {
25
+ throw new Error(
26
+ 'Not authenticated with GitHub CLI. Run: gh auth login'
27
+ );
28
+ }
29
+
30
+ console.log(chalk.dim(`\n Creating repository ${repoName}...`));
31
+
32
+ // Create repo
33
+ execSync(
34
+ `gh repo create ${repoName} --public --description "${config.projectDescription}" --source ${projectDir} --push`,
35
+ { stdio: 'inherit', cwd: process.cwd() }
36
+ );
37
+
38
+ // Initialize git and push
39
+ console.log(chalk.dim(' Initializing git...'));
40
+ execSync('git init', { cwd: projectDir, stdio: 'ignore' });
41
+ execSync('git add .', { cwd: projectDir, stdio: 'ignore' });
42
+ execSync('git commit -m "chore: initial bundle scaffold from create-minions-bundle"', {
43
+ cwd: projectDir,
44
+ stdio: 'ignore',
45
+ });
46
+ execSync('git branch -M main', { cwd: projectDir, stdio: 'ignore' });
47
+
48
+ // Push to main
49
+ console.log(chalk.dim(' Pushing to main...'));
50
+ execSync(`git remote add origin https://github.com/${repoName}.git`, {
51
+ cwd: projectDir,
52
+ stdio: 'ignore',
53
+ });
54
+ execSync('git push -u origin main', { cwd: projectDir, stdio: 'inherit' });
55
+
56
+ // Create dev branch
57
+ console.log(chalk.dim(' Creating dev branch...'));
58
+ execSync('git checkout -b dev', { cwd: projectDir, stdio: 'ignore' });
59
+ execSync('git push -u origin dev', { cwd: projectDir, stdio: 'inherit' });
60
+
61
+ // Switch back to dev as the working branch
62
+ execSync('git checkout dev', { cwd: projectDir, stdio: 'ignore' });
63
+
64
+ // Set repo topics
65
+ const topics = config.keywords.slice(0, 10).join(',');
66
+ try {
67
+ execSync(`gh repo edit ${repoName} --add-topic "${topics}"`, { stdio: 'ignore' });
68
+ } catch {
69
+ // topics are best-effort
70
+ }
71
+
72
+ // Enable issues and discussions
73
+ try {
74
+ execSync(`gh repo edit ${repoName} --enable-issues --enable-wiki=false`, { stdio: 'ignore' });
75
+ } catch {
76
+ // best-effort
77
+ }
78
+
79
+ // Set Secrets from Environment
80
+ if (process.env.NPM_TOKEN) {
81
+ console.log(chalk.dim(' Setting NPM_TOKEN secret...'));
82
+ try {
83
+ execSync(`gh secret set NPM_TOKEN --body "${process.env.NPM_TOKEN}"`, { cwd: projectDir, stdio: 'ignore' });
84
+ } catch (e) {
85
+ console.log(chalk.yellow(' Failed to set NPM_TOKEN secret'));
86
+ }
87
+ }
88
+
89
+ if (process.env.PYPI_TOKEN) {
90
+ console.log(chalk.dim(' Setting PYPI_TOKEN secret...'));
91
+ try {
92
+ execSync(`gh secret set PYPI_TOKEN --body "${process.env.PYPI_TOKEN}"`, { cwd: projectDir, stdio: 'ignore' });
93
+ } catch (e) {
94
+ console.log(chalk.yellow(' Failed to set PYPI_TOKEN secret'));
95
+ }
96
+ }
97
+
98
+ console.log(chalk.green(`\n ✅ Repository created: https://github.com/${repoName}`));
99
+ console.log(chalk.dim(` Branches: main, dev (currently on dev)`));
100
+ console.log('');
101
+ console.log(chalk.yellow(' ⚠️ Manual steps required:'));
102
+ if (!process.env.NPM_TOKEN) {
103
+ console.log(chalk.dim(' • Add NPM_TOKEN secret to repository'));
104
+ }
105
+ if (!process.env.PYPI_TOKEN) {
106
+ console.log(chalk.dim(' • Add PYPI_TOKEN secret to repository'));
107
+ }
108
+ console.log(chalk.dim(' • Enable branch protection on main'));
109
+ console.log(chalk.dim(' • See MANUAL.md for full details'));
110
+ }
package/src/index.js ADDED
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { runInteractivePrompts } from './prompts.js';
5
+ import { generateProject } from './generator.js';
6
+ import { setupGitHub } from './github.js';
7
+ import chalk from 'chalk';
8
+ import ora from 'ora';
9
+ import { readFileSync } from 'fs';
10
+ import { fileURLToPath } from 'url';
11
+ import { dirname, join, extname } from 'path';
12
+ import * as dotenv from 'dotenv';
13
+ import { parse } from 'smol-toml';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+
18
+ // Load .env from the CLI's installation directory
19
+ dotenv.config({ path: join(__dirname, '..', '.env') });
20
+
21
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
22
+
23
+ const program = new Command();
24
+
25
+ program
26
+ .name('create-minions-bundle')
27
+ .description('Scaffold a new Minions ecosystem bundle project')
28
+ .version(pkg.version)
29
+ .argument('[config-file]', 'Path to the TOML configuration file (e.g., crm-bundle.toml)')
30
+ .option('-o, --org <org>', 'GitHub org/user', 'mxn2020')
31
+ .option('-a, --author <name>', 'Author name', 'Mehdi Nabhani')
32
+ .option('-e, --email <email>', 'Author email', 'mehdi@the-mehdi.com')
33
+ .option('--github', 'Setup GitHub repository (requires gh CLI)')
34
+ .option('--dry-run', 'Print what would be created without writing files')
35
+ .option('--license <license>', 'License type', 'MIT')
36
+ .action(async (configFile, options) => {
37
+ console.log('');
38
+ console.log(chalk.bold.hex('#8B5CF6')(' 🚀 create-minions-bundle'));
39
+ console.log(chalk.dim(' Scaffold a new Minions organized bundle\n'));
40
+
41
+ let config;
42
+ let tomlData = null;
43
+
44
+ // Ensure we have a config file in non-interactive / mixed mode
45
+ if (configFile && extname(configFile) === '.toml') {
46
+ try {
47
+ const tomlContent = readFileSync(configFile, 'utf-8');
48
+ tomlData = parse(tomlContent);
49
+ console.log(chalk.dim(` Loaded configuration from ${configFile}`));
50
+
51
+ // Merge options with TOML
52
+ if (tomlData.org) options.org = tomlData.org;
53
+ if (tomlData.colors) {
54
+ if (tomlData.colors.accent) options.accent = tomlData.colors.accent;
55
+ if (tomlData.colors['accent-hover']) options.accentHover = tomlData.colors['accent-hover'];
56
+ }
57
+ } catch (err) {
58
+ console.error(chalk.red(` Failed to parse TOML file: ${err.message}`));
59
+ process.exit(1);
60
+ }
61
+ }
62
+
63
+ if (tomlData && tomlData.name && tomlData.description) {
64
+ // Non-interactive mode mostly — all required fields provided in TOML
65
+ config = buildConfigFromToml(tomlData, options);
66
+ } else {
67
+ // Interactive mode — prompt for missing values
68
+ config = await runInteractivePrompts(tomlData, options);
69
+ }
70
+
71
+ // Attach types, relations, views, and skills if parsed from TOML
72
+ if (tomlData) {
73
+ config.types = tomlData.types || {};
74
+ config.relations = tomlData.relations || {};
75
+ config.views = tomlData.views || {};
76
+ config.skills = tomlData.skills || {};
77
+ }
78
+
79
+ // Generate the project
80
+ const spinner = ora({ text: 'Generating bundle...', color: 'magenta' }).start();
81
+
82
+ try {
83
+ const result = await generateProject(config);
84
+ spinner.succeed(chalk.green(`Bundle generated at ${chalk.bold(result.outputDir)}`));
85
+
86
+ console.log('');
87
+ console.log(chalk.dim(' ─────────────────────────────────────'));
88
+ console.log(` 📁 ${chalk.bold(result.filesCreated)} files created`);
89
+ console.log(` 📂 ${chalk.bold(result.dirsCreated)} directories created`);
90
+ console.log('');
91
+
92
+ // Optional GitHub setup
93
+ if (config.setupGitHub) {
94
+ const ghSpinner = ora({ text: 'Setting up GitHub repository...', color: 'cyan' }).start();
95
+ try {
96
+ await setupGitHub(config);
97
+ ghSpinner.succeed(chalk.green('GitHub repository configured'));
98
+ } catch (err) {
99
+ ghSpinner.fail(chalk.red(`GitHub setup failed: ${err.message}`));
100
+ console.log(chalk.yellow(' You can set up the repo manually. See MANUAL.md'));
101
+ }
102
+ }
103
+
104
+ // Print next steps
105
+ console.log(chalk.bold('\n 📋 Next steps:\n'));
106
+ console.log(` ${chalk.cyan('cd')} ${config.projectName}`);
107
+ console.log(` ${chalk.cyan('pnpm install')}`);
108
+ console.log(` ${chalk.cyan('pnpm run build')}`);
109
+ console.log(` ${chalk.cyan('pnpm run test')}`);
110
+ console.log('');
111
+ console.log(chalk.yellow(` 📖 Read ${chalk.bold('MANUAL.md')} for manual setup steps`));
112
+
113
+ } catch (err) {
114
+ spinner.fail(chalk.red(`Failed: ${err.message}`));
115
+ process.exit(1);
116
+ }
117
+ });
118
+
119
+ function buildConfigFromToml(tomlData, options) {
120
+ const projectName = tomlData.name;
121
+ const slug = projectName.replace(/^minions-bundles-/, '');
122
+ const capitalizedSlug = slug.charAt(0).toUpperCase() + slug.slice(1);
123
+
124
+ return {
125
+ projectName,
126
+ projectSlug: slug,
127
+ projectCapitalized: `Minions Bundle: ${capitalizedSlug}`,
128
+ projectDescription: tomlData.description || `A curated Minions ecosystem bundle for ${slug}`,
129
+ authorName: options.author,
130
+ authorEmail: options.email,
131
+ authorUrl: 'https://the-mehdi.com',
132
+ githubOrg: options.org,
133
+ githubRepo: `${options.org}/${projectName}`,
134
+ license: options.license,
135
+ keywords: [slug, 'bundle', 'ai', 'minions'],
136
+ year: new Date().getFullYear().toString(),
137
+ accentColor: options.accent || '#8B5CF6',
138
+ accentHoverColor: options.accentHover || '#7C3AED',
139
+ setupGitHub: options.github || false,
140
+ dryRun: options.dryRun || false,
141
+ };
142
+ }
143
+
144
+ program.parse();