@jml6m/skeletor 0.1.0-alpha.1 → 0.1.0-alpha.5

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 (81) hide show
  1. package/README.md +139 -7
  2. package/package.json +35 -4
  3. package/src/index.js +345 -5
  4. package/templates/csharp/AGENTS.md +10 -0
  5. package/templates/csharp/Program.cs +18 -0
  6. package/templates/csharp/ProgramTests.cs +12 -0
  7. package/templates/csharp/Project.csproj.tmpl +17 -0
  8. package/templates/csharp/README.md +12 -0
  9. package/templates/csharp/template.json +10 -0
  10. package/templates/go/AGENTS.md +9 -0
  11. package/templates/go/README.md +13 -0
  12. package/templates/go/go.mod.tmpl +5 -0
  13. package/templates/go/main.go +20 -0
  14. package/templates/go/main_test.go +11 -0
  15. package/templates/go/template.json +11 -0
  16. package/templates/java/AGENTS.md +9 -0
  17. package/templates/java/README.md +12 -0
  18. package/templates/java/pom.xml.tmpl +38 -0
  19. package/templates/java/src/main/java/app/App.java +16 -0
  20. package/templates/java/src/test/java/app/AppTest.java +12 -0
  21. package/templates/java/template.json +10 -0
  22. package/templates/javascript/.editorconfig +16 -0
  23. package/templates/javascript/.eslintrc.json +82 -0
  24. package/templates/javascript/.github/workflows/README.md +7 -0
  25. package/templates/javascript/.github/workflows/ci.yml +39 -0
  26. package/templates/javascript/.husky/pre-commit +4 -0
  27. package/templates/javascript/.jscpd.json +13 -0
  28. package/templates/javascript/.prettierrc.json +24 -0
  29. package/templates/javascript/AGENTS.md +110 -0
  30. package/templates/javascript/README.md +38 -0
  31. package/templates/javascript/docs/CODING_STANDARDS.md +14 -0
  32. package/templates/javascript/jest.config.js +7 -0
  33. package/templates/javascript/jsconfig.json +19 -0
  34. package/templates/javascript/knip.json +7 -0
  35. package/templates/javascript/package.json.tmpl +60 -0
  36. package/templates/javascript/release.js +145 -0
  37. package/templates/javascript/scripts/generate-context.js +73 -0
  38. package/templates/javascript/scripts/reinstall.js +25 -0
  39. package/templates/javascript/src/config/env.config.js +25 -0
  40. package/templates/javascript/src/config/index.js +7 -0
  41. package/templates/javascript/src/constants/index.js +11 -0
  42. package/templates/javascript/src/index.js +19 -0
  43. package/templates/javascript/src/utils/logger.js +42 -0
  44. package/templates/javascript/template.json +13 -0
  45. package/templates/javascript/tests/example.test.js +10 -0
  46. package/templates/python/AGENTS.md +10 -0
  47. package/templates/python/README.md +16 -0
  48. package/templates/python/pyproject.toml.tmpl +45 -0
  49. package/templates/python/src/app/__init__.py +7 -0
  50. package/templates/python/src/app/main.py +8 -0
  51. package/templates/python/template.json +13 -0
  52. package/templates/python/tests/test_main.py +9 -0
  53. package/templates/rust/AGENTS.md +9 -0
  54. package/templates/rust/Cargo.toml.tmpl +10 -0
  55. package/templates/rust/README.md +12 -0
  56. package/templates/rust/src/main.rs +22 -0
  57. package/templates/rust/template.json +10 -0
  58. package/templates/typescript/.editorconfig +16 -0
  59. package/templates/typescript/.github/workflows/README.md +7 -0
  60. package/templates/typescript/.github/workflows/ci.yml +39 -0
  61. package/templates/typescript/.husky/pre-commit +4 -0
  62. package/templates/typescript/.jscpd.json +13 -0
  63. package/templates/typescript/.prettierrc.json +24 -0
  64. package/templates/typescript/AGENTS.md +110 -0
  65. package/templates/typescript/README.md +38 -0
  66. package/templates/typescript/docs/CODING_STANDARDS.md +14 -0
  67. package/templates/typescript/eslint.config.js +55 -0
  68. package/templates/typescript/jest.config.js +7 -0
  69. package/templates/typescript/knip.json +7 -0
  70. package/templates/typescript/package.json.tmpl +57 -0
  71. package/templates/typescript/release.js +145 -0
  72. package/templates/typescript/scripts/generate-context.js +73 -0
  73. package/templates/typescript/scripts/reinstall.js +25 -0
  74. package/templates/typescript/src/config/env.config.ts +25 -0
  75. package/templates/typescript/src/config/index.ts +7 -0
  76. package/templates/typescript/src/constants/index.ts +11 -0
  77. package/templates/typescript/src/index.ts +16 -0
  78. package/templates/typescript/src/utils/logger.ts +42 -0
  79. package/templates/typescript/template.json +14 -0
  80. package/templates/typescript/tests/example.test.js +10 -0
  81. package/templates/typescript/tsconfig.json +21 -0
