@timeax/scaffold 0.1.1 → 0.1.2

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/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import readline from 'readline';
3
- import path2 from 'path';
3
+ import path5 from 'path';
4
4
  import fs8 from 'fs';
5
5
  import { Command } from 'commander';
6
6
  import os from 'os';
@@ -8,6 +8,7 @@ import crypto from 'crypto';
8
8
  import { pathToFileURL } from 'url';
9
9
  import { transform } from 'esbuild';
10
10
  import { minimatch } from 'minimatch';
11
+ import pluralize from 'pluralize';
11
12
  import chokidar from 'chokidar';
12
13
 
13
14
  // src/schema/index.ts
@@ -120,31 +121,31 @@ function statSafeSync(targetPath) {
120
121
  }
121
122
  }
122
123
  function toProjectRelativePath(projectRoot, absolutePath) {
123
- const absRoot = path2.resolve(projectRoot);
124
- const absTarget = path2.resolve(absolutePath);
125
- const rootWithSep = absRoot.endsWith(path2.sep) ? absRoot : absRoot + path2.sep;
124
+ const absRoot = path5.resolve(projectRoot);
125
+ const absTarget = path5.resolve(absolutePath);
126
+ const rootWithSep = absRoot.endsWith(path5.sep) ? absRoot : absRoot + path5.sep;
126
127
  if (!absTarget.startsWith(rootWithSep) && absTarget !== absRoot) {
127
128
  throw new Error(
128
129
  `Path "${absTarget}" is not inside project root "${absRoot}".`
129
130
  );
130
131
  }
131
- const rel = path2.relative(absRoot, absTarget);
132
+ const rel = path5.relative(absRoot, absTarget);
132
133
  return toPosixPath(rel);
133
134
  }
134
135
 
135
136
  // src/core/config-loader.ts
136
137
  var logger = defaultLogger.child("[config]");
