@sha3/code-standards 0.1.4 → 0.1.6

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,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- # @sha3/code-standards
3
+ # 📏 @sha3/code-standards
4
4
 
5
5
  **Scaffold TypeScript projects + enforce how AI writes code.**
6
6
 
@@ -21,6 +21,12 @@ If you just want to start now:
21
21
  npx @sha3/code-standards init --template node-service --yes
22
22
  ```
23
23
 
24
+ If your project was already scaffolded and you updated this package:
25
+
26
+ ```bash
27
+ npx @sha3/code-standards refresh
28
+ ```
29
+
24
30
  Then in your AI chat, paste this:
25
31
 
26
32
  ```txt
@@ -111,8 +117,12 @@ After `init`, your new repo contains:
111
117
  - `ai/examples/rules/*.ts` (good/bad examples per rule)
112
118
  - `ai/examples/demo/src/*` (feature-folder demo with classes and section blocks)
113
119
  - `src/config.ts` for centralized hardcoded configuration values
120
+ - `README.md` generated with an icon emoji in the main header
114
121
  - `.gitignore` preconfigured for Node/TypeScript output
115
122
  - lint/format/typecheck/test-ready project template
123
+ - `package.json.codeStandards` metadata used by `refresh` (`template`, `profilePath`, `withAiAdapters`, `lastRefreshWith`)
124
+
125
+ `config.ts` convention: export a single default object and import it as `import CONFIG from "./config.js"`.
116
126
 
117
127
  That means the next step is **not** configuring tools. The next step is telling your assistant to obey `AGENTS.md` before coding.
118
128
 
@@ -270,6 +280,22 @@ npm run check
270
280
 
271
281
  Then use the prompts above in your AI tool.
272
282
 
283
+ ### 4) Sync updates from `@sha3/code-standards`
284
+
285
+ Run this inside an already scaffolded project:
286
+
287
+ ```bash
288
+ npx @sha3/code-standards refresh
289
+ ```
290
+
291
+ `refresh` default behavior:
292
+
293
+ - scope `Managed + AI` (template files + `AGENTS.md` + `ai/*` + `ai/examples/*`)
294
+ - overwrite conflicts
295
+ - auto-detect template (or force with `--template`)
296
+ - selective merge for `package.json` (managed scripts/devDependencies updated, custom keys preserved)
297
+ - no dependency install unless `--install`
298
+
273
299
  ---
274
300
 
275
301
  ## CLI Reference
@@ -279,6 +305,8 @@ code-standards <command> [options]
279
305
 
280
306
  Commands:
281
307
  init Initialize a project in the current directory
308
+ refresh Re-apply managed standards files and AI instructions
309
+ update Alias of refresh
282
310
  profile Create or update the AI style profile
