@timeax/scaffold 0.0.2 → 0.0.3

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
@@ -1,4 +1,4 @@
1
- # Scaffold - `@timeax/scaffold`
1
+ # @timeax/scaffold
2
2
 
3
3
  A tiny, opinionated scaffolding tool that keeps your project structure in sync with a **declarative tree** (like `structure.txt`) – Prisma‑style.
4
4
 
@@ -8,11 +8,13 @@ A tiny, opinionated scaffolding tool that keeps your project structure in sync w
8
8
  * Reverse‑engineer existing projects into `*.txt` structures.
9
9
  * Watch for changes and re‑apply automatically.
10
10
 
11
+ > **Supported structure files:** `.tss`, `.stx`, `structure.txt`, and any `.txt` files inside `.scaffold/`.
12
+
11
13
  ---
12
14
 
13
15
  ## Features
14
16
 
15
- * **Prisma‑style scaffold directory**: all config and structure lives under `scaffold/` by default.
17
+ * **Prisma‑style scaffold directory**: all config and structure lives under `.scaffold/` by default.
16
18
  * **Config‑driven groups**: declare multiple roots (e.g. `app`, `frontend`) with their own structure files.
17
19
  * **Plain‑text structure files**: strict, easy‑to‑read tree syntax with indentation and annotations.
18
20
  * **Safe apply**:
@@ -26,8 +28,14 @@ A tiny, opinionated scaffolding tool that keeps your project structure in sync w
26
28
  * Regular hooks around file create/delete.
27
29
  * Stub hooks around content generation.
28
30
  * **Stubs**: programmatic content generators for files (e.g. React pages, controllers, etc.).
29
- * **Watch mode**: watch `scaffold/` for changes and re‑run automatically.
31
+ * **Watch mode**: watch `.scaffold/` for changes and re‑run automatically.
30
32
  * **Scanner**: generate `structure.txt` (or per‑group `*.txt`) from an existing codebase.
33
+ * **VS Code integration** (via a companion extension):
34
+
35
+ * Syntax highlighting for `.tss`, `.stx`, `structure.txt`, and `.scaffold/**/*.txt`.
36
+ * Inline diagnostics using the same parser as the CLI.
37
+ * “Go to file” from a structure line.
38
+ * Simple formatting and sorting commands.
31
39
 
32
40
  ---
33
41
 
@@ -57,7 +65,7 @@ pnpm scaffold init
57
65
  This will create:
58
66
 
59
67
  ```txt
60
- scaffold/
68
+ .scaffold/
61
69
  config.ts # main ScaffoldConfig
62
70
  structure.txt # example structure (single-root mode)
63
71
  ```
@@ -74,7 +82,7 @@ scaffold init --dir tools/scaffold
74
82
 
75
83
  ### 2. Define your structure
76
84
 
77
- By default, `scaffold/structure.txt` is used in single‑root mode.
85
+ By default, `.scaffold/structure.txt` is used in single‑root mode.
78
86
 
79
87
  Example:
80
88
 
@@ -98,12 +106,16 @@ README.md
98
106
 
99
107
  **Rules:**
100
108
 
101
- * Indent with **2 spaces per level** (strict).
109
+ * Indent with **2 spaces per level** (strict by default; configurable).
102
110
  * Directories **must** end with `/`.
103
111
  * Files **must not** end with `/`.
104
112
  * You **cannot indent under a file** (files cannot have children).
105
113
  * You can’t “skip” levels (no jumping from depth 0 to depth 2 in one go).
106
- * Lines starting with `#` are comments.
114
+ * Lines starting with `#` or `//` (after indentation) are comments.
115
+ * Inline comments are supported:
116
+
117
+ * `index.ts # comment`
118
+ * `index.ts // comment`
107
119
 
108
120
  #### Annotations
109
121
 
@@ -124,20 +136,22 @@ Supported inline annotations:
124
136
 
125
137
  These map onto the `StructureEntry` fields in TypeScript.
126
138
 
139
+ > `:` is reserved for annotations (e.g. `@stub:page`). Paths themselves must **not** contain `:`.
140
+
127
141
  ---
128
142
 
129
143
  ### 3. Configure groups (optional but recommended)
130
144
 
