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