137
138
  async function loadScaffoldConfig(cwd, options = {}) {
138
- const absCwd = path2.resolve(cwd);
139
- const initialScaffoldDir = options.scaffoldDir ? path2.resolve(absCwd, options.scaffoldDir) : path2.join(absCwd, SCAFFOLD_ROOT_DIR);
139
+ const absCwd = path5.resolve(cwd);
140
+ const initialScaffoldDir = options.scaffoldDir ? path5.resolve(absCwd, options.scaffoldDir) : path5.join(absCwd, SCAFFOLD_ROOT_DIR);
140
141
  const configPath = options.configPath ?? resolveConfigPath(initialScaffoldDir);
141
142
  const config = await importConfig(configPath);
142
143
  let configRoot = absCwd;
143
144
  if (config.root) {
144
- configRoot = path2.resolve(absCwd, config.root);
145
+ configRoot = path5.resolve(absCwd, config.root);
145
146
  }
146
- const scaffoldDir = options.scaffoldDir ? path2.resolve(absCwd, options.scaffoldDir) : path2.join(configRoot, SCAFFOLD_ROOT_DIR);
147
- const baseRoot = config.base ? path2.resolve(configRoot, config.base) : configRoot;
147
+ const scaffoldDir = options.scaffoldDir ? path5.resolve(absCwd, options.scaffoldDir) : path5.join(configRoot, SCAFFOLD_ROOT_DIR);
148
+ const baseRoot = config.base ? path5.resolve(configRoot, config.base) : configRoot;
148
149
  logger.debug(
149
150
  `Loaded config: configRoot=${configRoot}, baseRoot=${baseRoot}, scaffoldDir=${scaffoldDir}`
150
151
  );
@@ -163,7 +164,7 @@ function resolveConfigPath(scaffoldDir) {
163
164
  "config.cjs"
164
165
  ];
165
166
  for (const file of candidates) {
166
- const full = path2.join(scaffoldDir, file);
167
+ const full = path5.join(scaffoldDir, file);
167
168
  if (fs8.existsSync(full)) {
168
169
  return full;
169
170
  }
@@ -175,7 +176,7 @@ function resolveConfigPath(scaffoldDir) {
175
176
  );
176
177
  }
177
178
  async function importConfig(configPath) {
178
- const ext = path2.extname(configPath).toLowerCase();
179
+ const ext = path5.extname(configPath).toLowerCase();
179
180
  if (ext === ".ts" || ext === ".tsx") {
180
181
  return importTsConfig(configPath);
181
182
  }
@@ -187,9 +188,9 @@ async function importTsConfig(configPath) {
187
188
  const source = fs8.readFileSync(configPath, "utf8");
188
189
  const stat = fs8.statSync(configPath);
189
190
  const hash = crypto.createHash("sha1").update(configPath).update(String(stat.mtimeMs)).digest("hex");
190
- const tmpDir = path2.join(os.tmpdir(), "timeax-scaffold-config");
191
+ const tmpDir = path5.join(os.tmpdir(), "timeax-scaffold-config");
191
192
  ensureDirSync(tmpDir);
192
- const tmpFile = path2.join(tmpDir, `${hash}.mjs`);
193
+ const tmpFile = path5.join(tmpDir, `${hash}.mjs`);
193
194
  if (!fs8.existsSync(tmpFile)) {
194
195
  const result = await transform(source, {
195
196
  loader: "ts",
@@ -783,7 +784,7 @@ function resolveGroupStructure(scaffoldDir, group, config) {
783
784
  return group.structure;
784
785
  }
785
786
  const fileName = group.structureFile ?? `${group.name}.txt`;
786
- const filePath = path2.join(scaffoldDir, fileName);
787
+ const filePath = path5.join(scaffoldDir, fileName);
787
788
  if (!fs8.existsSync(filePath)) {
788
789
  throw new Error(
789
790
  `@timeax/scaffold: Group "${group.name}" has no structure. Expected file "${fileName}" in "${scaffoldDir}".`
@@ -799,7 +800,7 @@ function resolveSingleStructure(scaffoldDir, config) {
799
800
  return config.structure;
800
801
  }
801
802
  const fileName = config.structureFile ?? "structure.txt";
802
- const filePath = path2.join(scaffoldDir, fileName);
803
+ const filePath = path5.join(scaffoldDir, fileName);
803
804
  if (!fs8.existsSync(filePath)) {
804
805
  throw new Error(
805
806
  `@timeax/scaffold: No structure defined. Expected "${fileName}" in "${scaffoldDir}".`
@@ -821,7 +822,7 @@ var CacheManager = class {
821
822
  }
822
823
  cache = DEFAULT_CACHE;
823
824
  get cachePathAbs() {
824
- return path2.resolve(this.projectRoot, this.cacheFileRelPath);
825
+ return path5.resolve(this.projectRoot, this.cacheFileRelPath);
825
826
  }
826
827
  load() {
827
828
  const cachePath = this.cachePathAbs;
@@ -845,7 +846,7 @@ var CacheManager = class {
845
846
  }
846
847
  save() {
847
848
  const cachePath = this.cachePathAbs;
848
- const dir = path2.dirname(cachePath);
849
+ const dir = path5.dirname(cachePath);
849
850
  ensureDirSync(dir);
850
851
  fs8.writeFileSync(cachePath, JSON.stringify(this.cache, null, 2), "utf8");
851
852
  }
@@ -930,9 +931,9 @@ async function applyStructure(opts) {
930
931
  interactiveDelete
931
932
  } = opts;
932
933
  const logger6 = opts.logger ?? defaultLogger.child(groupName ? `[apply:${groupName}]` : "[apply]");
933
- const projectRootAbs = path2.resolve(projectRoot);
934
- const baseDirAbs = path2.resolve(baseDir);
935
- baseDirAbs.endsWith(path2.sep) ? baseDirAbs : baseDirAbs + path2.sep;
934
+ const projectRootAbs = path5.resolve(projectRoot);
935
+ const baseDirAbs = path5.resolve(baseDir);
936
+ baseDirAbs.endsWith(path5.sep) ? baseDirAbs : baseDirAbs + path5.sep;
936
937
  const desiredPaths = /* @__PURE__ */ new Set();
937
938
  const threshold = sizePromptThreshold ?? config.sizePromptThreshold ?? 128 * 1024;
938
939
  async function walk(entry, inheritedStub) {
@@ -945,7 +946,7 @@ async function applyStructure(opts) {
945
946
  }
946
947
  async function handleDir(entry, inheritedStub) {
947
948
  const relFromBase = entry.path.replace(/^[./]+/, "");
948
- const absDir = path2.resolve(baseDirAbs, relFromBase);
949
+ const absDir = path5.resolve(baseDirAbs, relFromBase);
949
950
  const relFromRoot = toPosixPath(
950
951
  toProjectRelativePath(projectRootAbs, absDir)
951
952
  );
@@ -960,24 +961,30 @@ async function applyStructure(opts) {
960
961
  }
961
962
  async function handleFile(entry, inheritedStub) {
962
963
  const relFromBase = entry.path.replace(/^[./]+/, "");
963
- const absFile = path2.resolve(baseDirAbs, relFromBase);
964
+ const absFile = path5.resolve(baseDirAbs, relFromBase);
964
965
  const relFromRoot = toPosixPath(
965
966
  toProjectRelativePath(projectRootAbs, absFile)
966
967
  );
967
968
  desiredPaths.add(relFromRoot);
968
969
  const stubName = entry.stub ?? inheritedStub;
970
+ const extension = path5.extname(relFromRoot);
971
+ const fileName = path5.basename(relFromRoot, extension);
969
972
  const ctx = {
970
973
  projectRoot: projectRootAbs,
971
974
  targetPath: relFromRoot,
972
975
  absolutePath: absFile,
973
976
  isDirectory: false,
977
+ fileName,
978
+ dirName: path5.dirname(relFromRoot),
979
+ extension,
980
+ pluralFileName: pluralize.plural(fileName),
974
981
  stubName
975
982
  };
976
983
  if (fs8.existsSync(absFile)) {
977
984
  return;
978
985
  }
979
986
  await hooks.runRegular("preCreateFile", ctx);
980
- const dir = path2.dirname(absFile);
987
+ const dir = path5.dirname(absFile);
981
988
  ensureDirSync(dir);
982
989
  if (stubName) {
983
990
  await hooks.runStub("preStub", ctx);
@@ -1020,7 +1027,7 @@ async function applyStructure(opts) {
1020
1027
  if (desiredPaths.has(cachedPath)) {
1021
1028
  continue;
1022
1029
  }
1023
- const abs = path2.resolve(projectRoot, cachedPath);
1030
+ const abs = path5.resolve(projectRoot, cachedPath);
1024
1031
  const stats = statSafeSync(abs);
1025
1032
  if (!stats) {
1026
1033
  cache.delete(cachedPath);
@@ -1030,11 +1037,17 @@ async function applyStructure(opts) {
1030
1037
  cache.delete(cachedPath);
1031
1038
  continue;
1032
1039
  }
1040
+ const extension = path5.extname(abs);
1041
+ const fileName = path5.basename(abs, extension);
1033
1042
  const ctx = {
1034
1043
  projectRoot,
1035
1044
  targetPath: cachedPath,
1036
1045
  absolutePath: abs,
1037
1046
  isDirectory: false,
1047
+ fileName,
1048
+ dirName: path5.dirname(cachedPath),
1049
+ extension,
1050
+ pluralFileName: pluralize.plural(fileName),
1038
1051
  stubName: entry?.createdByStub
1039
1052
  };
1040
1053
  await hooks.runRegular("preDeleteFile", ctx);
@@ -1076,12 +1089,12 @@ var DEFAULT_IGNORE = [
1076
1089
  "coverage/**"
1077
1090
  ];
1078
1091
  function scanDirectoryToStructureText(rootDir, options = {}) {
1079
- const absRoot = path2.resolve(rootDir);
1092
+ const absRoot = path5.resolve(rootDir);
1080
1093
  const lines = [];
1081
1094
  const ignorePatterns = options.ignore ?? DEFAULT_IGNORE;
1082
1095
  const maxDepth = options.maxDepth ?? Infinity;
1083
1096
  function isIgnored(absPath) {
1084
- const rel = toPosixPath(path2.relative(absRoot, absPath));
1097
+ const rel = toPosixPath(path5.relative(absRoot, absPath));
1085
1098
  if (!rel || rel === ".") return false;
1086
1099
  return ignorePatterns.some(
1087
1100
  (pattern) => minimatch(rel, pattern, { dot: true })
@@ -1102,7 +1115,7 @@ function scanDirectoryToStructureText(rootDir, options = {}) {
1102
1115
  });
1103
1116
  for (const dirent of dirents) {
1104
1117
  const name = dirent.name;
1105
- const absPath = path2.join(currentAbs, name);
1118
+ const absPath = path5.join(currentAbs, name);
1106
1119
  if (isIgnored(absPath)) continue;
1107
1120
  const indent = " ".repeat(depth);
1108
1121
  if (dirent.isDirectory()) {
@@ -1125,13 +1138,13 @@ async function scanProjectFromConfig(cwd, options = {}) {
1125
1138
  const onlyGroups = options.groups;
1126
1139
  const results = [];
1127
1140
  function scanGroup(cfg, group) {
1128
- const rootAbs = path2.resolve(projectRoot, group.root);
1141
+ const rootAbs = path5.resolve(projectRoot, group.root);
1129
1142
  const text = scanDirectoryToStructureText(rootAbs, {
1130
1143
  ignore: ignorePatterns,
1131
1144
  maxDepth
1132
1145
  });
1133
1146
  const structureFileName = group.structureFile ?? `${group.name}.txt`;
1134
- const structureFilePath = path2.join(scaffoldDir, structureFileName);
1147
+ const structureFilePath = path5.join(scaffoldDir, structureFileName);
1135
1148
  return {
1136
1149
  groupName: group.name,
1137
1150
  groupRoot: group.root,
@@ -1158,7 +1171,7 @@ async function scanProjectFromConfig(cwd, options = {}) {
1158
1171
  maxDepth
1159
1172
  });
1160
1173
  const structureFileName = config.structureFile ?? "structure.txt";
1161
- const structureFilePath = path2.join(scaffoldDir, structureFileName);
1174
+ const structureFilePath = path5.join(scaffoldDir, structureFileName);
1162
1175
  results.push({
1163
1176
  groupName: "default",
1164
1177
  groupRoot: ".",
@@ -1192,8 +1205,8 @@ async function ensureStructureFilesFromConfig(cwd, options = {}) {
1192
1205
  const seen = /* @__PURE__ */ new Set();
1193
1206
  const ensureFile = (fileName) => {
1194
1207
  if (!fileName) return;
1195
- const filePath = path2.join(scaffoldDir, fileName);
1196
- const key = path2.resolve(filePath);
1208
+ const filePath = path5.join(scaffoldDir, fileName);
1209
+ const key = path5.resolve(filePath);
1197
1210
  if (seen.has(key)) return;
1198
1211
  seen.add(key);
1199
1212
  if (fs8.existsSync(filePath)) {
@@ -1224,16 +1237,16 @@ async function ensureStructureFilesFromConfig(cwd, options = {}) {
1224
1237
 
1225
1238
  // src/core/format.ts
1226
1239
  function getStructureFilesFromConfig(projectRoot, scaffoldDir, config) {
1227
- const baseDir = path2.resolve(projectRoot, scaffoldDir || SCAFFOLD_ROOT_DIR);
1240
+ const baseDir = path5.resolve(projectRoot, scaffoldDir || SCAFFOLD_ROOT_DIR);
1228
1241
  const files = [];
1229
1242
  if (config.groups && config.groups.length > 0) {
1230
1243
  for (const group of config.groups) {
1231
1244
  const structureFile = group.structureFile && group.structureFile.trim().length ? group.structureFile : `${group.name}.txt`;
1232
- files.push(path2.join(baseDir, structureFile));
1245
+ files.push(path5.join(baseDir, structureFile));
1233
1246
  }
1234
1247
  } else {
1235
1248
  const structureFile = config.structureFile || "structure.txt";
1236
- files.push(path2.join(baseDir, structureFile));
1249
+ files.push(path5.join(baseDir, structureFile));
1237
1250
  }
1238
1251
  return files;
1239
1252
  }
@@ -1275,7 +1288,7 @@ async function runOnce(cwd, options = {}) {
1275
1288
  const hooks = new HookRunner(config);
1276
1289
  if (config.groups && config.groups.length > 0) {
1277
1290
  for (const group of config.groups) {
1278
- const groupRootAbs = path2.resolve(projectRoot, group.root);
1291
+ const groupRootAbs = path5.resolve(projectRoot, group.root);
1279
1292
  const structure = resolveGroupStructure(scaffoldDir, group, config);
1280
1293
  const groupLogger = logger6.child(`[group:${group.name}]`);
1281
1294
  await applyStructure({
@@ -1311,7 +1324,7 @@ async function runOnce(cwd, options = {}) {
1311
1324
  }
1312
1325
  function watchScaffold(cwd, options = {}) {
1313
1326
  const logger6 = options.logger ?? defaultLogger.child("[watch]");
1314
- const scaffoldDir = options.scaffoldDir ? path2.resolve(cwd, options.scaffoldDir) : path2.resolve(cwd, SCAFFOLD_ROOT_DIR);
1327
+ const scaffoldDir = options.scaffoldDir ? path5.resolve(cwd, options.scaffoldDir) : path5.resolve(cwd, SCAFFOLD_ROOT_DIR);
1315
1328
  const debounceMs = options.debounceMs ?? 150;
1316
1329
  logger6.info(`Watching scaffold directory: ${scaffoldDir}`);
1317
1330
  let timer;
@@ -1346,11 +1359,11 @@ function watchScaffold(cwd, options = {}) {
1346
1359
  timer = setTimeout(run, debounceMs);
1347
1360
  }
1348
1361
  function isInteresting(filePath) {
1349
- const rel = path2.relative(scaffoldDir, filePath);
1362
+ const rel = path5.relative(scaffoldDir, filePath);
1350
1363
  if (rel.startsWith("..")) return false;
1351
- const base = path2.basename(filePath).toLowerCase();
1364
+ const base = path5.basename(filePath).toLowerCase();
1352
1365
  if (base.startsWith("config.")) return true;
1353
- const ext = path2.extname(base);
1366
+ const ext = path5.extname(base);
1354
1367
  return ext === ".txt" || ext === ".tss" || ext === ".stx";
1355
1368
  }
1356
1369
  const watcher = chokidar.watch(scaffoldDir, {
@@ -1467,12 +1480,12 @@ var DEFAULT_STRUCTURE_TXT = `# ${SCAFFOLD_ROOT_DIR}/structure.txt
1467
1480
  `;
1468
1481
  async function initScaffold(cwd, options = {}) {
1469
1482
  const scaffoldDirRel = options.scaffoldDir ?? SCAFFOLD_ROOT_DIR;
1470
- const scaffoldDirAbs = path2.resolve(cwd, scaffoldDirRel);
1483
+ const scaffoldDirAbs = path5.resolve(cwd, scaffoldDirRel);
1471
1484
  const configFileName = options.configFileName ?? "config.ts";
1472
1485
  const structureFileName = options.structureFileName ?? "structure.txt";
1473
1486
  ensureDirSync(scaffoldDirAbs);
1474
- const configPath = path2.join(scaffoldDirAbs, configFileName);
1475
- const structurePath = path2.join(scaffoldDirAbs, structureFileName);
1487
+ const configPath = path5.join(scaffoldDirAbs, configFileName);
1488
+ const structurePath = path5.join(scaffoldDirAbs, structureFileName);
1476
1489
  let createdConfig = false;
1477
1490
  let createdStructure = false;
1478
1491
  if (fs8.existsSync(configPath) && !options.force) {
@@ -1536,9 +1549,9 @@ function askYesNo(question) {
1536
1549
  }
1537
1550
  async function handleRunCommand(cwd, baseOpts) {
1538
1551
  const logger6 = createCliLogger(baseOpts);
1539
- const configPath = baseOpts.config ? path2.resolve(cwd, baseOpts.config) : void 0;
1540
- const scaffoldDir = baseOpts.dir ? path2.resolve(cwd, baseOpts.dir) : void 0;
1541
- const resolvedScaffoldDir = scaffoldDir ?? path2.resolve(cwd, SCAFFOLD_ROOT_DIR);
1552
+ const configPath = baseOpts.config ? path5.resolve(cwd, baseOpts.config) : void 0;
1553
+ const scaffoldDir = baseOpts.dir ? path5.resolve(cwd, baseOpts.dir) : void 0;
1554
+ const resolvedScaffoldDir = scaffoldDir ?? path5.resolve(cwd, SCAFFOLD_ROOT_DIR);
1542
1555
  logger6.debug(
1543
1556
  `Starting scaffold (cwd=${cwd}, config=${configPath ?? "auto"}, dir=${resolvedScaffoldDir}, watch=${baseOpts.watch ? "yes" : "no"})`
1544
1557
  );
@@ -1578,15 +1591,15 @@ async function handleScanCommand(cwd, scanOpts, baseOpts) {
1578
1591
  });
1579
1592
  return;
1580
1593
  }
1581
- const rootDir = path2.resolve(cwd, scanOpts.root ?? ".");
1594
+ const rootDir = path5.resolve(cwd, scanOpts.root ?? ".");
1582
1595
  const ignore = scanOpts.ignore ?? [];
1583
1596
  logger6.info(`Scanning directory for structure: ${rootDir}`);
1584
1597
  const text = scanDirectoryToStructureText(rootDir, {
1585
1598
  ignore
1586
1599
  });
1587
1600
  if (scanOpts.out) {
1588
- const outPath = path2.resolve(cwd, scanOpts.out);
1589
- const dir = path2.dirname(outPath);
1601
+ const outPath = path5.resolve(cwd, scanOpts.out);
1602
+ const dir = path5.dirname(outPath);
1590
1603
  ensureDirSync(dir);
1591
1604
  fs8.writeFileSync(outPath, text, "utf8");
1592
1605
  logger6.info(`Wrote structure to ${outPath}`);