@jml6m/skeletor 0.1.0-alpha.2 → 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.
package/README.md CHANGED
@@ -25,20 +25,20 @@ Each template includes a `template.json` manifest that declares `verifyCommands`
25
25
 
26
26
  We use `@clack/prompts` for a modern experience:
27
27
  - Beautiful template selector with descriptions/hints when you omit `--template`
28
- - Prompts for owner and description (when running interactively without `--yes`)
28
+ - Prompts for owner and description (interactive mode only)
29
29
  - Confirmation step
30
30
  - Proper cancel handling and nice spinners/intro/outro
31
31
 
32
- `--yes` stays fully non-interactive for scripts/CI.
32
+ `--auto` stays fully non-interactive for scripts (requires `--template`; uses default owner/description).
33
33
 
34
34
  ## Pushing to GitHub
35
35
 
36
36
  **Pushing is not automatic.** After every set of changes (new templates, CLI updates, tests, docs), you **must** explicitly:
37
37
  1. `git add -A`
38
38
  2. `git commit -m "..."` (good message)
39
- 3. `git push origin main`
39
+ 3. `git push -u origin HEAD`
40
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 shell commands in the agent loop (or documented in project instructions like AGENTS.md).
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
42
 
43
43
  ## Usage (alpha)
44
44
 
@@ -47,18 +47,16 @@ See the root `AGENTS.md` for the standing instruction given to agents working on
47
47
  node src/index.js new my-project
48
48
 
49
49
  # Or specify
50
- node src/index.js new my-api --template typescript --owner jml6m
50
+ node src/index.js new my-api --template typescript
51
51
 
52
- # Non-interactive (CI / scripts)
53
- npx @jml6m/skeletor new my-service --yes --template go
52
+ # Non-interactive (scripts)
53
+ npx @jml6m/skeletor new my-service --auto --template go
54
54
  ```
55
55
 
56
56
  Common flags:
57
57
  - `--template <id>` (javascript, typescript, python, go, rust, java, csharp, ...)
58
- - `--owner`
59
- - `--description`
60
- - `--yes` (skip all prompts)
61
- - `--no-git`
58
+ - `--auto` (skip all prompts; requires `--template`)
59
+ - `--no-git` (skip git init; git is on by default)
62
60
 
63
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.
64
62
 
@@ -84,7 +82,7 @@ Run `skeletor` (or `node src/index.js`) with no args for current help and templa
84
82
 
85
83
  ```bash
86
84
  # From a clone of this repo
87
- node src/index.js new my-test-project --yes
85
+ node src/index.js new my-test-project --auto --template javascript
88
86
 
89
87
  cd my-test-project
90
88
  npm install
