@timeax/scaffold 0.0.1 → 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/dist/index.d.cts CHANGED
@@ -305,6 +305,15 @@ interface ScaffoldConfig {
305
305
  * runner / CLI.
306
306
  */
307
307
  watch?: boolean;
308
+ /**
309
+ * Number of spaces per indent level in structure files.
310
+ * Default: 2.
311
+ *
312
+ * Examples:
313
+ * - 2 → "··entry"
314
+ * - 4 → "····entry"
315
+ */
316
+ indentStep?: number;
308
317
  }
309
318
  /**
310
319
  * Options when scanning an existing directory into a structure.txt tree.
@@ -336,6 +345,8 @@ interface ScanFromConfigOptions extends ScanStructureOptions {
336
345
  scaffoldDir?: string;
337
346
  }
338
347
 
348
+ declare const SCAFFOLD_ROOT_DIR = ".scaffold";
349
+
339
350
  type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';
340
351
  interface LoggerOptions {
341
352
  level?: LogLevel;
@@ -394,6 +405,49 @@ interface RunOptions {
394
405
  */
395
406
  declare function runOnce(cwd: string, options?: RunOptions): Promise<void>;
396
407
 
408
+ interface LoadScaffoldConfigOptions {
409
+ /**
410
+ * Optional explicit scaffold directory path (absolute or relative to cwd).
411
+ * If provided, this overrides config.root for locating the scaffold folder.
412
+ */
413
+ scaffoldDir?: string;
414
+ /**
415
+ * Optional explicit config file path (absolute or relative to cwd).
416
+ * If not provided, we look for config.* inside the scaffoldDir.
417
+ */
418
+ configPath?: string;
419
+ }
420
+ interface LoadScaffoldConfigResult {
421
+ /**
422
+ * Parsed scaffold configuration.
423
+ */
424
+ config: ScaffoldConfig;
425
+ /**
426
+ * Absolute path to the scaffold directory (where config & *.txt live).
427
+ */
428
+ scaffoldDir: string;
429
+ /**
430
+ * Effective project root BASE where structures are applied.
431
+ * This is derived from config.root + config.base.
432
+ */
433
+ projectRoot: string;
434
+ }
435
+ /**
436
+ * Load scaffold configuration based on CWD + optional overrides + config.root/base.
437
+ *
438
+ * Resolution rules:
439
+ * - configRoot:
440
+ * - Start from cwd.
441
+ * - Apply config.root (if defined) as a path relative to cwd.
442
+ * - scaffoldDir:
443
+ * - If options.scaffoldDir is provided → use it as-is (resolved from cwd).
444
+ * - Else → <configRoot>/scaffold.
445
+ * - projectRoot (base):
446
+ * - If config.base is defined → resolve relative to configRoot.
447
+ * - Else → configRoot.
448
+ */
449
+ declare function loadScaffoldConfig(cwd: string, options?: LoadScaffoldConfigOptions): Promise<LoadScaffoldConfigResult>;
450
+
397
451
  /**
398
452
  * Generate a structure.txt-style tree from an existing directory.
399
453
  *
@@ -435,5 +489,34 @@ declare function scanProjectFromConfig(cwd: string, options?: ScanFromConfigOpti
435
489
  * structure files.
436
490
  */
437
491
  declare function writeScannedStructuresFromConfig(cwd: string, options?: ScanFromConfigOptions): Promise<void>;
492
+ interface EnsureStructuresResult {
493
+ created: string[];
494
+ existing: string[];
495
+ }
496
+ /**
497
+ * Ensure all structure files declared in the config exist.
498
+ *
499
+ * - Grouped mode: one file per group (group.structureFile || `${group.name}.txt`)
500
+ * - Single-root mode: config.structureFile || "structure.txt"
501
+ *
502
+ * Existing files are left untouched. Only missing files are created with
503
+ * a small header comment.
504
+ */
505
+ declare function ensureStructureFilesFromConfig(cwd: string, options?: {
506
+ scaffoldDirOverride?: string;
507
+ }): Promise<EnsureStructuresResult>;
508
+
509
+ /**
510
+ * Convert a structure.txt content into a nested StructureEntry[].
511
+ *
512
+ * Rules:
513
+ * - Indentation is **indentStep** spaces per level (default: 2).
514
+ * - Indent must be a multiple of indentStep.
515
+ * - You cannot "skip" levels (no jumping from level 0 to 2 directly).
516
+ * - **Only directories can have children**:
517
+ * - If you indent under a file, an error is thrown.
518
+ * - Folders must end with "/" in the txt; paths are normalized to POSIX.
519
+ */
520
+ declare function parseStructureText(text: string, indentStep?: number): StructureEntry[];
438
521
 