131
- In `scaffold/config.ts` you can enable grouped mode:
145
+ In `.scaffold/config.ts` you can enable grouped mode:
132
146
 
133
147
  ```ts
134
148
  import type { ScaffoldConfig } from '@timeax/scaffold';
135
149
 
136
150
  const config: ScaffoldConfig = {
137
- root: '.', // project root (optional, defaults to cwd)
151
+ base: '.', // project root (optional, defaults to cwd)
138
152
 
139
153
  groups: [
140
- { name: 'app', root: 'app', structureFile: 'app.txt' },
154
+ { name: 'app', root: 'app', structureFile: 'app.txt' },
141
155
  { name: 'frontend', root: 'resources/js', structureFile: 'frontend.txt' },
142
156
  ],
143
157
 
@@ -148,21 +162,21 @@ const config: ScaffoldConfig = {
148
162
  export default config;
149
163
  ```
150
164
 
151
- Then create per‑group structure files in `scaffold/`:
165
+ Then create per‑group structure files in `.scaffold/`:
152
166
 
153
167
  ```txt
154
- # scaffold/app.txt
168
+ # .scaffold/app.txt
155
169
  App/Services/
156
170
  UserService.php
157
171
 
158
- # scaffold/frontend.txt
172
+ # .scaffold/frontend.txt
159
173
  src/
160
174
  index.tsx
161
175
  pages/
162
176
  home.tsx
163
177
  ```
164
178
 
165
- > When `groups` is defined and non‑empty, single‑root `structure`/`structureFile` is ignored.
179
+ > When `groups` is defined and non‑empty, single‑root `structure` / `structureFile` is ignored.
166
180
 
167
181
  ---
168
182
 
@@ -173,16 +187,16 @@ src/
173
187
  scaffold
174
188
 
175
189
  # or with explicit scaffold dir / config