package/README.md CHANGED
@@ -1,11 +1,143 @@
1
1
  # Skeletor
2
- Efficient scaffolding for custom development
2
+ Efficient scaffolding for custom development.
3
3
 
4
- ## Status: Active Development
5
- This project is currently in its early alpha stage. The global `skeletor` command is reserved for active development.
4
+ Generates empty (or lightly stubbed) projects pre-configured with **your** rules and recommendations, derived from analysis of many GitHub-linked workspaces (local-land-client/server, cuda-sandbox, x-app-skeleton, etc.).
5
+
6
+ ## Available Templates
7
+
8
+ Skeletor lets you **pick** the kind of project you want. Run `skeletor new my-project` (omit `--template`) for an interactive selector, or pass `--template <id>`.
9
+
10
+ Current templates (standard main libraries + your flavors where they existed):
11
+
12
+ - `javascript` — Your exact CJS conventions (Prettier 165, ESLint+unused-imports, aliases, health tools, release gates, AGENTS.md, husky+lint-staged, etc.)
13
+ - `typescript` — Modern ESM + flat ESLint + TypeScript, tsconfig, build, your quality gates + AGENTS.md
14
+ - `python` — Best-practice defaults (pyproject.toml + ruff for lint+format, pytest, mypy, src layout)
15
+ - `go` — Standard Go module (`go.mod` + main + tests)
16
+ - `rust` — Standard Cargo binary crate
17
+ - `java` — Maven + JUnit 5 structure
18
+ - `csharp` — .NET 8 console + xUnit
19
+
20
+ Each template includes a `template.json` manifest that declares `verifyCommands` — the exact steps (3 & 4) a developer should run after scaffolding. The test suite discovers templates and validates this contract.
21
+
22
+ > New languages/stacks: Drop a directory under `templates/<id>/` with a `template.json` + conventional source/build/test files using `{{PROJECT_NAME}}` etc. tokens. The CLI, listing, and tests pick it up automatically.
23
+
24
+ ## Rich Interactive CLI
25
+
26
+ We use `@clack/prompts` for a modern experience:
27
+ - Beautiful template selector with descriptions/hints when you omit `--template`
28
+ - Prompts for owner and description (interactive mode only)
29
+ - Confirmation step
30
+ - Proper cancel handling and nice spinners/intro/outro
31
+
32
+ `--auto` stays fully non-interactive for scripts (requires `--template`; uses default owner/description).
33
+
34
+ ## Pushing to GitHub
35
+
36
+ **Pushing is not automatic.** After every set of changes (new templates, CLI updates, tests, docs), you **must** explicitly:
37
+ 1. `git add -A`
38
+ 2. `git commit -m "..."` (good message)
39
+ 3. `git push -u origin HEAD`
40
+
41
+ See the root `AGENTS.md` for the standing instruction given to agents working on this repo. This ensures the GitHub repo (`https://github.com/jml6m/skeletor`) stays in sync with every request. There is no special Grok Build setting that auto-pushes; it must be done via explicit git commands in the agent loop (or documented in project instructions like AGENTS.md).
42
+
43
+ ## Usage (alpha)
6
44
 
