@sha3/code-standards 0.1.4 → 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.
package/README.md CHANGED
@@ -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
@@ -113,6 +119,7 @@ After `init`, your new repo contains:
113
119
  - `src/config.ts` for centralized hardcoded configuration values
114
120
  - `.gitignore` preconfigured for Node/TypeScript output
115
121
  - lint/format/typecheck/test-ready project template
122
+ - `package.json.codeStandards` metadata used by `refresh` (`template`, `profilePath`, `withAiAdapters`, `lastRefreshWith`)
116
123
 
117
124
  That means the next step is **not** configuring tools. The next step is telling your assistant to obey `AGENTS.md` before coding.
118
125
 
@@ -270,6 +277,22 @@ npm run check
270
277
 
271
278
  Then use the prompts above in your AI tool.
272
279
 
280
+ ### 4) Sync updates from `@sha3/code-standards`
281
+
282
+ Run this inside an already scaffolded project:
283
+
284
+ ```bash
285
+ npx @sha3/code-standards refresh
286
+ ```
287
+
288
+ `refresh` default behavior:
289
+
290
+ - scope `Managed + AI` (template files + `AGENTS.md` + `ai/*` + `ai/examples/*`)
291
+ - overwrite conflicts
292
+ - auto-detect template (or force with `--template`)
293
+ - selective merge for `package.json` (managed scripts/devDependencies updated, custom keys preserved)
294
+ - no dependency install unless `--install`
295
+
273
296
  ---
274
297
 
275
298
  ## CLI Reference
@@ -279,6 +302,8 @@ code-standards <command> [options]
279
302
 
280
303
  Commands:
281
304
  init Initialize a project in the current directory
305
+ refresh Re-apply managed standards files and AI instructions
306
+ update Alias of refresh
282
307
  profile Create or update the AI style profile
283
308
  ```
284
309
 
@@ -295,6 +320,18 @@ An existing `.git/` directory is allowed without `--force`.
295
320
  - `--no-ai-adapters`
296
321
  - `--profile <path>`
297
322
 
323
+ ### `refresh` options
324
+
325
+ `refresh` always uses the current working directory as target.
326
+
327
+ - `--template <node-lib|node-service>`
328
+ - `--profile <path>`
329
+ - `--with-ai-adapters`
330
+ - `--no-ai-adapters`
331
+ - `--dry-run`
332
+ - `--install`
333
+ - `--yes`
334
+
298
335
  ### `profile` options
299
336
 
300
337
  - `--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,196 @@ 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 (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
+
425
698
  function resolvePackageRoot() {
426
699
  const binPath = fileURLToPath(import.meta.url);
427
700
  return path.resolve(path.dirname(binPath), "..");
428
701
  }
429
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
+
430
714
  function getBundledProfilePath(packageRoot) {
431
715
  return path.join(packageRoot, "profiles", "default.profile.json");
432
716
  }
@@ -775,7 +1059,25 @@ async function maybeInitializeProfileInteractively(packageRoot, profilePath) {
775
1059
  console.log(`Profile initialized at ${profilePath}`);
776
1060
  }
777
1061
 
778
- 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) {
779
1081
  const bundledProfilePath = getBundledProfilePath(packageRoot);
780
1082
 
781
1083
  if (!rawOptions.profilePath) {
@@ -783,35 +1085,115 @@ async function resolveProfileForInit(packageRoot, rawOptions, schema) {
783
1085
 
784
1086
  if (!bundledExists) {
785
1087
  if (rawOptions.yes) {
786
- validateProfile(DEFAULT_PROFILE, schema, "hardcoded defaults");
787
- return normalizeProfile(DEFAULT_PROFILE);
1088
+ return resolveBundledOrDefaultProfile(packageRoot, schema);
788
1089
  }
789
1090
 
790
1091
  await writeJsonFile(bundledProfilePath, normalizeProfile(DEFAULT_PROFILE));
791
1092
  }
792
1093
 
793
- return readAndValidateProfile(bundledProfilePath, schema);
1094
+ return {
1095
+ profile: await readAndValidateProfile(bundledProfilePath, schema),
1096
+ profilePathForMetadata: null
1097
+ };
794
1098
  }
795
1099
 
796
- const requestedPath = path.resolve(process.cwd(), rawOptions.profilePath);
1100
+ const requestedPath = path.resolve(targetPath, rawOptions.profilePath);
797
1101
  const requestedExists = await pathExists(requestedPath);
798
1102
 
799
1103
  if (!requestedExists) {
800
1104
  if (rawOptions.yes) {
801
- const bundledExists = await pathExists(bundledProfilePath);
1105
+ return resolveBundledOrDefaultProfile(packageRoot, schema);
1106
+ }
802
1107
 
803
- if (bundledExists) {
804
- return readAndValidateProfile(bundledProfilePath, schema);
805
- }
1108
+ await maybeInitializeProfileInteractively(packageRoot, requestedPath);
1109
+ }
1110
+
1111
+ return {
1112
+ profile: await readAndValidateProfile(requestedPath, schema),
1113
+ profilePathForMetadata: getRelativeProfilePath(requestedPath, targetPath)
1114
+ };
1115
+ }
806
1116
 
807
- validateProfile(DEFAULT_PROFILE, schema, "hardcoded defaults");
808
- return normalizeProfile(DEFAULT_PROFILE);
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]);
1123
+
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);
809
1165
  }