176
- scaffold --dir scaffold --config scaffold/config.ts
190
+ scaffold --dir .scaffold --config .scaffold/config.ts
177
191
  ```
178
192
 
179
193
  What happens:
180
194
 
181
- * Config is loaded from `scaffold/config.*` (Prisma‑style resolution).
195
+ * Config is loaded from `.scaffold/config.*` (Prisma‑style resolution).
182
196
  * Structure(s) are resolved (grouped or single‑root).
183
197
  * Files/directories missing on disk are created.
184
198
  * New files are registered in `.scaffold-cache.json` (under project root by default).
185
- * Any previously created files that are no longer in the structure are candidates for deletion.
199
+ * Any previously created files that are no longer in the structure are candidates for deletion:
186
200
 
187
201
  * Small files are deleted automatically.
188
202
  * Large files (configurable threshold) trigger an interactive prompt.
@@ -195,8 +209,8 @@ scaffold --watch
195
209
 
196
210
  * Watches:
197
211
 
198
- * `scaffold/config.*`
199
- * `scaffold/*.txt`
212
+ * `.scaffold/config.*`
213
+ * `.scaffold/*.txt`
200
214
  * Debounces rapid edits.
201
215
  * Prevents overlapping runs.
202
216
 
@@ -213,11 +227,13 @@ scaffold [options]
213
227
  Options:
214
228
 
215
229
  * `-c, --config <path>` – override config file path.
216
- * `-d, --dir <path>` – override scaffold directory (default: `./scaffold`).
230
+ * `-d, --dir <path>` – override scaffold directory (default: `./.scaffold`).
217
231
  * `-w, --watch` – watch scaffold directory for changes.
218
232
  * `--quiet` – silence logs.
219
233
  * `--debug` – verbose debug logs.
220
234
 
235
+ ---
236
+
221
237
  ### `scaffold init`
222
238
 
223
239
  Initialize the scaffold directory + config + structure.
@@ -228,42 +244,44 @@ scaffold init [options]
228
244
 
229
245
  Options:
230
246
 
231
- * `-d, --dir <path>` – scaffold directory (default: `./scaffold`, inherited from root options).
247
+ * `-d, --dir <path>` – scaffold directory (default: `./.scaffold`, inherited from root options).
232
248
  * `--force` – overwrite existing `config.ts` / `structure.txt`.
233
249
 
250
+ ---
251
+
234
252
  ### `scaffold scan`
235
253
 
236
254
  Generate `structure.txt`‑style definitions from an existing project.
237
255
 
238
256
  Two modes:
239
257
 
240
- 1. **Config‑aware mode** (default if no `--root` / `--out` given):
258
+ #### 1. Config‑aware mode (default if no `--root` / `--out` given)
241
259
 
242
- ```bash
243
- scaffold scan
244
- scaffold scan --from-config
245
- scaffold scan --from-config --groups app frontend
246
- ```
260
+ ```bash
261
+ scaffold scan
262
+ scaffold scan --from-config
263
+ scaffold scan --from-config --groups app frontend
264
+ ```
247
265
 
248
- * Loads `scaffold/config.ts`.
249
- * For each `group` in config:
266
+ * Loads `.scaffold/config.ts`.
267
+ * For each `group` in config:
250
268
 
251
- * Scans `group.root` on disk.
252
- * Writes to `scaffold/<group.structureFile || group.name + '.txt'>`.
253
- * `--groups` filters which groups to scan.
269
+ * Scans `group.root` on disk.
270
+ * Writes to `.scaffold/<group.structureFile || group.name + '.txt'>`.
271
+ * `--groups` filters which groups to scan.
254
272
 
255
- 2. **Manual mode** (single root):
273
+ #### 2. Manual mode (single root)
256
274
 
257
- ```bash
258
- scaffold scan -r src
259
- scaffold scan -r src -o scaffold/src.txt
260
- ```
275
+ ```bash
276
+ scaffold scan -r src
277
+ scaffold scan -r src -o .scaffold/src.txt
278
+ ```
261
279
 
262
- Options:
280
+ Options:
263
281
 
264
- * `-r, --root <path>` – directory to scan.
265
- * `-o, --out <path>` – output file (otherwise prints to stdout).
266
- * `--ignore <patterns...>` – extra globs to ignore (in addition to defaults like `node_modules/**`, `.git/**`, etc.).
282
+ * `-r, --root <path>` – directory to scan.
283
+ * `-o, --out <path>` – output file (otherwise prints to stdout).
284
+ * `--ignore <patterns...>` – extra globs to ignore (in addition to defaults like `node_modules/**`, `.git/**`, etc.).
267
285
 
268
286
  ---
269
287
 
@@ -277,10 +295,10 @@ scaffold structures
277
295
 
278
296
  What it does:
279
297
 
280
- * Loads `scaffold/config.*`.
298
+ * Loads `.scaffold/config.*`.
281
299
  * Determines which structure files are expected:
282
300
 
283
- * **Grouped mode** (`config.groups` defined): each group gets `group.structureFile || \`${group.name}.txt``.
301
+ * **Grouped mode** (`config.groups` defined): each group gets `group.structureFile || `${group.name}.txt``.
284
302
  * **Single-root mode** (no groups): uses `config.structureFile || 'structure.txt'`.
285
303
  * For each expected structure file:
286
304
 
@@ -296,15 +314,17 @@ Examples:
296
314
  # { name: 'frontend', root: 'resources/js', structureFile: 'frontend.txt' },
297
315
  # ]
298
316
  scaffold structures
299
- # => ensures scaffold/app.txt and scaffold/frontend.txt exist
317
+ # => ensures .scaffold/app.txt and .scaffold/frontend.txt exist
300
318
 
301
319
  # With single-root config:
302
320
  # structureFile: 'structure.txt'
303
321
  scaffold structures
304
- # => ensures scaffold/structure.txt exists
322
+ # => ensures .scaffold/structure.txt exists
305
323
  ```
306
324
 
307
- This is useful right after setting up or editing `scaffold/config.ts` so that all declared structure files are present and ready to edit.
325
+ This is useful right after setting up or editing `.scaffold/config.ts` so that all declared structure files are present and ready to edit.
326
+
327
+ ---
308
328
 
309
329
  ## TypeScript API
310
330
 
@@ -315,8 +335,8 @@ import { runOnce } from '@timeax/scaffold';
315
335
 
316
336
  await runOnce(process.cwd(), {
317
337
  // optional overrides
318
- configPath: 'scaffold/config.ts',
319
- scaffoldDir: 'scaffold',
338
+ configPath: '.scaffold/config.ts',
339
+ scaffoldDir: '.scaffold',
320
340
  });
321
341
  ```
322
342
 
@@ -337,7 +357,7 @@ const results = await scanProjectFromConfig(process.cwd(), {
337
357
  groups: ['app', 'frontend'],
338
358
  });
339
359
 
340
- // write group structure files to scaffold/
360
+ // write group structure files to .scaffold/
341
361
  await writeScannedStructuresFromConfig(process.cwd(), {
342
362
  groups: ['app'],
343
363
  });
@@ -401,7 +421,9 @@ const config: ScaffoldConfig = {
401
421
  name: 'page',
402
422
  async getContent(ctx) {
403
423
  const name = ctx.targetPath.split('/').pop();
404
- return `export default function ${name}() {\n return <div>${name}</div>;\n}`;
424
+ return `export default function ${name}() {
425
+ return <div>${name}</div>;
426
+ }`;
405
427
  },
406
428
  hooks: {
407
429
  preStub: [
@@ -418,7 +440,7 @@ const config: ScaffoldConfig = {
418
440
  };
419
441
  ```
420
442
 
421
- In `structure.txt`:
443
+ In a structure file:
422
444
 
423
445
  ```txt
424
446
  src/
@@ -459,5 +481,82 @@ Some things this package is intentionally designed to grow into:
459
481
  * Stub groups (one logical stub creating multiple files).
460
482
  * Built‑in templates for common stacks (Laravel + Inertia, Next.js, etc.).
461
483
  * Better diff/dry‑run UX (show what will change without touching disk).
484
+ * Deeper VS Code integration:
485
+
486
+ * Tree-aware sorting.
487
+ * Visual tree editor.
488
+ * Code actions / quick fixes for common mistakes.
462
489
 
463
490
  PRs and ideas are welcome ✨
491
+
492
+ ---
493
+
494
+ ## VS Code extension
495
+
496
+ There is an official VS Code companion extension for `@timeax/scaffold` that makes working with your structure files much nicer.
497
+
498
+ ### Language support
499
+
500
+ The extension adds a custom language **Scaffold Structure** and:
501
+
502
+ * Highlights:
503
+
504
+ * Directories (lines ending with `/`)
505
+ * Files
506
+ * Inline annotations like `@stub:name`, `@include:pattern`, `@exclude:pattern`
507
+ * Comments using `#` or `//` (full-line and inline)
508
+ * Treats the following files as scaffold structures:
509
+
510
+ * `*.tss`
511
+ * `*.stx`
512
+ * `structure.txt`
513
+ * Any `.txt` file inside your `.scaffold/` directory
514
+
515
+ The syntax rules match the CLI parser:
516
+
517
+ * Indent is in fixed steps (configurable via `indentStep`).
518
+ * Only directories can have children.
519
+ * `:` is reserved for annotations and not allowed inside path names.
520
+
521
+ ### Editor commands
522
+
523
+ The extension contributes several commands (available via the Command Palette and context menus when editing a scaffold structure file):
524
+
525
+ * **Scaffold: Go to file**
526
+
527
+ * Reads the path on the current line and opens the corresponding file in your project.
528
+ * Respects `.scaffold/config.*`:
529
+
530
+ * Uses `base`/`root` to resolve paths.
531
+ * If the current structure file belongs to a `group`, it resolves relative to that group’s `root`.
532
+ * If the file doesn’t exist, it can prompt to create it and open it immediately.
533
+
534
+ * **Scaffold: Format structure file**
535
+
536
+ * Normalizes line endings and trims trailing whitespace.
537
+ * Designed to be safe even on partially-invalid files.
538
+ * Future versions may use the full AST from `@timeax/scaffold` to enforce indentation and ordering.
539
+
540
+ * **Scaffold: Sort entries**
541
+
542
+ * Naive helper that sorts non-comment lines lexicographically while keeping comment/blank lines in place.
543
+ * Useful for quick cleanups of small structure files.
544
+
545
+ * **Scaffold: Open config**
546
+
547
+ * Opens `.scaffold/config.*` for the current workspace (searching common extensions like `config.ts`, `config.mts`, etc.).
548
+
549
+ * **Scaffold: Open .scaffold folder**
550
+
551
+ * Reveals the `.scaffold/` directory in the VS Code Explorer.
552
+
553
+ ### Live validation (diagnostics)
554
+
555
+ Whenever you open or edit a scaffold structure file:
556
+
557
+ * The extension calls `parseStructureText` from `@timeax/scaffold` under the hood.
558
+ * If parsing fails, the error message (e.g. invalid indentation, children under a file, bad path, etc.) is shown as an editor diagnostic (squiggly underline) on the relevant line.
559
+
560
+ This means your editor and the CLI always agree on what is valid, since they share the same parser and rules.
561
+
562
+ > The extension is optional, but highly recommended if you edit `*.tss` / `*.stx` or `structure.txt` files frequently.
@@ -7,12 +7,13 @@ import crypto from 'crypto';
7
7
  import { pathToFileURL } from 'url';
8
8
  import { transform } from 'esbuild';
9
9
 
10
- import type { ScaffoldConfig } from '../schema';
10
+ import { SCAFFOLD_ROOT_DIR, type ScaffoldConfig } from '../schema';
11
11
  import { defaultLogger } from '../util/logger';
12
12
  import { ensureDirSync } from '../util/fs-utils';
13
13
 
14
14
  const logger = defaultLogger.child('[config]');
15
15
 
16
+
16
17
  export interface LoadScaffoldConfigOptions {
17
18
  /**
18
19
  * Optional explicit scaffold directory path (absolute or relative to cwd).
@@ -68,7 +69,7 @@ export async function loadScaffoldConfig(
68
69
  // First pass: figure out an initial scaffold dir just to locate config.*
69
70
  const initialScaffoldDir = options.scaffoldDir
70
71
  ? path.resolve(absCwd, options.scaffoldDir)
71
- : path.join(absCwd, 'scaffold');
72
+ : path.join(absCwd, SCAFFOLD_ROOT_DIR);
72
73
 
73
74
  const configPath =
74
75
  options.configPath ?? resolveConfigPath(initialScaffoldDir);
@@ -85,7 +86,7 @@ export async function loadScaffoldConfig(
85
86
  // Final scaffoldDir (can still be overridden by CLI)
86
87
  const scaffoldDir = options.scaffoldDir
87
88
  ? path.resolve(absCwd, options.scaffoldDir)
88
- : path.join(configRoot, 'scaffold');
89
+ : path.join(configRoot, SCAFFOLD_ROOT_DIR);
89
90
 
90
91
  // projectRoot (base) is relative to configRoot
91
92
  const baseRoot = config.base
@@ -4,6 +4,7 @@ import fs from 'fs';
4
4
  import path from 'path';
5
5
  import { ensureDirSync } from '../util/fs-utils';
6
6
  import { defaultLogger } from '../util/logger';
7
+ import { SCAFFOLD_ROOT_DIR } from '../schema';
7
8
 
8
9
  const logger = defaultLogger.child('[init]');
9
10
 
@@ -52,7 +53,10 @@ const config: ScaffoldConfig = {
52
53
  // base: 'src', // apply to <root>/src
53
54
  // base: '..', // apply to parent of <root>
54
55
  // base: '.',
55
-
56
+
57
+ // Number of spaces per indent level in structure files (default: 2).
58
+ // indentStep: 2,
59
+
56
60
  // Cache file path, relative to base.
57
61
  // cacheFile: '.scaffold-cache.json',
58
62
 
@@ -85,7 +89,8 @@ const config: ScaffoldConfig = {
85
89
  export default config;
86
90
  `;
87
91
 
88
- const DEFAULT_STRUCTURE_TXT = `# scaffold/structure.txt
92
+
93
+ const DEFAULT_STRUCTURE_TXT = `# ${SCAFFOLD_ROOT_DIR}/structure.txt
89
94
  # Example structure definition.
90
95
  # - Indent with 2 spaces per level
91
96
  # - Directories must end with "/"
@@ -113,7 +118,7 @@ export async function initScaffold(
113
118
  structurePath: string;
114
119
  created: { config: boolean; structure: boolean };
115
120
  }> {
116
- const scaffoldDirRel = options.scaffoldDir ?? 'scaffold';
121
+ const scaffoldDirRel = options.scaffoldDir ?? SCAFFOLD_ROOT_DIR;
117
122
  const scaffoldDirAbs = path.resolve(cwd, scaffoldDirRel);
118
123
  const configFileName = options.configFileName ?? 'config.ts';
119
124
  const structureFileName = options.structureFileName ?? 'structure.txt';
@@ -12,6 +12,55 @@ interface ParsedLine {
12
12
  exclude?: string[];
13
13
  }
14
14
 
15
+ /**
16
+ * Strip inline comments from a content segment.
17
+ *
18
+ * Supports:
19
+ * - "index.ts # comment"
20
+ * - "index.ts // comment"
21
+ *
22
+ * Rules:
23
+ * - We assume leading indentation has already been removed.
24
+ * - Leading '#' or '//' (full-line comments) are handled BEFORE this function.
25
+ * - A comment starts at the first '#' or '//' that is
26
+ * preceded by whitespace (space or tab).
27
+ */
28
+ function stripInlineComment(content: string): string {
29
+ let cutIndex = -1;
30
+ const len = content.length;
31
+
32
+ for (let i = 0; i < len; i++) {
33
+ const ch = content[i];
34
+ const prev = i > 0 ? content[i - 1] : '';
35
+
36
+ // Inline "# ..."
37
+ if (ch === '#') {
38
+ if (i === 0) continue; // full-line handled earlier
39
+ if (prev === ' ' || prev === '\t') {
40
+ cutIndex = i;
41
+ break;
42
+ }
43
+ }
44
+
45
+ // Inline "// ..."
46
+ if (
47
+ ch === '/' &&
48
+ i + 1 < len &&
49
+ content[i + 1] === '/' &&
50
+ (prev === ' ' || prev === '\t')
51
+ ) {
52
+ cutIndex = i;
53
+ break;
54
+ }
55
+ }
56
+
57
+ if (cutIndex === -1) {
58
+ return content.trimEnd();
59
+ }
60
+
61
+ return content.slice(0, cutIndex).trimEnd();
62
+ }
63
+
15
64
  /**
16
65
  * Parse a single non-empty, non-comment line into a ParsedLine.
17
66
  * Supports inline annotations:
@@ -20,16 +69,39 @@ interface ParsedLine {
20
69
  * - @exclude:pattern,pattern2
21
70
  */
