@timeax/scaffold 0.0.9 → 0.0.11

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@timeax/scaffold",
3
3
  "private": false,
4
- "version": "0.0.9",
4
+ "version": "0.0.11",
5
5
  "description": "A CLI tool that scaffolds project file structures based on a user-defined, type-safe configuration file.",
6
6
  "keywords": [
7
7
  "scaffold"
@@ -44,6 +44,7 @@
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^24.10.1",
47
+ "prettier": "^3.7.1",
47
48
  "tsup": "^8.5.1",
48
49
  "typescript": "^5.9.3",
49
50
  "vitest": "^4.0.14"
package/src/cli/main.ts CHANGED
@@ -1,286 +1,285 @@
1
- #!/usr/bin/env node
2
- // noinspection RequiredAttributes
3
-
4
- import readline from 'readline';
5
- import path from 'path';
6
- import fs from 'fs';
7
- import { Command } from 'commander';
8
- import { runOnce, RunOptions } from '../core/runner';
9
- import { watchScaffold } from '../core/watcher';
10
- import {
11
- ensureStructureFilesFromConfig,
12
- scanDirectoryToStructureText,
13
- writeScannedStructuresFromConfig,
14
- } from '../core/scan-structure';
15
- import { initScaffold } from '../core/init-scaffold';
16
- import {
17
- defaultLogger,
18
- Logger,
19
-
20
- } from '../util/logger';
21
- import { ensureDirSync } from '../util/fs-utils';
22
- import {SCAFFOLD_ROOT_DIR} from "../schema";
23
-
24
- interface BaseCliOptions {
25
- config?: string;
26
- dir?: string;
27
- watch?: boolean;
28
- quiet?: boolean;
29
- debug?: boolean;
30
- }
31
-
32
- interface ScanCliOptions {
33
- root?: string;
34
- out?: string;
35
- ignore?: string[];
36
- fromConfig?: boolean;
37
- groups?: string[];
38
- }
39
-
40
- interface InitCliOptions {
41
- force?: boolean;
42
- }
43
-
44
- /**
45
- * Create a logger with the appropriate level from CLI flags.
46
- */
47
- function createCliLogger(opts: { quiet?: boolean; debug?: boolean }): Logger {
48
- if (opts.quiet) {
49
- defaultLogger.setLevel('silent');
50
- } else if (opts.debug) {
51
- defaultLogger.setLevel('debug');
52
- }
53
- return defaultLogger.child('[cli]');
54
- }
55
-
56
- function askYesNo(question: string): Promise<'delete' | 'keep'> {
57
- const rl = readline.createInterface({
58
- input: process.stdin,
59
- output: process.stdout,
60
- });
61
-
62
- return new Promise((resolve) => {
63
- rl.question(`${question} [y/N] `, (answer) => {
64
- rl.close();
65
- const val = answer.trim().toLowerCase();
66
- if (val === 'y' || val === 'yes') {
67
- resolve('delete');
68
- } else {
69
- resolve('keep');
70
- }
71
- });
72
- });
73
- }
74
-
75
- async function handleRunCommand(cwd: string, baseOpts: BaseCliOptions) {
76
- const logger = createCliLogger(baseOpts);
77
-
78
- const configPath = baseOpts.config
79
- ? path.resolve(cwd, baseOpts.config)
80
- : undefined;
81
- const scaffoldDir = baseOpts.dir
82
- ? path.resolve(cwd, baseOpts.dir)
83
- : undefined;
84
-
85
- logger.debug(
86
- `Starting scaffold (cwd=${cwd}, config=${configPath ?? 'auto'}, dir=${scaffoldDir ?? 'scaffold/'
87
- }, watch=${baseOpts.watch ? 'yes' : 'no'})`,
88
- );
89
-
90
- const runnerOptions: RunOptions = {
91
- configPath,
92
- scaffoldDir,
93
- logger,
94
- interactiveDelete: async ({
95
- relativePath,
96
- size,
97
- createdByStub,
98
- groupName,
99
- }) => {
100
- const sizeKb = (size / 1024).toFixed(1);
101
- const stubInfo = createdByStub ? ` (stub: ${createdByStub})` : '';
102
- const groupInfo = groupName ? ` [group: ${groupName}]` : '';
103
- const question =
104
- `File "${relativePath}"${groupInfo} is ~${sizeKb}KB and no longer in structure${stubInfo}. Delete it?`;
105
-
106
- return askYesNo(question);
107
- },
108
- };
109
-
110
- if (baseOpts.watch) {
111
- // Watch mode – this will not return
112
- watchScaffold(cwd, runnerOptions);
113
- } else {
114
- await runOnce(cwd, runnerOptions);
115
- }
116
- }
117
-
118
- async function handleScanCommand(
119
- cwd: string,
120
- scanOpts: ScanCliOptions,
121
- baseOpts: BaseCliOptions,
122
- ) {
123
- const logger = createCliLogger(baseOpts);
124
-
125
- const useConfigMode =
126
- scanOpts.fromConfig || (!scanOpts.root && !scanOpts.out);
127
-
128
- if (useConfigMode) {
129
- logger.info('Scanning project using scaffold config/groups...');
130
- await writeScannedStructuresFromConfig(cwd, {
131
- ignore: scanOpts.ignore,
132
- groups: scanOpts.groups,
133
- });
134
- return;
135
- }
136
-
137
- // Manual single-root mode
138
- const rootDir = path.resolve(cwd, scanOpts.root ?? '.');
139
- const ignore = scanOpts.ignore ?? [];
140
-
141
- logger.info(`Scanning directory for structure: ${rootDir}`);
142
- const text = scanDirectoryToStructureText(rootDir, {
143
- ignore,
144
- });
145
-
146
- if (scanOpts.out) {
147
- const outPath = path.resolve(cwd, scanOpts.out);
148
- const dir = path.dirname(outPath);
149
- ensureDirSync(dir);
150
- fs.writeFileSync(outPath, text, 'utf8');
151
- logger.info(`Wrote structure to ${outPath}`);
152
- } else {
153
- process.stdout.write(text + '\n');
154
- }
155
- }
156
-
157
- async function handleInitCommand(
158
- cwd: string,
159
- initOpts: InitCliOptions,
160
- baseOpts: BaseCliOptions,
161
- ) {
162
- const logger = createCliLogger(baseOpts);
163
-
164
- const scaffoldDirRel = baseOpts.dir ?? SCAFFOLD_ROOT_DIR;
165
-
166
- logger.info(`Initializing scaffold directory at "${scaffoldDirRel}"...`);
167
-
168
- const result = await initScaffold(cwd, {
169
- scaffoldDir: scaffoldDirRel,
170
- force: initOpts.force,
171
- });
172
-
173
- logger.info(
174
- `Done. Config: ${result.configPath}, Structure: ${result.structurePath}`,
175
- );
176
- }
177
-
178
-
179
- async function handleStructuresCommand(
180
- cwd: string,
181
- baseOpts: BaseCliOptions,
182
- ) {
183
- const logger = createCliLogger(baseOpts);
184
-
185
- logger.info('Ensuring structure files declared in config exist...');
186
-
187
- const { created, existing } = await ensureStructureFilesFromConfig(cwd, {
188
- scaffoldDirOverride: baseOpts.dir,
189
- });
190
-
191
- if (created.length === 0) {
192
- logger.info('All structure files already exist. Nothing to do.');
193
- } else {
194
- for (const filePath of created) {
195
- logger.info(`Created structure file: ${filePath}`);
196
- }
197
- }
198
-
199
- existing.forEach((p) => logger.debug(`Structure file already exists: ${p}`));
200
- }
201
-
202
- async function main() {
203
- const cwd = process.cwd();
204
-
205
- const program = new Command();
206
-
207
- program
208
- .name('scaffold')
209
- .description('@timeax/scaffold – structure-based project scaffolding')
210
- // global-ish options used by base + scan + init
211
- .option('-c, --config <path>', 'Path to scaffold config file')
212
- .option('-d, --dir <path>', 'Path to scaffold directory (default: ./scaffold)')
213
- .option('-w, --watch', 'Watch scaffold directory for changes')
214
- .option('--quiet', 'Silence logs')
215
- .option('--debug', 'Enable debug logging');
216
-
217
- // scan subcommand
218
- program
219
- .command('scan')
220
- .description(
221
- 'Generate structure.txt-style output (config-aware by default, or manual root/out)',
222
- )
223
- .option(
224
- '--from-config',
225
- 'Scan based on scaffold config/groups and write structure files into scaffold/ (default if no root/out specified)',
226
- )
227
- .option(
228
- '-r, --root <path>',
229
- 'Root directory to scan (manual mode)',
230
- )
231
- .option(
232
- '-o, --out <path>',
233
- 'Output file path (manual mode)',
234
- )
235
- .option(
236
- '--ignore <patterns...>',
237
- 'Additional glob patterns to ignore (relative to root)',
238
- )
239
- .option(
240
- '--groups <names...>',
241
- 'Limit config-based scanning to specific groups (by name)',
242
- )
243
- .action(async (scanOpts: ScanCliOptions, cmd: Command) => {
244
- const baseOpts = cmd.parent?.opts<BaseCliOptions>() ?? {};
245
- await handleScanCommand(cwd, scanOpts, baseOpts);
246
- });
247
-
248
- // init subcommand
249
- program
250
- .command('init')
251
- .description('Initialize scaffold folder and config/structure files')
252
- .option(
253
- '--force',
254
- 'Overwrite existing config/structure files if they already exist',
255
- )
256
- .action(async (initOpts: InitCliOptions, cmd: Command) => {
257
- const baseOpts = cmd.parent?.opts<BaseCliOptions>() ?? {};
258
- await handleInitCommand(cwd, initOpts, baseOpts);
259
- });
260
-
261
- // Base command: run scaffold once or in watch mode
262
- program.action(async (opts: BaseCliOptions) => {
263
- await handleRunCommand(cwd, opts);
264
- });
265
-
266
- interface StructuresCliOptions { }
267
-
268
- program
269
- .command('structures')
270
- .description(
271
- 'Create missing structure files specified in the config (does not overwrite existing files)',
272
- )
273
- .action(async (_opts: StructuresCliOptions, cmd: Command) => {
274
- const baseOpts = cmd.parent?.opts<BaseCliOptions>() ?? {};
275
- await handleStructuresCommand(cwd, baseOpts);
276
- });
277
-
278
- await program.parseAsync(process.argv);
279
- }
280
-
281
-
282
- // Run and handle errors
283
- main().catch((err) => {
284
- defaultLogger.error(err);
285
- process.exit(1);
286
- });
1
+ #!/usr/bin/env node
2
+ // noinspection RequiredAttributes
3
+
4
+ import readline from "readline";
5
+ import path from "path";
6
+ import fs from "fs";
7
+ import { Command } from "commander";
8
+ import { runOnce, type RunOptions } from "../core/runner";
9
+ import { watchScaffold } from "../core/watcher";
10
+ import {
11
+ ensureStructureFilesFromConfig,
12
+ scanDirectoryToStructureText,
13
+ writeScannedStructuresFromConfig,
14
+ } from "../core/scan-structure";
15
+ import { initScaffold } from "../core/init-scaffold";
16
+ import { defaultLogger, type Logger } from "../util/logger";
17
+ import { ensureDirSync } from "../util/fs-utils";
18
+ import { SCAFFOLD_ROOT_DIR } from "../schema";
19
+
20
+ interface BaseCliOptions {
21
+ config?: string;
22
+ dir?: string;
23
+ watch?: boolean;
24
+ quiet?: boolean;
25
+ debug?: boolean;
26
+ }
27
+
28
+ interface ScanCliOptions {
29
+ root?: string;
30
+ out?: string;
31
+ ignore?: string[];
32
+ fromConfig?: boolean;
33
+ groups?: string[];
34
+ maxDepth?: number;
35
+ }
36
+
37
+ interface InitCliOptions {
38
+ force?: boolean;
39
+ }
40
+
41
+ interface StructuresCliOptions {} // reserved for future options
42
+
43
+ /**
44
+ * Create a logger with the appropriate level from CLI flags.
45
+ */
46
+ function createCliLogger(opts: { quiet?: boolean; debug?: boolean }): Logger {
47
+ if (opts.quiet) {
48
+ defaultLogger.setLevel("silent");
49
+ } else if (opts.debug) {
50
+ defaultLogger.setLevel("debug");
51
+ }
52
+ return defaultLogger.child("[cli]");
53
+ }
54
+
55
+ function askYesNo(question: string): Promise<"delete" | "keep"> {
56
+ const rl = readline.createInterface({
57
+ input: process.stdin,
58
+ output: process.stdout,
59
+ });
60
+
61
+ return new Promise((resolve) => {
62
+ rl.question(`${question} [y/N] `, (answer) => {
63
+ rl.close();
64
+ const val = answer.trim().toLowerCase();
65
+ if (val === "y" || val === "yes") {
66
+ resolve("delete");
67
+ } else {
68
+ resolve("keep");
69
+ }
70
+ });
71
+ });
72
+ }
73
+
74
+ async function handleRunCommand(cwd: string, baseOpts: BaseCliOptions) {
75
+ const logger = createCliLogger(baseOpts);
76
+
77
+ const configPath = baseOpts.config
78
+ ? path.resolve(cwd, baseOpts.config)
79
+ : undefined;
80
+
81
+ // NOTE: scaffoldDir is optional – if omitted, runOnce/loadScaffoldConfig
82
+ // will default to SCAFFOLD_ROOT_DIR.
83
+ const scaffoldDir = baseOpts.dir
84
+ ? path.resolve(cwd, baseOpts.dir)
85
+ : undefined;
86
+
87
+ const resolvedScaffoldDir =
88
+ scaffoldDir ?? path.resolve(cwd, SCAFFOLD_ROOT_DIR);
89
+
90
+ logger.debug(
91
+ `Starting scaffold (cwd=${cwd}, config=${configPath ?? "auto"}, dir=${resolvedScaffoldDir}, watch=${baseOpts.watch ? "yes" : "no"})`,
92
+ );
93
+
94
+ const runnerOptions: RunOptions = {
95
+ configPath,
96
+ scaffoldDir,
97
+ logger,
98
+ interactiveDelete: async ({
99
+ relativePath,
100
+ size,
101
+ createdByStub,
102
+ groupName,
103
+ }) => {
104
+ const sizeKb = (size / 1024).toFixed(1);
105
+ const stubInfo = createdByStub ? ` (stub: ${createdByStub})` : "";
106
+ const groupInfo = groupName ? ` [group: ${groupName}]` : "";
107
+ const question = `File "${relativePath}"${groupInfo} is ~${sizeKb}KB and no longer in structure${stubInfo}. Delete it?`;
108
+
109
+ return askYesNo(question);
110
+ },
111
+ };
112
+
113
+ if (baseOpts.watch) {
114
+ // Watch mode – this will not return
115
+ watchScaffold(cwd, runnerOptions);
116
+ } else {
117
+ await runOnce(cwd, runnerOptions);
118
+ }
119
+ }
120
+
121
+ async function handleScanCommand(
122
+ cwd: string,
123
+ scanOpts: ScanCliOptions,
124
+ baseOpts: BaseCliOptions,
125
+ ) {
126
+ const logger = createCliLogger(baseOpts);
127
+
128
+ const useConfigMode =
129
+ scanOpts.fromConfig || (!scanOpts.root && !scanOpts.out);
130
+
131
+ if (useConfigMode) {
132
+ logger.info("Scanning project using scaffold config/groups...");
133
+ await writeScannedStructuresFromConfig(cwd, {
134
+ ignore: scanOpts.ignore,
135
+ groups: scanOpts.groups,
136
+ scaffoldDir: baseOpts.dir,
137
+ maxDepth: scanOpts.maxDepth
138
+ });
139
+ return;
140
+ }
141
+
142
+ // Manual single-root mode
143
+ const rootDir = path.resolve(cwd, scanOpts.root ?? ".");
144
+ const ignore = scanOpts.ignore ?? [];
145
+
146
+ logger.info(`Scanning directory for structure: ${rootDir}`);
147
+ const text = scanDirectoryToStructureText(rootDir, {
148
+ ignore,
149
+ });
150
+
151
+ if (scanOpts.out) {
152
+ const outPath = path.resolve(cwd, scanOpts.out);
153
+ const dir = path.dirname(outPath);
154
+ ensureDirSync(dir);
155
+ fs.writeFileSync(outPath, text, "utf8");
156
+ logger.info(`Wrote structure to ${outPath}`);
157
+ } else {
158
+ process.stdout.write(text + "\n");
159
+ }
160
+ }
161
+
162
+ async function handleInitCommand(
163
+ cwd: string,
164
+ initOpts: InitCliOptions,
165
+ baseOpts: BaseCliOptions,
166
+ ) {
167
+ const logger = createCliLogger(baseOpts);
168
+
169
+ const scaffoldDirRel = baseOpts.dir ?? SCAFFOLD_ROOT_DIR;
170
+
171
+ logger.info(`Initializing scaffold directory at "${scaffoldDirRel}"...`);
172
+
173
+ const result = await initScaffold(cwd, {
174
+ scaffoldDir: scaffoldDirRel,
175
+ force: initOpts.force,
176
+ });
177
+
178
+ logger.info(
179
+ `Done. Config: ${result.configPath}, Structure: ${result.structurePath}`,
180
+ );
181
+ }
182
+
183
+ async function handleStructuresCommand(cwd: string, baseOpts: BaseCliOptions) {
184
+ const logger = createCliLogger(baseOpts);
185
+
186
+ logger.info("Ensuring structure files declared in config exist...");
187
+
188
+ const { created, existing } = await ensureStructureFilesFromConfig(cwd, {
189
+ scaffoldDirOverride: baseOpts.dir,
190
+ });
191
+
192
+ if (created.length === 0) {
193
+ logger.info("All structure files already exist. Nothing to do.");
194
+ } else {
195
+ for (const filePath of created) {
196
+ logger.info(`Created structure file: ${filePath}`);
197
+ }
198
+ }
199
+
200
+ existing.forEach((p) => logger.debug(`Structure file already exists: ${p}`));
201
+ }
202
+
203
+ async function main() {
204
+ const cwd = process.cwd();
205
+
206
+ const program = new Command();
207
+
208
+ program
209
+ .name("scaffold")
210
+ .description("@timeax/scaffold – structure-based project scaffolding")
211
+ // global-ish options used by base + scan + init + structures
212
+ .option("-c, --config <path>", "Path to scaffold config file")
213
+ .option(
214
+ "-d, --dir <path>",
215
+ `Path to scaffold directory (default: ./${SCAFFOLD_ROOT_DIR})`,
216
+ )
217
+ .option("-w, --watch", "Watch scaffold directory for changes")
218
+ .option("--quiet", "Silence logs")
219
+ .option("--debug", "Enable debug logging");
220
+
221
+ // scan subcommand
222
+ program
223
+ .command("scan")
224
+ .description(
225
+ "Generate structure.txt-style output (config-aware by default, or manual root/out)",
226
+ )
227
+ .option(
228
+ "--from-config",
229
+ `Scan based on scaffold config/groups and write structure files into ${SCAFFOLD_ROOT_DIR}/ (default if no root/out specified)`,
230
+ )
231
+ .option("-r, --root <path>", "Root directory to scan (manual mode)")
232
+ .option("-o, --out <path>", "Output file path (manual mode)")
233
+ .option("-d, --depth <number>", "Max directory depth to scan (default: infinity, 0 = only scan root dir")
234
+ .option(
235
+ "--ignore <patterns...>",
236
+ "Additional glob patterns to ignore (relative to root)",
237
+ )
238
+ .option(
239
+ "--groups <names...>",
240
+ "Limit config-based scanning to specific groups (by name)",
241
+ )
242
+ .action(async (scanOpts: ScanCliOptions, cmd: Command) => {
243
+ const baseOpts = cmd.parent?.opts<BaseCliOptions>() ?? {};
244
+ await handleScanCommand(cwd, scanOpts, baseOpts);
245
+ });
246
+
247
+ // init subcommand
248
+ program
249
+ .command("init")
250
+ .description(
251
+ `Initialize ${SCAFFOLD_ROOT_DIR} folder and config/structure files`,
252
+ )
253
+ .option(
254
+ "--force",
255
+ "Overwrite existing config/structure files if they already exist",
256
+ )
257
+ .action(async (initOpts: InitCliOptions, cmd: Command) => {
258
+ const baseOpts = cmd.parent?.opts<BaseCliOptions>() ?? {};
259
+ await handleInitCommand(cwd, initOpts, baseOpts);
260
+ });
261
+
262
+ // structures subcommand
263
+ program
264
+ .command("structures")
265
+ .description(
266
+ "Create missing structure files specified in the config (does not overwrite existing files)",
267
+ )
268
+ .action(async (_opts: StructuresCliOptions, cmd: Command) => {
269
+ const baseOpts = cmd.parent?.opts<BaseCliOptions>() ?? {};
270
+ await handleStructuresCommand(cwd, baseOpts);
271
+ });
272
+
273
+ // Base command: run scaffold once or in watch mode
274
+ program.action(async (opts: BaseCliOptions) => {
275
+ await handleRunCommand(cwd, opts);
276
+ });
277
+
278
+ await program.parseAsync(process.argv);
279
+ }
280
+
281
+ // Run and handle errors
282
+ main().catch((err) => {
283
+ defaultLogger.error(err);
284
+ process.exit(1);
285
+ });