scaffinity 1.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Deviprasad Rai P
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # ⚡ Scaffinity
2
+
3
+ > Generate, export, and share project structures as portable JSON blueprints — with a single command.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/scaffinity.svg)](https://www.npmjs.com/package/scaffinity)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/devi5040/scaffinity/pulls)
8
+
9
+ ---
10
+
11
+ ## The Problem
12
+
13
+ Every new project starts the same way — manually creating folders, files, config boilerplate. You either do it by hand each time or maintain a growing list of templates nobody on your team can find.
14
+
15
+ **Scaffinity fixes this.** Define your entire project structure in a single JSON file. Generate it anywhere. Share it with anyone.
16
+
17
+ ---
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install -g scaffinity
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Three Commands
28
+
29
+ ### 1. `generate` — JSON blueprint → real project
30
+
31
+ ```bash
32
+ scaffinity generate blueprint.json
33
+ scaffinity generate blueprint.json -o ./my-new-project
34
+ scaffinity generate blueprint.json --dry --verbose # preview first
35
+ ```
36
+
37
+ ### 2. `export` — existing project → JSON blueprint
38
+
39
+ ```bash
40
+ scaffinity export ./my-project -o blueprint.json
41
+ scaffinity export . --ignore dist coverage --depth 4
42
+ ```
43
+
44
+ ### 3. `init` — build a blueprint interactively
45
+
46
+ ```bash
47
+ scaffinity init
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Blueprint Format
53
+
54
+ A blueprint is just a JSON file. Objects are directories, strings or `null` are files.
55
+
56
+ ```json
57
+ {
58
+ "src": {
59
+ "modules": {
60
+ "auth": {
61
+ "auth.controller.ts": "",
62
+ "auth.service.ts": "",
63
+ "auth.routes.ts": ""
64
+ }
65
+ },
66
+ "middleware": {
67
+ "error.middleware.ts": ""
68
+ },
69
+ "main.ts": "import express from 'express'\n\nconst app = express()\n"
70
+ },
71
+ ".env.example": "PORT=3000\nNODE_ENV=development",
72
+ ".gitignore": "node_modules\ndist\n.env\n"
73
+ }
74
+ ```
75
+
76
+ Files with string values get that content written directly. `null` creates an empty file.
77
+
78
+ ---
79
+
80
+ ## Real World Example
81
+
82
+ **Generate a full Express + TypeScript API structure:**
83
+
84
+ ```bash
85
+ # Download the community blueprint
86
+ curl -O https://raw.githubusercontent.com/devi5040/scaffinity/main/examples/express-ts-api.json
87
+
88
+ # Generate the project
89
+ scaffinity generate express-ts-api.json -o ./my-api
90
+ ```
91
+
92
+ Output:
93
+
94
+ ```
95
+ ✅ Blueprint loaded
96
+
97
+ mkdir: my-api/src
98
+ mkdir: my-api/src/modules
99
+ mkdir: my-api/src/modules/auth
100
+ create: my-api/src/modules/auth/auth.controller.ts
101
+ create: my-api/src/modules/auth/auth.service.ts
102
+ ...
103
+
104
+ 📦 Scaffinity Summary
105
+ ────────────────────────────────────────
106
+ ✓ Created : 25 files/dirs
107
+ ────────────────────────────────────────
108
+
109
+ ✅ Project structure generated at: ./my-api
110
+ ```
111
+
112
+ **Export your current project as a shareable blueprint:**
113
+
114
+ ```bash
115
+ scaffinity export ./my-project -o team-blueprint.json
116
+ # Share team-blueprint.json with your team — they run one command to replicate your structure
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Options
122
+
123
+ ### `generate`
124
+
125
+ | Option | Description |
126
+ | -------------------- | --------------------------------------------- |
127
+ | `-o, --output <dir>` | Output directory (default: current directory) |
128
+ | `-d, --dry` | Dry run — preview without creating files |
129
+ | `-v, --verbose` | Show each file/folder being created |
130
+
131
+ ### `export`
132
+
133
+ | Option | Description |
134
+ | ---------------------------- | ---------------------------------------- |
135
+ | `-o, --output <file>` | Save blueprint to file (default: stdout) |
136
+ | `-i, --ignore <patterns...>` | Additional patterns to ignore |
137
+ | `--depth <number>` | Maximum directory depth (default: 10) |
138
+
139
+ ---
140
+
141
+ ## Community Blueprints
142
+
143
+ Ready-made blueprints in the [`/examples`](./examples) folder:
144
+
145
+ | Blueprint | Description |
146
+ | ------------------------------------------------------- | ----------------------------- |
147
+ | [`express-ts-api.json`](./examples/express-ts-api.json) | Express + TypeScript REST API |
148
+ | [`nextjs-app.json`](./examples/nextjs-app.json) | Next.js App Router project |
149
+
150
+ **Have a blueprint to share?** Open a PR and add it to `/examples`.
151
+
152
+ ---
153
+
154
+ ## Programmatic Usage
155
+
156
+ ```typescript
157
+ import { generateCommand, exportCommand } from "scaffinity";
158
+
159
+ // Generate from a blueprint object directly
160
+ await generateCommand("blueprint.json", {
161
+ output: "./my-project",
162
+ verbose: true,
163
+ });
164
+
165
+ // Export a project to blueprint
166
+ await exportCommand("./existing-project", {
167
+ output: "blueprint.json",
168
+ ignore: ["dist", "coverage"],
169
+ });
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Why Scaffinity over Yeoman / degit / framework CLIs?
175
+
176
+ | | Scaffinity | Yeoman | degit | Framework CLIs |
177
+ | ----------------------- | ---------- | ------ | ----- | -------------- |
178
+ | Language agnostic | ✅ | ✅ | ✅ | ❌ |
179
+ | Portable JSON format | ✅ | ❌ | ❌ | ❌ |
180
+ | Export existing project | ✅ | ❌ | ❌ | ❌ |
181
+ | Zero config | ✅ | ❌ | ✅ | ✅ |
182
+ | File content support | ✅ | ✅ | ✅ | ✅ |
183
+ | Shareable blueprint | ✅ | ❌ | ✅ | ❌ |
184
+
185
+ ---
186
+
187
+ ## Contributing
188
+
189
+ PRs are welcome. To add a community blueprint, add a JSON file to `/examples` and open a PR.
190
+
191
+ ```bash
192
+ git clone https://github.com/devi5040/scaffinity
193
+ cd scaffinity
194
+ npm install
195
+ npm run dev -- generate examples/express-ts-api.json --dry --verbose
196
+ ```
197
+
198
+ ---
199
+
200
+ ## License
201
+
202
+ MIT © [Deviprasad Rai](https://github.com/devi5040)
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_chalk4 = __toESM(require("chalk"));
28
+ var import_commander = require("commander");
29
+
30
+ // src/commands/generate.ts
31
+ var import_path = __toESM(require("path"));
32
+ var import_chalk = __toESM(require("chalk"));
33
+ var import_fs_extra = __toESM(require("fs-extra"));
34
+ var import_ora = __toESM(require("ora"));
35
+ function isFileContent(value) {
36
+ return typeof value === "string" || value === null || typeof value === "object" && value != null && "template" in value;
37
+ }
38
+ function resolveContent(value) {
39
+ if (value === null || value === void 0) return "";
40
+ if (typeof value === "string") return value;
41
+ if (typeof value === "object" && "template" in value) {
42
+ const v = value;
43
+ return `//Generated from template: ${v.template}
44
+ // Options: ${JSON.stringify(v.options ?? {}, null, 2)}
45
+ `;
46
+ }
47
+ return "";
48
+ }
49
+ async function processNode(node, currentPath, result, options) {
50
+ for (const [key, value] of Object.entries(node)) {
51
+ const fullPath = import_path.default.join(currentPath, key);
52
+ if (isFileContent(value)) {
53
+ if (options.dry) {
54
+ result.created.push(fullPath);
55
+ if (options.verbose) {
56
+ console.log(
57
+ import_chalk.default.cyan(" [dry] create file:"),
58
+ import_chalk.default.white(fullPath)
59
+ );
60
+ }
61
+ } else {
62
+ try {
63
+ await import_fs_extra.default.ensureDir(import_path.default.dirname(fullPath));
64
+ if (await import_fs_extra.default.pathExists(fullPath)) {
65
+ result.skipped.push(fullPath);
66
+ if (options.verbose)
67
+ console.log(import_chalk.default.yellow(" skip:"), import_chalk.default.white(fullPath));
68
+ } else {
69
+ await import_fs_extra.default.writeFile(fullPath, resolveContent(value), "utf-8");
70
+ result.created.push(fullPath);
71
+ if (options.verbose)
72
+ console.log(import_chalk.default.green(" create:"), import_chalk.default.white(fullPath));
73
+ }
74
+ } catch (error) {
75
+ result.errors.push(fullPath);
76
+ console.log(import_chalk.default.red(" error:"), import_chalk.default.white(fullPath));
77
+ }
78
+ }
79
+ } else if (typeof value === "object" && value !== null) {
80
+ if (!options.dry) await import_fs_extra.default.ensureDir(fullPath);
81
+ if (options.verbose)
82
+ console.log(import_chalk.default.blue(" makedir:"), import_chalk.default.white(fullPath));
83
+ await processNode(value, fullPath, result, options);
84
+ }
85
+ }
86
+ }
87
+ async function generateCommand(jsonFile, options) {
88
+ const spinner = (0, import_ora.default)({
89
+ text: "Reading blueprint...",
90
+ stream: process.stdout
91
+ }).start();
92
+ try {
93
+ const absoluteJson = import_path.default.resolve(process.cwd(), jsonFile);
94
+ if (!await import_fs_extra.default.pathExists(absoluteJson)) {
95
+ spinner.fail(import_chalk.default.red(`Blueprint file not found: ${jsonFile}`));
96
+ process.exit(1);
97
+ }
98
+ const raw = await import_fs_extra.default.readFile(absoluteJson, "utf-8");
99
+ let structre;
100
+ try {
101
+ structre = JSON.parse(raw);
102
+ } catch {
103
+ spinner.fail(import_chalk.default.red("Invalid Json in blueprint file"));
104
+ process.exit(1);
105
+ }
106
+ const outputDir = import_path.default.resolve(process.cwd(), options.output ?? ".");
107
+ await import_fs_extra.default.ensureDir(outputDir);
108
+ spinner.succeed("Blueprint loaded");
109
+ if (options.dry)
110
+ console.log(import_chalk.default.yellow("\nDry-run - no files will be created\n"));
111
+ if (options.verbose) console.log(import_chalk.default.dim("\nProcessing Structure:\n"));
112
+ const result = { created: [], skipped: [], errors: [] };
113
+ await processNode(structre, outputDir, result, options);
114
+ console.log("\n" + import_chalk.default.bold("Scaffinity summary"));
115
+ console.log(import_chalk.default.dim("-".repeat(40)));
116
+ console.log(
117
+ import_chalk.default.green(` \u2713 Created : ${result.created.length} files/dirs`)
118
+ );
119
+ if (result.skipped.length > 0)
120
+ console.log(
121
+ import_chalk.default.yellow(` \u26A0 Skipped : ${result.skipped.length} (already exist)`)
122
+ );
123
+ if (result.errors.length > 0)
124
+ console.log(import_chalk.default.red(` \u2717 Errors : ${result.errors.length}`));
125
+ console.log(import_chalk.default.dim("-".repeat(40)));
126
+ if (!options.dry && result.created.length > 0)
127
+ console.log(
128
+ import_chalk.default.green(
129
+ `
130
+ \u2705 Project structure generated at: ${import_chalk.default.bold(outputDir)}
131
+ `
132
+ )
133
+ );
134
+ } catch (error) {
135
+ spinner.fail(import_chalk.default.red("Generation failed"));
136
+ console.error(error);
137
+ process.exit(1);
138
+ }
139
+ }
140
+
141
+ // src/commands/export.ts
142
+ var import_path2 = __toESM(require("path"));
143
+ var import_fs_extra2 = __toESM(require("fs-extra"));
144
+ var import_chalk2 = __toESM(require("chalk"));
145
+ var import_ora2 = __toESM(require("ora"));
146
+ var DEFAULT_IGNORE = [
147
+ "node_modules",
148
+ ".git",
149
+ "dist",
150
+ "build",
151
+ ".next",
152
+ ".nuxt",
153
+ "coverage",
154
+ ".DS_Store",
155
+ "Thumbs.db",
156
+ "*.log",
157
+ ".env",
158
+ ".env.local"
159
+ ];
160
+ function matchesIgnore(name, ignoreList) {
161
+ return ignoreList.some((pattern) => {
162
+ if (pattern.startsWith("*")) return name.endsWith(pattern.slice(1));
163
+ return name === pattern;
164
+ });
165
+ }
166
+ async function buildStructure(dirPath, ignoreList, currentDepth, maxDepth) {
167
+ const result = {};
168
+ if (currentDepth > maxDepth) return result;
169
+ const entries = await import_fs_extra2.default.readdir(dirPath, { withFileTypes: true });
170
+ for (const entry of entries) {
171
+ if (matchesIgnore(entry.name, ignoreList)) continue;
172
+ const fullPath = import_path2.default.join(dirPath, entry.name);
173
+ if (entry.isDirectory())
174
+ result[entry.name] = await buildStructure(
175
+ fullPath,
176
+ ignoreList,
177
+ currentDepth + 1,
178
+ maxDepth
179
+ );
180
+ else if (entry.isFile()) result[entry.name] = null;
181
+ }
182
+ return result;
183
+ }
184
+ async function exportCommand(projectPath, options) {
185
+ const spinner = (0, import_ora2.default)("Scanning project structure...").start();
186
+ try {
187
+ const absolutePath = import_path2.default.resolve(process.cwd(), projectPath);
188
+ if (!await import_fs_extra2.default.pathExists(absolutePath)) {
189
+ spinner.fail(import_chalk2.default.red(`Path not found: ${projectPath}`));
190
+ process.exit(1);
191
+ }
192
+ const stat = await import_fs_extra2.default.stat(absolutePath);
193
+ if (!stat.isDirectory()) {
194
+ spinner.fail(import_chalk2.default.red(`Path is not a directory: ${projectPath}`));
195
+ process.exit(1);
196
+ }
197
+ const ignoreList = [...DEFAULT_IGNORE, ...options.ignore ?? []];
198
+ const maxDepth = options.depth ?? 10;
199
+ const structure = await buildStructure(
200
+ absolutePath,
201
+ ignoreList,
202
+ 0,
203
+ maxDepth
204
+ );
205
+ spinner.succeed("Structure scanned");
206
+ const json = JSON.stringify(structure, null, 2);
207
+ if (options.output) {
208
+ const outputPath = import_path2.default.resolve(process.cwd(), options.output);
209
+ await import_fs_extra2.default.writeFile(outputPath, json, "utf-8");
210
+ console.log(
211
+ import_chalk2.default.green(`
212
+ \u2705 Blueprint exported to: ${import_chalk2.default.bold(outputPath)}
213
+ `)
214
+ );
215
+ } else
216
+ console.log(json);
217
+ const fileCount = (json.match(/:null/g) ?? []).length;
218
+ const dirCount = Object.keys(structure).length;
219
+ if (options.output) {
220
+ console.log(
221
+ import_chalk2.default.dim(` Directories: ${dirCount} | Files: ${fileCount}`)
222
+ );
223
+ console.log(import_chalk2.default.dim(` Ignored: ${ignoreList.join(", ")}
224
+ `));
225
+ }
226
+ } catch (error) {
227
+ spinner.fail(import_chalk2.default.red("Export failed"));
228
+ console.error(error);
229
+ process.exit(1);
230
+ }
231
+ }
232
+
233
+ // src/commands/init.ts
234
+ var import_chalk3 = __toESM(require("chalk"));
235
+ var import_prompts = __toESM(require("prompts"));
236
+ var import_path3 = __toESM(require("path"));
237
+ var import_fs_extra3 = __toESM(require("fs-extra"));
238
+ async function buildInteractive() {
239
+ const root = {};
240
+ const queue = [{ node: root, currentPath: "/", label: "root" }];
241
+ console.log(import_chalk3.default.bold.cyan("\n Scaffinity Interactive Builder"));
242
+ console.log(import_chalk3.default.dim("Build your project structure step by step.\n"));
243
+ console.log(
244
+ import_chalk3.default.dim("Commands: add file -> type name with extension (e.g. index.ts)")
245
+ );
246
+ console.log(
247
+ import_chalk3.default.dim(" add dir -> type name without extension (e.g. src)")
248
+ );
249
+ console.log(import_chalk3.default.dim(" done -> finish current directory\n"));
250
+ while (queue.length > 0) {
251
+ const current = queue.shift();
252
+ console.log(import_chalk3.default.blue(`
253
+ ${current?.label} (${current?.currentPath})`));
254
+ let continueAdding = true;
255
+ while (continueAdding) {
256
+ const { action } = await (0, import_prompts.default)({
257
+ type: "select",
258
+ name: "action",
259
+ message: `Add to ${import_chalk3.default.cyan(current.label)}:`,
260
+ choices: [
261
+ { title: "Add file", value: "file" },
262
+ { title: "Add directory", value: "dir" },
263
+ { title: "Done with this directory", value: "done" }
264
+ ]
265
+ });
266
+ if (!action || action === "done") {
267
+ continueAdding = false;
268
+ break;
269
+ }
270
+ if (action === "file") {
271
+ const { fileName } = await (0, import_prompts.default)({
272
+ type: "text",
273
+ name: "fileName",
274
+ message: "File name (e.g. index.ts, .env.example):",
275
+ validate: (v) => v.trim().length > 0 ? true : "File name should not be empty"
276
+ });
277
+ if (fileName) {
278
+ const { content } = await (0, import_prompts.default)({
279
+ type: "text",
280
+ name: "content",
281
+ message: "File content (leave empty for blank file):"
282
+ });
283
+ current.node[fileName.trim()] = content?.trim() ?? null;
284
+ console.log(import_chalk3.default.green(` Added file: ${fileName.trim()}`));
285
+ }
286
+ }
287
+ if (action === "dir") {
288
+ const { dirName } = await (0, import_prompts.default)({
289
+ type: "text",
290
+ name: "dirName",
291
+ message: "Directory Name",
292
+ validate: (v) => v.trim().length > 0 ? true : "Directory name cannot be empty"
293
+ });
294
+ if (dirName) {
295
+ const newNode = {};
296
+ current.node[dirName.trim()] = newNode;
297
+ console.log(import_chalk3.default.blue(`Added directory: ${dirName.trim()}`));
298
+ queue.push({
299
+ node: newNode,
300
+ currentPath: import_path3.default.join(current.currentPath, dirName.trim()),
301
+ label: dirName.trim()
302
+ });
303
+ }
304
+ }
305
+ }
306
+ }
307
+ return root;
308
+ }
309
+ async function initCommand() {
310
+ try {
311
+ const structure = await buildInteractive();
312
+ console.log(import_chalk3.default.bold("\n Your blueprint:\n"));
313
+ console.log(import_chalk3.default.dim(JSON.stringify(structure, null, 2)));
314
+ const { outputFile } = await (0, import_prompts.default)({
315
+ type: "text",
316
+ name: "outputFile",
317
+ message: "Save blueprint as: ",
318
+ initial: "scaffold.json"
319
+ });
320
+ if (outputFile) {
321
+ const outputPath = import_path3.default.resolve(process.cwd(), outputFile);
322
+ await import_fs_extra3.default.writeFile(
323
+ outputPath,
324
+ JSON.stringify(structure, null, 2),
325
+ "utf-8"
326
+ );
327
+ console.log(
328
+ import_chalk3.default.green(`
329
+ Blueprint saved to: ${import_chalk3.default.bold(outputPath)}`)
330
+ );
331
+ console.log(
332
+ import_chalk3.default.dim(
333
+ `
334
+ Run: Scaffinity generate ${outputFile} to create your structure
335
+ `
336
+ )
337
+ );
338
+ }
339
+ } catch (error) {
340
+ console.error(import_chalk3.default.red("Init failed"), error);
341
+ process.exit(1);
342
+ }
343
+ }
344
+
345
+ // src/cli.ts
346
+ var program = new import_commander.Command();
347
+ console.log(
348
+ import_chalk4.default.bold.cyan("\n Scaffinity") + import_chalk4.default.dim(" - Project structure generator\n")
349
+ );
350
+ program.name("scaffinity").description(
351
+ "Generate, export, and share project structures as portable JSON blueprints"
352
+ ).version("1.0.0");
353
+ program.command("generate <blueprint>").alias("g").description("Generate project structure from a JSON blueprint file").option(
354
+ "-o, --output <dir>",
355
+ "Output directory (default: current repository)"
356
+ ).option("-d, --dry", "Dry run - preview without creating files").option("-v, --verbose", "Show each file/folder being created").action(
357
+ async (blueprint, options) => {
358
+ await generateCommand(blueprint, {
359
+ output: options.output,
360
+ dry: options.dry ?? false,
361
+ verbose: options.verbose ?? false
362
+ });
363
+ }
364
+ );
365
+ program.command("export <path>").alias("e").description("Export an existing project structure to a JSON blueprint").option("-o, --output <file>", "Save blueprint to file (default: stdout)").option("-i, --ignore <patterns...>", "Additional patterns to ignore").option("--depth <number>", "Maximum directory depth (default: 10)", parseInt).action(
366
+ async (projectPath, options) => {
367
+ await exportCommand(projectPath, {
368
+ output: options.output,
369
+ ignore: options.ignore,
370
+ depth: options.depth
371
+ });
372
+ }
373
+ );
374
+ program.command("init").alias("i").description("Interactively build a JSON blueprint step by step").action(async () => {
375
+ await initCommand();
376
+ });
377
+ program.addHelpText(
378
+ "after",
379
+ `
380
+ ${import_chalk4.default.bold("Examples:")}
381
+ ${import_chalk4.default.cyan("scaffinity generate blueprint.json")} Generate from blueprint
382
+ ${import_chalk4.default.cyan("scaffinity generate blueprint.json -o ./app")} Generate into specific folder
383
+ ${import_chalk4.default.cyan("scaffinity export ./my-project -o out.json")} Export existing project
384
+ ${import_chalk4.default.cyan("scaffinity init")} Interactive builder
385
+
386
+ ${import_chalk4.default.bold("Blueprint format:")}
387
+ ${import_chalk4.default.dim("{")}
388
+ ${import_chalk4.default.dim(' "src": {')}
389
+ ${import_chalk4.default.dim(' "index.ts":"",')}
390
+ ${import_chalk4.default.dim(' "utils":{')}
391
+ ${import_chalk4.default.dim(' "helper.ts":"// Your content here"')}
392
+ ${import_chalk4.default.dim(" }")}
393
+ ${import_chalk4.default.dim(" },")}
394
+ ${import_chalk4.default.dim(' "env.example":"PORT=3000\\nNODE_ENV=development"')}
395
+ ${import_chalk4.default.dim("}")}
396
+
397
+ ${import_chalk4.default.dim("Github: https://github.com/devi5040/scaffinity")}`
398
+ );
399
+ program.parse(process.argv);
400
+ if (!process.argv.slice(2).length) program.outputHelp;
@@ -0,0 +1,30 @@
1
+ type FileContent = string | {
2
+ template: string;
3
+ options?: Record<string, unknown>;
4
+ };
5
+ type StructureNode = {
6
+ [key: string]: StructureNode | FileContent | null;
7
+ };
8
+ interface GenerateOptions {
9
+ output?: string;
10
+ dry?: boolean;
11
+ verbose?: boolean;
12
+ }
13
+ interface ExportOptions {
14
+ output?: string;
15
+ ignore?: string[];
16
+ depth?: number;
17
+ }
18
+ interface ScaffoldResult {
19
+ created: string[];
20
+ skipped: string[];
21
+ errors: string[];
22
+ }
23
+
24
+ declare function generateCommand(jsonFile: string, options: GenerateOptions): Promise<void>;
25
+
26
+ declare function exportCommand(projectPath: string, options: ExportOptions): Promise<void>;
27
+
28
+ declare function initCommand(): Promise<void>;
29
+
30
+ export { type ExportOptions, type GenerateOptions, type ScaffoldResult, type StructureNode, exportCommand, generateCommand, initCommand };
package/dist/index.js ADDED
@@ -0,0 +1,358 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ exportCommand: () => exportCommand,
34
+ generateCommand: () => generateCommand,
35
+ initCommand: () => initCommand
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/commands/generate.ts
40
+ var import_path = __toESM(require("path"));
41
+ var import_chalk = __toESM(require("chalk"));
42
+ var import_fs_extra = __toESM(require("fs-extra"));
43
+ var import_ora = __toESM(require("ora"));
44
+ function isFileContent(value) {
45
+ return typeof value === "string" || value === null || typeof value === "object" && value != null && "template" in value;
46
+ }
47
+ function resolveContent(value) {
48
+ if (value === null || value === void 0) return "";
49
+ if (typeof value === "string") return value;
50
+ if (typeof value === "object" && "template" in value) {
51
+ const v = value;
52
+ return `//Generated from template: ${v.template}
53
+ // Options: ${JSON.stringify(v.options ?? {}, null, 2)}
54
+ `;
55
+ }
56
+ return "";
57
+ }
58
+ async function processNode(node, currentPath, result, options) {
59
+ for (const [key, value] of Object.entries(node)) {
60
+ const fullPath = import_path.default.join(currentPath, key);
61
+ if (isFileContent(value)) {
62
+ if (options.dry) {
63
+ result.created.push(fullPath);
64
+ if (options.verbose) {
65
+ console.log(
66
+ import_chalk.default.cyan(" [dry] create file:"),
67
+ import_chalk.default.white(fullPath)
68
+ );
69
+ }
70
+ } else {
71
+ try {
72
+ await import_fs_extra.default.ensureDir(import_path.default.dirname(fullPath));
73
+ if (await import_fs_extra.default.pathExists(fullPath)) {
74
+ result.skipped.push(fullPath);
75
+ if (options.verbose)
76
+ console.log(import_chalk.default.yellow(" skip:"), import_chalk.default.white(fullPath));
77
+ } else {
78
+ await import_fs_extra.default.writeFile(fullPath, resolveContent(value), "utf-8");
79
+ result.created.push(fullPath);
80
+ if (options.verbose)
81
+ console.log(import_chalk.default.green(" create:"), import_chalk.default.white(fullPath));
82
+ }
83
+ } catch (error) {
84
+ result.errors.push(fullPath);
85
+ console.log(import_chalk.default.red(" error:"), import_chalk.default.white(fullPath));
86
+ }
87
+ }
88
+ } else if (typeof value === "object" && value !== null) {
89
+ if (!options.dry) await import_fs_extra.default.ensureDir(fullPath);
90
+ if (options.verbose)
91
+ console.log(import_chalk.default.blue(" makedir:"), import_chalk.default.white(fullPath));
92
+ await processNode(value, fullPath, result, options);
93
+ }
94
+ }
95
+ }
96
+ async function generateCommand(jsonFile, options) {
97
+ const spinner = (0, import_ora.default)({
98
+ text: "Reading blueprint...",
99
+ stream: process.stdout
100
+ }).start();
101
+ try {
102
+ const absoluteJson = import_path.default.resolve(process.cwd(), jsonFile);
103
+ if (!await import_fs_extra.default.pathExists(absoluteJson)) {
104
+ spinner.fail(import_chalk.default.red(`Blueprint file not found: ${jsonFile}`));
105
+ process.exit(1);
106
+ }
107
+ const raw = await import_fs_extra.default.readFile(absoluteJson, "utf-8");
108
+ let structre;
109
+ try {
110
+ structre = JSON.parse(raw);
111
+ } catch {
112
+ spinner.fail(import_chalk.default.red("Invalid Json in blueprint file"));
113
+ process.exit(1);
114
+ }
115
+ const outputDir = import_path.default.resolve(process.cwd(), options.output ?? ".");
116
+ await import_fs_extra.default.ensureDir(outputDir);
117
+ spinner.succeed("Blueprint loaded");
118
+ if (options.dry)
119
+ console.log(import_chalk.default.yellow("\nDry-run - no files will be created\n"));
120
+ if (options.verbose) console.log(import_chalk.default.dim("\nProcessing Structure:\n"));
121
+ const result = { created: [], skipped: [], errors: [] };
122
+ await processNode(structre, outputDir, result, options);
123
+ console.log("\n" + import_chalk.default.bold("Scaffinity summary"));
124
+ console.log(import_chalk.default.dim("-".repeat(40)));
125
+ console.log(
126
+ import_chalk.default.green(` \u2713 Created : ${result.created.length} files/dirs`)
127
+ );
128
+ if (result.skipped.length > 0)
129
+ console.log(
130
+ import_chalk.default.yellow(` \u26A0 Skipped : ${result.skipped.length} (already exist)`)
131
+ );
132
+ if (result.errors.length > 0)
133
+ console.log(import_chalk.default.red(` \u2717 Errors : ${result.errors.length}`));
134
+ console.log(import_chalk.default.dim("-".repeat(40)));
135
+ if (!options.dry && result.created.length > 0)
136
+ console.log(
137
+ import_chalk.default.green(
138
+ `
139
+ \u2705 Project structure generated at: ${import_chalk.default.bold(outputDir)}
140
+ `
141
+ )
142
+ );
143
+ } catch (error) {
144
+ spinner.fail(import_chalk.default.red("Generation failed"));
145
+ console.error(error);
146
+ process.exit(1);
147
+ }
148
+ }
149
+
150
+ // src/commands/export.ts
151
+ var import_path2 = __toESM(require("path"));
152
+ var import_fs_extra2 = __toESM(require("fs-extra"));
153
+ var import_chalk2 = __toESM(require("chalk"));
154
+ var import_ora2 = __toESM(require("ora"));
155
+ var DEFAULT_IGNORE = [
156
+ "node_modules",
157
+ ".git",
158
+ "dist",
159
+ "build",
160
+ ".next",
161
+ ".nuxt",
162
+ "coverage",
163
+ ".DS_Store",
164
+ "Thumbs.db",
165
+ "*.log",
166
+ ".env",
167
+ ".env.local"
168
+ ];
169
+ function matchesIgnore(name, ignoreList) {
170
+ return ignoreList.some((pattern) => {
171
+ if (pattern.startsWith("*")) return name.endsWith(pattern.slice(1));
172
+ return name === pattern;
173
+ });
174
+ }
175
+ async function buildStructure(dirPath, ignoreList, currentDepth, maxDepth) {
176
+ const result = {};
177
+ if (currentDepth > maxDepth) return result;
178
+ const entries = await import_fs_extra2.default.readdir(dirPath, { withFileTypes: true });
179
+ for (const entry of entries) {
180
+ if (matchesIgnore(entry.name, ignoreList)) continue;
181
+ const fullPath = import_path2.default.join(dirPath, entry.name);
182
+ if (entry.isDirectory())
183
+ result[entry.name] = await buildStructure(
184
+ fullPath,
185
+ ignoreList,
186
+ currentDepth + 1,
187
+ maxDepth
188
+ );
189
+ else if (entry.isFile()) result[entry.name] = null;
190
+ }
191
+ return result;
192
+ }
193
+ async function exportCommand(projectPath, options) {
194
+ const spinner = (0, import_ora2.default)("Scanning project structure...").start();
195
+ try {
196
+ const absolutePath = import_path2.default.resolve(process.cwd(), projectPath);
197
+ if (!await import_fs_extra2.default.pathExists(absolutePath)) {
198
+ spinner.fail(import_chalk2.default.red(`Path not found: ${projectPath}`));
199
+ process.exit(1);
200
+ }
201
+ const stat = await import_fs_extra2.default.stat(absolutePath);
202
+ if (!stat.isDirectory()) {
203
+ spinner.fail(import_chalk2.default.red(`Path is not a directory: ${projectPath}`));
204
+ process.exit(1);
205
+ }
206
+ const ignoreList = [...DEFAULT_IGNORE, ...options.ignore ?? []];
207
+ const maxDepth = options.depth ?? 10;
208
+ const structure = await buildStructure(
209
+ absolutePath,
210
+ ignoreList,
211
+ 0,
212
+ maxDepth
213
+ );
214
+ spinner.succeed("Structure scanned");
215
+ const json = JSON.stringify(structure, null, 2);
216
+ if (options.output) {
217
+ const outputPath = import_path2.default.resolve(process.cwd(), options.output);
218
+ await import_fs_extra2.default.writeFile(outputPath, json, "utf-8");
219
+ console.log(
220
+ import_chalk2.default.green(`
221
+ \u2705 Blueprint exported to: ${import_chalk2.default.bold(outputPath)}
222
+ `)
223
+ );
224
+ } else
225
+ console.log(json);
226
+ const fileCount = (json.match(/:null/g) ?? []).length;
227
+ const dirCount = Object.keys(structure).length;
228
+ if (options.output) {
229
+ console.log(
230
+ import_chalk2.default.dim(` Directories: ${dirCount} | Files: ${fileCount}`)
231
+ );
232
+ console.log(import_chalk2.default.dim(` Ignored: ${ignoreList.join(", ")}
233
+ `));
234
+ }
235
+ } catch (error) {
236
+ spinner.fail(import_chalk2.default.red("Export failed"));
237
+ console.error(error);
238
+ process.exit(1);
239
+ }
240
+ }
241
+
242
+ // src/commands/init.ts
243
+ var import_chalk3 = __toESM(require("chalk"));
244
+ var import_prompts = __toESM(require("prompts"));
245
+ var import_path3 = __toESM(require("path"));
246
+ var import_fs_extra3 = __toESM(require("fs-extra"));
247
+ async function buildInteractive() {
248
+ const root = {};
249
+ const queue = [{ node: root, currentPath: "/", label: "root" }];
250
+ console.log(import_chalk3.default.bold.cyan("\n Scaffinity Interactive Builder"));
251
+ console.log(import_chalk3.default.dim("Build your project structure step by step.\n"));
252
+ console.log(
253
+ import_chalk3.default.dim("Commands: add file -> type name with extension (e.g. index.ts)")
254
+ );
255
+ console.log(
256
+ import_chalk3.default.dim(" add dir -> type name without extension (e.g. src)")
257
+ );
258
+ console.log(import_chalk3.default.dim(" done -> finish current directory\n"));
259
+ while (queue.length > 0) {
260
+ const current = queue.shift();
261
+ console.log(import_chalk3.default.blue(`
262
+ ${current?.label} (${current?.currentPath})`));
263
+ let continueAdding = true;
264
+ while (continueAdding) {
265
+ const { action } = await (0, import_prompts.default)({
266
+ type: "select",
267
+ name: "action",
268
+ message: `Add to ${import_chalk3.default.cyan(current.label)}:`,
269
+ choices: [
270
+ { title: "Add file", value: "file" },
271
+ { title: "Add directory", value: "dir" },
272
+ { title: "Done with this directory", value: "done" }
273
+ ]
274
+ });
275
+ if (!action || action === "done") {
276
+ continueAdding = false;
277
+ break;
278
+ }
279
+ if (action === "file") {
280
+ const { fileName } = await (0, import_prompts.default)({
281
+ type: "text",
282
+ name: "fileName",
283
+ message: "File name (e.g. index.ts, .env.example):",
284
+ validate: (v) => v.trim().length > 0 ? true : "File name should not be empty"
285
+ });
286
+ if (fileName) {
287
+ const { content } = await (0, import_prompts.default)({
288
+ type: "text",
289
+ name: "content",
290
+ message: "File content (leave empty for blank file):"
291
+ });
292
+ current.node[fileName.trim()] = content?.trim() ?? null;
293
+ console.log(import_chalk3.default.green(` Added file: ${fileName.trim()}`));
294
+ }
295
+ }
296
+ if (action === "dir") {
297
+ const { dirName } = await (0, import_prompts.default)({
298
+ type: "text",
299
+ name: "dirName",
300
+ message: "Directory Name",
301
+ validate: (v) => v.trim().length > 0 ? true : "Directory name cannot be empty"
302
+ });
303
+ if (dirName) {
304
+ const newNode = {};
305
+ current.node[dirName.trim()] = newNode;
306
+ console.log(import_chalk3.default.blue(`Added directory: ${dirName.trim()}`));
307
+ queue.push({
308
+ node: newNode,
309
+ currentPath: import_path3.default.join(current.currentPath, dirName.trim()),
310
+ label: dirName.trim()
311
+ });
312
+ }
313
+ }
314
+ }
315
+ }
316
+ return root;
317
+ }
318
+ async function initCommand() {
319
+ try {
320
+ const structure = await buildInteractive();
321
+ console.log(import_chalk3.default.bold("\n Your blueprint:\n"));
322
+ console.log(import_chalk3.default.dim(JSON.stringify(structure, null, 2)));
323
+ const { outputFile } = await (0, import_prompts.default)({
324
+ type: "text",
325
+ name: "outputFile",
326
+ message: "Save blueprint as: ",
327
+ initial: "scaffold.json"
328
+ });
329
+ if (outputFile) {
330
+ const outputPath = import_path3.default.resolve(process.cwd(), outputFile);
331
+ await import_fs_extra3.default.writeFile(
332
+ outputPath,
333
+ JSON.stringify(structure, null, 2),
334
+ "utf-8"
335
+ );
336
+ console.log(
337
+ import_chalk3.default.green(`
338
+ Blueprint saved to: ${import_chalk3.default.bold(outputPath)}`)
339
+ );
340
+ console.log(
341
+ import_chalk3.default.dim(
342
+ `
343
+ Run: Scaffinity generate ${outputFile} to create your structure
344
+ `
345
+ )
346
+ );
347
+ }
348
+ } catch (error) {
349
+ console.error(import_chalk3.default.red("Init failed"), error);
350
+ process.exit(1);
351
+ }
352
+ }
353
+ // Annotate the CommonJS export names for ESM import in node:
354
+ 0 && (module.exports = {
355
+ exportCommand,
356
+ generateCommand,
357
+ initCommand
358
+ });
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "scaffinity",
3
+ "version": "1.0.1",
4
+ "description": "Generate, export, and share project structures as portable JSON blueprints",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "scaffinity": "dist/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup src/cli.ts src/index.ts --format cjs --dts",
12
+ "dev": "ts-node src/cli.ts",
13
+ "start": "node dist/cli.js"
14
+ },
15
+ "keywords": [
16
+ "scaffinity",
17
+ "cli",
18
+ "boilerplate",
19
+ "project-structure",
20
+ "generator",
21
+ "typescript",
22
+ "blueprint"
23
+ ],
24
+ "author": "Deviprasad Rai <dpraidola@gmail.com>",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "chalk": "^5.6.2",
28
+ "commander": "^15.0.0",
29
+ "fs-extra": "^11.3.5",
30
+ "ora": "^9.4.1",
31
+ "prompts": "^2.4.2"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^26.0.1",
35
+ "@types/fs-extra": "^11.0.4",
36
+ "@types/prompts": "^2.4.9",
37
+ "ts-node": "^10.9.2",
38
+ "tsup": "^8.5.1",
39
+ "typescript": "^6.0.3"
40
+ },
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "README.md",
47
+ "LICENSE"
48
+ ]
49
+ }