@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/.idea/prettier.xml +6 -0
- package/dist/cli.cjs +38 -22
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +38 -22
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +21 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +21 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/cli/main.ts +285 -286
- package/src/core/apply-structure.ts +306 -255
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timeax/scaffold",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.
|
|
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
|
|
5
|
-
import path from
|
|
6
|
-
import fs from
|
|
7
|
-
import { Command } from
|
|
8
|
-
import { runOnce, RunOptions } from
|
|
9
|
-
import { watchScaffold } from
|
|
10
|
-
import {
|
|
11
|
-
ensureStructureFilesFromConfig,
|
|
12
|
-
scanDirectoryToStructureText,
|
|
13
|
-
writeScannedStructuresFromConfig,
|
|
14
|
-
} from
|
|
15
|
-
import { initScaffold } from
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
configPath,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
.option(
|
|
213
|
-
.option(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
227
|
-
.option(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
)
|
|
231
|
-
.option(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
await program.parseAsync(process.argv);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
});
|