@@ -124,6 +122,7 @@ The source of truth lives in `templates/<id>/` (each with its own `template.json
124
122
 
125
123
  - Edit files inside an existing template → next generation gets the change.
126
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.
127
126
 
128
127
  Always run `npm test` after template work.
129
128
 
@@ -133,7 +132,7 @@ When happy, commit + push (see root `AGENTS.md` for the standing rule).
133
132
 
134
133
  Pushing is **explicit** (not automatic). After changes:
135
134
  - Run tests
136
- - `git add -A && git commit -m "..." && git push origin main`
135
+ - `git add -A && git commit -m "..." && git push -u origin HEAD`
137
136
 
138
137
  See the root `AGENTS.md` (the instruction file for agents) for the exact rule.
139
138
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jml6m/skeletor",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.5",
4
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",
@@ -9,7 +9,12 @@
9
9
  },
10
10
  "scripts": {
11
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"
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"
13
18
  },
14
19
  "files": [
15
20
  "src",
@@ -26,7 +31,7 @@
26
31
  "knip"
27
32
  ],
28
33
  "dependencies": {
29
- "@clack/prompts": "^0.9.0"
34
+ "@clack/prompts": "^1.5.1"
30
35
  },
31
36
  "devDependencies": {
32
37
  "jest": "^29.7.0"
@@ -35,8 +40,5 @@
35
40
  "license": "ISC",
36
41
  "engines": {
37
42
  "node": ">=20.19.0"
38
- },
39
- "dependencies": {
40
- "@clack/prompts": "^1.5.1"
41
43
  }
42
44
  }
package/src/index.js CHANGED
@@ -24,17 +24,17 @@ Usage:
24
24
  skeletor --help
25
25
 
26
26
  Options:
27
- --template <name> Template to use (e.g. javascript, typescript, python)
28
- Omit to choose interactively (or use --yes for default)
29
- --owner <user> GitHub owner/org for release scripts (default: jml6m)
30
- --description <text> Short project description
31
- --yes Non-interactive, use defaults + first available template
32
- --no-git Skip git init
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)
33
32
 
34
33
  Examples:
34
+ skeletor new my-api # interactive: pick stack, owner, description
35
35
  skeletor new my-api --template typescript
36
- skeletor new my-lib --yes
37
- skeletor new my-tool
36
+ skeletor new my-lib --auto --template go
37
+ skeletor new my-lib --auto --template go --no-git
38
38
  `;
39
39
 
40
40
 
@@ -47,9 +47,7 @@ function parseArgs(argv) {
47
47
  command: null,
48
48
  name: null,
49
49
  template: null, // resolved later, can be interactive
50
- owner: 'jml6m',
51
- description: 'A new project scaffolded with skeletor.',
52
- yes: false,
50
+ auto: false,
53
51
  git: true,
54
52
  };
55
53
 
@@ -64,9 +62,7 @@ function parseArgs(argv) {
64
62
  for (let i = 2; i < args.length; i++) {
65
63
  const a = args[i];
66
64
  if (a === '--template' || a === '-t') result.template = args[++i] || null;
67
- else if (a === '--owner' || a === '-o') result.owner = args[++i] || 'jml6m';
68
- else if (a === '--description' || a === '-d') result.description = args[++i] || result.description;
69
- else if (a === '--yes' || a === '-y') result.yes = true;
65
+ else if (a === '--auto') result.auto = true;
70
66
  else if (a === '--no-git') result.git = false;
71
67
  else if (!a.startsWith('-') && !result.name) result.name = a;
72
68
  }
@@ -86,6 +82,28 @@ function render(content, vars) {
86
82
  return out;
87
83
  }
88
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
+
89
107
  function ensureDir(dir) {
90
108
  if (!fs.existsSync(dir)) {
91
109
  fs.mkdirSync(dir, { recursive: true });
@@ -97,9 +115,11 @@ function copyAndRender(src, dest, vars) {
97
115
  if (stat.isDirectory()) {
98
116
  ensureDir(dest);
99
117
  for (const entry of fs.readdirSync(src)) {
100
- // never copy node_modules or .git inside templates
101
- if (entry === 'node_modules' || entry === '.git') continue;
102
- copyAndRender(path.join(src, entry), path.join(dest, entry), vars);
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);
103
123
  }
104
124
  return;
105
125
  }
@@ -145,13 +165,9 @@ function getTemplatesWithManifests() {
145
165
  return ids.map((id) => loadTemplateManifest(id));
146
166
  }
147
167
 
148
- async function chooseTemplateInteractively(templates, isYes) {
149
- if (isYes || templates.length === 0) {
150
- return templates[0] ? templates[0].id : null;
151
- }
152
-
153
- if (!process.stdout.isTTY) {
154
- return templates[0] ? templates[0].id : null;
168
+ async function chooseTemplateInteractively(templates) {
169
+ if (templates.length === 0) {
170
+ return null;
155
171
  }
156
172
 
157
173
  p.intro('💀 Skeletor — pick your scaffolding');
@@ -163,7 +179,7 @@ async function chooseTemplateInteractively(templates, isYes) {
163
179
  }));
164
180
 
165
181
  const selected = await p.select({
166
- message: 'Choose a template',
182
+ message: 'What language or stack are you building?',
167
183
  options,
168
184
  });
169
185
 
@@ -175,8 +191,11 @@ async function chooseTemplateInteractively(templates, isYes) {
175
191
  return selected;
176
192
  }
177
193
 
194
+ const DEFAULT_OWNER = 'jml6m';
195
+ const DEFAULT_DESCRIPTION = 'A new project scaffolded with skeletor.';
196
+
178
197
  async function runNew(opts) {
179
- const { name, git, yes } = opts;
198
+ const { name, git, auto } = opts;
180
199
 
181
200
  if (!name || name === '.' || name === '..') {
182
201
  logError('❌ Please provide a valid project name (e.g. "my-api").');
@@ -190,13 +209,28 @@ async function runNew(opts) {
190
209
  }
191
210
 
192
211
  let chosenTemplateId = opts.template;
193
- let finalOwner = opts.owner || 'jml6m';
194
- let finalDesc = opts.description || 'A new project scaffolded with skeletor.';
212
+ let finalOwner = DEFAULT_OWNER;
213
+ let finalDesc = DEFAULT_DESCRIPTION;
195
214
 
196
- const isInteractive = !yes && process.stdout.isTTY;
215
+ const isInteractive = !auto && process.stdout.isTTY;
197
216
 
198
217
  if (!chosenTemplateId) {
199
- chosenTemplateId = await chooseTemplateInteractively(allTemplates, yes);
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);
200
234
  }
201
235
 
202
236
  const templateInfo = allTemplates.find((t) => t.id === chosenTemplateId);
@@ -206,7 +240,7 @@ async function runNew(opts) {
206
240
  process.exit(1);
207
241
  }
208
242
 
209
- // Richer interactive prompts for missing details (only when not --yes)
243
+ // Owner, description, and confirm interactive only (--auto uses defaults above)
210
244
  if (isInteractive) {
211
245
  const ownerInput = await p.text({
212
246
  message: 'GitHub owner / org',
@@ -240,14 +274,7 @@ async function runNew(opts) {
240
274
  process.exit(1);
241
275
  }
242
276
 
243
- const repoName = name;
244
- const vars = {
245
- PROJECT_NAME: name,
246
- REPO_OWNER: finalOwner,
247
- REPO_NAME: repoName,
248
- DESCRIPTION: finalDesc,
249
- YEAR: new Date().getFullYear(),
250
- };
277
+ const vars = buildRenderVars({ name, owner: finalOwner, description: finalDesc });
251
278
 
252
279
  p.log.info(`Creating "${name}" using ${templateInfo.name}...`);
253
280
 
@@ -303,6 +330,8 @@ function main() {
303
330
  export {
304
331
  parseArgs,
305
332
  render,
333
+ buildRenderVars,
334
+ sanitizeIdentifierSegment,
306
335
  getAvailableTemplates,
307
336
  getTemplatesWithManifests,
308
337
  loadTemplateManifest,
@@ -1,7 +1,7 @@
1
1
  // {{PROJECT_NAME}}
2
2
  // {{DESCRIPTION}}
3
3
 
4
- namespace {{PROJECT_NAME | replace('-', '_')}};
4
+ namespace {{NAMESPACE}};
5
5
 
6
6
  class Program
7
7
  {
@@ -1,6 +1,6 @@
1
1
  using Xunit;
2
2
 
3
- namespace {{PROJECT_NAME | replace('-', '_')}};
3
+ namespace {{NAMESPACE}};
4
4
 
5
5
  public class ProgramTests
6
6
  {
@@ -1,17 +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>{{PROJECT_NAME | replace('-', '_')}}</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>
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>
@@ -1,4 +1,4 @@
1
- module {{PROJECT_NAME}}
1
+ module {{GO_MODULE}}
2
2
 
3
3
  go 1.22
4
4
 
@@ -3,7 +3,7 @@
3
3
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4
4
  <modelVersion>4.0.0</modelVersion>
5
5
 
6
- <groupId>com.example</groupId>
6
+ <groupId>{{GROUP_ID}}</groupId>
7
7
  <artifactId>{{PROJECT_NAME}}</artifactId>
8
8
  <version>0.1.0</version>
9
9
  <packaging>jar</packaging>
@@ -1,4 +1,4 @@
1
- package com.example;
1
+ package app;
2
2
 
3
3
  /**
4
4
  * {{PROJECT_NAME}}
@@ -13,4 +13,4 @@ public class App {
13
13
  public static String hello(String name) {
14
14
  return String.format("Hello, %s from {{PROJECT_NAME}}!", name);
15
15
  }
16
- }
16
+ }
@@ -1,4 +1,4 @@
1
- package com.example;
1
+ package app;
2
2
 
3
3
  import org.junit.jupiter.api.Test;
4
4
  import static org.junit.jupiter.api.Assertions.*;
@@ -9,4 +9,4 @@ class AppTest {
9
9
  void helloReturnsGreeting() {
10
10
  assertEquals("Hello, tester from {{PROJECT_NAME}}!", App.hello("tester"));
11
11
  }
12
- }
12
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "threshold": 5,
3
+ "reporters": ["html", "console"],
4
+ "ignore": ["**/node_modules/**", "**/dist/**", "**/build/**", "**/*.spec.ts", "**/*.test.ts"],
5
+ "absolute": true,
6
+ "gitignore": true,
7
+ "minLines": 5,
8
+ "minTokens": 50,
9
+ "formatsExts": {
10
+ "javascript": ["js", "jsx"],
11
+ "typescript": ["ts", "tsx"]
12
+ }
13
+ }
@@ -6,7 +6,7 @@ readme = "README.md"
6
6
  requires-python = ">=3.11"
7
7
  license = {text = "UNLICENSED"}
8
8
  authors = [
9
- {name = "Your Name", email = "you@example.com"}
9
+ {name = "{{REPO_OWNER}}"}
10
10
  ]
11
11
  dependencies = []
12
12
 
@@ -7,6 +7,7 @@
7
7
  "python -m pip install -e '.[dev]' --quiet",
8
8
  "python -m ruff format --check .",
9
9
  "python -m ruff check .",
10
- "python -m pytest -q"
10
+ "python -m pytest -q",
11
+ "python -m mypy src"
11
12
  ]
12
13
  }
@@ -0,0 +1,13 @@
1
+ {
2
+ "threshold": 5,
3
+ "reporters": ["html", "console"],
4
+ "ignore": ["**/node_modules/**", "**/dist/**", "**/build/**", "**/*.spec.ts", "**/*.test.ts"],
5
+ "absolute": true,
6
+ "gitignore": true,
7
+ "minLines": 5,
8
+ "minTokens": 50,
9
+ "formatsExts": {
10
+ "javascript": ["js", "jsx"],
11
+ "typescript": ["ts", "tsx"]
12
+ }
13
+ }
File without changes