439
- export { type BaseEntryOptions, type DirEntry, type FileEntry, type HookContext, type HookFilter, type RegularHookConfig, type RegularHookFn, type RegularHookKind, type RunOptions, type ScaffoldConfig, type ScanFromConfigOptions, type ScanFromConfigResult, type ScanStructureOptions, type StructureEntry, type StructureGroupConfig, type StubConfig, type StubHookConfig, type StubHookFn, type StubHookKind, runOnce, scanDirectoryToStructureText, scanProjectFromConfig, writeScannedStructuresFromConfig };
522
+ export { type BaseEntryOptions, type DirEntry, type EnsureStructuresResult, type FileEntry, type HookContext, type HookFilter, type LoadScaffoldConfigOptions, type LoadScaffoldConfigResult, type RegularHookConfig, type RegularHookFn, type RegularHookKind, type RunOptions, SCAFFOLD_ROOT_DIR, type ScaffoldConfig, type ScanFromConfigOptions, type ScanFromConfigResult, type ScanStructureOptions, type StructureEntry, type StructureGroupConfig, type StubConfig, type StubHookConfig, type StubHookFn, type StubHookKind, ensureStructureFilesFromConfig, loadScaffoldConfig, parseStructureText, runOnce, scanDirectoryToStructureText, scanProjectFromConfig, writeScannedStructuresFromConfig };
package/dist/index.d.ts CHANGED
@@ -305,6 +305,15 @@ interface ScaffoldConfig {
305
305
  * runner / CLI.
306
306
  */
307
307
  watch?: boolean;
308
+ /**
309
+ * Number of spaces per indent level in structure files.
310
+ * Default: 2.
311
+ *
312
+ * Examples:
313
+ * - 2 → "··entry"
314
+ * - 4 → "····entry"
315
+ */
316
+ indentStep?: number;
308
317
  }
309
318
  /**
310
319
  * Options when scanning an existing directory into a structure.txt tree.
@@ -336,6 +345,8 @@ interface ScanFromConfigOptions extends ScanStructureOptions {
336
345
  scaffoldDir?: string;
337
346
  }
338
347
 
348
+ declare const SCAFFOLD_ROOT_DIR = ".scaffold";
349
+
339
350
  type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';
340
351
  interface LoggerOptions {
341
352
  level?: LogLevel;
@@ -394,6 +405,49 @@ interface RunOptions {
394
405
  */
395
406
  declare function runOnce(cwd: string, options?: RunOptions): Promise<void>;
396
407
 
