@timeax/scaffold 0.0.2 → 0.0.4

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/dist/ast.mjs ADDED
@@ -0,0 +1,433 @@
1
+ // src/util/fs-utils.ts
2
+ function toPosixPath(p) {
3
+ return p.replace(/\\/g, "/");
4
+ }
5
+
6
+ // src/ast/parser.ts
7
+ function parseStructureAst(text, opts = {}) {
8
+ const indentStep = opts.indentStep ?? 2;
9
+ const mode = opts.mode ?? "loose";
10
+ const diagnostics = [];
11
+ const lines = [];
12
+ const rawLines = text.split(/\r?\n/);
13
+ for (let i = 0; i < rawLines.length; i++) {
14
+ const raw = rawLines[i];
15
+ const lineNo = i + 1;
16
+ const m = raw.match(/^(\s*)(.*)$/);
17
+ const indentRaw = m ? m[1] : "";
18
+ const content = m ? m[2] : "";
19
+ const { indentSpaces, hasTabs } = measureIndent(indentRaw, indentStep);
20
+ if (hasTabs) {
21
+ diagnostics.push({
22
+ line: lineNo,
23
+ message: "Tabs detected in indentation. Consider using spaces only for consistent levels.",
24
+ severity: mode === "strict" ? "warning" : "info",
25
+ code: "indent-tabs"
26
+ });
27
+ }
28
+ const trimmed = content.trim();
29
+ let kind;
30
+ if (!trimmed) {
31
+ kind = "blank";
32
+ } else if (trimmed.startsWith("#") || trimmed.startsWith("//")) {
33
+ kind = "comment";
34
+ } else {
35
+ kind = "entry";
36
+ }
37
+ lines.push({
38
+ index: i,
39
+ lineNo,
40
+ raw,
41
+ kind,
42
+ indentSpaces,
43
+ content
44
+ });
45
+ }
46
+ const rootNodes = [];
47
+ const stack = [];
48
+ const depthCtx = {
49
+ lastIndentSpaces: null,
50
+ lastDepth: null,
51
+ lastWasFile: false
52
+ };
53
+ for (const line of lines) {
54
+ if (line.kind !== "entry") continue;
55
+ const { entry, depth, diags } = parseEntryLine(
56
+ line,
57
+ indentStep,
58
+ mode,
59
+ depthCtx
60
+ );
61
+ diagnostics.push(...diags);
62
+ if (!entry) {
63
+ continue;
64
+ }
65
+ attachNode(entry, depth, line, rootNodes, stack, diagnostics, mode);
66
+ depthCtx.lastWasFile = !entry.isDir;
67
+ }
68
+ return {
69
+ rootNodes,
70
+ lines,
71
+ diagnostics,
72
+ options: {
73
+ indentStep,
74
+ mode
75
+ }
76
+ };
77
+ }
78
+ function measureIndent(rawIndent, indentStep) {
79
+ let spaces = 0;
80
+ let hasTabs = false;
81
+ for (const ch of rawIndent) {
82
+ if (ch === " ") {
83
+ spaces += 1;
84
+ } else if (ch === " ") {
85
+ hasTabs = true;
86
+ spaces += indentStep;
87
+ }
88
+ }
89
+ return { indentSpaces: spaces, hasTabs };
90
+ }
91
+ function computeDepth(line, indentStep, mode, ctx, diagnostics) {
92
+ let spaces = line.indentSpaces;
93
+ if (spaces < 0) spaces = 0;
94
+ let depth;
95
+ if (ctx.lastIndentSpaces == null || ctx.lastDepth == null) {
96
+ depth = 0;
97
+ } else {
98
+ const prevSpaces = ctx.lastIndentSpaces;
99
+ const prevDepth = ctx.lastDepth;
100
+ if (spaces > prevSpaces) {
101
+ const diff = spaces - prevSpaces;
102
+ if (ctx.lastWasFile) {
103
+ diagnostics.push({
104
+ line: line.lineNo,
105
+ message: "Entry appears indented under a file; treating it as a sibling of the file instead of a child.",
106
+ severity: mode === "strict" ? "error" : "warning",
107
+ code: "child-of-file-loose"
108
+ });
109
+ depth = prevDepth;
110
+ } else {
111
+ if (diff > indentStep) {
112
+ diagnostics.push({
113
+ line: line.lineNo,
114
+ message: `Indentation jumps from ${prevSpaces} to ${spaces} spaces; treating as one level deeper.`,
115
+ severity: mode === "strict" ? "error" : "warning",
116
+ code: "indent-skip-level"
117
+ });
118
+ }
119
+ depth = prevDepth + 1;
120
+ }
121
+ } else if (spaces === prevSpaces) {
122
+ depth = prevDepth;
123
+ } else {
124
+ const diff = prevSpaces - spaces;
125
+ const steps = Math.round(diff / indentStep);
126
+ if (diff % indentStep !== 0) {
127
+ diagnostics.push({
128
+ line: line.lineNo,
129
+ message: `Indentation decreases from ${prevSpaces} to ${spaces} spaces, which is not a multiple of indent step (${indentStep}).`,
130
+ severity: mode === "strict" ? "error" : "warning",
131
+ code: "indent-misaligned"
132
+ });
133
+ }
134
+ depth = Math.max(prevDepth - steps, 0);
135
+ }
136
+ }
137
+ ctx.lastIndentSpaces = spaces;
138
+ ctx.lastDepth = depth;
139
+ return depth;
140
+ }
141
+ function parseEntryLine(line, indentStep, mode, ctx) {
142
+ const diags = [];
143
+ const depth = computeDepth(line, indentStep, mode, ctx, diags);
144
+ const { contentWithoutComment } = extractInlineCommentParts(line.content);
145
+ const trimmed = contentWithoutComment.trim();
146
+ if (!trimmed) {
147
+ return { entry: null, depth, diags };
148
+ }
149
+ const parts = trimmed.split(/\s+/);
150
+ const pathToken = parts[0];
151
+ const annotationTokens = parts.slice(1);
152
+ if (pathToken.includes(":")) {
153
+ diags.push({
154
+ line: line.lineNo,
155
+ message: 'Path token contains ":" which is reserved for annotations. This is likely a mistake.',
156
+ severity: mode === "strict" ? "error" : "warning",
157
+ code: "path-colon"
158
+ });
159
+ }
160
+ const isDir = pathToken.endsWith("/");
161
+ const segmentName = pathToken;
162
+ let stub;
163
+ const include = [];
164
+ const exclude = [];
165
+ for (const token of annotationTokens) {
166
+ if (token.startsWith("@stub:")) {
167
+ stub = token.slice("@stub:".length);
168
+ } else if (token.startsWith("@include:")) {
169
+ const val = token.slice("@include:".length);
170
+ if (val) {
171
+ include.push(
172
+ ...val.split(",").map((s) => s.trim()).filter(Boolean)
173
+ );
174
+ }
175
+ } else if (token.startsWith("@exclude:")) {
176
+ const val = token.slice("@exclude:".length);
177
+ if (val) {
178
+ exclude.push(
179
+ ...val.split(",").map((s) => s.trim()).filter(Boolean)
180
+ );
181
+ }
182
+ } else if (token.startsWith("@")) {
183
+ diags.push({
184
+ line: line.lineNo,
185
+ message: `Unknown annotation token "${token}".`,
186
+ severity: "info",
187
+ code: "unknown-annotation"
188
+ });
189
+ }
190
+ }
191
+ const entry = {
192
+ segmentName,
193
+ isDir,
194
+ stub,
195
+ include: include.length ? include : void 0,
196
+ exclude: exclude.length ? exclude : void 0
197
+ };
198
+ return { entry, depth, diags };
199
+ }
200
+ function mapThrough(content) {
201
+ let cutIndex = -1;
202
+ const len = content.length;
203
+ for (let i = 0; i < len; i++) {
204
+ const ch = content[i];
205
+ const prev = i > 0 ? content[i - 1] : "";
206
+ if (ch === "#") {
207
+ if (i === 0) {
208
+ continue;
209
+ }
210
+ if (prev === " " || prev === " ") {
211
+ cutIndex = i;
212
+ break;
213
+ }
214
+ }
215
+ if (ch === "/" && i + 1 < len && content[i + 1] === "/" && (prev === " " || prev === " ")) {
216
+ cutIndex = i;
217
+ break;
218
+ }
219
+ }
220
+ return cutIndex;
221
+ }
222
+ function extractInlineCommentParts(content) {
223
+ const cutIndex = mapThrough(content);
224
+ if (cutIndex === -1) {
225
+ return {
226
+ contentWithoutComment: content,
227
+ inlineComment: null
228
+ };
229
+ }
230
+ return {
231
+ contentWithoutComment: content.slice(0, cutIndex),
232
+ inlineComment: content.slice(cutIndex)
233
+ };
234
+ }
235
+ function attachNode(entry, depth, line, rootNodes, stack, diagnostics, mode) {
236
+ const lineNo = line.lineNo;
237
+ while (stack.length > depth) {
238
+ stack.pop();
239
+ }
240
+ let parent = null;
241
+ if (depth > 0) {
242
+ const candidate = stack[depth - 1];
243
+ if (!candidate) {
244
+ diagnostics.push({
245
+ line: lineNo,
246
+ message: `Entry has indent depth ${depth} but no parent at depth ${depth - 1}. Treating as root.`,
247
+ severity: mode === "strict" ? "error" : "warning",
248
+ code: "missing-parent"
249
+ });
250
+ } else if (candidate.type === "file") {
251
+ if (mode === "strict") {
252
+ diagnostics.push({
253
+ line: lineNo,
254
+ message: `Cannot attach child under file "${candidate.path}".`,
255
+ severity: "error",
256
+ code: "child-of-file"
257
+ });
258
+ } else {
259
+ diagnostics.push({
260
+ line: lineNo,
261
+ message: `Entry appears under file "${candidate.path}". Attaching as sibling at depth ${candidate.depth}.`,
262
+ severity: "warning",
263
+ code: "child-of-file-loose"
264
+ });
265
+ while (stack.length > candidate.depth) {
266
+ stack.pop();
267
+ }
268
+ }
269
+ } else {
270
+ parent = candidate;
271
+ }
272
+ }
273
+ const parentPath = parent ? parent.path.replace(/\/$/, "") : "";
274
+ const normalizedSegment = toPosixPath(entry.segmentName.replace(/\/+$/, ""));
275
+ const fullPath = parentPath ? `${parentPath}/${normalizedSegment}${entry.isDir ? "/" : ""}` : `${normalizedSegment}${entry.isDir ? "/" : ""}`;
276
+ const baseNode = {
277
+ type: entry.isDir ? "dir" : "file",
278
+ name: entry.segmentName,
279
+ depth,
280
+ line: lineNo,
281
+ path: fullPath,
282
+ parent,
283
+ ...entry.stub ? { stub: entry.stub } : {},
284
+ ...entry.include ? { include: entry.include } : {},
285
+ ...entry.exclude ? { exclude: entry.exclude } : {}
286
+ };
287
+ if (entry.isDir) {
288
+ const dirNode = {
289
+ ...baseNode,
290
+ type: "dir",
291
+ children: []
292
+ };
293
+ if (parent) {
294
+ parent.children.push(dirNode);
295
+ } else {
296
+ rootNodes.push(dirNode);
297
+ }
298
+ while (stack.length > depth) {
299
+ stack.pop();
300
+ }
301
+ stack[depth] = dirNode;
302
+ } else {
303
+ const fileNode = {
304
+ ...baseNode,
305
+ type: "file"
306
+ };
307
+ if (parent) {
308
+ parent.children.push(fileNode);
309
+ } else {
310
+ rootNodes.push(fileNode);
311
+ }
312
+ }
313
+ }
314
+
315
+ // src/ast/format.ts
316
+ function formatStructureText(text, options = {}) {
317
+ const indentStep = options.indentStep ?? 2;
318
+ const mode = options.mode ?? "loose";
319
+ const normalizeNewlines = options.normalizeNewlines === void 0 ? true : options.normalizeNewlines;
320
+ const trimTrailingWhitespace = options.trimTrailingWhitespace === void 0 ? true : options.trimTrailingWhitespace;
321
+ const normalizeAnnotations = options.normalizeAnnotations === void 0 ? true : options.normalizeAnnotations;
322
+ const ast = parseStructureAst(text, {
323
+ indentStep,
324
+ mode
325
+ });
326
+ const rawLines = text.split(/\r?\n/);
327
+ const lineCount = rawLines.length;
328
+ if (ast.lines.length !== lineCount) {
329
+ return {
330
+ text: basicNormalize(text, { normalizeNewlines, trimTrailingWhitespace }),
331
+ ast
332
+ };
333
+ }
334
+ const entryLineIndexes = [];
335
+ const inlineComments = [];
336
+ for (let i = 0; i < lineCount; i++) {
337
+ const lineMeta = ast.lines[i];
338
+ if (lineMeta.kind === "entry") {
339
+ entryLineIndexes.push(i);
340
+ const { inlineComment } = extractInlineCommentParts(lineMeta.content);
341
+ inlineComments.push(inlineComment);
342
+ }
343
+ }
344
+ const flattened = [];
345
+ flattenAstNodes(ast.rootNodes, 0, flattened);
346
+ if (flattened.length !== entryLineIndexes.length) {
347
+ return {
348
+ text: basicNormalize(text, { normalizeNewlines, trimTrailingWhitespace }),
349
+ ast
350
+ };
351
+ }
352
+ const canonicalEntryLines = flattened.map(
353
+ ({ node, level }) => formatAstNodeLine(node, level, indentStep, normalizeAnnotations)
354
+ );
355
+ const resultLines = [];
356
+ let entryIdx = 0;
357
+ for (let i = 0; i < lineCount; i++) {
358
+ const lineMeta = ast.lines[i];
359
+ const originalLine = rawLines[i];
360
+ if (lineMeta.kind === "entry") {
361
+ const base = canonicalEntryLines[entryIdx].replace(/[ \t]+$/g, "");
362
+ const inline = inlineComments[entryIdx];
363
+ entryIdx++;
364
+ if (inline) {
365
+ resultLines.push(base + " " + inline);
366
+ } else {
367
+ resultLines.push(base);
368
+ }
369
+ } else {
370
+ let out = originalLine;
371
+ if (trimTrailingWhitespace) {
372
+ out = out.replace(/[ \t]+$/g, "");
373
+ }
374
+ resultLines.push(out);
375
+ }
376
+ }
377
+ const eol = normalizeNewlines ? detectPreferredEol(text) : getRawEol(text);
378
+ return {
379
+ text: resultLines.join(eol),
380
+ ast
381
+ };
382
+ }
383
+ function basicNormalize(text, opts) {
384
+ const lines = text.split(/\r?\n/);
385
+ const normalizedLines = opts.trimTrailingWhitespace ? lines.map((line) => line.replace(/[ \t]+$/g, "")) : lines;
386
+ const eol = opts.normalizeNewlines ? detectPreferredEol(text) : getRawEol(text);
387
+ return normalizedLines.join(eol);
388
+ }
389
+ function detectPreferredEol(text) {
390
+ const crlfCount = (text.match(/\r\n/g) || []).length;
391
+ const lfCount = (text.match(/(?<!\r)\n/g) || []).length;
392
+ if (crlfCount === 0 && lfCount === 0) {
393
+ return "\n";
394
+ }
395
+ if (crlfCount > lfCount) {
396
+ return "\r\n";
397
+ }
398
+ return "\n";
399
+ }
400
+ function getRawEol(text) {
401
+ return text.includes("\r\n") ? "\r\n" : "\n";
402
+ }
403
+ function flattenAstNodes(nodes, level, out) {
404
+ for (const node of nodes) {
405
+ out.push({ node, level });
406
+ if (node.type === "dir" && node.children && node.children.length) {
407
+ flattenAstNodes(node.children, level + 1, out);
408
+ }
409
+ }
410
+ }
411
+ function formatAstNodeLine(node, level, indentStep, normalizeAnnotations) {
412
+ const indent = " ".repeat(indentStep * level);
413
+ const baseName = node.name;
414
+ if (!normalizeAnnotations) {
415
+ return indent + baseName;
416
+ }
417
+ const tokens = [];
418
+ if (node.stub) {
419
+ tokens.push(`@stub:${node.stub}`);
420
+ }
421
+ if (node.include && node.include.length > 0) {
422
+ tokens.push(`@include:${node.include.join(",")}`);
423
+ }
424
+ if (node.exclude && node.exclude.length > 0) {
425
+ tokens.push(`@exclude:${node.exclude.join(",")}`);
426
+ }
427
+ const annotations = tokens.length ? " " + tokens.join(" ") : "";
428
+ return indent + baseName + annotations;
429
+ }
430
+
431
+ export { extractInlineCommentParts, formatStructureText, mapThrough, parseStructureAst };
432
+ //# sourceMappingURL=ast.mjs.map
433
+ //# sourceMappingURL=ast.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util/fs-utils.ts","../src/ast/parser.ts","../src/ast/format.ts"],"names":[],"mappings":"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","file":"ast.mjs","sourcesContent":["// src/util/fs-utils.ts\r\n\r\nimport fs from 'fs';\r\nimport path from 'path';\r\n\r\n/**\r\n * Convert any path to a POSIX-style path with forward slashes.\r\n */\r\nexport function toPosixPath(p: string): string {\r\n return p.replace(/\\\\/g, '/');\r\n}\r\n\r\n/**\r\n * Ensure a directory exists (like mkdir -p).\r\n * Returns the absolute path of the directory.\r\n */\r\nexport function ensureDirSync(dirPath: string): string {\r\n if (!fs.existsSync(dirPath)) {\r\n fs.mkdirSync(dirPath, { recursive: true });\r\n }\r\n return dirPath;\r\n}\r\n\r\n/**\r\n * Synchronous check for file or directory existence.\r\n */\r\nexport function existsSync(targetPath: string): boolean {\r\n return fs.existsSync(targetPath);\r\n}\r\n\r\n/**\r\n * Read a file as UTF-8, returning null if it doesn't exist\r\n * or if an error occurs (no exceptions thrown).\r\n */\r\nexport function readFileSafeSync(filePath: string): string | null {\r\n try {\r\n return fs.readFileSync(filePath, 'utf8');\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * Write a UTF-8 file, creating parent directories if needed.\r\n */\r\nexport function writeFileSafeSync(filePath: string, contents: string): void {\r\n const dir = path.dirname(filePath);\r\n ensureDirSync(dir);\r\n fs.writeFileSync(filePath, contents, 'utf8');\r\n}\r\n\r\n/**\r\n * Remove a file if it exists. Does nothing on error.\r\n */\r\nexport function removeFileSafeSync(filePath: string): void {\r\n try {\r\n if (fs.existsSync(filePath)) {\r\n fs.unlinkSync(filePath);\r\n }\r\n } catch {\r\n // ignore\r\n }\r\n}\r\n\r\n/**\r\n * Get file stats if they exist, otherwise null.\r\n */\r\nexport function statSafeSync(targetPath: string): fs.Stats | null {\r\n try {\r\n return fs.statSync(targetPath);\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * Resolve an absolute path from projectRoot + relative path,\r\n * and assert it stays within the project root.\r\n *\r\n * Throws if the resolved path escapes the project root.\r\n */\r\nexport function resolveProjectPath(projectRoot: string, relPath: string): string {\r\n const absRoot = path.resolve(projectRoot);\r\n const absTarget = path.resolve(absRoot, relPath);\r\n\r\n // Normalise for safety check\r\n const rootWithSep = absRoot.endsWith(path.sep) ? absRoot : absRoot + path.sep;\r\n if (!absTarget.startsWith(rootWithSep) && absTarget !== absRoot) {\r\n throw new Error(\r\n `Attempted to resolve path outside project root: ` +\r\n `root=\"${absRoot}\", target=\"${absTarget}\"`,\r\n );\r\n }\r\n\r\n return absTarget;\r\n}\r\n\r\n/**\r\n * Convert an absolute path back to a project-relative path.\r\n * Throws if the path is not under projectRoot.\r\n */\r\nexport function toProjectRelativePath(projectRoot: string, absolutePath: string): string {\r\n const absRoot = path.resolve(projectRoot);\r\n const absTarget = path.resolve(absolutePath);\r\n\r\n const rootWithSep = absRoot.endsWith(path.sep) ? absRoot : absRoot + path.sep;\r\n if (!absTarget.startsWith(rootWithSep) && absTarget !== absRoot) {\r\n throw new Error(\r\n `Path \"${absTarget}\" is not inside project root \"${absRoot}\".`,\r\n );\r\n }\r\n\r\n const rel = path.relative(absRoot, absTarget);\r\n return toPosixPath(rel);\r\n}\r\n\r\n/**\r\n * Check if `target` is inside (or equal to) `base` directory.\r\n */\r\nexport function isSubPath(base: string, target: string): boolean {\r\n const absBase = path.resolve(base);\r\n const absTarget = path.resolve(target);\r\n\r\n const baseWithSep = absBase.endsWith(path.sep) ? absBase : absBase + path.sep;\r\n return absTarget === absBase || absTarget.startsWith(baseWithSep);\r\n}","// src/ast/parser.ts\r\n\r\nimport {toPosixPath} from '../util/fs-utils';\r\n\r\nexport type AstMode = 'strict' | 'loose';\r\n\r\nexport type DiagnosticSeverity = 'info' | 'warning' | 'error';\r\n\r\nexport interface Diagnostic {\r\n line: number; // 1-based\r\n column?: number; // 1-based (optional)\r\n message: string;\r\n severity: DiagnosticSeverity;\r\n code?: string;\r\n}\r\n\r\n/**\r\n * How a physical line in the text was classified.\r\n */\r\nexport type LineKind = 'blank' | 'comment' | 'entry';\r\n\r\nexport interface StructureAstLine {\r\n index: number; // 0-based\r\n lineNo: number; // 1-based\r\n raw: string;\r\n kind: LineKind;\r\n indentSpaces: number;\r\n content: string; // after leading whitespace (includes path+annotations+inline comment)\r\n}\r\n\r\n/**\r\n * AST node base for structure entries.\r\n */\r\ninterface AstNodeBase {\r\n type: 'dir' | 'file';\r\n /** The last segment name, e.g. \"schema/\" or \"index.ts\". */\r\n name: string;\r\n /** Depth level (0 = root, 1 = child of root, etc.). */\r\n depth: number;\r\n /** 1-based source line number. */\r\n line: number;\r\n /** Normalized POSIX path from root, e.g. \"src/schema/index.ts\" or \"src/schema/\". */\r\n path: string;\r\n /** Stub annotation, if any. */\r\n stub?: string;\r\n /** Include glob patterns, if any. */\r\n include?: string[];\r\n /** Exclude glob patterns, if any. */\r\n exclude?: string[];\r\n /** Parent node; null for roots. */\r\n parent: DirNode | null;\r\n}\r\n\r\nexport interface DirNode extends AstNodeBase {\r\n type: 'dir';\r\n children: AstNode[];\r\n}\r\n\r\nexport interface FileNode extends AstNodeBase {\r\n type: 'file';\r\n children?: undefined;\r\n}\r\n\r\nexport type AstNode = DirNode | FileNode;\r\n\r\nexport interface AstOptions {\r\n /**\r\n * Spaces per indent level.\r\n * Default: 2.\r\n */\r\n indentStep?: number;\r\n\r\n /**\r\n * Parser mode:\r\n * - \"strict\": mismatched indentation / impossible structures are errors.\r\n * - \"loose\" : tries to recover from bad indentation, demotes some issues to warnings.\r\n *\r\n * Default: \"loose\".\r\n */\r\n mode?: AstMode;\r\n}\r\n\r\n/**\r\n * Full AST result: nodes + per-line meta + diagnostics.\r\n */\r\nexport interface StructureAst {\r\n /** Root-level nodes (depth 0). */\r\n rootNodes: AstNode[];\r\n /** All lines as seen in the source file. */\r\n lines: StructureAstLine[];\r\n /** Collected diagnostics (errors + warnings + infos). */\r\n diagnostics: Diagnostic[];\r\n /** Resolved options used by the parser. */\r\n options: Required<AstOptions>;\r\n}\r\n\r\n/**\r\n * Main entry: parse a structure text into an AST tree with diagnostics.\r\n *\r\n * - Does NOT throw on parse errors.\r\n * - Always returns something (even if diagnostics contain errors).\r\n * - In \"loose\" mode, attempts to repair:\r\n * - odd/misaligned indentation → snapped via relative depth rules with warnings.\r\n * - large indent jumps → treated as \"one level deeper\" with warnings.\r\n * - children under files → attached to nearest viable ancestor with warnings.\r\n */\r\nexport function parseStructureAst(\r\n text: string,\r\n opts: AstOptions = {},\r\n): StructureAst {\r\n const indentStep = opts.indentStep ?? 2;\r\n const mode: AstMode = opts.mode ?? 'loose';\r\n\r\n const diagnostics: Diagnostic[] = [];\r\n const lines: StructureAstLine[] = [];\r\n\r\n const rawLines = text.split(/\\r?\\n/);\r\n\r\n // First pass: classify + measure indentation.\r\n for (let i = 0; i < rawLines.length; i++) {\r\n const raw = rawLines[i];\r\n const lineNo = i + 1;\r\n\r\n const m = raw.match(/^(\\s*)(.*)$/);\r\n const indentRaw = m ? m[1] : '';\r\n const content = m ? m[2] : '';\r\n\r\n const {indentSpaces, hasTabs} = measureIndent(indentRaw, indentStep);\r\n\r\n if (hasTabs) {\r\n diagnostics.push({\r\n line: lineNo,\r\n message:\r\n 'Tabs detected in indentation. Consider using spaces only for consistent levels.',\r\n severity: mode === 'strict' ? 'warning' : 'info',\r\n code: 'indent-tabs',\r\n });\r\n }\r\n\r\n const trimmed = content.trim();\r\n let kind: LineKind;\r\n if (!trimmed) {\r\n kind = 'blank';\r\n } else if (trimmed.startsWith('#') || trimmed.startsWith('//')) {\r\n kind = 'comment';\r\n } else {\r\n kind = 'entry';\r\n }\r\n\r\n lines.push({\r\n index: i,\r\n lineNo,\r\n raw,\r\n kind,\r\n indentSpaces,\r\n content,\r\n });\r\n }\r\n\r\n const rootNodes: AstNode[] = [];\r\n const stack: AstNode[] = []; // nodes by depth index (0 = level 0, 1 = level 1, ...)\r\n\r\n const depthCtx: DepthContext = {\r\n lastIndentSpaces: null,\r\n lastDepth: null,\r\n lastWasFile: false,\r\n };\r\n\r\n for (const line of lines) {\r\n if (line.kind !== 'entry') continue;\r\n\r\n const {entry, depth, diags} = parseEntryLine(\r\n line,\r\n indentStep,\r\n mode,\r\n depthCtx,\r\n );\r\n diagnostics.push(...diags);\r\n\r\n if (!entry) {\r\n continue;\r\n }\r\n\r\n attachNode(entry, depth, line, rootNodes, stack, diagnostics, mode);\r\n depthCtx.lastWasFile = !entry.isDir;\r\n }\r\n\r\n return {\r\n rootNodes,\r\n lines,\r\n diagnostics,\r\n options: {\r\n indentStep,\r\n mode,\r\n },\r\n };\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal: indentation measurement & depth fixing (relative model)\r\n// ---------------------------------------------------------------------------\r\n\r\nfunction measureIndent(rawIndent: string, indentStep: number): {\r\n indentSpaces: number;\r\n hasTabs: boolean;\r\n} {\r\n let spaces = 0;\r\n let hasTabs = false;\r\n\r\n for (const ch of rawIndent) {\r\n if (ch === ' ') {\r\n spaces += 1;\r\n } else if (ch === '\\t') {\r\n hasTabs = true;\r\n // Treat tab as one level to avoid chaos. This is arbitrary but stable-ish.\r\n spaces += indentStep;\r\n }\r\n }\r\n\r\n return {indentSpaces: spaces, hasTabs};\r\n}\r\n\r\ninterface DepthContext {\r\n lastIndentSpaces: number | null;\r\n lastDepth: number | null;\r\n lastWasFile: boolean;\r\n}\r\n\r\n/**\r\n * Compute logical depth using a relative algorithm:\r\n *\r\n * First entry line:\r\n * - depth = 0\r\n *\r\n * For each subsequent entry line:\r\n * Let prevSpaces = lastIndentSpaces, prevDepth = lastDepth.\r\n *\r\n * - if spaces > prevSpaces:\r\n * - if spaces > prevSpaces + indentStep → warn about a \"skip\"\r\n * - depth = prevDepth + 1\r\n *\r\n * - else if spaces === prevSpaces:\r\n * - depth = prevDepth\r\n *\r\n * - else (spaces < prevSpaces):\r\n * - diff = prevSpaces - spaces\r\n * - steps = round(diff / indentStep)\r\n * - if diff is not a clean multiple → warn about misalignment\r\n * - depth = max(prevDepth - steps, 0)\r\n */\r\nfunction computeDepth(\r\n line: StructureAstLine,\r\n indentStep: number,\r\n mode: AstMode,\r\n ctx: DepthContext,\r\n diagnostics: Diagnostic[],\r\n): number {\r\n let spaces = line.indentSpaces;\r\n if (spaces < 0) spaces = 0;\r\n\r\n let depth: number;\r\n\r\n if (ctx.lastIndentSpaces == null || ctx.lastDepth == null) {\r\n // First entry line: treat as root.\r\n depth = 0;\r\n } else {\r\n const prevSpaces = ctx.lastIndentSpaces;\r\n const prevDepth = ctx.lastDepth;\r\n\r\n if (spaces > prevSpaces) {\r\n const diff = spaces - prevSpaces;\r\n\r\n // NEW: indenting under a file → child-of-file-loose\r\n if (ctx.lastWasFile) {\r\n diagnostics.push({\r\n line: line.lineNo,\r\n message:\r\n 'Entry appears indented under a file; treating it as a sibling of the file instead of a child.',\r\n severity: mode === 'strict' ? 'error' : 'warning',\r\n code: 'child-of-file-loose',\r\n });\r\n\r\n // Treat as sibling of the file, not a child:\r\n depth = prevDepth;\r\n } else {\r\n if (diff > indentStep) {\r\n diagnostics.push({\r\n line: line.lineNo,\r\n message: `Indentation jumps from ${prevSpaces} to ${spaces} spaces; treating as one level deeper.`,\r\n severity: mode === 'strict' ? 'error' : 'warning',\r\n code: 'indent-skip-level',\r\n });\r\n }\r\n depth = prevDepth + 1;\r\n }\r\n } else if (spaces === prevSpaces) {\r\n depth = prevDepth;\r\n } else {\r\n const diff = prevSpaces - spaces;\r\n const steps = Math.round(diff / indentStep);\r\n\r\n if (diff % indentStep !== 0) {\r\n diagnostics.push({\r\n line: line.lineNo,\r\n message: `Indentation decreases from ${prevSpaces} to ${spaces} spaces, which is not a multiple of indent step (${indentStep}).`,\r\n severity: mode === 'strict' ? 'error' : 'warning',\r\n code: 'indent-misaligned',\r\n });\r\n }\r\n\r\n depth = Math.max(prevDepth - steps, 0);\r\n }\r\n }\r\n\r\n ctx.lastIndentSpaces = spaces;\r\n ctx.lastDepth = depth;\r\n\r\n return depth;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal: entry line parsing (path + annotations)\r\n// ---------------------------------------------------------------------------\r\n\r\ninterface ParsedEntry {\r\n segmentName: string;\r\n isDir: boolean;\r\n stub?: string;\r\n include?: string[];\r\n exclude?: string[];\r\n}\r\n\r\n/**\r\n * Parse a single entry line into a ParsedEntry + depth.\r\n */\r\nfunction parseEntryLine(\r\n line: StructureAstLine,\r\n indentStep: number,\r\n mode: AstMode,\r\n ctx: DepthContext,\r\n): {\r\n entry: ParsedEntry | null;\r\n depth: number;\r\n diags: Diagnostic[];\r\n} {\r\n const diags: Diagnostic[] = [];\r\n const depth = computeDepth(line, indentStep, mode, ctx, diags);\r\n\r\n // Extract before inline comment\r\n const {contentWithoutComment} = extractInlineCommentParts(line.content);\r\n const trimmed = contentWithoutComment.trim();\r\n if (!trimmed) {\r\n // Structural line that became empty after stripping inline comment; treat as no-op.\r\n return {entry: null, depth, diags};\r\n }\r\n\r\n const parts = trimmed.split(/\\s+/);\r\n const pathToken = parts[0];\r\n const annotationTokens = parts.slice(1);\r\n\r\n // Path sanity checks\r\n if (pathToken.includes(':')) {\r\n diags.push({\r\n line: line.lineNo,\r\n message:\r\n 'Path token contains \":\" which is reserved for annotations. This is likely a mistake.',\r\n severity: mode === 'strict' ? 'error' : 'warning',\r\n code: 'path-colon',\r\n });\r\n }\r\n\r\n const isDir = pathToken.endsWith('/');\r\n const segmentName = pathToken;\r\n\r\n let stub: string | undefined;\r\n const include: string[] = [];\r\n const exclude: string[] = [];\r\n\r\n for (const token of annotationTokens) {\r\n if (token.startsWith('@stub:')) {\r\n stub = token.slice('@stub:'.length);\r\n } else if (token.startsWith('@include:')) {\r\n const val = token.slice('@include:'.length);\r\n if (val) {\r\n include.push(\r\n ...val\r\n .split(',')\r\n .map((s) => s.trim())\r\n .filter(Boolean),\r\n );\r\n }\r\n } else if (token.startsWith('@exclude:')) {\r\n const val = token.slice('@exclude:'.length);\r\n if (val) {\r\n exclude.push(\r\n ...val\r\n .split(',')\r\n .map((s) => s.trim())\r\n .filter(Boolean),\r\n );\r\n }\r\n } else if (token.startsWith('@')) {\r\n diags.push({\r\n line: line.lineNo,\r\n message: `Unknown annotation token \"${token}\".`,\r\n severity: 'info',\r\n code: 'unknown-annotation',\r\n });\r\n }\r\n }\r\n\r\n const entry: ParsedEntry = {\r\n segmentName,\r\n isDir,\r\n stub,\r\n include: include.length ? include : undefined,\r\n exclude: exclude.length ? exclude : undefined,\r\n };\r\n\r\n return {entry, depth, diags};\r\n}\r\n\r\nexport function mapThrough(content: string) {\r\n let cutIndex = -1;\r\n const len = content.length;\r\n\r\n for (let i = 0; i < len; i++) {\r\n const ch = content[i];\r\n const prev = i > 0 ? content[i - 1] : '';\r\n\r\n // Inline \"# ...\"\r\n if (ch === '#') {\r\n if (i === 0) {\r\n // full-line comment; not our case (we only call this for \"entry\" lines)\r\n continue;\r\n }\r\n if (prev === ' ' || prev === '\\t') {\r\n cutIndex = i;\r\n break;\r\n }\r\n }\r\n\r\n // Inline \"// ...\"\r\n if (\r\n ch === '/' &&\r\n i + 1 < len &&\r\n content[i + 1] === '/' &&\r\n (prev === ' ' || prev === '\\t')\r\n ) {\r\n cutIndex = i;\r\n break;\r\n }\r\n }\r\n\r\n return cutIndex;\r\n}\r\n\r\n/**\r\n * Extracts the inline comment portion (if any) from the content area (no leading indent).\r\n */\r\nexport function extractInlineCommentParts(content: string): {\r\n contentWithoutComment: string;\r\n inlineComment: string | null;\r\n} {\r\n const cutIndex = mapThrough(content);\r\n\r\n if (cutIndex === -1) {\r\n return {\r\n contentWithoutComment: content,\r\n inlineComment: null,\r\n };\r\n }\r\n\r\n return {\r\n contentWithoutComment: content.slice(0, cutIndex),\r\n inlineComment: content.slice(cutIndex),\r\n };\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal: tree construction\r\n// ---------------------------------------------------------------------------\r\n\r\nfunction attachNode(\r\n entry: ParsedEntry,\r\n depth: number,\r\n line: StructureAstLine,\r\n rootNodes: AstNode[],\r\n stack: AstNode[],\r\n diagnostics: Diagnostic[],\r\n mode: AstMode,\r\n): void {\r\n const lineNo = line.lineNo;\r\n\r\n // Pop stack until we’re at or above the desired depth.\r\n while (stack.length > depth) {\r\n stack.pop();\r\n }\r\n\r\n let parent: DirNode | null = null;\r\n if (depth > 0) {\r\n const candidate = stack[depth - 1];\r\n if (!candidate) {\r\n // Indented but no parent; in strict mode error, in loose mode, treat as root.\r\n diagnostics.push({\r\n line: lineNo,\r\n message: `Entry has indent depth ${depth} but no parent at depth ${\r\n depth - 1\r\n }. Treating as root.`,\r\n severity: mode === 'strict' ? 'error' : 'warning',\r\n code: 'missing-parent',\r\n });\r\n } else if (candidate.type === 'file') {\r\n // Child under file, impossible by design.\r\n if (mode === 'strict') {\r\n diagnostics.push({\r\n line: lineNo,\r\n message: `Cannot attach child under file \"${candidate.path}\".`,\r\n severity: 'error',\r\n code: 'child-of-file',\r\n });\r\n // Force it to root to at least keep the node.\r\n } else {\r\n diagnostics.push({\r\n line: lineNo,\r\n message: `Entry appears under file \"${candidate.path}\". Attaching as sibling at depth ${\r\n candidate.depth\r\n }.`,\r\n severity: 'warning',\r\n code: 'child-of-file-loose',\r\n });\r\n // Treat as sibling at candidate's depth.\r\n while (stack.length > candidate.depth) {\r\n stack.pop();\r\n }\r\n }\r\n } else {\r\n parent = candidate as DirNode;\r\n }\r\n }\r\n\r\n const parentPath = parent ? parent.path.replace(/\\/$/, '') : '';\r\n const normalizedSegment = toPosixPath(entry.segmentName.replace(/\\/+$/, ''));\r\n const fullPath = parentPath\r\n ? `${parentPath}/${normalizedSegment}${entry.isDir ? '/' : ''}`\r\n : `${normalizedSegment}${entry.isDir ? '/' : ''}`;\r\n\r\n const baseNode: AstNodeBase = {\r\n type: entry.isDir ? 'dir' : 'file',\r\n name: entry.segmentName,\r\n depth,\r\n line: lineNo,\r\n path: fullPath,\r\n parent,\r\n ...(entry.stub ? {stub: entry.stub} : {}),\r\n ...(entry.include ? {include: entry.include} : {}),\r\n ...(entry.exclude ? {exclude: entry.exclude} : {}),\r\n };\r\n\r\n if (entry.isDir) {\r\n const dirNode: DirNode = {\r\n ...baseNode,\r\n type: 'dir',\r\n children: [],\r\n };\r\n\r\n if (parent) {\r\n parent.children.push(dirNode);\r\n } else {\r\n rootNodes.push(dirNode);\r\n }\r\n\r\n // Ensure stack[depth] is this dir.\r\n while (stack.length > depth) {\r\n stack.pop();\r\n }\r\n stack[depth] = dirNode;\r\n } else {\r\n const fileNode: FileNode = {\r\n ...baseNode,\r\n type: 'file',\r\n };\r\n\r\n if (parent) {\r\n parent.children.push(fileNode);\r\n } else {\r\n rootNodes.push(fileNode);\r\n }\r\n\r\n // Files themselves are NOT placed on the stack to prevent children,\r\n // but attachNode will repair children-under-file in loose mode.\r\n }\r\n}","// src/ast/format.ts\r\n\r\nimport {\r\n parseStructureAst,\r\n type AstMode,\r\n type StructureAst,\r\n type AstNode, extractInlineCommentParts,\r\n} from './parser';\r\n\r\nexport interface FormatOptions {\r\n /**\r\n * Spaces per indent level for re-printing entries.\r\n * Defaults to 2.\r\n */\r\n indentStep?: number;\r\n\r\n /**\r\n * Parser mode to use for the AST.\r\n * - \"loose\": attempt to repair mis-indents / bad parents (default).\r\n * - \"strict\": report issues as errors, less repair.\r\n */\r\n mode?: AstMode;\r\n\r\n /**\r\n * Normalize newlines to the dominant style in the original text (LF vs. CRLF).\r\n * Defaults to true.\r\n */\r\n normalizeNewlines?: boolean;\r\n\r\n /**\r\n * Trim trailing whitespace on non-entry lines (comments / blanks).\r\n * Defaults to true.\r\n */\r\n trimTrailingWhitespace?: boolean;\r\n\r\n /**\r\n * Whether to normalize annotation ordering and spacing:\r\n * name @stub:... @include:... @exclude:...\r\n * Defaults to true.\r\n */\r\n normalizeAnnotations?: boolean;\r\n}\r\n\r\nexport interface FormatResult {\r\n /** Formatted text. */\r\n text: string;\r\n /** Underlying AST that was used. */\r\n ast: StructureAst;\r\n}\r\n\r\n/**\r\n * Smart formatter for scaffold structure files.\r\n *\r\n * - Uses the loose AST parser (parseStructureAst) to understand structure.\r\n * - Auto-fixes indentation based on tree depth (indentStep).\r\n * - Keeps **all** blank lines and full-line comments in place.\r\n * - Preserves inline comments (# / //) on entry lines.\r\n * - Canonicalizes annotation order (stub → include → exclude) if enabled.\r\n *\r\n * It does **not** throw on invalid input:\r\n * - parseStructureAst always returns an AST + diagnostics.\r\n * - If something is catastrophically off (entry/node counts mismatch),\r\n * it falls back to a minimal normalization pass.\r\n */\r\nexport function formatStructureText(\r\n text: string,\r\n options: FormatOptions = {},\r\n): FormatResult {\r\n const indentStep = options.indentStep ?? 2;\r\n const mode: AstMode = options.mode ?? 'loose';\r\n const normalizeNewlines =\r\n options.normalizeNewlines === undefined ? true : options.normalizeNewlines;\r\n const trimTrailingWhitespace =\r\n options.trimTrailingWhitespace === undefined\r\n ? true\r\n : options.trimTrailingWhitespace;\r\n const normalizeAnnotations =\r\n options.normalizeAnnotations === undefined\r\n ? true\r\n : options.normalizeAnnotations;\r\n\r\n // 1. Parse to our \"smart\" AST (non-throwing).\r\n const ast = parseStructureAst(text, {\r\n indentStep,\r\n mode,\r\n });\r\n\r\n const rawLines = text.split(/\\r?\\n/);\r\n const lineCount = rawLines.length;\r\n\r\n // Sanity check: AST lines length should match raw lines length.\r\n if (ast.lines.length !== lineCount) {\r\n return {\r\n text: basicNormalize(text, {normalizeNewlines, trimTrailingWhitespace}),\r\n ast,\r\n };\r\n }\r\n\r\n // 2. Collect entry line indices and inline comments from the original text.\r\n const entryLineIndexes: number[] = [];\r\n const inlineComments: (string | null)[] = [];\r\n\r\n for (let i = 0; i < lineCount; i++) {\r\n const lineMeta = ast.lines[i];\r\n if (lineMeta.kind === 'entry') {\r\n entryLineIndexes.push(i);\r\n const {inlineComment} = extractInlineCommentParts(lineMeta.content);\r\n inlineComments.push(inlineComment);\r\n }\r\n }\r\n\r\n // 3. Flatten AST nodes in depth-first order to get an ordered node list.\r\n const flattened: { node: AstNode; level: number }[] = [];\r\n flattenAstNodes(ast.rootNodes, 0, flattened);\r\n\r\n if (flattened.length !== entryLineIndexes.length) {\r\n // If counts don't match, something is inconsistent – do not risk corruption.\r\n return {\r\n text: basicNormalize(text, {normalizeNewlines, trimTrailingWhitespace}),\r\n ast,\r\n };\r\n }\r\n\r\n // 4. Build canonical entry lines from AST nodes.\r\n const canonicalEntryLines: string[] = flattened.map(({node, level}) =>\r\n formatAstNodeLine(node, level, indentStep, normalizeAnnotations),\r\n );\r\n\r\n // 5. Merge canonical entry lines + inline comments back into original structure.\r\n const resultLines: string[] = [];\r\n let entryIdx = 0;\r\n\r\n for (let i = 0; i < lineCount; i++) {\r\n const lineMeta = ast.lines[i];\r\n const originalLine = rawLines[i];\r\n\r\n if (lineMeta.kind === 'entry') {\r\n const base = canonicalEntryLines[entryIdx].replace(/[ \\t]+$/g, '');\r\n const inline = inlineComments[entryIdx];\r\n entryIdx++;\r\n\r\n if (inline) {\r\n // Always ensure a single space before the inline comment marker.\r\n resultLines.push(base + ' ' + inline);\r\n } else {\r\n resultLines.push(base);\r\n }\r\n } else {\r\n let out = originalLine;\r\n if (trimTrailingWhitespace) {\r\n out = out.replace(/[ \\t]+$/g, '');\r\n }\r\n resultLines.push(out);\r\n }\r\n }\r\n\r\n const eol = normalizeNewlines ? detectPreferredEol(text) : getRawEol(text);\r\n return {\r\n text: resultLines.join(eol),\r\n ast,\r\n };\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal helpers\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Fallback: basic normalization when we can't safely map AST ↔ text.\r\n */\r\nfunction basicNormalize(\r\n text: string,\r\n opts: { normalizeNewlines: boolean; trimTrailingWhitespace: boolean },\r\n): string {\r\n const lines = text.split(/\\r?\\n/);\r\n const normalizedLines = opts.trimTrailingWhitespace\r\n ? lines.map((line) => line.replace(/[ \\t]+$/g, ''))\r\n : lines;\r\n\r\n const eol = opts.normalizeNewlines ? detectPreferredEol(text) : getRawEol(text);\r\n return normalizedLines.join(eol);\r\n}\r\n\r\n/**\r\n * Detect whether the file is more likely LF or CRLF and reuse that.\r\n * If mixed or no clear signal, default to \"\\n\".\r\n */\r\nfunction detectPreferredEol(text: string): string {\r\n const crlfCount = (text.match(/\\r\\n/g) || []).length;\r\n const lfCount = (text.match(/(?<!\\r)\\n/g) || []).length;\r\n\r\n if (crlfCount === 0 && lfCount === 0) {\r\n return '\\n';\r\n }\r\n\r\n if (crlfCount > lfCount) {\r\n return '\\r\\n';\r\n }\r\n\r\n return '\\n';\r\n}\r\n\r\n/**\r\n * If you really want the raw style, detect only CRLF vs. LF.\r\n */\r\nfunction getRawEol(text: string): string {\r\n return text.includes('\\r\\n') ? '\\r\\n' : '\\n';\r\n}\r\n\r\n/**\r\n * Flatten AST nodes into a depth-first list while tracking indent level.\r\n */\r\nfunction flattenAstNodes(\r\n nodes: AstNode[],\r\n level: number,\r\n out: { node: AstNode; level: number }[],\r\n): void {\r\n for (const node of nodes) {\r\n out.push({node, level});\r\n if (node.type === 'dir' && node.children && node.children.length) {\r\n flattenAstNodes(node.children, level + 1, out);\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Format a single AST node into one canonical line.\r\n *\r\n * - Uses `level * indentStep` spaces as indentation.\r\n * - Uses the node's `name` as provided by the parser (e.g. \"src/\" or \"index.ts\").\r\n * - Annotations are printed in a stable order if normalizeAnnotations is true:\r\n * @stub:..., @include:..., @exclude:...\r\n */\r\nfunction formatAstNodeLine(\r\n node: AstNode,\r\n level: number,\r\n indentStep: number,\r\n normalizeAnnotations: boolean,\r\n): string {\r\n const indent = ' '.repeat(indentStep * level);\r\n const baseName = node.name;\r\n\r\n if (!normalizeAnnotations) {\r\n return indent + baseName;\r\n }\r\n\r\n const tokens: string[] = [];\r\n\r\n if (node.stub) {\r\n tokens.push(`@stub:${node.stub}`);\r\n }\r\n if (node.include && node.include.length > 0) {\r\n tokens.push(`@include:${node.include.join(',')}`);\r\n }\r\n if (node.exclude && node.exclude.length > 0) {\r\n tokens.push(`@exclude:${node.exclude.join(',')}`);\r\n }\r\n\r\n const annotations = tokens.length ? ' ' + tokens.join(' ') : '';\r\n return indent + baseName + annotations;\r\n}"]}
package/dist/index.cjs CHANGED
@@ -15,7 +15,8 @@ var fs2__default = /*#__PURE__*/_interopDefault(fs2);
15
15
  var os__default = /*#__PURE__*/_interopDefault(os);