283
311
  ```
284
312
 
@@ -295,6 +323,18 @@ An existing `.git/` directory is allowed without `--force`.
295
323
  - `--no-ai-adapters`
296
324
  - `--profile <path>`
297
325
 
326
+ ### `refresh` options
327
+
328
+ `refresh` always uses the current working directory as target.
329
+
330
+ - `--template <node-lib|node-service>`
331
+ - `--profile <path>`
332
+ - `--with-ai-adapters`
333
+ - `--no-ai-adapters`
334
+ - `--dry-run`
335
+ - `--install`
336
+ - `--yes`
337
+
298
338
  ### `profile` options
299
339
 
300
340
  - `--profile <path>`
@@ -10,6 +10,12 @@ import { fileURLToPath } from "node:url";
10
10
  import Ajv2020 from "ajv/dist/2020.js";
11
11
 
12
12
  const TEMPLATE_NAMES = ["node-lib", "node-service"];
13
+ const CODE_STANDARDS_METADATA_KEY = "codeStandards";
14
+ const NODE_LIB_REFRESH_SIGNATURE = {
15
+ main: "dist/index.js",
16
+ types: "dist/index.d.ts"
17
+ };
18
+ const NODE_SERVICE_START_SIGNATURE = "node --import tsx src/index.ts";
13
19
  const PROFILE_KEY_ORDER = [
14
20
  "version",
15
21
  "paradigm",
@@ -165,6 +171,8 @@ function printUsage() {
165
171
 
166
172
  Commands:
167
173
  init Initialize a project in the current directory
174
+ refresh Re-apply managed standards files and AI instructions
175
+ update Alias of refresh
168
176
  profile Create or update the AI style profile
169
177
 
170
178
  Init options:
@@ -176,6 +184,15 @@ Init options:
176
184
  --no-ai-adapters
177
185
  --profile <path>
178
186
 
187
+ Refresh options:
188
+ --template <node-lib|node-service>
189
+ --profile <path>
190
+ --with-ai-adapters
191
+ --no-ai-adapters
192
+ --dry-run
193
+ --install
194
+ --yes
195
+
179
196
  Profile options:
180
197
  --profile <path>
181
198
  --non-interactive
@@ -271,6 +288,88 @@ function parseInitArgs(argv) {
271
288
  return options;
272
289
  }
273
290
 
291
+ function parseRefreshArgs(argv) {
292
+ const options = {
293
+ template: undefined,
294
+ profilePath: undefined,
295
+ withAiAdapters: true,
296
+ dryRun: false,
297
+ install: false,
298
+ yes: false,
299
+ help: false
300
+ };
301
+
302
+ for (let i = 0; i < argv.length; i += 1) {
303
+ const token = argv[i];
304
+
305
+ if (!token.startsWith("-")) {
306
+ throw new Error(`Positional arguments are not supported for refresh: ${token}.`);
307
+ }
308
+
309
+ if (token === "--template") {
310
+ const value = argv[i + 1];
311
+
312
+ if (!value || value.startsWith("-")) {
313
+ throw new Error("Missing value for --template");
314
+ }
315
+
316
+ if (!TEMPLATE_NAMES.includes(value)) {
317
+ throw new Error(`Invalid template: ${value}`);
318
+ }
319
+
320
+ options.template = value;
321
+ i += 1;
322
+ continue;
323
+ }
324
+
325
+ if (token === "--profile") {
326
+ const value = argv[i + 1];
327
+
328
+ if (!value || value.startsWith("-")) {
329
+ throw new Error("Missing value for --profile");
330
+ }
331
+
332
+ options.profilePath = value;
333
+ i += 1;
334
+ continue;
335
+ }
336
+
337
+ if (token === "--with-ai-adapters") {
338
+ options.withAiAdapters = true;
339
+ continue;
340
+ }
341
+
342
+ if (token === "--no-ai-adapters") {
343
+ options.withAiAdapters = false;
344
+ continue;
345
+ }
346
+
347
+ if (token === "--dry-run") {
348
+ options.dryRun = true;
349
+ continue;
350
+ }
351
+
352
+ if (token === "--install") {
353
+ options.install = true;
354
+ continue;
355
+ }
356
+
357
+ if (token === "--yes") {
358
+ options.yes = true;
359
+ continue;
360
+ }
361
+
362
+ if (token === "-h" || token === "--help") {
363
+ options.help = true;
364
+ continue;
365
+ }
366
+
367
+ throw new Error(`Unknown option: ${token}`);
368
+ }
369
+
370
+ return options;
371
+ }
372
+
274
373
  function parseProfileArgs(argv) {
275
374
  const options = {
276
375
  profilePath: undefined,
@@ -422,11 +521,198 @@ async function copyTemplateDirectory(sourceDir, targetDir, tokens) {
422
521
  }
423
522
  }
424
523
 
524
+ function asPlainObject(value) {
525
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
526
+ return {};
527
+ }
528
+
529
+ return value;
530
+ }
531
+
532
+ function getRelativeProfilePath(profilePath, targetPath) {
533
+ if (!profilePath) {
534
+ return null;
535
+ }
536
+
537
+ const resolvedProfilePath = path.resolve(targetPath, profilePath);
538
+ const relativePath = path.relative(targetPath, resolvedProfilePath);
539
+
540
+ // If the profile is outside the project directory, return null instead of absolute path
541
+ // to avoid storing machine-specific paths in package.json
542
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
543
+ return null;
544
+ }
545
+
546
+ return relativePath;
547
+ }
548
+
549
+ async function readProjectPackageJson(targetPath) {
550
+ const packageJsonPath = path.join(targetPath, "package.json");
551
+
552
+ if (!(await pathExists(packageJsonPath))) {
553
+ throw new Error(`package.json was not found in ${targetPath}. Run refresh from the project root.`);
554
+ }
555
+
556
+ return {
557
+ packageJsonPath,
558
+ packageJson: await readJsonFile(packageJsonPath)
559
+ };
560
+ }
561
+
562
+ async function writeProjectPackageJson(packageJsonPath, packageJson) {
563
+ await writeJsonFile(packageJsonPath, packageJson);
564
+ }
565
+
566
+ function updateCodeStandardsMetadata(projectPackageJson, metadataPatch) {
567
+ const existingMetadata = asPlainObject(projectPackageJson[CODE_STANDARDS_METADATA_KEY]);
568
+ const nextMetadata = {
569
+ ...existingMetadata,
570
+ ...metadataPatch
571
+ };
572
+
573
+ return {
574
+ ...projectPackageJson,
575
+ [CODE_STANDARDS_METADATA_KEY]: nextMetadata
576
+ };
577
+ }
578
+
579
+ function mergePackageJsonFromTemplate(projectPackageJson, templatePackageJson, templateName) {
580
+ const mergedPackageJson = { ...projectPackageJson };
581
+ const templateScripts = asPlainObject(templatePackageJson.scripts);
582
+ const templateDevDependencies = asPlainObject(templatePackageJson.devDependencies);
583
+ const mergedScripts = {
584
+ ...asPlainObject(projectPackageJson.scripts)
585
+ };
586
+ const mergedDevDependencies = {
587
+ ...asPlainObject(projectPackageJson.devDependencies)
588
+ };
589
+
590
+ for (const [scriptName, scriptValue] of Object.entries(templateScripts)) {
591
+ mergedScripts[scriptName] = scriptValue;
592
+ }
593
+
594
+ for (const [dependencyName, dependencyVersion] of Object.entries(templateDevDependencies)) {
595
+ mergedDevDependencies[dependencyName] = dependencyVersion;
596
+ }
597
+
598
+ if (Object.keys(mergedScripts).length > 0) {
599
+ mergedPackageJson.scripts = mergedScripts;
600
+ }
601
+
602
+ if (Object.keys(mergedDevDependencies).length > 0) {
603
+ mergedPackageJson.devDependencies = mergedDevDependencies;
604
+ }
605
+
606
+ if (typeof templatePackageJson.type === "string") {
607
+ mergedPackageJson.type = templatePackageJson.type;
608
+ }
609
+
610
+ if (templateName === "node-lib") {
611
+ if (typeof templatePackageJson.main === "string") {
612
+ mergedPackageJson.main = templatePackageJson.main;
613
+ }
614
+
615
+ if (typeof templatePackageJson.types === "string") {
616
+ mergedPackageJson.types = templatePackageJson.types;
617
+ }
618
+
619
+ if (Array.isArray(templatePackageJson.files)) {
620
+ mergedPackageJson.files = templatePackageJson.files;
621
+ }
622
+ }
623
+
624
+ return mergedPackageJson;
625
+ }
626
+
627
+ async function collectTemplateFiles(templateDir, baseDir = templateDir) {
628
+ const entries = await readdir(templateDir, { withFileTypes: true });
629
+ const files = [];
630
+
631
+ for (const entry of entries) {
632
+ const sourcePath = path.join(templateDir, entry.name);
633
+
634
+ if (entry.isDirectory()) {
635
+ const nestedFiles = await collectTemplateFiles(sourcePath, baseDir);
636
+ files.push(...nestedFiles);
637
+ continue;
638
+ }
639
+
640
+ if (!entry.isFile()) {
641
+ continue;
642
+ }
643
+
644
+ const sourceRelativePath = path.relative(baseDir, sourcePath);
645
+ const sourceDirectory = path.dirname(sourceRelativePath);
646
+ const sourceFileName = path.basename(sourceRelativePath);
647
+ const mappedFileName = mapTemplateFileName(sourceFileName);
648
+ const targetRelativePath = sourceDirectory === "." ? mappedFileName : path.join(sourceDirectory, mappedFileName);
649
+
650
+ files.push({
651
+ sourcePath,
652
+ sourceRelativePath,
653
+ targetRelativePath
654
+ });
655
+ }
656
+
657
+ return files.sort((left, right) => left.targetRelativePath.localeCompare(right.targetRelativePath));
658
+ }
659
+
660
+ async function applyManagedFiles(options) {
661
+ const { templateDir, targetDir, tokens, templateName, projectPackageJson, dryRun } = options;
662
+ const templateFiles = await collectTemplateFiles(templateDir);
663
+ const updatedFiles = [];
664
+ let mergedPackageJson = { ...projectPackageJson };
665
+
666
+ for (const templateFile of templateFiles) {
667
+ if (templateFile.targetRelativePath === "package.json") {
668
+ const rawTemplatePackageJson = await readFile(templateFile.sourcePath, "utf8");
669
+ const renderedTemplatePackageJson = replaceTokens(rawTemplatePackageJson, tokens);
670
+ const templatePackageJson = JSON.parse(renderedTemplatePackageJson);
671
+ mergedPackageJson = mergePackageJsonFromTemplate(mergedPackageJson, templatePackageJson, templateName);
672
+ updatedFiles.push("package.json");
673
+ continue;
674
+ }
675
+
676
+ const raw = await readFile(templateFile.sourcePath, "utf8");
677
+ const rendered = replaceTokens(raw, tokens);
678
+ const targetPath = path.join(targetDir, templateFile.targetRelativePath);
679
+ updatedFiles.push(templateFile.targetRelativePath);
680
+
681
+ if (dryRun) {
682
+ continue;
683
+ }
684
+
685
+ await mkdir(path.dirname(targetPath), { recursive: true });
686
+ await writeFile(targetPath, rendered, "utf8");
687
+ }
688
+
689
+ if (!dryRun) {
690
+ const packageJsonPath = path.join(targetDir, "package.json");
691
+ await writeProjectPackageJson(packageJsonPath, mergedPackageJson);
692
+ }
693
+
694
+ return {
695
+ updatedFiles,
696
+ mergedPackageJson
697
+ };
698
+ }
699
+
425
700
  function resolvePackageRoot() {
426
701
  const binPath = fileURLToPath(import.meta.url);
427
702
  return path.resolve(path.dirname(binPath), "..");
428
703
  }
429
704
 
705
+ async function readPackageVersion(packageRoot) {
706
+ const packageJsonPath = path.join(packageRoot, "package.json");
707
+ const packageJson = await readJsonFile(packageJsonPath);
708
+
709
+ if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
710
+ throw new Error("Package version is missing in the CLI package.json.");
711
+ }
712
+
713
+ return packageJson.version;
714
+ }
715
+
430
716
  function getBundledProfilePath(packageRoot) {
431
717
  return path.join(packageRoot, "profiles", "default.profile.json");
432
718
  }
@@ -775,7 +1061,25 @@ async function maybeInitializeProfileInteractively(packageRoot, profilePath) {
775
1061
  console.log(`Profile initialized at ${profilePath}`);
776
1062
  }
777
1063
 
778
- async function resolveProfileForInit(packageRoot, rawOptions, schema) {
1064
+ async function resolveBundledOrDefaultProfile(packageRoot, schema) {
1065
+ const bundledProfilePath = getBundledProfilePath(packageRoot);
1066
+ const bundledExists = await pathExists(bundledProfilePath);
1067
+
1068
+ if (bundledExists) {
1069
+ return {
1070
+ profile: await readAndValidateProfile(bundledProfilePath, schema),
1071
+ profilePathForMetadata: null
1072
+ };
1073
+ }
1074
+
1075
+ validateProfile(DEFAULT_PROFILE, schema, "hardcoded defaults");
1076
+ return {
1077
+ profile: normalizeProfile(DEFAULT_PROFILE),
1078
+ profilePathForMetadata: null
1079
+ };
1080
+ }
1081
+
1082
+ async function resolveProfileForInit(packageRoot, targetPath, rawOptions, schema) {
779
1083
  const bundledProfilePath = getBundledProfilePath(packageRoot);
780
1084
 
781
1085
  if (!rawOptions.profilePath) {
@@ -783,35 +1087,115 @@ async function resolveProfileForInit(packageRoot, rawOptions, schema) {
783
1087
 
784
1088
  if (!bundledExists) {
785
1089
  if (rawOptions.yes) {
786
- validateProfile(DEFAULT_PROFILE, schema, "hardcoded defaults");
787
- return normalizeProfile(DEFAULT_PROFILE);
1090
+ return resolveBundledOrDefaultProfile(packageRoot, schema);
788
1091
  }
789
1092
 
790
1093
  await writeJsonFile(bundledProfilePath, normalizeProfile(DEFAULT_PROFILE));
791
1094
  }
792
1095
 
793
- return readAndValidateProfile(bundledProfilePath, schema);
1096
+ return {
1097
+ profile: await readAndValidateProfile(bundledProfilePath, schema),
1098
+ profilePathForMetadata: null
1099
+ };
794
1100
  }
795
1101
 
796
- const requestedPath = path.resolve(process.cwd(), rawOptions.profilePath);
1102
+ const requestedPath = path.resolve(targetPath, rawOptions.profilePath);
797
1103
  const requestedExists = await pathExists(requestedPath);
798
1104
 
799
1105
  if (!requestedExists) {
800
1106
  if (rawOptions.yes) {
801
- const bundledExists = await pathExists(bundledProfilePath);
1107
+ return resolveBundledOrDefaultProfile(packageRoot, schema);
1108
+ }
802
1109
 
803
- if (bundledExists) {
804
- return readAndValidateProfile(bundledProfilePath, schema);
805
- }
1110
+ await maybeInitializeProfileInteractively(packageRoot, requestedPath);
1111
+ }
1112
+
1113
+ return {
1114
+ profile: await readAndValidateProfile(requestedPath, schema),
1115
+ profilePathForMetadata: getRelativeProfilePath(requestedPath, targetPath)
1116
+ };
1117
+ }
806
1118
 
807
- validateProfile(DEFAULT_PROFILE, schema, "hardcoded defaults");
808
- return normalizeProfile(DEFAULT_PROFILE);
1119
+ async function resolveTemplateForRefresh(rawOptions, projectPackageJson, targetPath) {
1120
+ if (rawOptions.template) {
1121
+ return rawOptions.template;
1122
+ }
1123
+
1124
+ const metadata = asPlainObject(projectPackageJson[CODE_STANDARDS_METADATA_KEY]);
1125
+
1126
+ if (typeof metadata.template === "string" && TEMPLATE_NAMES.includes(metadata.template)) {
1127
+ return metadata.template;
1128
+ }
1129
+
1130
+ const projectScripts = asPlainObject(projectPackageJson.scripts);
1131
+ const hasNodeLibSignature =
1132
+ (await pathExists(path.join(targetPath, "tsconfig.build.json"))) &&
1133
+ projectPackageJson.main === NODE_LIB_REFRESH_SIGNATURE.main &&
1134
+ projectPackageJson.types === NODE_LIB_REFRESH_SIGNATURE.types;
1135
+
1136
+ if (hasNodeLibSignature) {
1137
+ return "node-lib";
1138
+ }
1139
+
1140
+ const startScript = typeof projectScripts.start === "string" ? projectScripts.start : "";
1141
+
1142
+ if (startScript.includes(NODE_SERVICE_START_SIGNATURE)) {
1143
+ return "node-service";
1144
+ }
1145
+
1146
+ throw new Error("Unable to infer template for refresh. Use --template <node-lib|node-service>.");
1147
+ }
1148
+
1149
+ async function resolveProfileForRefresh(packageRoot, targetPath, rawOptions, schema, projectMetadata) {
1150
+ let selectedProfilePath;
1151
+
1152
+ if (rawOptions.profilePath) {
1153
+ selectedProfilePath = path.resolve(targetPath, rawOptions.profilePath);
1154
+ } else if (typeof projectMetadata.profilePath === "string" && projectMetadata.profilePath.trim().length > 0) {
1155
+ selectedProfilePath = path.resolve(targetPath, projectMetadata.profilePath);
1156
+ }
1157
+
1158
+ if (!selectedProfilePath) {
1159
+ return resolveBundledOrDefaultProfile(packageRoot, schema);
1160
+ }
1161
+
1162
+ const selectedExists = await pathExists(selectedProfilePath);
1163
+
1164
+ if (!selectedExists) {
1165
+ if (rawOptions.yes) {
1166
+ return resolveBundledOrDefaultProfile(packageRoot, schema);
809
1167
  }
810
1168
 
811
- await maybeInitializeProfileInteractively(packageRoot, requestedPath);
1169
+ await maybeInitializeProfileInteractively(packageRoot, selectedProfilePath);
812
1170
  }
813
1171
 
814
- return readAndValidateProfile(requestedPath, schema);
1172
+ return {
1173
+ profile: await readAndValidateProfile(selectedProfilePath, schema),
1174
+ profilePathForMetadata: getRelativeProfilePath(selectedProfilePath, targetPath)
1175
+ };
1176
+ }
1177
+
1178
+ async function collectAiFiles(packageRoot) {
1179
+ const aiFiles = ["AGENTS.md"];
1180
+ const adaptersTemplateDir = path.join(packageRoot, "resources", "ai", "templates", "adapters");
1181
+ const examplesTemplateDir = path.join(packageRoot, "resources", "ai", "templates", "examples");
1182
+ const adapterEntries = await readdir(adaptersTemplateDir, { withFileTypes: true });
1183
+
1184
+ for (const entry of adapterEntries) {
1185
+ if (!entry.isFile() || !entry.name.endsWith(".template.md")) {
1186
+ continue;
1187
+ }
1188
+
1189
+ aiFiles.push(path.join("ai", entry.name.replace(/\.template\.md$/, ".md")));
1190
+ }
1191
+
1192
+ const exampleTemplateFiles = await collectTemplateFiles(examplesTemplateDir);
1193
+
1194
+ for (const exampleFile of exampleTemplateFiles) {
1195
+ aiFiles.push(path.join("ai", "examples", exampleFile.targetRelativePath));
1196
+ }
1197
+
1198
+ return aiFiles.sort((left, right) => left.localeCompare(right));
815
1199
  }
816
1200
 
817
1201
  async function promptForMissing(options) {
@@ -880,11 +1264,12 @@ async function runInit(rawOptions) {
880
1264
  throw new Error(`Invalid template: ${template}`);
881
1265
  }
882
1266
 
1267
+ const targetPath = path.resolve(process.cwd());
883
1268
  const packageRoot = resolvePackageRoot();
1269
+ const packageVersion = await readPackageVersion(packageRoot);
884
1270
  const schema = await loadProfileSchema(packageRoot);
885
- const profile = await resolveProfileForInit(packageRoot, options, schema);
886
-
887
- const targetPath = path.resolve(process.cwd());
1271
+ const profileResolution = await resolveProfileForInit(packageRoot, targetPath, options, schema);
1272
+ const profile = profileResolution.profile;
888
1273
  const inferredProjectName = path.basename(targetPath);
889
1274
  const projectName = inferredProjectName && inferredProjectName !== path.sep ? inferredProjectName : "my-project";
890
1275
  const packageName = sanitizePackageName(projectName);
@@ -910,11 +1295,93 @@ async function runInit(rawOptions) {
910
1295
  await runCommand("npm", ["install"], targetPath);
911
1296
  }
912
1297
 
1298
+ const { packageJsonPath, packageJson } = await readProjectPackageJson(targetPath);
1299
+ const packageWithMetadata = updateCodeStandardsMetadata(packageJson, {
1300
+ template,
1301
+ profilePath: profileResolution.profilePathForMetadata,
1302
+ withAiAdapters: options.withAiAdapters,
1303
+ lastRefreshWith: packageVersion
1304
+ });
1305
+ await writeProjectPackageJson(packageJsonPath, packageWithMetadata);
1306
+
913
1307
  console.log(`Project created at ${targetPath}`);
914
1308
  console.log("Next steps:");
915
1309
  console.log(" npm run check");
916
1310
  }
917
1311
 
1312
+ async function runRefresh(rawOptions) {
1313
+ if (rawOptions.help) {
1314
+ printUsage();
1315
+ return;
1316
+ }
1317
+
1318
+ const targetPath = path.resolve(process.cwd());
1319
+ const packageRoot = resolvePackageRoot();
1320
+ const packageVersion = await readPackageVersion(packageRoot);
1321
+ const schema = await loadProfileSchema(packageRoot);
1322
+ const { packageJsonPath, packageJson: projectPackageJson } = await readProjectPackageJson(targetPath);
1323
+ const projectMetadata = asPlainObject(projectPackageJson[CODE_STANDARDS_METADATA_KEY]);
1324
+ const template = await resolveTemplateForRefresh(rawOptions, projectPackageJson, targetPath);
1325
+ const { templateDir } = await validateInitResources(packageRoot, template);
1326
+ const profileResolution = await resolveProfileForRefresh(packageRoot, targetPath, rawOptions, schema, projectMetadata);
1327
+ const inferredProjectName = path.basename(targetPath);
1328
+ const projectName = inferredProjectName && inferredProjectName !== path.sep ? inferredProjectName : "my-project";
1329
+ const currentPackageName =
1330
+ typeof projectPackageJson.name === "string" && projectPackageJson.name.length > 0 ? projectPackageJson.name : sanitizePackageName(projectName);
1331
+ const tokens = {
1332
+ projectName,
1333
+ packageName: currentPackageName,
1334
+ year: String(new Date().getFullYear()),
1335
+ profileSummary: JSON.stringify(profileResolution.profile)
1336
+ };
1337
+
1338
+ const managedResults = await applyManagedFiles({
1339
+ templateDir,
1340
+ targetDir: targetPath,
1341
+ tokens,
1342
+ templateName: template,
1343
+ projectPackageJson,
1344
+ dryRun: rawOptions.dryRun
1345
+ });
1346
+ const aiFiles = rawOptions.withAiAdapters ? await collectAiFiles(packageRoot) : [];
1347
+
1348
+ if (rawOptions.dryRun) {
1349
+ const uniqueFiles = [...new Set([...managedResults.updatedFiles, ...aiFiles])].sort((left, right) => left.localeCompare(right));
1350
+ console.log(`Dry run: refresh would update ${uniqueFiles.length} file(s).`);
1351
+
1352
+ for (const filePath of uniqueFiles) {
1353
+ console.log(` - ${filePath}`);
1354
+ }
1355
+
1356
+ if (rawOptions.install) {
1357
+ console.log("Dry run: npm install would be executed.");
1358
+ }
1359
+
1360
+ return;
1361
+ }
1362
+
1363
+ if (rawOptions.withAiAdapters) {
1364
+ await generateAiInstructions(packageRoot, targetPath, tokens, profileResolution.profile);
1365
+ }
1366
+
1367
+ if (rawOptions.install) {
1368
+ console.log("Installing dependencies...");
1369
+ await runCommand("npm", ["install"], targetPath);
1370
+ }
1371
+
1372
+ const packageWithMetadata = updateCodeStandardsMetadata(managedResults.mergedPackageJson, {
1373
+ template,
1374
+ profilePath: profileResolution.profilePathForMetadata,
1375
+ withAiAdapters: rawOptions.withAiAdapters,
1376
+ lastRefreshWith: packageVersion
1377
+ });
1378
+ await writeProjectPackageJson(packageJsonPath, packageWithMetadata);
1379
+
1380
+ console.log(`Project refreshed at ${targetPath}`);
1381
+ console.log("Next steps:");
1382
+ console.log(" npm run check");
1383
+ }
1384
+
918
1385
  async function main() {
919
1386
  const argv = process.argv.slice(2);
920
1387
 
@@ -931,6 +1398,11 @@ async function main() {
931
1398
  return;
932
1399
  }
933
1400
 
1401
+ if (command === "refresh" || command === "update") {
1402
+ await runRefresh(parseRefreshArgs(rest));
1403
+ return;
1404
+ }
1405
+
934
1406
  if (command === "profile") {
935
1407
  await runProfile(parseProfileArgs(rest));
936
1408
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sha3/code-standards",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "AI-first code standards, tooling exports, and project initializer",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  * @section imports:internals
9
9
  */
10
10
 
11
- import { BILLING_CURRENCY_SYMBOL, STATUS_SERVICE_URL } from "../config.js";
11
+ import CONFIG from "../config.js";
12
12
  import type { InvoiceService } from "../invoices/invoice-service.js";
13
13
  import type { InvoiceSummary } from "../invoices/invoice-types.js";
14
14
 
@@ -83,7 +83,7 @@ export class BillingService {
83
83
  */
84
84
 
85
85
  private formatCurrency(amount: number): string {
86
- const formattedAmount = `${BILLING_CURRENCY_SYMBOL}${amount.toFixed(2)}`;
86
+ const formattedAmount = `${CONFIG.BILLING_CURRENCY_SYMBOL}${amount.toFixed(2)}`;
87
87
  return formattedAmount;
88
88
  }
89
89
 
@@ -104,7 +104,7 @@ export class BillingService {
104
104
  invoiceCount: summary.count,
105
105
  totalAmount: summary.totalAmount,
106
106
  formattedTotal: this.formatCurrency(summary.totalAmount),
107
- statusServiceUrl: STATUS_SERVICE_URL
107
+ statusServiceUrl: CONFIG.STATUS_SERVICE_URL
108
108
  };
109
109
  return snapshot;
110
110
  }
@@ -1,3 +1,7 @@
1
- export const MINIMUM_INVOICE_AMOUNT = 0;
2
- export const BILLING_CURRENCY_SYMBOL = "$";
3
- export const STATUS_SERVICE_URL = "https://status.example.com/health";
1
+ const CONFIG = {
2
+ MINIMUM_INVOICE_AMOUNT: 0,
3
+ BILLING_CURRENCY_SYMBOL: "$",
4
+ STATUS_SERVICE_URL: "https://status.example.com/health"
5
+ } as const;
6
+
7
+ export default CONFIG;
@@ -8,7 +8,7 @@ import { randomUUID } from "node:crypto";
8
8
  * @section imports:internals
9
9
  */
10
10
 
11
- import { MINIMUM_INVOICE_AMOUNT } from "../config.js";
11
+ import CONFIG from "../config.js";
12
12
  import { InvalidInvoiceCommandError } from "./invoice-errors.js";
13
13
  import type { CreateInvoiceCommand, Invoice, InvoiceSummary } from "./invoice-types.js";
14
14
 
@@ -81,7 +81,7 @@ export class InvoiceService {
81
81
  throw InvalidInvoiceCommandError.forReason("customerId is required");
82
82
  }
83
83
 
84
- if (command.amount <= MINIMUM_INVOICE_AMOUNT) {
84
+ if (command.amount <= CONFIG.MINIMUM_INVOICE_AMOUNT) {
85
85
  throw InvalidInvoiceCommandError.forReason("amount must be greater than zero");
86
86
  }
87
87
  }
@@ -4,6 +4,7 @@
4
4
  - Each feature MUST keep its own domain model, application services, and infrastructure adapters grouped by feature.
5
5
  - Cross-feature imports MUST happen through explicit public entry points.
6
6
  - Hardcoded, non-parameterized configuration MUST be centralized in `src/config.ts` (for example, external service URLs).
7
+ - `src/config.ts` MUST export a single default object and it MUST always be imported as `import CONFIG from ".../config.js"`.
7
8
 
8
9
  Good example:
9
10
 
@@ -7,6 +7,7 @@
7
7
  - New implementation and test files MUST be `.ts`.
8
8
  - JavaScript source files (`.js`, `.mjs`, `.cjs`) are not allowed in `src/` or `test/`.
9
9
  - If a declaration/expression fits in one line within 160 chars, it MUST stay on one line.
10
+ - If a function/method needs many inputs, define a named `<FunctionName>Options` type and pass one `options` parameter.
10
11
 
11
12
  Good example:
12
13
 
@@ -23,3 +23,4 @@ Both templates SHOULD keep this baseline structure:
23
23
  - Keep feature modules cohesive.
24
24
  - Avoid cyclic dependencies.
25
25
  - Keep hardcoded application configuration centralized in `src/config.ts`.
26
+ - `src/config.ts` MUST export a default object (for example `CONFIG`) and consumers MUST import it as `import CONFIG from "./config.js"`.
@@ -18,7 +18,9 @@ All code MUST follow the canonical rules in `standards/manifest.json`.
18
18
  - Avoid `any` unless there is no viable alternative.
19
19
  - Prefer explicit return types for exported functions.
20
20
  - Use type-only imports when possible.
21
+ - If a function/method needs many inputs, define a named `<FunctionName>Options` type and pass a single `options` parameter.
21
22
  - Always use braces in control flow (`if`, `else`, `for`, `while`, `do`).
23
+ - `src/config.ts` MUST export a default object and it MUST always be imported as `import CONFIG from ".../config.js"`.
22
24
 
23
25
  ## Naming
24
26
 
@@ -0,0 +1,37 @@
1
+ # 📚 {{packageName}}
2
+
3
+ Libreria TypeScript lista para publicar y reutilizar.
4
+
5
+ ## TL;DR
6
+
7
+ ```bash
8
+ npm install
9
+ npm run check
10
+ npm run build
11
+ ```
12
+
13
+ ## Uso
14
+
15
+ ```ts
16
+ import { greet } from "./dist/index.js";
17
+
18
+ console.log(greet("world"));
19
+ ```
20
+
21
+ ## Scripts
22
+
23
+ - `npm run check`: lint + format check + typecheck + tests
24
+ - `npm run fix`: aplica autofix de lint y prettier
25
+ - `npm run build`: compila a `dist/`
26
+ - `npm run test`: ejecuta tests con Node test runner
27
+
28
+ ## Estructura
29
+
30
+ - `src/`: implementacion
31
+ - `src/config.ts`: configuracion hardcodeada centralizada
32
+ - `test/`: pruebas
33
+ - `dist/`: salida de build
34
+
35
+ ## AI Workflow
36
+
37
+ Si trabajas con asistentes, usa `AGENTS.md` y `ai/*.md` como reglas bloqueantes.
@@ -1 +1,5 @@
1
- export const GREETING_PREFIX = "Hello";
1
+ const CONFIG = {
2
+ GREETING_PREFIX: "Hello"
3
+ } as const;
4
+
5
+ export default CONFIG;
@@ -1,5 +1,5 @@
1
- import { GREETING_PREFIX } from "./config.js";
1
+ import CONFIG from "./config.js";
2
2
 
3
3
  export function greet(name: string): string {
4
- return `${GREETING_PREFIX}, ${name}`;
4
+ return `${CONFIG.GREETING_PREFIX}, ${name}`;
5
5
  }
@@ -0,0 +1,36 @@
1
+ # 🚀 {{packageName}}
2
+
3
+ Servicio TypeScript listo para ejecutar en local y evolucionar por features.
4
+
5
+ ## TL;DR
6
+
7
+ ```bash
8
+ npm install
9
+ npm run check
10
+ npm run start
11
+ ```
12
+
13
+ ## Ejecucion
14
+
15
+ ```bash
16
+ npm run start
17
+ ```
18
+
19
+ El servicio arranca por defecto en `http://localhost:3000`.
20
+
21
+ ## Scripts
22
+
23
+ - `npm run start`: inicia el servicio con `tsx`
24
+ - `npm run check`: lint + format check + typecheck + tests
25
+ - `npm run fix`: aplica autofix de lint y prettier
26
+ - `npm run test`: ejecuta tests con Node test runner
27
+
28
+ ## Estructura
29
+
30
+ - `src/`: implementacion
31
+ - `src/config.ts`: configuracion hardcodeada centralizada
32
+ - `test/`: pruebas
33
+
34
+ ## AI Workflow
35
+
36
+ Si trabajas con asistentes, usa `AGENTS.md` y `ai/*.md` como reglas bloqueantes.
@@ -1,3 +1,7 @@
1
- export const RESPONSE_CONTENT_TYPE = "application/json";
2
- export const DEFAULT_PORT = 3000;
3
- export const EXTERNAL_STATUS_URL = "https://status.example.com/health";
1
+ const CONFIG = {
2
+ RESPONSE_CONTENT_TYPE: "application/json",
3
+ DEFAULT_PORT: 3000,
4
+ EXTERNAL_STATUS_URL: "https://status.example.com/health"
5
+ } as const;
6
+
7
+ export default CONFIG;
@@ -1,16 +1,16 @@
1
1
  import { createServer } from "node:http";
2
- import { DEFAULT_PORT, EXTERNAL_STATUS_URL, RESPONSE_CONTENT_TYPE } from "./config.js";
2
+ import CONFIG from "./config.js";
3
3
 
4
4
  export function buildServer() {
5
5
  return createServer((_, response) => {
6
6
  response.statusCode = 200;
7
- response.setHeader("content-type", RESPONSE_CONTENT_TYPE);
8
- response.end(JSON.stringify({ ok: true, statusSource: EXTERNAL_STATUS_URL }));
7
+ response.setHeader("content-type", CONFIG.RESPONSE_CONTENT_TYPE);
8
+ response.end(JSON.stringify({ ok: true, statusSource: CONFIG.EXTERNAL_STATUS_URL }));
9
9
  });
10
10
  }
11
11
 
12
12
  if (process.env.NODE_ENV !== "test") {
13
- const port = Number(process.env.PORT ?? String(DEFAULT_PORT));
13
+ const port = Number(process.env.PORT ?? String(CONFIG.DEFAULT_PORT));
14
14
  const server = buildServer();
15
15
  server.listen(port, () => {
16
16
  console.log(`service listening on http://localhost:${port}`);