22
71
  function parseLine(line: string, lineNo: number): ParsedLine | null {
23
- const match = line.match(/^(\s*)(.+)$/);
72
+ const match = line.match(/^(\s*)(.*)$/);
24
73
  if (!match) return null;
25
74
 
26
75
  const indentSpaces = match[1].length;
27
- const rest = match[2].trim();
28
- if (!rest || rest.startsWith('#')) return null;
76
+ let rest = match[2];
77
+
78
+ // If line (after indent) is empty, skip
79
+ if (!rest.trim()) return null;
80
+
81
+ // Full-line comments after indent
82
+ const trimmedRest = rest.trimStart();
83
+ if (trimmedRest.startsWith('#') || trimmedRest.startsWith('//')) {
84
+ return null;
85
+ }
86
+
87
+ // Strip inline comments (# or //) before parsing tokens
88
+ const stripped = stripInlineComment(rest);
89
+ const trimmed = stripped.trim();
90
+ if (!trimmed) return null;
91
+
92
+ const parts = trimmed.split(/\s+/);
93
+ if (!parts.length) return null;
29
94
 
30
- const parts = rest.split(/\s+/);
31
95
  const pathToken = parts[0];
32
96
 
97
+ // 🚫 Reserve ":" for annotations only – paths may not contain it.
98
+ if (pathToken.includes(':')) {
99
+ throw new Error(
100
+ `structure.txt: ":" is reserved for annotations (@stub:, @include:, etc). ` +
101
+ `Invalid path "${pathToken}" on line ${lineNo}.`,
102
+ );
103
+ }
104
+
33
105
  let stub: string | undefined;
34
106
  const include: string[] = [];
35
107
  const exclude: string[] = [];
@@ -74,14 +146,17 @@ function parseLine(line: string, lineNo: number): ParsedLine | null {
74
146
  * Convert a structure.txt content into a nested StructureEntry[].
75
147
  *
76
148
  * Rules:
77
- * - Indentation is **2 spaces per level** (strict).
78
- * - Indent must be a multiple of 2.
149
+ * - Indentation is **indentStep** spaces per level (default: 2).
150
+ * - Indent must be a multiple of indentStep.
79
151
  * - You cannot "skip" levels (no jumping from level 0 to 2 directly).
80
152
  * - **Only directories can have children**:
81
153
  * - If you indent under a file, an error is thrown.
82
154
  * - Folders must end with "/" in the txt; paths are normalized to POSIX.
83
155
  */
84
- export function parseStructureText(text: string): StructureEntry[] {
156
+ export function parseStructureText(
157
+ text: string,
158
+ indentStep = 2,
159
+ ): StructureEntry[] {
85
160
  const lines = text.split(/\r?\n/);
86
161
  const parsed: ParsedLine[] = [];
87
162
 
@@ -100,23 +175,22 @@ export function parseStructureText(text: string): StructureEntry[] {
100
175
  };
101
176
 
102
177
  const stack: StackItem[] = [];
103
- const INDENT_STEP = 2;
104
178
 
105
179
  for (const p of parsed) {
106
180
  const { indentSpaces, lineNo } = p;
107
181
 
108
- if (indentSpaces % INDENT_STEP !== 0) {
182
+ if (indentSpaces % indentStep !== 0) {
109
183
  throw new Error(
110
184
  `structure.txt: Invalid indent on line ${lineNo}. ` +
111
- `Indent must be multiples of ${INDENT_STEP} spaces.`,
185
+ `Indent must be multiples of ${indentStep} spaces.`,
112
186
  );
113
187
  }
114
188
 
115
- const level = indentSpaces / INDENT_STEP;
189
+ const level = indentSpaces / indentStep;
116
190
 
117
191
  // Determine parent level and enforce no skipping
118
192
  if (level > stack.length) {
119
- // e.g. current stack depth 1, but line level=2+ is invalid
193
+ // e.g. current stack depth 1, but line level=3 is invalid
120
194
  if (level !== stack.length + 1) {
121
195
  throw new Error(
122
196
  `structure.txt: Invalid indentation on line ${lineNo}. ` +
@@ -146,7 +220,6 @@ export function parseStructureText(text: string): StructureEntry[] {
146
220
  const clean = p.rawPath.replace(/\/$/, '');
147
221
  const basePath = toPosixPath(clean);
148
222
 
149
- // Determine parent based on level
150
223
  // Pop stack until we are at the correct depth
151
224
  while (stack.length > level) {
152
225
  stack.pop();
@@ -193,9 +266,9 @@ export function parseStructureText(text: string): StructureEntry[] {
193
266
  rootEntries.push(fileEntry);
194
267
  }
195
268
 
196
- // files are not added to the stack; they cannot have children
269
+ // We still push files into the stack at this level so that
270
+ // bad indentation under them can be detected and rejected.
197
271
  stack.push({ level, entry: fileEntry, isDir: false });
198
- // but next lines at same or lower level will pop correctly
199
272
  }
200
273
  }
201
274
 
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/index.ts
2
-
3
2
  export * from './schema';
4
3
  export * from './core/runner';
5
- export * from './core/scan-structure';
4
+ export * from './core/config-loader';
5
+ export * from './core/scan-structure';
6
+ export * from './core/structure-txt';
@@ -143,9 +143,18 @@ export interface ScaffoldConfig {
143
143
  * runner / CLI.
144
144
  */
145
145
  watch?: boolean;
146
- }
147
146
 
148
147
 
148
+ /**
149
+ * Number of spaces per indent level in structure files.
150
+ * Default: 2.
151
+ *
152
+ * Examples:
153
+ * - 2 → "··entry"
154
+ * - 4 → "····entry"
155
+ */
156
+ indentStep?: number;
157
+ }
149
158
  /**
150
159
  * Options when scanning an existing directory into a structure.txt tree.
151
160
  */
@@ -1,4 +1,5 @@
1
1
  // src/schema/index.ts
2
+ export const SCAFFOLD_ROOT_DIR = '.scaffold';
2
3
  export * from './structure';
3
4
  export * from './hooks';
4
5
  export * from './config';