16
16
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
17
17
 
18
- // src/core/runner.ts
18
+ // src/schema/index.ts
19
+ var SCAFFOLD_ROOT_DIR = ".scaffold";
19
20
 
20
21
  // src/util/logger.ts
21
22
  var supportsColor = typeof process !== "undefined" && process.stdout && process.stdout.isTTY && process.env.NO_COLOR !== "1";
@@ -140,14 +141,14 @@ function toProjectRelativePath(projectRoot, absolutePath) {
140
141
  var logger = defaultLogger.child("[config]");
141
142
  async function loadScaffoldConfig(cwd, options = {}) {
142
143
  const absCwd = path2__default.default.resolve(cwd);
143
- const initialScaffoldDir = options.scaffoldDir ? path2__default.default.resolve(absCwd, options.scaffoldDir) : path2__default.default.join(absCwd, "scaffold");
144
+ const initialScaffoldDir = options.scaffoldDir ? path2__default.default.resolve(absCwd, options.scaffoldDir) : path2__default.default.join(absCwd, SCAFFOLD_ROOT_DIR);
144
145
  const configPath = options.configPath ?? resolveConfigPath(initialScaffoldDir);
145
146
  const config = await importConfig(configPath);
146
147
  let configRoot = absCwd;
147
148
  if (config.root) {
148
149
  configRoot = path2__default.default.resolve(absCwd, config.root);
149
150
  }
150
- const scaffoldDir = options.scaffoldDir ? path2__default.default.resolve(absCwd, options.scaffoldDir) : path2__default.default.join(configRoot, "scaffold");
151
+ const scaffoldDir = options.scaffoldDir ? path2__default.default.resolve(absCwd, options.scaffoldDir) : path2__default.default.join(configRoot, SCAFFOLD_ROOT_DIR);
151
152
  const baseRoot = config.base ? path2__default.default.resolve(configRoot, config.base) : configRoot;
152
153
  logger.debug(
153
154
  `Loaded config: configRoot=${configRoot}, baseRoot=${baseRoot}, scaffoldDir=${scaffoldDir}`
@@ -211,15 +212,59 @@ async function importTsConfig(configPath) {
211
212
  return mod.default ?? mod;
212
213
  }
213
214
 
215
+ // src/ast/parser.ts
216
+ function mapThrough(content) {
217
+ let cutIndex = -1;
218
+ const len = content.length;
219
+ for (let i = 0; i < len; i++) {
220
+ const ch = content[i];
221
+ const prev = i > 0 ? content[i - 1] : "";
222
+ if (ch === "#") {
223
+ if (i === 0) {
224
+ continue;
225
+ }
226
+ if (prev === " " || prev === " ") {
227
+ cutIndex = i;
228
+ break;
229
+ }
230
+ }
231
+ if (ch === "/" && i + 1 < len && content[i + 1] === "/" && (prev === " " || prev === " ")) {
232
+ cutIndex = i;
233
+ break;
234
+ }
235
+ }
236
+ return cutIndex;
237
+ }
238
+
214
239
  // src/core/structure-txt.ts
240
+ function stripInlineComment(content) {
241
+ const cutIndex = mapThrough(content);
242
+ if (cutIndex === -1) {
243
+ return content.trimEnd();
244
+ }
245
+ return content.slice(0, cutIndex).trimEnd();
246
+ }
215
247
  function parseLine(line, lineNo) {
216
- const match = line.match(/^(\s*)(.+)$/);
248
+ const match = line.match(/^(\s*)(.*)$/);
217
249
  if (!match) return null;
218
250
  const indentSpaces = match[1].length;
219
- const rest = match[2].trim();
220
- if (!rest || rest.startsWith("#")) return null;
221
- const parts = rest.split(/\s+/);
251
+ let rest = match[2];
252
+ if (!rest.trim()) return null;
253
+ const trimmedRest = rest.trimStart();
254
+ if (trimmedRest.startsWith("#") || trimmedRest.startsWith("//")) {
255
+ return null;
256
+ }
257
+ const stripped = stripInlineComment(rest);
258
+ const trimmed = stripped.trim();
259
+ if (!trimmed) return null;
260
+ const parts = trimmed.split(/\s+/);
261
+ if (!parts.length) return null;
222
262
  const pathToken = parts[0];
263
+ if (pathToken.includes(":")) {
264
+ throw new Error(
265
+ `structure.txt: ":" is reserved for annotations (@stub:, @include:, etc). Invalid path "${pathToken}" on line ${lineNo}.`
266
+ );
267
+ }
223
268
  let stub;
224
269
  const include = [];
225
270
  const exclude = [];
@@ -251,7 +296,7 @@ function parseLine(line, lineNo) {
251
296
  exclude: exclude.length ? exclude : void 0
252
297
  };
253
298
  }
254
- function parseStructureText(text) {
299
+ function parseStructureText(text, indentStep = 2) {
255
300
  const lines = text.split(/\r?\n/);
256
301
  const parsed = [];
257
302
  for (let i = 0; i < lines.length; i++) {
@@ -261,15 +306,14 @@ function parseStructureText(text) {
261
306
  }
262
307
  const rootEntries = [];
263
308
  const stack = [];
264
- const INDENT_STEP = 2;
265
309
  for (const p of parsed) {
266
310
  const { indentSpaces, lineNo } = p;
267
- if (indentSpaces % INDENT_STEP !== 0) {
311
+ if (indentSpaces % indentStep !== 0) {
268
312
  throw new Error(
269
- `structure.txt: Invalid indent on line ${lineNo}. Indent must be multiples of ${INDENT_STEP} spaces.`
313
+ `structure.txt: Invalid indent on line ${lineNo}. Indent must be multiples of ${indentStep} spaces.`
270
314
  );
271
315
  }
272
- const level = indentSpaces / INDENT_STEP;
316
+ const level = indentSpaces / indentStep;
273
317
  if (level > stack.length) {
274
318
  if (level !== stack.length + 1) {
275
319
  throw new Error(
@@ -816,7 +860,10 @@ async function ensureStructureFilesFromConfig(cwd, options = {}) {
816
860
  return { created, existing };
817
861
  }
818
862
 
863
+ exports.SCAFFOLD_ROOT_DIR = SCAFFOLD_ROOT_DIR;
819
864
  exports.ensureStructureFilesFromConfig = ensureStructureFilesFromConfig;
865
+ exports.loadScaffoldConfig = loadScaffoldConfig;
866
+ exports.parseStructureText = parseStructureText;
820
867
  exports.runOnce = runOnce;
821
868
  exports.scanDirectoryToStructureText = scanDirectoryToStructureText;
822
869
  exports.scanProjectFromConfig = scanProjectFromConfig;