@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.
- package/README.md +42 -0
- package/bin/code-standards.mjs +507 -96
- package/eslint/test.mjs +1 -5
- package/package.json +1 -1
- package/prettier/index.cjs +1 -1
- package/profiles/default.profile.json +2 -0
- package/profiles/schema.json +7 -7
- package/resources/ai/templates/examples/demo/src/billing/billing-service.ts +18 -3
- package/resources/ai/templates/examples/demo/src/config.ts +3 -0
- package/resources/ai/templates/examples/demo/src/invoices/invoice-errors.ts +12 -0
- package/resources/ai/templates/examples/demo/src/invoices/invoice-service.ts +14 -1
- package/resources/ai/templates/examples/rules/async-bad.ts +12 -0
- package/resources/ai/templates/examples/rules/async-good.ts +12 -0
- package/resources/ai/templates/examples/rules/class-first-bad.ts +12 -0
- package/resources/ai/templates/examples/rules/class-first-good.ts +12 -0
- package/resources/ai/templates/examples/rules/constructor-bad.ts +12 -0
- package/resources/ai/templates/examples/rules/constructor-good.ts +12 -0
- package/resources/ai/templates/examples/rules/control-flow-bad.ts +12 -0
- package/resources/ai/templates/examples/rules/control-flow-good.ts +12 -0
- package/resources/ai/templates/examples/rules/errors-bad.ts +12 -0
- package/resources/ai/templates/examples/rules/errors-good.ts +12 -0
- package/resources/ai/templates/examples/rules/functions-bad.ts +12 -0
- package/resources/ai/templates/examples/rules/functions-good.ts +12 -0
- package/resources/ai/templates/examples/rules/returns-bad.ts +12 -0
- package/resources/ai/templates/examples/rules/returns-good.ts +12 -0
- package/resources/ai/templates/examples/rules/testing-bad.ts +12 -0
- package/resources/ai/templates/examples/rules/testing-good.ts +12 -0
- package/resources/ai/templates/rules/architecture.md +2 -0
- package/resources/ai/templates/rules/class-first.md +2 -0
- package/resources/ai/templates/rules/functions.md +2 -0
- package/standards/architecture.md +2 -1
- package/standards/manifest.json +1 -1
- package/standards/schema.json +2 -11
- package/standards/style.md +13 -9
- package/templates/node-lib/src/config.ts +1 -0
- package/templates/node-lib/src/index.ts +3 -1
- package/templates/node-service/src/config.ts +3 -0
- package/templates/node-service/src/index.ts +4 -3
- package/templates/node-service/test/smoke.test.ts +1 -1
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",
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
1094
|
+
return {
|
|
1095
|
+
profile: await readAndValidateProfile(bundledProfilePath, schema),
|
|
1096
|
+
profilePathForMetadata: null
|
|
1097
|
+
};
|
|
844
1098
|
}
|
|
845
1099
|
|
|
846
|
-
const requestedPath = path.resolve(
|
|
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
|
-
|
|
1105
|
+
return resolveBundledOrDefaultProfile(packageRoot, schema);
|
|
1106
|
+
}
|
|
852
1107
|
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
858
|
-
|
|
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,
|
|
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
|
|
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
|
|
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;
|