408
+ interface LoadScaffoldConfigOptions {
409
+ /**
410
+ * Optional explicit scaffold directory path (absolute or relative to cwd).
411
+ * If provided, this overrides config.root for locating the scaffold folder.
412
+ */
413
+ scaffoldDir?: string;
414
+ /**
415
+ * Optional explicit config file path (absolute or relative to cwd).
416
+ * If not provided, we look for config.* inside the scaffoldDir.
417
+ */
418
+ configPath?: string;
419
+ }
420
+ interface LoadScaffoldConfigResult {
421
+ /**
422
+ * Parsed scaffold configuration.
423
+ */
424
+ config: ScaffoldConfig;
425
+ /**
426
+ * Absolute path to the scaffold directory (where config & *.txt live).
427
+ */
428
+ scaffoldDir: string;
429
+ /**
430
+ * Effective project root BASE where structures are applied.
431
+ * This is derived from config.root + config.base.
432
+ */
433
+ projectRoot: string;
434
+ }
435
+ /**
436
+ * Load scaffold configuration based on CWD + optional overrides + config.root/base.
437
+ *
438
+ * Resolution rules:
439
+ * - configRoot:
440
+ * - Start from cwd.
441
+ * - Apply config.root (if defined) as a path relative to cwd.
442
+ * - scaffoldDir:
443
+ * - If options.scaffoldDir is provided → use it as-is (resolved from cwd).
444
+ * - Else → <configRoot>/scaffold.
445
+ * - projectRoot (base):
446
+ * - If config.base is defined → resolve relative to configRoot.
447
+ * - Else → configRoot.
448
+ */
449
+ declare function loadScaffoldConfig(cwd: string, options?: LoadScaffoldConfigOptions): Promise<LoadScaffoldConfigResult>;
450
+
397
451
  /**
398
452
  * Generate a structure.txt-style tree from an existing directory.
399
453
  *
@@ -435,5 +489,34 @@ declare function scanProjectFromConfig(cwd: string, options?: ScanFromConfigOpti
435
489
  * structure files.
436
490
  */
437
491
  declare function writeScannedStructuresFromConfig(cwd: string, options?: ScanFromConfigOptions): Promise<void>;
492
+ interface EnsureStructuresResult {
493
+ created: string[];
494
+ existing: string[];
495
+ }
496
+ /**
497
+ * Ensure all structure files declared in the config exist.
498
+ *
499
+ * - Grouped mode: one file per group (group.structureFile || `${group.name}.txt`)
500
+ * - Single-root mode: config.structureFile || "structure.txt"
501
+ *
502
+ * Existing files are left untouched. Only missing files are created with
503
+ * a small header comment.
504
+ */
505
+ declare function ensureStructureFilesFromConfig(cwd: string, options?: {
506
+ scaffoldDirOverride?: string;
507
+ }): Promise<EnsureStructuresResult>;
508
+
509
+ /**
510
+ * Convert a structure.txt content into a nested StructureEntry[].
511
+ *
512
+ * Rules:
513
+ * - Indentation is **indentStep** spaces per level (default: 2).
514
+ * - Indent must be a multiple of indentStep.
515
+ * - You cannot "skip" levels (no jumping from level 0 to 2 directly).
516
+ * - **Only directories can have children**:
517
+ * - If you indent under a file, an error is thrown.
518
+ * - Folders must end with "/" in the txt; paths are normalized to POSIX.
519
+ */
520
+ declare function parseStructureText(text: string, indentStep?: number): StructureEntry[];
438
521
 
439
- export { type BaseEntryOptions, type DirEntry, type FileEntry, type HookContext, type HookFilter, type RegularHookConfig, type RegularHookFn, type RegularHookKind, type RunOptions, type ScaffoldConfig, type ScanFromConfigOptions, type ScanFromConfigResult, type ScanStructureOptions, type StructureEntry, type StructureGroupConfig, type StubConfig, type StubHookConfig, type StubHookFn, type StubHookKind, runOnce, scanDirectoryToStructureText, scanProjectFromConfig, writeScannedStructuresFromConfig };
522
+ export { type BaseEntryOptions, type DirEntry, type EnsureStructuresResult, type FileEntry, type HookContext, type HookFilter, type LoadScaffoldConfigOptions, type LoadScaffoldConfigResult, type RegularHookConfig, type RegularHookFn, type RegularHookKind, type RunOptions, SCAFFOLD_ROOT_DIR, type ScaffoldConfig, type ScanFromConfigOptions, type ScanFromConfigResult, type ScanStructureOptions, type StructureEntry, type StructureGroupConfig, type StubConfig, type StubHookConfig, type StubHookFn, type StubHookKind, ensureStructureFilesFromConfig, loadScaffoldConfig, parseStructureText, runOnce, scanDirectoryToStructureText, scanProjectFromConfig, writeScannedStructuresFromConfig };
package/dist/index.mjs CHANGED
@@ -6,7 +6,8 @@ import { pathToFileURL } from 'url';
6
6
  import { transform } from 'esbuild';
