create-scaffold-ai 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.
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../src/index.js';
3
+
4
+ try {
5
+ await run(process.argv.slice(2));
6
+ } catch (err) {
7
+ console.error(`\nerror: ${err.message}`);
8
+ process.exit(1);
9
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "create-scaffold-ai",
3
+ "version": "1.0.0",
4
+ "description": "Tiny zero-deps project scaffolder: local + git templates, {{vars}}, pm install",
5
+ "keywords": [
6
+ "scaffold",
7
+ "scaffolding",
8
+ "generator",
9
+ "template",
10
+ "create",
11
+ "starter-kit"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "sameer_dhara",
15
+ "type": "module",
16
+ "main": "scaffold.config.js",
17
+ "bin": {
18
+ "create-scaffold-ai": "bin/create-scaffold-ai.js"
19
+ },
20
+ "directories": {
21
+ "test": "test"
22
+ },
23
+ "files": [
24
+ "bin/",
25
+ "src/",
26
+ "templates/",
27
+ "scaffold.config.js"
28
+ ],
29
+ "scripts": {
30
+ "test": "node --test test/"
31
+ },
32
+ "engines": {
33
+ "node": ">=18"
34
+ }
35
+ }
@@ -0,0 +1,9 @@
1
+ export default {
2
+ templates: [
3
+ // Local example:
4
+ // { name: 'my-api', source: './templates/my-api' },
5
+ // Git examples (all supported):
6
+ // { name: 'starter', source: 'gh:user/repo' },
7
+ // { name: 'starter', source: 'https://github.com/user/repo.git' },
8
+ ],
9
+ };
package/src/copy.js ADDED
@@ -0,0 +1,62 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export function replaceVars(str, vars) {
5
+ let out = str;
6
+ for (const [k, v] of Object.entries(vars)) {
7
+ out = out.split(`{{${k}}}`).join(String(v ?? ''));
8
+ out = out.split(`__${k}__`).join(String(v ?? ''));
9
+ }
10
+ return out;
11
+ }
12
+
13
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.DS_Store']);
14
+ const BINARY_EXT = new Set([
15
+ '.png', '.jpg', '.jpeg', '.gif', '.ico', '.pdf', '.zip',
16
+ '.woff', '.woff2', '.ttf', '.eot', '.mp4', '.mov',
17
+ ]);
18
+
19
+ export function listFiles(dir, base = dir) {
20
+ const out = [];
21
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
22
+ if (SKIP_DIRS.has(entry.name)) continue;
23
+ const full = path.join(dir, entry.name);
24
+ if (entry.isDirectory()) out.push(...listFiles(full, base));
25
+ else out.push(path.relative(base, full));
26
+ }
27
+ return out;
28
+ }
29
+
30
+ export function copyTemplate(srcDir, destDir, vars, { dryRun = false } = {}) {
31
+ const files = listFiles(srcDir);
32
+ const written = [];
33
+ for (const rel of files) {
34
+ if (rel === 'scaffold.json') continue; // per-template meta, don't copy
35
+ const targetRel = replaceVars(rel, vars);
36
+ const srcFile = path.join(srcDir, rel);
37
+ const destFile = path.join(destDir, targetRel);
38
+ written.push(targetRel);
39
+
40
+ if (dryRun) continue;
41
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
42
+
43
+ const ext = path.extname(srcFile).toLowerCase();
44
+ const buf = fs.readFileSync(srcFile);
45
+ if (BINARY_EXT.has(ext) || buf.includes(0)) {
46
+ fs.copyFileSync(srcFile, destFile);
47
+ } else {
48
+ fs.writeFileSync(destFile, replaceVars(buf.toString('utf8'), vars));
49
+ }
50
+ }
51
+ return written;
52
+ }
53
+
54
+ export function readTemplateMeta(templateDir) {
55
+ const metaPath = path.join(templateDir, 'scaffold.json');
56
+ if (!fs.existsSync(metaPath)) return { variables: [] };
57
+ try {
58
+ return JSON.parse(fs.readFileSync(metaPath, 'utf8'));
59
+ } catch {
60
+ return { variables: [] };
61
+ }
62
+ }
package/src/git.js ADDED
@@ -0,0 +1,26 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ export function isGitSource(s) {
4
+ if (!s) return false;
5
+ return (
6
+ s.startsWith('http://') ||
7
+ s.startsWith('https://') ||
8
+ s.startsWith('git@') ||
9
+ s.startsWith('gh:') ||
10
+ s.startsWith('github:') ||
11
+ s.endsWith('.git') ||
12
+ /^[\w-]+\/[\w.-]+$/.test(s)
13
+ );
14
+ }
15
+
16
+ export function resolveGitUrl(s) {
17
+ if (s.startsWith('gh:')) return `https://github.com/${s.slice(3)}.git`;
18
+ if (s.startsWith('github:')) return `https://github.com/${s.slice(7)}.git`;
19
+ if (/^[\w-]+\/[\w.-]+$/.test(s)) return `https://github.com/${s}.git`;
20
+ return s;
21
+ }
22
+
23
+ export function cloneShallow(gitUrl, destDir) {
24
+ const r = spawnSync('git', ['clone', '--depth', '1', gitUrl, destDir], { stdio: 'inherit' });
25
+ if (r.status !== 0) throw new Error(`git clone failed for ${gitUrl}`);
26
+ }
package/src/index.js ADDED
@@ -0,0 +1,179 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { ask, choose, confirm } from './prompt.js';
7
+ import { copyTemplate, readTemplateMeta } from './copy.js';
8
+ import { isGitSource, resolveGitUrl, cloneShallow } from './git.js';
9
+
10
+ const here = path.dirname(fileURLToPath(import.meta.url));
11
+ const PKG_ROOT = path.resolve(here, '..');
12
+
13
+ export function parseArgs(argv) {
14
+ const opts = {
15
+ name: null,
16
+ template: null,
17
+ pm: null,
18
+ install: true,
19
+ git: true,
20
+ yes: false,
21
+ dryRun: false,
22
+ };
23
+ const pos = [];
24
+ for (let i = 0; i < argv.length; i++) {
25
+ const a = argv[i];
26
+ if (a === '--template' || a === '-t') opts.template = argv[++i];
27
+ else if (a === '--pm') opts.pm = argv[++i];
28
+ else if (a === '--no-install') opts.install = false;
29
+ else if (a === '--no-git') opts.git = false;
30
+ else if (a === '--yes' || a === '-y') opts.yes = true;
31
+ else if (a === '--dry-run') opts.dryRun = true;
32
+ else if (a === '--help' || a === '-h') opts.help = true;
33
+ else if (!a.startsWith('-')) pos.push(a);
34
+ }
35
+ if (pos[0] && !opts.name) opts.name = pos[0];
36
+ return opts;
37
+ }
38
+
39
+ export async function loadUserConfig(cwd) {
40
+ const names = ['scaffold.config.js', 'scaffold.config.mjs'];
41
+ for (const n of names) {
42
+ const p = path.join(cwd, n);
43
+ if (fs.existsSync(p)) {
44
+ const mod = await import(`file://${p}?t=${Date.now()}`);
45
+ return mod.default ?? mod;
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+
51
+ function builtinTemplates() {
52
+ return [{ name: 'basic', source: path.join(PKG_ROOT, 'templates/basic') }];
53
+ }
54
+
55
+ export function resolveTemplate(nameOrSource, userConfig) {
56
+ const all = [...builtinTemplates(), ...((userConfig && userConfig.templates) || [])];
57
+ const found = all.find((t) => t.name === nameOrSource);
58
+ if (found) return found;
59
+ // direct path or git url also allowed
60
+ if (nameOrSource) return { name: nameOrSource, source: nameOrSource };
61
+ return null;
62
+ }
63
+
64
+ function runInstall(pm, cwd) {
65
+ const cmds = {
66
+ npm: ['npm', ['install'], {}],
67
+ pnpm: ['pnpm', ['install'], {}],
68
+ yarn: ['yarn', [], {}],
69
+ bun: ['bun', ['install'], {}],
70
+ };
71
+ const [cmd, args] = cmds[pm] || cmds.npm;
72
+ const r = spawnSync(cmd, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' });
73
+ if (r.status !== 0) throw new Error(`${pm} install failed`);
74
+ }
75
+
76
+ export async function run(argv, cwd = process.cwd()) {
77
+ const opts = parseArgs(argv);
78
+ if (opts.help || !opts.name) {
79
+ if (!opts.help && opts.yes && !opts.name) throw new Error('project name required with --yes');
80
+ if (!opts.help) {
81
+ // interactive mode
82
+ opts.name = opts.name ?? (await ask('Project name', 'my-app'));
83
+ const userConfig = await loadUserConfig(cwd);
84
+ const all = [...builtinTemplates(), ...((userConfig && userConfig.templates) || [])];
85
+ const names = all.map((t) => t.name);
86
+ opts.template = opts.template ?? (names.length ? await choose('Template', names, 0) : './templates/basic');
87
+ opts.pm = opts.pm ?? (await choose('Package manager', ['npm', 'pnpm', 'yarn', 'bun'], 0));
88
+ if (opts.install) opts.install = await confirm('Install dependencies?', true);
89
+ if (opts.git) opts.git = await confirm('Init git repo?', true);
90
+ return await scaffold({ ...opts, userConfig });
91
+ }
92
+ }
93
+ if (opts.help) {
94
+ console.log(`Usage: create-scaffold-ai <name> [options]
95
+ Options:
96
+ -t, --template <name|path|git-url> template to use
97
+ --pm <npm|pnpm|yarn|bun> package manager
98
+ --no-install skip dependency install
99
+ --no-git skip git init
100
+ -y, --yes skip prompts, use defaults
101
+ --dry-run list files without writing
102
+ -h, --help show this help`);
103
+ return;
104
+ }
105
+ const userConfig = await loadUserConfig(cwd);
106
+ if (!opts.template) {
107
+ const all = [...builtinTemplates(), ...((userConfig && userConfig.templates) || [])];
108
+ opts.template = opts.yes ? all[0].name : await choose('Template', all.map((t) => t.name), 0);
109
+ }
110
+ if (!opts.pm) opts.pm = opts.yes ? 'npm' : await choose('Package manager', ['npm', 'pnpm', 'yarn', 'bun'], 0);
111
+ return await scaffold({ ...opts, userConfig });
112
+ }
113
+
114
+ export async function scaffold({ name, template, pm = 'npm', install = true, git = true, yes = false, dryRun = false, userConfig = null, cwd = process.cwd() }) {
115
+ const targetDir = path.resolve(cwd, name);
116
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0 && !dryRun) {
117
+ throw new Error(`directory ${name} already exists and is not empty`);
118
+ }
119
+
120
+ const tpl = resolveTemplate(template, userConfig);
121
+ if (!tpl) throw new Error(`template not found: ${template}`);
122
+ let source = tpl.source;
123
+
124
+ // temp dir for git sources
125
+ let tmpDir = null;
126
+ let templateDir = source;
127
+ if (isGitSource(source) && !fs.existsSync(path.resolve(cwd, source))) {
128
+ const url = resolveGitUrl(source);
129
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scaffold-'));
130
+ console.log(`Cloning ${url} ...`);
131
+ if (!dryRun) cloneShallow(url, tmpDir);
132
+ templateDir = tmpDir;
133
+ } else {
134
+ templateDir = path.isAbsolute(source) ? source : path.resolve(cwd, source);
135
+ // fall back to built-in if relative to package root
136
+ if (!fs.existsSync(templateDir)) {
137
+ const inPkg = path.join(PKG_ROOT, source);
138
+ if (fs.existsSync(inPkg)) templateDir = inPkg;
139
+ }
140
+ }
141
+ if (!dryRun && !fs.existsSync(templateDir)) throw new Error(`template directory not found: ${templateDir}`);
142
+
143
+ const meta = dryRun ? { variables: [] } : readTemplateMeta(templateDir);
144
+ const vars = { name: path.basename(targetDir), pm };
145
+ if (!yes) {
146
+ vars.description = await ask('Description', `${vars.name} — built with create-scaffold-ai`);
147
+ vars.author = await ask('Author', '');
148
+ for (const key of meta.variables || []) {
149
+ if (!(key in vars)) vars[key] = await ask(key, '');
150
+ }
151
+ } else {
152
+ vars.description = `${vars.name} — built with create-scaffold-ai`;
153
+ vars.author = '';
154
+ }
155
+
156
+ console.log(`\nScaffolding ${vars.name} from ${tpl.name} -> ${targetDir}`);
157
+ const written = dryRun
158
+ ? []
159
+ : copyTemplate(templateDir, targetDir, vars, { dryRun });
160
+
161
+ if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
162
+
163
+ if (dryRun) {
164
+ console.log('(dry run) nothing written.');
165
+ return { targetDir, vars, written: [] };
166
+ }
167
+
168
+ console.log(`Wrote ${written.length} files.`);
169
+ if (git) {
170
+ const r = spawnSync('git', ['init'], { cwd: targetDir, stdio: 'ignore' });
171
+ if (r.status === 0) console.log('Initialized git repo.');
172
+ }
173
+ if (install) {
174
+ console.log(`Installing with ${pm} ...`);
175
+ runInstall(pm, targetDir);
176
+ }
177
+ console.log(`\nDone. Next:\n cd ${name}\n ${pm === 'npm' ? 'npm run dev' : `${pm} dev`}`);
178
+ return { targetDir, vars, written };
179
+ }
package/src/prompt.js ADDED
@@ -0,0 +1,30 @@
1
+ import readline from 'node:readline';
2
+
3
+ function rl() {
4
+ return readline.createInterface({ input: process.stdin, output: process.stdout });
5
+ }
6
+
7
+ export async function ask(question, def = '') {
8
+ const r = rl();
9
+ const suffix = def ? ` (${def})` : '';
10
+ const ans = await new Promise((resolve) => r.question(`${question}${suffix}: `, resolve));
11
+ r.close();
12
+ const trimmed = ans.trim();
13
+ return trimmed || def;
14
+ }
15
+
16
+ export async function choose(question, options, defIndex = 0) {
17
+ console.log(`\n${question}`);
18
+ options.forEach((o, i) => console.log(` ${i + 1}) ${o}`));
19
+ const ans = await ask(`Enter 1-${options.length}`, String(defIndex + 1));
20
+ const n = parseInt(ans, 10);
21
+ if (Number.isInteger(n) && n >= 1 && n <= options.length) return options[n - 1];
22
+ return options[defIndex];
23
+ }
24
+
25
+ export async function confirm(question, def = true) {
26
+ const hint = def ? 'Y/n' : 'y/N';
27
+ const ans = await ask(`${question} [${hint}]`, '');
28
+ if (!ans) return def;
29
+ return /^y(es)?$/i.test(ans.trim());
30
+ }
@@ -0,0 +1,2 @@
1
+ console.log('Hello from {{name}}!');
2
+ console.log('Built with {{pm}} · {{description}}');
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "0.1.0",
4
+ "description": "{{description}}",
5
+ "author": "{{author}}",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "node index.js"
9
+ }
10
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "variables": []
3
+ }