810
1166
 
811
- await maybeInitializeProfileInteractively(packageRoot, requestedPath);
1167
+ await maybeInitializeProfileInteractively(packageRoot, selectedProfilePath);
812
1168
  }
813
1169
 
814
- return readAndValidateProfile(requestedPath, schema);
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));
1194
+ }
1195
+
1196
+ return aiFiles.sort((left, right) => left.localeCompare(right));
815
1197
  }
816
1198
 
817
1199
  async function promptForMissing(options) {
@@ -880,11 +1262,12 @@ async function runInit(rawOptions) {
880
1262
  throw new Error(`Invalid template: ${template}`);
881
1263
  }
882
1264
 
1265
+ const targetPath = path.resolve(process.cwd());
883
1266
  const packageRoot = resolvePackageRoot();
1267
+ const packageVersion = await readPackageVersion(packageRoot);
884
1268
  const schema = await loadProfileSchema(packageRoot);
885
- const profile = await resolveProfileForInit(packageRoot, options, schema);
886
-
887
- const targetPath = path.resolve(process.cwd());
1269
+ const profileResolution = await resolveProfileForInit(packageRoot, targetPath, options, schema);
1270
+ const profile = profileResolution.profile;
888
1271
  const inferredProjectName = path.basename(targetPath);
889
1272
  const projectName = inferredProjectName && inferredProjectName !== path.sep ? inferredProjectName : "my-project";
890
1273
  const packageName = sanitizePackageName(projectName);
@@ -910,11 +1293,93 @@ async function runInit(rawOptions) {
910
1293
  await runCommand("npm", ["install"], targetPath);
911
1294
  }
912
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
+
913
1305
  console.log(`Project created at ${targetPath}`);
914
1306
  console.log("Next steps:");
915
1307
  console.log(" npm run check");
916
1308
  }
917
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
+
918
1383
  async function main() {
919
1384
  const argv = process.argv.slice(2);
920
1385
 
@@ -931,6 +1396,11 @@ async function main() {
931
1396
  return;
932
1397
  }
933
1398
 
1399
+ if (command === "refresh" || command === "update") {
1400
+ await runRefresh(parseRefreshArgs(rest));
1401
+ return;
1402
+ }
1403
+
934
1404
  if (command === "profile") {
935
1405
  await runProfile(parseProfileArgs(rest));
936
1406
  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.5",
4
4
  "description": "AI-first code standards, tooling exports, and project initializer",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
 
@@ -18,6 +18,7 @@ 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`).
22
23
 
23
24
  ## Naming