@sha3/code-standards 0.1.3 → 0.1.5

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.
Files changed (39) hide show
  1. package/README.md +42 -0
  2. package/bin/code-standards.mjs +507 -96
  3. package/eslint/test.mjs +1 -5
  4. package/package.json +1 -1
  5. package/prettier/index.cjs +1 -1
  6. package/profiles/default.profile.json +2 -0
  7. package/profiles/schema.json +7 -7
  8. package/resources/ai/templates/examples/demo/src/billing/billing-service.ts +18 -3
  9. package/resources/ai/templates/examples/demo/src/config.ts +3 -0
  10. package/resources/ai/templates/examples/demo/src/invoices/invoice-errors.ts +12 -0
  11. package/resources/ai/templates/examples/demo/src/invoices/invoice-service.ts +14 -1
  12. package/resources/ai/templates/examples/rules/async-bad.ts +12 -0
  13. package/resources/ai/templates/examples/rules/async-good.ts +12 -0
  14. package/resources/ai/templates/examples/rules/class-first-bad.ts +12 -0
  15. package/resources/ai/templates/examples/rules/class-first-good.ts +12 -0
  16. package/resources/ai/templates/examples/rules/constructor-bad.ts +12 -0
  17. package/resources/ai/templates/examples/rules/constructor-good.ts +12 -0
  18. package/resources/ai/templates/examples/rules/control-flow-bad.ts +12 -0
  19. package/resources/ai/templates/examples/rules/control-flow-good.ts +12 -0
  20. package/resources/ai/templates/examples/rules/errors-bad.ts +12 -0
  21. package/resources/ai/templates/examples/rules/errors-good.ts +12 -0
  22. package/resources/ai/templates/examples/rules/functions-bad.ts +12 -0
  23. package/resources/ai/templates/examples/rules/functions-good.ts +12 -0
  24. package/resources/ai/templates/examples/rules/returns-bad.ts +12 -0
  25. package/resources/ai/templates/examples/rules/returns-good.ts +12 -0
  26. package/resources/ai/templates/examples/rules/testing-bad.ts +12 -0
  27. package/resources/ai/templates/examples/rules/testing-good.ts +12 -0
  28. package/resources/ai/templates/rules/architecture.md +2 -0
  29. package/resources/ai/templates/rules/class-first.md +2 -0
  30. package/resources/ai/templates/rules/functions.md +2 -0
  31. package/standards/architecture.md +2 -1
  32. package/standards/manifest.json +1 -1
  33. package/standards/schema.json +2 -11
  34. package/standards/style.md +13 -9
  35. package/templates/node-lib/src/config.ts +1 -0
  36. package/templates/node-lib/src/index.ts +3 -1
  37. package/templates/node-service/src/config.ts +3 -0
  38. package/templates/node-service/src/index.ts +4 -3
  39. package/templates/node-service/test/smoke.test.ts +1 -1
@@ -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",
@@ -55,12 +61,14 @@ const DEFAULT_PROFILE = {
55
61
  "consts",
56
62
  "types",
57
63
  "private:attributes",
64
+ "protected:attributes",
58
65
  "private:properties",
59
66
  "public:properties",
60
67
  "constructor",
61
68
  "static:properties",
62
69
  "factory",
63
70
  "private:methods",
71
+ "protected:methods",
64
72
  "public:methods",
65
73
  "static:methods"
66
74
  ],