7
7
  import { minimatch } from 'minimatch';
8
8
 
9
- // src/core/runner.ts
9
+ // src/schema/index.ts
10
+ var SCAFFOLD_ROOT_DIR = ".scaffold";
10
11
 
11
12
  // src/util/logger.ts
12
13
  var supportsColor = typeof process !== "undefined" && process.stdout && process.stdout.isTTY && process.env.NO_COLOR !== "1";
@@ -131,14 +132,14 @@ function toProjectRelativePath(projectRoot, absolutePath) {
131
132
  var logger = defaultLogger.child("[config]");
132
133
  async function loadScaffoldConfig(cwd, options = {}) {
133
134
  const absCwd = path2.resolve(cwd);
134
- const initialScaffoldDir = options.scaffoldDir ? path2.resolve(absCwd, options.scaffoldDir) : path2.join(absCwd, "scaffold");
135
+ const initialScaffoldDir = options.scaffoldDir ? path2.resolve(absCwd, options.scaffoldDir) : path2.join(absCwd, SCAFFOLD_ROOT_DIR);
135
136
  const configPath = options.configPath ?? resolveConfigPath(initialScaffoldDir);
136
137
  const config = await importConfig(configPath);
137
138
  let configRoot = absCwd;
138
139
  if (config.root) {
139
140
  configRoot = path2.resolve(absCwd, config.root);
140
141
  }
141
- const scaffoldDir = options.scaffoldDir ? path2.resolve(absCwd, options.scaffoldDir) : path2.join(configRoot, "scaffold");
142
+ const scaffoldDir = options.scaffoldDir ? path2.resolve(absCwd, options.scaffoldDir) : path2.join(configRoot, SCAFFOLD_ROOT_DIR);
142
143
  const baseRoot = config.base ? path2.resolve(configRoot, config.base) : configRoot;
143
144
  logger.debug(
144
145
  `Loaded config: configRoot=${configRoot}, baseRoot=${baseRoot}, scaffoldDir=${scaffoldDir}`
@@ -203,14 +204,50 @@ async function importTsConfig(configPath) {
203
204
  }
204
205
 
205
206
  // src/core/structure-txt.ts
207
+ function stripInlineComment(content) {
208
+ let cutIndex = -1;
209
+ const len = content.length;
210
+ for (let i = 0; i < len; i++) {
211
+ const ch = content[i];
212
+ const prev = i > 0 ? content[i - 1] : "";
213
+ if (ch === "#") {
214
+ if (i === 0) continue;
215
+ if (prev === " " || prev === " ") {
216
+ cutIndex = i;
217
+ break;
218
+ }
219
+ }
220
+ if (ch === "/" && i + 1 < len && content[i + 1] === "/" && (prev === " " || prev === " ")) {
221
+ cutIndex = i;
222
+ break;
223
+ }
224
+ }
225
+ if (cutIndex === -1) {
226
+ return content.trimEnd();
227
+ }
228
+ return content.slice(0, cutIndex).trimEnd();
229
+ }
206
230
  function parseLine(line, lineNo) {
207
- const match = line.match(/^(\s*)(.+)$/);
231
+ const match = line.match(/^(\s*)(.*)$/);
208
232
  if (!match) return null;
209
233
  const indentSpaces = match[1].length;
210
- const rest = match[2].trim();
211
- if (!rest || rest.startsWith("#")) return null;
212
- const parts = rest.split(/\s+/);
234
+ let rest = match[2];
235
+ if (!rest.trim()) return null;
236
+ const trimmedRest = rest.trimStart();
237
+ if (trimmedRest.startsWith("#") || trimmedRest.startsWith("//")) {
238
+ return null;
239
+ }
240
+ const stripped = stripInlineComment(rest);
241
+ const trimmed = stripped.trim();
242
+ if (!trimmed) return null;
243
+ const parts = trimmed.split(/\s+/);
244
+ if (!parts.length) return null;
213
245
  const pathToken = parts[0];
246
+ if (pathToken.includes(":")) {
247
+ throw new Error(
248
+ `structure.txt: ":" is reserved for annotations (@stub:, @include:, etc). Invalid path "${pathToken}" on line ${lineNo}.`
249
+ );
250
+ }
214
251
  let stub;
215
252
  const include = [];
216
253
  const exclude = [];
@@ -242,7 +279,7 @@ function parseLine(line, lineNo) {
242
279
  exclude: exclude.length ? exclude : void 0
243
280
  };
244
281
  }
245
- function parseStructureText(text) {
282
+ function parseStructureText(text, indentStep = 2) {
246
283
  const lines = text.split(/\r?\n/);
247
284
  const parsed = [];
248
285
  for (let i = 0; i < lines.length; i++) {
@@ -252,15 +289,14 @@ function parseStructureText(text) {
252
289
  }
253
290
  const rootEntries = [];
254
291
  const stack = [];
255
- const INDENT_STEP = 2;
256
292
  for (const p of parsed) {
257
293
  const { indentSpaces, lineNo } = p;
258
- if (indentSpaces % INDENT_STEP !== 0) {
294
+ if (indentSpaces % indentStep !== 0) {
259
295
  throw new Error(
260
- `structure.txt: Invalid indent on line ${lineNo}. Indent must be multiples of ${INDENT_STEP} spaces.`
296
+ `structure.txt: Invalid indent on line ${lineNo}. Indent must be multiples of ${indentStep} spaces.`
261
297
  );
262
298
  }
263
- const level = indentSpaces / INDENT_STEP;
299
+ const level = indentSpaces / indentStep;
264
300
  if (level > stack.length) {
265
301
  if (level !== stack.length + 1) {
266
302
  throw new Error(
@@ -767,7 +803,46 @@ async function writeScannedStructuresFromConfig(cwd, options = {}) {
767
803
  );
768
804
  }
769
805
  }
806
+ async function ensureStructureFilesFromConfig(cwd, options = {}) {
807
+ const { config, scaffoldDir } = await loadScaffoldConfig(cwd, {
808
+ scaffoldDir: options.scaffoldDirOverride
809
+ });
810
+ ensureDirSync(scaffoldDir);
811
+ const created = [];
812
+ const existing = [];
813
+ const seen = /* @__PURE__ */ new Set();
814
+ const ensureFile = (fileName) => {
815
+ if (!fileName) return;
816
+ const filePath = path2.join(scaffoldDir, fileName);
817
+ const key = path2.resolve(filePath);
818
+ if (seen.has(key)) return;
819
+ seen.add(key);
820
+ if (fs2.existsSync(filePath)) {
821
+ existing.push(filePath);
822
+ return;
823
+ }
824
+ const header = `# ${fileName}
825
+ # Structure file for @timeax/scaffold
826
+ # Define your desired folders/files here.
827
+ `;
828
+ fs2.writeFileSync(filePath, header, "utf8");
829
+ created.push(filePath);
830
+ };
831
+ if (config.groups && config.groups.length > 0) {
832
+ for (const group of config.groups) {
833
+ const fileName = group.structureFile ?? `${group.name}.txt`;
834
+ ensureFile(fileName);
835
+ }
836
+ } else {
837
+ const fileName = config.structureFile ?? "structure.txt";
838
+ ensureFile(fileName);
839
+ }
840
+ logger4.debug(
841
+ `ensureStructureFilesFromConfig: created=${created.length}, existing=${existing.length}`
842
+ );
843
+ return { created, existing };
844
+ }
770
845
 
771
- export { runOnce, scanDirectoryToStructureText, scanProjectFromConfig, writeScannedStructuresFromConfig };
846
+ export { SCAFFOLD_ROOT_DIR, ensureStructureFilesFromConfig, loadScaffoldConfig, parseStructureText, runOnce, scanDirectoryToStructureText, scanProjectFromConfig, writeScannedStructuresFromConfig };
772
847
  //# sourceMappingURL=index.mjs.map
773
848
  //# sourceMappingURL=index.mjs.map