@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 +37 -0
- package/bin/code-standards.mjs +486 -16
- package/package.json +1 -1
- package/resources/ai/templates/rules/functions.md +1 -0
- package/standards/style.md +1 -0
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>`
|
package/bin/code-standards.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
1094
|
+
return {
|
|
1095
|
+
profile: await readAndValidateProfile(bundledProfilePath, schema),
|
|
1096
|
+
profilePathForMetadata: null
|
|
1097
|
+
};
|
|
794
1098
|
}
|
|
795
1099
|
|
|
796
|
-
const requestedPath = path.resolve(
|
|
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
|
-
|
|
1105
|
+
return resolveBundledOrDefaultProfile(packageRoot, schema);
|
|
1106
|
+
}
|
|
802
1107
|
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
808
|
-
|
|
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,
|
|
1167
|
+
await maybeInitializeProfileInteractively(packageRoot, selectedProfilePath);
|
|
812
1168
|
}
|
|
813
1169
|
|
|
814
|
-
return
|
|
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
|
|
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
|
@@ -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
|
|
package/standards/style.md
CHANGED
|
@@ -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
|