7
- ## Installation
8
- You can run the alpha placeholder using npx:
9
45
  ```bash
10
- npx skeletor
11
- ```
46
+ # Interactive (recommended for exploration)
47
+ node src/index.js new my-project
48
+
49
+ # Or specify
50
+ node src/index.js new my-api --template typescript
51
+
52
+ # Non-interactive (scripts)
53
+ npx @jml6m/skeletor new my-service --auto --template go
54
+ ```
55
+
56
+ Common flags:
57
+ - `--template <id>` (javascript, typescript, python, go, rust, java, csharp, ...)
58
+ - `--auto` (skip all prompts; requires `--template`)
59
+ - `--no-git` (skip git init; git is on by default)
60
+
61
+ After scaffolding, follow the `verifyCommands` from that template's `template.json` (shown in the success message). E.g. for most JS/TS: `npm install && npm run health:full` etc.
62
+
63
+ Run `skeletor` (or `node src/index.js`) with no args to see current templates.
64
+
65
+ ## Philosophy / Goals
66
+
67
+ - Developers **pick** the scaffolding they want (language + flavor).
68
+ - Your personal conventions (from the workspaces analysis) are encoded in the relevant templates (javascript/typescript for now).
69
+ - Everything else starts from clean "standard main library" best practices and can be refined later.
70
+ - The `template.json` manifest per template is the contract for post-generation steps (this is what the test suite validates as steps 3 & 4).
71
+ - Easy to evolve: add/edit under `templates/<id>/`. New templates are auto-discovered by the CLI and tests.
72
+
73
+ ## Status
74
+
75
+ Active. Multiple language templates + rich clack-based interactive UI. Test suite validates generation + manifest contracts for every template.
76
+
77
+ Run `skeletor` (or `node src/index.js`) with no args for current help and template list.
78
+
79
+ ## For Testers & Reviewers
80
+
81
+ ### Quick try (no install needed)
82
+
83
+ ```bash
84
+ # From a clone of this repo
85
+ node src/index.js new my-test-project --auto --template javascript
86
+
87
+ cd my-test-project
88
+ npm install
89
+ npm run format
90
+ npm run lint
91
+ npm test
92
+ npm run health:full
93
+ ```
94
+
95
+ Inspect the generated files, especially:
96
+ - `AGENTS.md`
97
+ - `release.js` (owner/repo substituted)
98
+ - `.prettierrc.json`, `.eslintrc.json`, `knip.json`
99
+ - `.husky/pre-commit` and `lint-staged` config
100
+ - `.github/workflows/ci.yml`
101
+ - `src/` structure + alias usage in `src/index.js`
102
+ - `package.json` scripts and `_moduleAliases`
103
+
104
+ ### Running the unit + integration test suite
105
+
106
+ ```bash
107
+ npm install # pulls jest (devDep)
108
+ npm test
109
+ ```
110
+
111
+ The tests cover:
112
+ - CLI argument parsing
113
+ - Template token rendering (`{{PROJECT_NAME}}` etc.)
114
+ - Full generation into temp dirs (verifies file presence, JSON validity, token substitution, new husky/github artifacts, refusal to overwrite, etc.)
115
+ - The generated projects are exercised for basic structure (no full `npm install` of heavy tools in every test to keep it fast).
116
+
117
+ All tests are in `tests/`.
118
+
119
+ ### Updating / Adding Templates
120
+
121
+ The source of truth lives in `templates/<id>/` (each with its own `template.json`).
122
+
123
+ - Edit files inside an existing template → next generation gets the change.
124
+ - Add a whole new `templates/newlang/` with `template.json` + files → it appears in the picker, help, and tests automatically.
125
+ - **Ecosystem manifest files** (e.g. `package.json`, `pyproject.toml`, `Cargo.toml`, `pom.xml`, `*.csproj`, `go.mod`) must be named with a `.tmpl` suffix inside the template (e.g. `package.json.tmpl`). The generator strips the suffix on output, so the generated project sees the conventional filename. This prevents GitHub's dependency graph from treating template dependencies as skeletor's own.
126
+
127
+ Always run `npm test` after template work.
128
+
129
+ When happy, commit + push (see root `AGENTS.md` for the standing rule).
130
+
131
+ ### GitHub push & external review
132
+
133
+ Pushing is **explicit** (not automatic). After changes:
134
+ - Run tests
135
+ - `git add -A && git commit -m "..." && git push -u origin HEAD`
136
+
137
+ See the root `AGENTS.md` (the instruction file for agents) for the exact rule.
138
+
139
+ The repo lives at https://github.com/jml6m/skeletor.
140
+
141
+ Open issues/PRs with feedback on new templates, UI, etc.
142
+
143
+ Now, finish the UI polish if needed and do the push.
package/package.json CHANGED
@@ -1,13 +1,44 @@
1
1
  {
2
2
  "name": "@jml6m/skeletor",
3
- "version": "0.1.0-alpha.1",
4
- "description": "Efficient scaffolding for custom development",
3
+ "version": "0.1.0-alpha.5",
4
+ "description": "Efficient scaffolding for custom development. Generates projects wired with personal lint/format/health/release/alias/AGENTS.md conventions.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "skeletor": "src/index.js"
9
9
  },
10
- "keywords": [],
10
+ "scripts": {
11
+ "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
12
+ "test:watch": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --watch",
13
+ "prepublishOnly": "npm test"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/jml6m/skeletor.git"
18
+ },
19
+ "files": [
20
+ "src",
21
+ "templates",
22
+ "README.md"
23
+ ],
24
+ "keywords": [
25
+ "scaffolding",
26
+ "generator",
27
+ "yeoman",
28
+ "template",
29
+ "eslint",
30
+ "prettier",
31
+ "knip"
32
+ ],
33
+ "dependencies": {
34
+ "@clack/prompts": "^1.5.1"
35
+ },
36
+ "devDependencies": {
37
+ "jest": "^29.7.0"
38
+ },
11
39
  "author": "",
12
- "license": "ISC"
40
+ "license": "ISC",
41
+ "engines": {
42
+ "node": ">=20.19.0"
43
+ }
13
44
  }
package/src/index.js CHANGED
@@ -1,7 +1,347 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * Skeletor — Efficient scaffolding for custom development
4
+ * Generates projects pre-configured with personal standards derived from
5
+ * local-land-*, cuda-sandbox, x-app-skeleton and similar workspaces.
6
+ */
2
7
 
3
- console.log("\n💀 Skeletor CLI — Alpha Release");
4
- console.log("================================");
5
- console.log("This tool is currently under active development.");
6
- console.log("Please check back later or view our progress on GitHub:\n");
7
- console.log("👉 https://github.com/jml6m/skeletor\n");
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import { execSync } from 'child_process';
12
+ import * as p from '@clack/prompts';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+ const ROOT = path.resolve(__dirname, '..');
17
+ const TEMPLATES_DIR = path.join(ROOT, 'templates');
18
+
19
+ const USAGE = `
20
+ 💀 skeletor — pick your scaffolding
21
+
22
+ Usage:
23
+ skeletor new <name> [options]
24
+ skeletor --help
25
+
26
+ Options:
27
+ --template <name> Stack to scaffold (javascript, typescript, python, go, …)
28
+ Omit for an interactive language/stack picker (TTY required)
29
+ --auto Non-interactive; requires --template (uses defaults — edit
30
+ owner/description in generated files afterward)
31
+ --no-git Skip git init (git is initialized by default)
32
+
33
+ Examples:
34
+ skeletor new my-api # interactive: pick stack, owner, description
35
+ skeletor new my-api --template typescript
36
+ skeletor new my-lib --auto --template go
37
+ skeletor new my-lib --auto --template go --no-git
38
+ `;
39
+
40
+
41
+ function log(msg) { console.log(msg); }
42
+ function logError(msg) { console.error(msg); }
43
+
44
+ function parseArgs(argv) {
45
+ const args = argv.slice(2);
46
+ const result = {
47
+ command: null,
48
+ name: null,
49
+ template: null, // resolved later, can be interactive
50
+ auto: false,
51
+ git: true,
52
+ };
53
+
54
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
55
+ result.command = 'help';
56
+ return result;
57
+ }
58
+
59
+ if (args[0] === 'new') {
60
+ result.command = 'new';
61
+ result.name = args[1];
62
+ for (let i = 2; i < args.length; i++) {
63
+ const a = args[i];
64
+ if (a === '--template' || a === '-t') result.template = args[++i] || null;
65
+ else if (a === '--auto') result.auto = true;
66
+ else if (a === '--no-git') result.git = false;
67
+ else if (!a.startsWith('-') && !result.name) result.name = a;
68
+ }
69
+ } else {
70
+ result.command = 'help';
71
+ }
72
+
73
+ return result;
74
+ }
75
+
76
+ function render(content, vars) {
77
+ let out = content;
78
+ for (const [k, v] of Object.entries(vars)) {
79
+ const token = new RegExp(`\\{\\{${k}\\}\\}`, 'g');
80
+ out = out.replace(token, String(v));
81
+ }
82
+ return out;
83
+ }
84
+
85
+ /** Sanitize a segment for use in Java groupId / Go module paths. */
86
+ function sanitizeIdentifierSegment(value) {
87
+ return String(value).replace(/[^a-zA-Z0-9]/g, '').toLowerCase() || 'example';
88
+ }
89
+
90
+ /** Build the full token map passed to copyAndRender. */
91
+ function buildRenderVars({ name, owner, description }) {
92
+ const repoOwner = owner || 'jml6m';
93
+ const ownerSegment = sanitizeIdentifierSegment(repoOwner);
94
+ const namespace = String(name).replace(/-/g, '_');
95
+ return {
96
+ PROJECT_NAME: name,
97
+ REPO_OWNER: repoOwner,
98
+ REPO_NAME: name,
99
+ DESCRIPTION: description || 'A new project scaffolded with skeletor.',
100
+ YEAR: new Date().getFullYear(),
101
+ NAMESPACE: namespace,
102
+ GROUP_ID: `io.github.${ownerSegment}`,
103
+ GO_MODULE: `github.com/${repoOwner}/${name}`,
104
+ };
105
+ }
106
+
107
+ function ensureDir(dir) {
108
+ if (!fs.existsSync(dir)) {
109
+ fs.mkdirSync(dir, { recursive: true });
110
+ }
111
+ }
112
+
113
+ function copyAndRender(src, dest, vars) {
114
+ const stat = fs.statSync(src);
115
+ if (stat.isDirectory()) {
116
+ ensureDir(dest);
117
+ for (const entry of fs.readdirSync(src)) {
118
+ // never copy node_modules, .git, or skeletor manifest inside templates
119
+ if (entry === 'node_modules' || entry === '.git' || entry === 'template.json') continue;
120
+ // strip .tmpl suffix from output filename so e.g. package.json.tmpl → package.json
121
+ const outEntry = entry.endsWith('.tmpl') ? entry.slice(0, -'.tmpl'.length) : entry;
122
+ copyAndRender(path.join(src, entry), path.join(dest, outEntry), vars);
123
+ }
124
+ return;
125
+ }
126
+
127
+ // text file -> render
128
+ let content = fs.readFileSync(src, 'utf8');
129
+ content = render(content, vars);
130
+ ensureDir(path.dirname(dest));
131
+ fs.writeFileSync(dest, content, 'utf8');
132
+ }
133
+
134
+ function getAvailableTemplates() {
135
+ if (!fs.existsSync(TEMPLATES_DIR)) return [];
136
+ return fs.readdirSync(TEMPLATES_DIR).filter((d) => {
137
+ const p = path.join(TEMPLATES_DIR, d);
138
+ return fs.statSync(p).isDirectory();
139
+ });
140
+ }
141
+
142
+ function loadTemplateManifest(templateId) {
143
+ const dir = path.join(TEMPLATES_DIR, templateId);
144
+ const manifestPath = path.join(dir, 'template.json');
145
+ let manifest = {
146
+ id: templateId,
147
+ name: templateId,
148
+ description: 'A custom template.',
149
+ language: templateId,
150
+ verifyCommands: [],
151
+ };
152
+ if (fs.existsSync(manifestPath)) {
153
+ try {
154
+ const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
155
+ manifest = { ...manifest, ...raw };
156
+ } catch (e) {
157
+ // ignore bad manifest, fall back to defaults
158
+ }
159
+ }
160
+ return { ...manifest, dir };
161
+ }
162
+
163
+ function getTemplatesWithManifests() {
164
+ const ids = getAvailableTemplates();
165
+ return ids.map((id) => loadTemplateManifest(id));
166
+ }
167
+
168
+ async function chooseTemplateInteractively(templates) {
169
+ if (templates.length === 0) {
170
+ return null;
171
+ }
172
+
173
+ p.intro('💀 Skeletor — pick your scaffolding');
174
+
175
+ const options = templates.map((t) => ({
176
+ value: t.id,
177
+ label: t.name,
178
+ hint: t.description.length > 60 ? t.description.slice(0, 57) + '...' : t.description,
179
+ }));
180
+
181
+ const selected = await p.select({
182
+ message: 'What language or stack are you building?',
183
+ options,
184
+ });
185
+
186
+ if (p.isCancel(selected)) {
187
+ p.cancel('Operation cancelled.');
188
+ process.exit(0);
189
+ }
190
+
191
+ return selected;
192
+ }
193
+
194
+ const DEFAULT_OWNER = 'jml6m';
195
+ const DEFAULT_DESCRIPTION = 'A new project scaffolded with skeletor.';
196
+
197
+ async function runNew(opts) {
198
+ const { name, git, auto } = opts;
199
+
200
+ if (!name || name === '.' || name === '..') {
201
+ logError('❌ Please provide a valid project name (e.g. "my-api").');
202
+ process.exit(1);
203
+ }
204
+
205
+ const allTemplates = getTemplatesWithManifests();
206
+ if (allTemplates.length === 0) {
207
+ logError('❌ No templates found in templates/ directory.');
208
+ process.exit(1);
209
+ }
210
+
211
+ let chosenTemplateId = opts.template;
212
+ let finalOwner = DEFAULT_OWNER;
213
+ let finalDesc = DEFAULT_DESCRIPTION;
214
+
215
+ const isInteractive = !auto && process.stdout.isTTY;
216
+
217
+ if (!chosenTemplateId) {
218
+ if (auto) {
219
+ logError('❌ --auto requires --template <id>. Pick a stack interactively by omitting --auto.');
220
+ logError(` Available: ${allTemplates.map((t) => t.id).join(', ')}`);
221
+ process.exit(1);
222
+ }
223
+ if (!process.stdout.isTTY) {
224
+ logError('❌ No --template provided and this is not an interactive terminal.');
225
+ logError(' Use --auto --template <id> for scripts and piped environments.');
226
+ logError(` Available: ${allTemplates.map((t) => t.id).join(', ')}`);
227
+ process.exit(1);
228
+ }
229
+ chosenTemplateId = await chooseTemplateInteractively(allTemplates);
230
+ } else if (!process.stdout.isTTY && !auto) {
231
+ logError('❌ --template requires --auto when not running in an interactive terminal.');
232
+ logError(` Example: skeletor new ${name} --auto --template ${chosenTemplateId}`);
233
+ process.exit(1);
234
+ }
235
+
236
+ const templateInfo = allTemplates.find((t) => t.id === chosenTemplateId);
237
+ if (!templateInfo) {
238
+ const available = allTemplates.map((t) => t.id).join(', ');
239
+ logError(`❌ Unknown template "${chosenTemplateId}". Available: ${available}`);
240
+ process.exit(1);
241
+ }
242
+
243
+ // Owner, description, and confirm — interactive only (--auto uses defaults above)
244
+ if (isInteractive) {
245
+ const ownerInput = await p.text({
246
+ message: 'GitHub owner / org',
247
+ placeholder: finalOwner,
248
+ initialValue: finalOwner,
249
+ });
250
+ if (p.isCancel(ownerInput)) { p.cancel('Cancelled.'); process.exit(0); }
251
+ finalOwner = ownerInput || finalOwner;
252
+
253
+ const descInput = await p.text({
254
+ message: 'Project description',
255
+ placeholder: finalDesc,
256
+ initialValue: finalDesc,
257
+ });
258
+ if (p.isCancel(descInput)) { p.cancel('Cancelled.'); process.exit(0); }
259
+ finalDesc = descInput || finalDesc;
260
+
261
+ const shouldContinue = await p.confirm({
262
+ message: `Scaffold "${name}" as ${templateInfo.name}?`,
263
+ initialValue: true,
264
+ });
265
+ if (p.isCancel(shouldContinue) || !shouldContinue) {
266
+ p.cancel('Scaffolding cancelled.');
267
+ process.exit(0);
268
+ }
269
+ }
270
+
271
+ const targetDir = path.resolve(process.cwd(), name);
272
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
273
+ logError(`❌ Target directory "${name}" already exists and is not empty.`);
274
+ process.exit(1);
275
+ }
276
+
277
+ const vars = buildRenderVars({ name, owner: finalOwner, description: finalDesc });
278
+
279
+ p.log.info(`Creating "${name}" using ${templateInfo.name}...`);
280
+
281
+ copyAndRender(templateInfo.dir, targetDir, vars);
282
+
283
+ if (git) {
284
+ try {
285
+ execSync('git init -q', { cwd: targetDir, stdio: 'ignore' });
286
+ execSync('git add -A', { cwd: targetDir, stdio: 'ignore' });
287
+ execSync('git commit -q -m "chore: initial commit from skeletor"', { cwd: targetDir, stdio: 'ignore' });
288
+ p.log.success('Git repository initialized');
289
+ } catch (e) {
290
+ p.log.warn('Git init skipped (git not available or failed)');
291
+ }
292
+ }
293
+
294
+ p.outro('✅ Done!');
295
+
296
+ console.log(` cd ${name}`);
297
+ const suggested = templateInfo.verifyCommands && templateInfo.verifyCommands.length
298
+ ? templateInfo.verifyCommands[0]
299
+ : 'Run your language-specific install + test commands';
300
+ console.log(` ${suggested}`);
301
+ console.log(' Then run the other steps from the template manifest (lint, test, etc.)');
302
+ console.log('\n Edit AGENTS.md (or equivalent) to customize the AI contract.');
303
+ console.log(' Happy scaffolding. Your rules, your choice of stack.\n');
304
+ }
305
+
306
+ function main() {
307
+ const opts = parseArgs(process.argv);
308
+
309
+ if (opts.command === 'help' || !opts.command) {
310
+ log(USAGE.trim());
311
+ const tmpls = getTemplatesWithManifests();
312
+ if (tmpls.length) {
313
+ log('\nAvailable templates:');
314
+ tmpls.forEach((t) => log(` • ${t.id} — ${t.name}: ${t.description}`));
315
+ } else {
316
+ log('\n(No templates found yet — run from a full skeletor checkout with templates/ )');
317
+ }
318
+ process.exit(0);
319
+ }
320
+
321
+ if (opts.command === 'new') {
322
+ runNew(opts).catch((e) => {
323
+ logError('❌ Generation failed: ' + (e?.message || e));
324
+ process.exit(1);
325
+ });
326
+ }
327
+ }
328
+
329
+ // Exports for testing / programmatic use
330
+ export {
331
+ parseArgs,
332
+ render,
333
+ buildRenderVars,
334
+ sanitizeIdentifierSegment,
335
+ getAvailableTemplates,
336
+ getTemplatesWithManifests,
337
+ loadTemplateManifest,
338
+ ensureDir,
339
+ copyAndRender,
340
+ runNew,
341
+ USAGE,
342
+ };
343
+
344
+ const isTestEnv = process.env.SKELETOR_CLI_TEST === '1' || process.env.JEST_WORKER_ID != null;
345
+ if (!isTestEnv) {
346
+ main();
347
+ }
@@ -0,0 +1,10 @@
1
+ # C# Project — Agent Guidelines
2
+
3
+ **Project:** {{PROJECT_NAME}}
4
+ **Language:** C# (.NET)
5
+
6
+ - Build: `dotnet build`, test: `dotnet test`.
7
+ - Use modern C# features (records, pattern matching, nullable).
8
+ - Prefer xUnit for tests.
9
+ - Keep Program.cs or entry points minimal.
10
+ - Update this file with any additional .NET conventions.
@@ -0,0 +1,18 @@
1
+ // {{PROJECT_NAME}}
2
+ // {{DESCRIPTION}}
3
+
4
+ namespace {{NAMESPACE}};
5
+
6
+ class Program
7
+ {
8
+ static void Main(string[] args)
9
+ {
10
+ var name = args.Length > 0 ? args[0] : "world";
11
+ Console.WriteLine(Hello(name));
12
+ }
13
+
14
+ public static string Hello(string name)
15
+ {
16
+ return $"Hello, {name} from {{PROJECT_NAME}}!";
17
+ }
18
+ }
@@ -0,0 +1,12 @@
1
+ using Xunit;
2
+
3
+ namespace {{NAMESPACE}};
4
+
5
+ public class ProgramTests
6
+ {
7
+ [Fact]
8
+ public void Hello_ReturnsGreeting()
9
+ {
10
+ Assert.Equal("Hello, tester from {{PROJECT_NAME}}!", Program.Hello("tester"));
11
+ }
12
+ }
@@ -0,0 +1,17 @@
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+
3
+ <PropertyGroup>
4
+ <OutputType>Exe</OutputType>
5
+ <TargetFramework>net8.0</TargetFramework>
6
+ <ImplicitUsings>enable</ImplicitUsings>
7
+ <Nullable>enable</Nullable>
8
+ <RootNamespace>{{NAMESPACE}}</RootNamespace>
9
+ </PropertyGroup>
10
+
11
+ <ItemGroup>
12
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
13
+ <PackageReference Include="xunit" Version="2.6.6" />
14
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.5.6" />
15
+ </ItemGroup>
16
+
17
+ </Project>
@@ -0,0 +1,12 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ {{DESCRIPTION}}
4
+
5
+ ## Development
6
+
7
+ ```bash
8
+ dotnet build
9
+ dotnet test
10
+ ```
11
+
12
+ Standard .NET 8 console + xUnit project.
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "csharp",
3
+ "name": "C# (.NET)",
4
+ "description": "Standard .NET console project with .csproj, Program.cs, and xUnit tests.",
5
+ "language": "csharp",
6
+ "verifyCommands": [
7
+ "dotnet build",
8
+ "dotnet test"
9
+ ]
10
+ }
@@ -0,0 +1,9 @@
1
+ # Go Project — Agent Guidelines
2
+
3
+ **Project:** {{PROJECT_NAME}}
4
+ **Language:** Go
5
+
6
+ - Use `go build`, `go test`, `go mod tidy`.
7
+ - Keep packages small and well-tested.
8
+ - Follow standard Go conventions (gofmt, effective go).
9
+ - Update this file + README when standards evolve.
@@ -0,0 +1,13 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ {{DESCRIPTION}}
4
+
5
+ ## Development
6
+
7
+ ```bash
8
+ go mod tidy
9
+ go build .
10
+ go test ./...
11
+ ```
12
+
13
+ This is a standard Go project scaffold.
@@ -0,0 +1,5 @@
1
+ module {{GO_MODULE}}
2
+
3
+ go 1.22
4
+
5
+ toolchain go1.22.0
@@ -0,0 +1,20 @@
1
+ package main
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ )
7
+
8
+ // main is the entry point for {{PROJECT_NAME}}.
9
+ func main() {
10
+ name := "world"
11
+ if len(os.Args) > 1 {
12
+ name = os.Args[1]
13
+ }
14
+ fmt.Println(hello(name))
15
+ }
16
+
17
+ // hello returns a greeting. Extracted for easy testing.
18
+ func hello(name string) string {
19
+ return fmt.Sprintf("Hello, %s from {{PROJECT_NAME}}!", name)
20
+ }