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.
- package/README.md +16 -0
- package/package.json +41 -0
- package/src/bundle-codegen.js +259 -0
- package/src/generator.js +107 -0
- package/src/github.js +110 -0
- package/src/index.js +144 -0
- package/src/manual.js +171 -0
- package/src/prompts.js +128 -0
- package/src/template.js +62 -0
- package/templates/apps/blog/astro.config.mjs +25 -0
- package/templates/apps/blog/netlify.toml +3 -0
- package/templates/apps/blog/package.json +34 -0
- package/templates/apps/blog/public/favicon.svg +4 -0
- package/templates/apps/blog/src/layouts/BaseLayout.astro +39 -0
- package/templates/apps/blog/src/pages/index.astro +18 -0
- package/templates/apps/blog/src/pages/posts/welcome.md +23 -0
- package/templates/apps/blog/src/styles/global.css.template +6 -0
- package/templates/apps/blog/tsconfig.json +11 -0
- package/templates/apps/docs/astro.config.mjs +42 -0
- package/templates/apps/docs/netlify.toml +4 -0
- package/templates/apps/docs/package.json +21 -0
- package/templates/apps/docs/src/content/docs/api/python.md +24 -0
- package/templates/apps/docs/src/content/docs/api/typescript.md +24 -0
- package/templates/apps/docs/src/content/docs/getting-started/installation.md +27 -0
- package/templates/apps/docs/src/content/docs/getting-started/introduction.md +22 -0
- package/templates/apps/docs/src/content/docs/getting-started/quick-start.md +28 -0
- package/templates/apps/docs/src/content/docs/index.mdx.template +24 -0
- package/templates/apps/docs/src/styles/custom.css.template +14 -0
- package/templates/apps/docs/tsconfig.json +3 -0
- package/templates/apps/web/index.html +17 -0
- package/templates/apps/web/netlify.toml +6 -0
- package/templates/apps/web/package.json +34 -0
- package/templates/apps/web/postcss.config.js +5 -0
- package/templates/apps/web/public/favicon.svg +4 -0
- package/templates/apps/web/src/App.css +111 -0
- package/templates/apps/web/src/App.tsx +46 -0
- package/templates/apps/web/src/index.css.template +12 -0
- package/templates/apps/web/src/main.tsx +10 -0
- package/templates/apps/web/tsconfig.json +36 -0
- package/templates/apps/web/tsconfig.node.json +13 -0
- package/templates/apps/web/vite.config.ts +21 -0
- package/templates/github/CODE_OF_CONDUCT.md +5 -0
- package/templates/github/CONTRIBUTING.md +49 -0
- package/templates/github/FUNDING.yml +2 -0
- package/templates/github/FUNDING.yml.template +1 -0
- package/templates/github/ISSUE_TEMPLATE/bug_report.md +20 -0
- package/templates/github/ISSUE_TEMPLATE/feature_request.md +18 -0
- package/templates/github/workflows/ci.yml +65 -0
- package/templates/github/workflows/publish.yml +81 -0
- package/templates/github/workflows/release.yml +16 -0
- package/templates/packages/cli/README.md +19 -0
- package/templates/packages/cli/package.json +38 -0
- package/templates/packages/cli/src/index.ts +353 -0
- package/templates/packages/cli/tsconfig.json +23 -0
- package/templates/packages/python/README.md +21 -0
- package/templates/packages/python/__pythonModule__/__init__.py +24 -0
- package/templates/packages/python/__pythonModule__/schemas.py.template +8 -0
- package/templates/packages/python/pyproject.toml +34 -0
- package/templates/packages/python/tests/test_basic.py +20 -0
- package/templates/packages/sdk/README.md.template +25 -0
- package/templates/packages/sdk/package.json.template +43 -0
- package/templates/packages/sdk/src/__tests__/bundle.test.ts.template +18 -0
- package/templates/packages/sdk/src/bundle.ts.template +6 -0
- package/templates/packages/sdk/src/index.ts.template +13 -0
- package/templates/packages/sdk/src/relations.ts.template +6 -0
- package/templates/packages/sdk/src/views.ts.template +6 -0
- package/templates/packages/sdk/tsconfig.json +26 -0
- package/templates/root/.release-please-manifest.json +3 -0
- package/templates/root/CHANGELOG.md +15 -0
- package/templates/root/CHANGELOG.md.template +10 -0
- package/templates/root/LICENSE.template +21 -0
- package/templates/root/README.md.template +42 -0
- package/templates/root/SECURITY.md +35 -0
- package/templates/root/SECURITY.md.template +11 -0
- package/templates/root/SKILLS.md.template +10 -0
- package/templates/root/package.json.template +28 -0
- package/templates/root/pnpm-workspace.yaml +4 -0
- package/templates/root/release-please-config.json +61 -0
- 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
|
+
}
|
package/src/generator.js
ADDED
|
@@ -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();
|