@@ -88,11 +96,7 @@ const PROFILE_QUESTIONS = [
88
96
  {
89
97
  key: "return_policy",
90
98
  prompt: "Return policy",
91
- options: [
92
- "single_return_strict_no_exceptions",
93
- "single_return_with_guard_clauses",
94
- "free_return_style"
95
- ]
99
+ options: ["single_return_strict_no_exceptions", "single_return_with_guard_clauses", "free_return_style"]
96
100
  },
97
101
  {
98
102
  key: "class_design",
@@ -167,6 +171,8 @@ function printUsage() {
167
171
 
168
172
  Commands:
169
173
  init Initialize a project in the current directory
174
+ refresh Re-apply managed standards files and AI instructions
175
+ update Alias of refresh
170
176
  profile Create or update the AI style profile
171
177
 
172
178
  Init options:
@@ -178,6 +184,15 @@ Init options:
178
184
  --no-ai-adapters
179
185
  --profile <path>
180
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
+
181
196
  Profile options:
182
197
  --profile <path>
183
198
  --non-interactive
@@ -202,9 +217,7 @@ function parseInitArgs(argv) {
202
217
  const token = argv[i];
203
218
 
204
219
  if (!token.startsWith("-")) {
205
- throw new Error(
206
- `Positional project names are not supported: ${token}. Run init from your target directory.`
207
- );
220
+ throw new Error(`Positional project names are not supported: ${token}. Run init from your target directory.`);
208
221
  }
209
222
 
210
223
  if (token === "--template") {
@@ -275,6 +288,88 @@ function parseInitArgs(argv) {
275
288
  return options;
276
289
  }
277
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
+
278
373
  function parseProfileArgs(argv) {
279
374
  const options = {
280
375
  profilePath: undefined,
@@ -397,9 +492,7 @@ async function ensureTargetReady(targetPath, force) {
397
492
  const nonGitEntries = entries.filter((entry) => entry !== ".git");
398
493
 
399
494
  if (nonGitEntries.length > 0 && !force) {
400
- throw new Error(
401
- `Target directory is not empty: ${targetPath}. Use --force to continue and overwrite files.`
402
- );
495
+ throw new Error(`Target directory is not empty: ${targetPath}. Use --force to continue and overwrite files.`);
403
496
  }
404
497
  }
405
498
 
@@ -428,11 +521,196 @@ async function copyTemplateDirectory(sourceDir, targetDir, tokens) {
428
521
  }
429
522
  }
430
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 (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
541
+ return resolvedProfilePath;
542
+ }
543
+
544
+ return relativePath;
545
+ }
546
+
547
+ async function readProjectPackageJson(targetPath) {
548
+ const packageJsonPath = path.join(targetPath, "package.json");
549
+
550
+ if (!(await pathExists(packageJsonPath))) {
551
+ throw new Error(`package.json was not found in ${targetPath}. Run refresh from the project root.`);
552
+ }
553
+
554
+ return {
555
+ packageJsonPath,
556
+ packageJson: await readJsonFile(packageJsonPath)
557
+ };
558
+ }
559
+
560
+ async function writeProjectPackageJson(packageJsonPath, packageJson) {
561
+ await writeJsonFile(packageJsonPath, packageJson);
562
+ }
563
+
564
+ function updateCodeStandardsMetadata(projectPackageJson, metadataPatch) {
565
+ const existingMetadata = asPlainObject(projectPackageJson[CODE_STANDARDS_METADATA_KEY]);
566
+ const nextMetadata = {
567
+ ...existingMetadata,
568
+ ...metadataPatch
569
+ };
570
+
571
+ return {
572
+ ...projectPackageJson,
573
+ [CODE_STANDARDS_METADATA_KEY]: nextMetadata
574
+ };
575
+ }
576
+
577
+ function mergePackageJsonFromTemplate(projectPackageJson, templatePackageJson, templateName) {
578
+ const mergedPackageJson = { ...projectPackageJson };
579
+ const templateScripts = asPlainObject(templatePackageJson.scripts);
580
+ const templateDevDependencies = asPlainObject(templatePackageJson.devDependencies);
581
+ const mergedScripts = {
582
+ ...asPlainObject(projectPackageJson.scripts)
583
+ };
584
+ const mergedDevDependencies = {
585
+ ...asPlainObject(projectPackageJson.devDependencies)
586
+ };
587
+
588
+ for (const [scriptName, scriptValue] of Object.entries(templateScripts)) {
589
+ mergedScripts[scriptName] = scriptValue;
590
+ }
591
+
592
+ for (const [dependencyName, dependencyVersion] of Object.entries(templateDevDependencies)) {
593
+ mergedDevDependencies[dependencyName] = dependencyVersion;
594
+ }
595
+
596
+ if (Object.keys(mergedScripts).length > 0) {
597
+ mergedPackageJson.scripts = mergedScripts;
598
+ }
599
+
600
+ if (Object.keys(mergedDevDependencies).length > 0) {
601
+ mergedPackageJson.devDependencies = mergedDevDependencies;
602
+ }
603
+
604
+ if (typeof templatePackageJson.type === "string") {
605
+ mergedPackageJson.type = templatePackageJson.type;
606
+ }
607
+
608
+ if (templateName === "node-lib") {
609
+ if (typeof templatePackageJson.main === "string") {
610
+ mergedPackageJson.main = templatePackageJson.main;
611
+ }
612
+
613
+ if (typeof templatePackageJson.types === "string") {
614
+ mergedPackageJson.types = templatePackageJson.types;
615
+ }
616
+
617
+ if (Array.isArray(templatePackageJson.files)) {
618
+ mergedPackageJson.files = templatePackageJson.files;
619
+ }
620
+ }
621
+
622
+ return mergedPackageJson;
623
+ }
624
+
625
+ async function collectTemplateFiles(templateDir, baseDir = templateDir) {
626
+ const entries = await readdir(templateDir, { withFileTypes: true });
627
+ const files = [];
628
+
629
+ for (const entry of entries) {
630
+ const sourcePath = path.join(templateDir, entry.name);
631
+
632
+ if (entry.isDirectory()) {
633
+ const nestedFiles = await collectTemplateFiles(sourcePath, baseDir);
634
+ files.push(...nestedFiles);
635
+ continue;
636
+ }
637
+
638
+ if (!entry.isFile()) {
639
+ continue;
640
+ }
641
+
642
+ const sourceRelativePath = path.relative(baseDir, sourcePath);
643
+ const sourceDirectory = path.dirname(sourceRelativePath);
644
+ const sourceFileName = path.basename(sourceRelativePath);
645
+ const mappedFileName = mapTemplateFileName(sourceFileName);
646
+ const targetRelativePath = sourceDirectory === "." ? mappedFileName : path.join(sourceDirectory, mappedFileName);
647
+
648
+ files.push({
649
+ sourcePath,
650
+ sourceRelativePath,
651
+ targetRelativePath
652
+ });
653
+ }
654
+
655
+ return files.sort((left, right) => left.targetRelativePath.localeCompare(right.targetRelativePath));
656
+ }
657
+
658
+ async function applyManagedFiles(options) {
659
+ const { templateDir, targetDir, tokens, templateName, projectPackageJson, dryRun } = options;
660
+ const templateFiles = await collectTemplateFiles(templateDir);
661
+ const updatedFiles = [];
662
+ let mergedPackageJson = { ...projectPackageJson };
663
+
664
+ for (const templateFile of templateFiles) {
665
+ if (templateFile.targetRelativePath === "package.json") {
666
+ const rawTemplatePackageJson = await readFile(templateFile.sourcePath, "utf8");
667
+ const renderedTemplatePackageJson = replaceTokens(rawTemplatePackageJson, tokens);
668
+ const templatePackageJson = JSON.parse(renderedTemplatePackageJson);
669
+ mergedPackageJson = mergePackageJsonFromTemplate(mergedPackageJson, templatePackageJson, templateName);
670
+ updatedFiles.push("package.json");
671
+ continue;
672
+ }
673
+
674
+ const raw = await readFile(templateFile.sourcePath, "utf8");
675
+ const rendered = replaceTokens(raw, tokens);
676
+ const targetPath = path.join(targetDir, templateFile.targetRelativePath);
677
+ updatedFiles.push(templateFile.targetRelativePath);
678
+
679
+ if (dryRun) {
680
+ continue;
681
+ }
682
+
683
+ await mkdir(path.dirname(targetPath), { recursive: true });
684
+ await writeFile(targetPath, rendered, "utf8");
685
+ }
686
+
687
+ if (!dryRun) {
688
+ const packageJsonPath = path.join(targetDir, "package.json");
689
+ await writeProjectPackageJson(packageJsonPath, mergedPackageJson);
690
+ }
691
+
692
+ return {
693
+ updatedFiles,
694
+ mergedPackageJson
695
+ };
696
+ }
697
+
431
698
  function resolvePackageRoot() {
432
699
  const binPath = fileURLToPath(import.meta.url);
433
700
  return path.resolve(path.dirname(binPath), "..");
434
701
  }
435
702
 
703
+ async function readPackageVersion(packageRoot) {
704
+ const packageJsonPath = path.join(packageRoot, "package.json");
705
+ const packageJson = await readJsonFile(packageJsonPath);
706
+
707
+ if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
708
+ throw new Error("Package version is missing in the CLI package.json.");
709
+ }
710
+
711
+ return packageJson.version;
712
+ }
713
+
436
714
  function getBundledProfilePath(packageRoot) {
437
715
  return path.join(packageRoot, "profiles", "default.profile.json");
438
716
  }
@@ -456,9 +734,7 @@ function validateProfile(profile, schema, sourceLabel) {
456
734
  return;
457
735
  }
458
736
 
459
- const details = (validate.errors ?? [])
460
- .map((issue) => `${issue.instancePath || "/"}: ${issue.message ?? "invalid"}`)
461
- .join("; ");
737
+ const details = (validate.errors ?? []).map((issue) => `${issue.instancePath || "/"}: ${issue.message ?? "invalid"}`).join("; ");
462
738
 
463
739
  throw new Error(`Invalid profile at ${sourceLabel}: ${details}`);
464
740
  }
@@ -550,12 +826,7 @@ async function createProfileInteractively(baseProfile) {
550
826
 
551
827
  try {
552
828
  for (const question of PROFILE_QUESTIONS) {
553
- profile[question.key] = await askChoice(
554
- rl,
555
- question.prompt,
556
- question.options,
557
- profile[question.key]
558
- );
829
+ profile[question.key] = await askChoice(rl, question.prompt, question.options, profile[question.key]);
559
830
  }
560
831
  } finally {
561
832
  rl.close();
@@ -573,9 +844,7 @@ async function runProfile(rawOptions) {
573
844
  const packageRoot = resolvePackageRoot();
574
845
  const schema = await loadProfileSchema(packageRoot);
575
846
  const defaultProfilePath = getBundledProfilePath(packageRoot);
576
- const outputPath = rawOptions.profilePath
577
- ? path.resolve(process.cwd(), rawOptions.profilePath)
578
- : defaultProfilePath;
847
+ const outputPath = rawOptions.profilePath ? path.resolve(process.cwd(), rawOptions.profilePath) : defaultProfilePath;
579
848
  const shouldUseNonInteractive = rawOptions.nonInteractive || !process.stdin.isTTY;
580
849
  const exists = await pathExists(outputPath);
581
850
 
@@ -590,11 +859,7 @@ async function runProfile(rawOptions) {
590
859
  });
591
860
 
592
861
  try {
593
- const shouldOverwrite = await promptYesNo(
594
- rl,
595
- `Profile already exists at ${outputPath}. Overwrite?`,
596
- false
597
- );
862
+ const shouldOverwrite = await promptYesNo(rl, `Profile already exists at ${outputPath}. Overwrite?`, false);
598
863
 
599
864
  if (!shouldOverwrite) {
600
865
  console.log("Profile update cancelled.");
@@ -656,44 +921,30 @@ function buildAlternativeRules(profile) {
656
921
 
657
922
  if (profile.return_policy !== "single_return_strict_no_exceptions") {
658
923
  codeRules.push(
659
- "### Return Policy Override (MUST)\n\n" +
660
- `- The active return policy is \`${profile.return_policy}\` and MUST be respected for all new functions.`
924
+ "### Return Policy Override (MUST)\n\n" + `- The active return policy is \`${profile.return_policy}\` and MUST be respected for all new functions.`
661
925
  );
662
926
  }
663
927
 
664
928
  if (profile.function_size_policy !== "max_30_lines_soft") {
665
- codeRules.push(
666
- "### Function Size Override (MUST)\n\n" +
667
- `- The active function-size policy is \`${profile.function_size_policy}\` and MUST be enforced.`
668
- );
929
+ codeRules.push("### Function Size Override (MUST)\n\n" + `- The active function-size policy is \`${profile.function_size_policy}\` and MUST be enforced.`);
669
930
  }
670
931
 
671
932
  if (profile.error_handling !== "exceptions_with_typed_errors") {
672
- codeRules.push(
673
- "### Error Handling Override (MUST)\n\n" +
674
- `- The active error-handling policy is \`${profile.error_handling}\`.`
675
- );
933
+ codeRules.push("### Error Handling Override (MUST)\n\n" + `- The active error-handling policy is \`${profile.error_handling}\`.`);
676
934
  }
677
935
 
678
936
  if (profile.async_style !== "async_await_only") {
679
- codeRules.push(
680
- "### Async Style Override (MUST)\n\n" +
681
- `- The active async policy is \`${profile.async_style}\` and MUST be followed in new code.`
682
- );
937
+ codeRules.push("### Async Style Override (MUST)\n\n" + `- The active async policy is \`${profile.async_style}\` and MUST be followed in new code.`);
683
938
  }
684
939
 
685
940
  let architectureRule = "";
686
941
  if (profile.architecture !== "feature_folders") {
687
- architectureRule =
688
- "### Architecture Override (MUST)\n\n" +
689
- `- The active architecture is \`${profile.architecture}\` and MUST take precedence.`;
942
+ architectureRule = "### Architecture Override (MUST)\n\n" + `- The active architecture is \`${profile.architecture}\` and MUST take precedence.`;
690
943
  }
691
944
 
692
945
  let testingRule = "";
693
946
  if (profile.testing_policy !== "tests_required_for_behavior_change") {
694
- testingRule =
695
- "### Testing Override (MUST)\n\n" +
696
- `- The active testing policy is \`${profile.testing_policy}\`.`;
947
+ testingRule = "### Testing Override (MUST)\n\n" + `- The active testing policy is \`${profile.testing_policy}\`.`;
697
948
  }
698
949
 
699
950
  return {
@@ -722,14 +973,7 @@ async function buildRuleSections(packageRoot, profile) {
722
973
  const readmeRule = await readRule("readme.md");
723
974
 
724
975
  const alternatives = buildAlternativeRules(profile);
725
- const codeGenerationRules = [
726
- classRule,
727
- functionRule,
728
- returnRule,
729
- controlFlowRule,
730
- errorRule,
731
- asyncRule
732
- ];
976
+ const codeGenerationRules = [classRule, functionRule, returnRule, controlFlowRule, errorRule, asyncRule];
733
977
 
734
978
  if (alternatives.codeRules.length > 0) {
735
979
  codeGenerationRules.push(...alternatives.codeRules);
@@ -743,13 +987,7 @@ async function buildRuleSections(packageRoot, profile) {
743
987
  }
744
988
 
745
989
  async function renderProjectAgents(packageRoot, targetDir, projectName, profile) {
746
- const templatePath = path.join(
747
- packageRoot,
748
- "resources",
749
- "ai",
750
- "templates",
751
- "agents.project.template.md"
752
- );
990
+ const templatePath = path.join(packageRoot, "resources", "ai", "templates", "agents.project.template.md");
753
991
  const template = await readFile(templatePath, "utf8");
754
992
  const sections = await buildRuleSections(packageRoot, profile);
755
993
 
@@ -807,11 +1045,7 @@ async function maybeInitializeProfileInteractively(packageRoot, profilePath) {
807
1045
  });
808
1046
 
809
1047
  try {
810
- const shouldInit = await promptYesNo(
811
- rl,
812
- `Profile not found at ${profilePath}. Initialize it with package defaults?`,
813
- true
814
- );
1048
+ const shouldInit = await promptYesNo(rl, `Profile not found at ${profilePath}. Initialize it with package defaults?`, true);
815
1049
 
816
1050
  if (!shouldInit) {
817
1051
  throw new Error("Profile initialization declined by user.");
@@ -825,7 +1059,25 @@ async function maybeInitializeProfileInteractively(packageRoot, profilePath) {
825
1059
  console.log(`Profile initialized at ${profilePath}`);
826
1060
  }
827
1061
 
828
- async function resolveProfileForInit(packageRoot, rawOptions, schema) {
1062
+ async function resolveBundledOrDefaultProfile(packageRoot, schema) {
1063
+ const bundledProfilePath = getBundledProfilePath(packageRoot);
1064
+ const bundledExists = await pathExists(bundledProfilePath);
1065
+
1066
+ if (bundledExists) {
1067
+ return {
1068
+ profile: await readAndValidateProfile(bundledProfilePath, schema),
1069
+ profilePathForMetadata: null
1070
+ };
1071
+ }
1072
+
1073
+ validateProfile(DEFAULT_PROFILE, schema, "hardcoded defaults");
1074
+ return {
1075
+ profile: normalizeProfile(DEFAULT_PROFILE),
1076
+ profilePathForMetadata: null
1077
+ };
1078
+ }
1079
+
1080
+ async function resolveProfileForInit(packageRoot, targetPath, rawOptions, schema) {
829
1081
  const bundledProfilePath = getBundledProfilePath(packageRoot);
830
1082
 
831
1083
  if (!rawOptions.profilePath) {
@@ -833,35 +1085,115 @@ async function resolveProfileForInit(packageRoot, rawOptions, schema) {
833
1085
 
834
1086
  if (!bundledExists) {
835
1087
  if (rawOptions.yes) {
836
- validateProfile(DEFAULT_PROFILE, schema, "hardcoded defaults");
837
- return normalizeProfile(DEFAULT_PROFILE);
1088
+ return resolveBundledOrDefaultProfile(packageRoot, schema);
838
1089
  }
839
1090
 
840
1091
  await writeJsonFile(bundledProfilePath, normalizeProfile(DEFAULT_PROFILE));
841
1092
  }
842
1093
 
843
- return readAndValidateProfile(bundledProfilePath, schema);
1094
+ return {
1095
+ profile: await readAndValidateProfile(bundledProfilePath, schema),
1096
+ profilePathForMetadata: null
1097
+ };
844
1098
  }
845
1099
 
846
- const requestedPath = path.resolve(process.cwd(), rawOptions.profilePath);
1100
+ const requestedPath = path.resolve(targetPath, rawOptions.profilePath);
847
1101
  const requestedExists = await pathExists(requestedPath);
848
1102
 
849
1103
  if (!requestedExists) {
850
1104
  if (rawOptions.yes) {
851
- const bundledExists = await pathExists(bundledProfilePath);
1105
+ return resolveBundledOrDefaultProfile(packageRoot, schema);
1106
+ }
852
1107
 
853
- if (bundledExists) {
854
- return readAndValidateProfile(bundledProfilePath, schema);
855
- }
1108
+ await maybeInitializeProfileInteractively(packageRoot, requestedPath);
1109
+ }
1110
+
1111
+ return {
1112
+ profile: await readAndValidateProfile(requestedPath, schema),
1113
+ profilePathForMetadata: getRelativeProfilePath(requestedPath, targetPath)
1114
+ };
1115
+ }
1116
+
1117
+ async function resolveTemplateForRefresh(rawOptions, projectPackageJson, targetPath) {
1118
+ if (rawOptions.template) {
1119
+ return rawOptions.template;
1120
+ }
1121
+
1122
+ const metadata = asPlainObject(projectPackageJson[CODE_STANDARDS_METADATA_KEY]);
856
1123
 
857
- validateProfile(DEFAULT_PROFILE, schema, "hardcoded defaults");
858
- return normalizeProfile(DEFAULT_PROFILE);
1124
+ if (typeof metadata.template === "string" && TEMPLATE_NAMES.includes(metadata.template)) {
1125
+ return metadata.template;
1126
+ }
1127
+
1128
+ const projectScripts = asPlainObject(projectPackageJson.scripts);
1129
+ const hasNodeLibSignature =
1130
+ (await pathExists(path.join(targetPath, "tsconfig.build.json"))) &&
1131
+ projectPackageJson.main === NODE_LIB_REFRESH_SIGNATURE.main &&
1132
+ projectPackageJson.types === NODE_LIB_REFRESH_SIGNATURE.types;
1133
+
1134
+ if (hasNodeLibSignature) {
1135
+ return "node-lib";
1136
+ }
1137
+
1138
+ const startScript = typeof projectScripts.start === "string" ? projectScripts.start : "";
1139
+
1140
+ if (startScript.includes(NODE_SERVICE_START_SIGNATURE)) {
1141
+ return "node-service";
1142
+ }
1143
+
1144
+ throw new Error("Unable to infer template for refresh. Use --template <node-lib|node-service>.");
1145
+ }
1146
+
1147
+ async function resolveProfileForRefresh(packageRoot, targetPath, rawOptions, schema, projectMetadata) {
1148
+ let selectedProfilePath;
1149
+
1150
+ if (rawOptions.profilePath) {
1151
+ selectedProfilePath = path.resolve(targetPath, rawOptions.profilePath);
1152
+ } else if (typeof projectMetadata.profilePath === "string" && projectMetadata.profilePath.trim().length > 0) {
1153
+ selectedProfilePath = path.resolve(targetPath, projectMetadata.profilePath);
1154
+ }
1155
+
1156
+ if (!selectedProfilePath) {
1157
+ return resolveBundledOrDefaultProfile(packageRoot, schema);
1158
+ }
1159
+
1160
+ const selectedExists = await pathExists(selectedProfilePath);
1161
+
1162
+ if (!selectedExists) {
1163
+ if (rawOptions.yes) {
1164
+ return resolveBundledOrDefaultProfile(packageRoot, schema);
859
1165
  }
860
1166
 
861
- await maybeInitializeProfileInteractively(packageRoot, requestedPath);
1167
+ await maybeInitializeProfileInteractively(packageRoot, selectedProfilePath);
1168
+ }
1169
+
1170
+ return {
1171
+ profile: await readAndValidateProfile(selectedProfilePath, schema),
1172
+ profilePathForMetadata: getRelativeProfilePath(selectedProfilePath, targetPath)
1173
+ };
1174
+ }
1175
+
1176
+ async function collectAiFiles(packageRoot) {
1177
+ const aiFiles = ["AGENTS.md"];
1178
+ const adaptersTemplateDir = path.join(packageRoot, "resources", "ai", "templates", "adapters");
1179
+ const examplesTemplateDir = path.join(packageRoot, "resources", "ai", "templates", "examples");
1180
+ const adapterEntries = await readdir(adaptersTemplateDir, { withFileTypes: true });
1181
+
1182
+ for (const entry of adapterEntries) {
1183
+ if (!entry.isFile() || !entry.name.endsWith(".template.md")) {
1184
+ continue;
1185
+ }
1186
+
1187
+ aiFiles.push(path.join("ai", entry.name.replace(/\.template\.md$/, ".md")));
1188
+ }
1189
+
1190
+ const exampleTemplateFiles = await collectTemplateFiles(examplesTemplateDir);
1191
+
1192
+ for (const exampleFile of exampleTemplateFiles) {
1193
+ aiFiles.push(path.join("ai", "examples", exampleFile.targetRelativePath));
862
1194
  }
863
1195
 
864
- return readAndValidateProfile(requestedPath, schema);
1196
+ return aiFiles.sort((left, right) => left.localeCompare(right));
865
1197
  }
866
1198
 
867
1199
  async function promptForMissing(options) {
@@ -874,9 +1206,7 @@ async function promptForMissing(options) {
874
1206
  const resolved = { ...options };
875
1207
 
876
1208
  if (!resolved.template) {
877
- const templateAnswer = await rl.question(
878
- "Choose template (node-lib/node-service) [node-lib]: "
879
- );
1209
+ const templateAnswer = await rl.question("Choose template (node-lib/node-service) [node-lib]: ");
880
1210
  const normalized = templateAnswer.trim() || "node-lib";
881
1211
 
882
1212
  if (!TEMPLATE_NAMES.includes(normalized)) {
@@ -900,13 +1230,7 @@ async function promptForMissing(options) {
900
1230
 
901
1231
  async function validateInitResources(packageRoot, templateName) {
902
1232
  const templateDir = path.join(packageRoot, "templates", templateName);
903
- const agentsTemplatePath = path.join(
904
- packageRoot,
905
- "resources",
906
- "ai",
907
- "templates",
908
- "agents.project.template.md"
909
- );
1233
+ const agentsTemplatePath = path.join(packageRoot, "resources", "ai", "templates", "agents.project.template.md");
910
1234
  const adaptersTemplateDir = path.join(packageRoot, "resources", "ai", "templates", "adapters");
911
1235
  const examplesTemplateDir = path.join(packageRoot, "resources", "ai", "templates", "examples");
912
1236
 
@@ -938,14 +1262,14 @@ async function runInit(rawOptions) {
938
1262
  throw new Error(`Invalid template: ${template}`);
939
1263
  }
940
1264
 
1265
+ const targetPath = path.resolve(process.cwd());
941
1266
  const packageRoot = resolvePackageRoot();
1267
+ const packageVersion = await readPackageVersion(packageRoot);
942
1268
  const schema = await loadProfileSchema(packageRoot);
943
- const profile = await resolveProfileForInit(packageRoot, options, schema);
944
-
945
- const targetPath = path.resolve(process.cwd());
1269
+ const profileResolution = await resolveProfileForInit(packageRoot, targetPath, options, schema);
1270
+ const profile = profileResolution.profile;
946
1271
  const inferredProjectName = path.basename(targetPath);
947
- const projectName =
948
- inferredProjectName && inferredProjectName !== path.sep ? inferredProjectName : "my-project";
1272
+ const projectName = inferredProjectName && inferredProjectName !== path.sep ? inferredProjectName : "my-project";
949
1273
  const packageName = sanitizePackageName(projectName);
950
1274
 
951
1275
  await ensureTargetReady(targetPath, options.force);
@@ -969,11 +1293,93 @@ async function runInit(rawOptions) {
969
1293
  await runCommand("npm", ["install"], targetPath);
970
1294
  }
971
1295
 
1296
+ const { packageJsonPath, packageJson } = await readProjectPackageJson(targetPath);
1297
+ const packageWithMetadata = updateCodeStandardsMetadata(packageJson, {
1298
+ template,
1299
+ profilePath: profileResolution.profilePathForMetadata,
1300
+ withAiAdapters: options.withAiAdapters,
1301
+ lastRefreshWith: packageVersion
1302
+ });
1303
+ await writeProjectPackageJson(packageJsonPath, packageWithMetadata);
1304
+
972
1305
  console.log(`Project created at ${targetPath}`);
973
1306
  console.log("Next steps:");
974
1307
  console.log(" npm run check");
975
1308
  }
976
1309
 
1310
+ async function runRefresh(rawOptions) {
1311
+ if (rawOptions.help) {
1312
+ printUsage();
1313
+ return;
1314
+ }
1315
+
1316
+ const targetPath = path.resolve(process.cwd());
1317
+ const packageRoot = resolvePackageRoot();
1318
+ const packageVersion = await readPackageVersion(packageRoot);
1319
+ const schema = await loadProfileSchema(packageRoot);
1320
+ const { packageJsonPath, packageJson: projectPackageJson } = await readProjectPackageJson(targetPath);
1321
+ const projectMetadata = asPlainObject(projectPackageJson[CODE_STANDARDS_METADATA_KEY]);
1322
+ const template = await resolveTemplateForRefresh(rawOptions, projectPackageJson, targetPath);
1323
+ const { templateDir } = await validateInitResources(packageRoot, template);
1324
+ const profileResolution = await resolveProfileForRefresh(packageRoot, targetPath, rawOptions, schema, projectMetadata);
1325
+ const inferredProjectName = path.basename(targetPath);
1326
+ const projectName = inferredProjectName && inferredProjectName !== path.sep ? inferredProjectName : "my-project";
1327
+ const currentPackageName =
1328
+ typeof projectPackageJson.name === "string" && projectPackageJson.name.length > 0 ? projectPackageJson.name : sanitizePackageName(projectName);
1329
+ const tokens = {
1330
+ projectName,
1331
+ packageName: currentPackageName,
1332
+ year: String(new Date().getFullYear()),
1333
+ profileSummary: JSON.stringify(profileResolution.profile)
1334
+ };
1335
+
1336
+ const managedResults = await applyManagedFiles({
1337
+ templateDir,
1338
+ targetDir: targetPath,
1339
+ tokens,
1340
+ templateName: template,
1341
+ projectPackageJson,
1342
+ dryRun: rawOptions.dryRun
1343
+ });
1344
+ const aiFiles = rawOptions.withAiAdapters ? await collectAiFiles(packageRoot) : [];
1345
+
1346
+ if (rawOptions.dryRun) {
1347
+ const uniqueFiles = [...new Set([...managedResults.updatedFiles, ...aiFiles])].sort((left, right) => left.localeCompare(right));
1348
+ console.log(`Dry run: refresh would update ${uniqueFiles.length} file(s).`);
1349
+
1350
+ for (const filePath of uniqueFiles) {
1351
+ console.log(` - ${filePath}`);
1352
+ }
1353
+
1354
+ if (rawOptions.install) {
1355
+ console.log("Dry run: npm install would be executed.");
1356
+ }
1357
+
1358
+ return;
1359
+ }
1360
+
1361
+ if (rawOptions.withAiAdapters) {
1362
+ await generateAiInstructions(packageRoot, targetPath, tokens, profileResolution.profile);
1363
+ }
1364
+
1365
+ if (rawOptions.install) {
1366
+ console.log("Installing dependencies...");
1367
+ await runCommand("npm", ["install"], targetPath);
1368
+ }
1369
+
1370
+ const packageWithMetadata = updateCodeStandardsMetadata(managedResults.mergedPackageJson, {
1371
+ template,
1372
+ profilePath: profileResolution.profilePathForMetadata,
1373
+ withAiAdapters: rawOptions.withAiAdapters,
1374
+ lastRefreshWith: packageVersion
1375
+ });
1376
+ await writeProjectPackageJson(packageJsonPath, packageWithMetadata);
1377
+
1378
+ console.log(`Project refreshed at ${targetPath}`);
1379
+ console.log("Next steps:");
1380
+ console.log(" npm run check");
1381
+ }
1382
+
977
1383
  async function main() {
978
1384
  const argv = process.argv.slice(2);
979
1385
 
@@ -990,6 +1396,11 @@ async function main() {
990
1396
  return;
991
1397
  }
992
1398
 
1399
+ if (command === "refresh" || command === "update") {
1400
+ await runRefresh(parseRefreshArgs(rest));
1401
+ return;
1402
+ }
1403
+
993
1404
  if (command === "profile") {
994
1405
  await runProfile(parseProfileArgs(rest));
995
1406
  return;