@sha3/code-standards 0.1.0
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/AGENTS.md +19 -0
- package/README.md +368 -0
- package/ai/adapters/codex.md +5 -0
- package/ai/adapters/copilot.md +5 -0
- package/ai/adapters/cursor.md +5 -0
- package/ai/adapters/windsurf.md +6 -0
- package/ai/constitution.md +9 -0
- package/bin/code-standards.mjs +1010 -0
- package/eslint/base.mjs +36 -0
- package/eslint/node.mjs +14 -0
- package/eslint/test.mjs +19 -0
- package/index.mjs +19 -0
- package/package.json +64 -0
- package/prettier/index.cjs +10 -0
- package/profiles/default.profile.json +38 -0
- package/profiles/schema.json +159 -0
- package/resources/ai/AGENTS.md +16 -0
- package/resources/ai/adapters/codex.md +5 -0
- package/resources/ai/adapters/copilot.md +5 -0
- package/resources/ai/adapters/cursor.md +5 -0
- package/resources/ai/adapters/windsurf.md +5 -0
- package/resources/ai/templates/adapters/codex.template.md +6 -0
- package/resources/ai/templates/adapters/copilot.template.md +6 -0
- package/resources/ai/templates/adapters/cursor.template.md +6 -0
- package/resources/ai/templates/adapters/windsurf.template.md +6 -0
- package/resources/ai/templates/agents.project.template.md +33 -0
- package/resources/ai/templates/rules/architecture.md +26 -0
- package/resources/ai/templates/rules/async.md +23 -0
- package/resources/ai/templates/rules/class-first.md +96 -0
- package/resources/ai/templates/rules/control-flow.md +19 -0
- package/resources/ai/templates/rules/errors.md +22 -0
- package/resources/ai/templates/rules/functions.md +27 -0
- package/resources/ai/templates/rules/readme.md +25 -0
- package/resources/ai/templates/rules/returns.md +33 -0
- package/resources/ai/templates/rules/testing.md +26 -0
- package/standards/architecture.md +24 -0
- package/standards/changelog-policy.md +12 -0
- package/standards/manifest.json +47 -0
- package/standards/readme.md +25 -0
- package/standards/schema.json +122 -0
- package/standards/style.md +48 -0
- package/standards/testing.md +18 -0
- package/standards/tooling.md +32 -0
- package/templates/node-lib/eslint.config.mjs +4 -0
- package/templates/node-lib/package.json +30 -0
- package/templates/node-lib/prettier.config.cjs +9 -0
- package/templates/node-lib/src/index.ts +3 -0
- package/templates/node-lib/test/smoke.test.ts +8 -0
- package/templates/node-lib/tsconfig.build.json +7 -0
- package/templates/node-lib/tsconfig.json +7 -0
- package/templates/node-service/eslint.config.mjs +4 -0
- package/templates/node-service/package.json +25 -0
- package/templates/node-service/prettier.config.cjs +9 -0
- package/templates/node-service/src/index.ts +17 -0
- package/templates/node-service/test/smoke.test.ts +37 -0
- package/templates/node-service/tsconfig.json +7 -0
- package/tsconfig/base.json +16 -0
- package/tsconfig/node-lib.json +12 -0
- package/tsconfig/node-service.json +10 -0
|
@@ -0,0 +1,1010 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { constants } from "node:fs";
|
|
5
|
+
import { access, copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import readline from "node:readline/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
11
|
+
|
|
12
|
+
const TEMPLATE_NAMES = ["node-lib", "node-service"];
|
|
13
|
+
const PROFILE_KEY_ORDER = [
|
|
14
|
+
"version",
|
|
15
|
+
"paradigm",
|
|
16
|
+
"function_size_policy",
|
|
17
|
+
"return_policy",
|
|
18
|
+
"class_design",
|
|
19
|
+
"comments_policy",
|
|
20
|
+
"testing_policy",
|
|
21
|
+
"architecture",
|
|
22
|
+
"error_handling",
|
|
23
|
+
"async_style",
|
|
24
|
+
"class_file_policy",
|
|
25
|
+
"type_contract_policy",
|
|
26
|
+
"mutability",
|
|
27
|
+
"comment_section_blocks",
|
|
28
|
+
"comment_section_format",
|
|
29
|
+
"comment_sections_required_when_empty",
|
|
30
|
+
"if_requires_braces",
|
|
31
|
+
"readme_style",
|
|
32
|
+
"rule_severity_model",
|
|
33
|
+
"language",
|
|
34
|
+
"examples_density",
|
|
35
|
+
"instruction_file_location"
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const DEFAULT_PROFILE = {
|
|
39
|
+
version: "v1",
|
|
40
|
+
paradigm: "class-first",
|
|
41
|
+
function_size_policy: "max_30_lines_soft",
|
|
42
|
+
return_policy: "single_return_strict_no_exceptions",
|
|
43
|
+
class_design: "constructor_injection",
|
|
44
|
+
comments_policy: "extensive",
|
|
45
|
+
testing_policy: "tests_required_for_behavior_change",
|
|
46
|
+
architecture: "feature_folders",
|
|
47
|
+
error_handling: "exceptions_with_typed_errors",
|
|
48
|
+
async_style: "async_await_only",
|
|
49
|
+
class_file_policy: "one_public_class_per_file",
|
|
50
|
+
type_contract_policy: "prefer_types_over_interfaces",
|
|
51
|
+
mutability: "immutability_preferred",
|
|
52
|
+
comment_section_blocks: [
|
|
53
|
+
"imports:externals",
|
|
54
|
+
"imports:internals",
|
|
55
|
+
"consts",
|
|
56
|
+
"types",
|
|
57
|
+
"private:attributes",
|
|
58
|
+
"private:properties",
|
|
59
|
+
"public:properties",
|
|
60
|
+
"constructor",
|
|
61
|
+
"static:properties",
|
|
62
|
+
"factory",
|
|
63
|
+
"private:methods",
|
|
64
|
+
"public:methods",
|
|
65
|
+
"static:methods"
|
|
66
|
+
],
|
|
67
|
+
comment_section_format: "jsdoc-section-tag",
|
|
68
|
+
comment_sections_required_when_empty: true,
|
|
69
|
+
if_requires_braces: true,
|
|
70
|
+
readme_style: "top-tier",
|
|
71
|
+
rule_severity_model: "all_blocking",
|
|
72
|
+
language: "english_technical",
|
|
73
|
+
examples_density: "rule_with_good_bad_examples",
|
|
74
|
+
instruction_file_location: "root_agents_md"
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const PROFILE_QUESTIONS = [
|
|
78
|
+
{
|
|
79
|
+
key: "paradigm",
|
|
80
|
+
prompt: "Preferred paradigm",
|
|
81
|
+
options: ["class-first", "hybrid", "functional-first"]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
key: "function_size_policy",
|
|
85
|
+
prompt: "Function size policy",
|
|
86
|
+
options: ["max_20_lines_hard", "max_30_lines_soft", "no_fixed_limit"]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: "return_policy",
|
|
90
|
+
prompt: "Return policy",
|
|
91
|
+
options: [
|
|
92
|
+
"single_return_strict_no_exceptions",
|
|
93
|
+
"single_return_with_guard_clauses",
|
|
94
|
+
"free_return_style"
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: "class_design",
|
|
99
|
+
prompt: "Class design policy",
|
|
100
|
+
options: ["constructor_injection", "internal_instantiation", "mixed"]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
key: "comments_policy",
|
|
104
|
+
prompt: "Comments policy",
|
|
105
|
+
options: ["extensive", "complex_logic_only", "minimal"]
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: "testing_policy",
|
|
109
|
+
prompt: "Testing policy",
|
|
110
|
+
options: ["tests_required_for_behavior_change", "tests_critical_only", "tests_optional"]
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
key: "architecture",
|
|
114
|
+
prompt: "Architecture style",
|
|
115
|
+
options: ["feature_folders", "layered", "simple_src_lib"]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
key: "error_handling",
|
|
119
|
+
prompt: "Error handling style",
|
|
120
|
+
options: ["exceptions_with_typed_errors", "result_either", "mixed"]
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
key: "async_style",
|
|
124
|
+
prompt: "Async style",
|
|
125
|
+
options: ["async_await_only", "promise_chains", "both"]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
key: "class_file_policy",
|
|
129
|
+
prompt: "Class/file policy",
|
|
130
|
+
options: ["one_public_class_per_file", "multiple_classes_allowed", "no_rule"]
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: "type_contract_policy",
|
|
134
|
+
prompt: "Type contract policy",
|
|
135
|
+
options: ["prefer_types_over_interfaces", "interfaces_everywhere", "interfaces_public_only"]
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
key: "mutability",
|
|
139
|
+
prompt: "Mutability policy",
|
|
140
|
+
options: ["immutability_preferred", "immutability_strict", "mutable_pragmatic"]
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
key: "rule_severity_model",
|
|
144
|
+
prompt: "Rule severity model",
|
|
145
|
+
options: ["all_blocking", "tiered", "all_preferred"]
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
key: "language",
|
|
149
|
+
prompt: "Instruction language",
|
|
150
|
+
options: ["english_technical", "spanish_technical", "bilingual"]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
key: "examples_density",
|
|
154
|
+
prompt: "Examples density",
|
|
155
|
+
options: ["rule_with_good_bad_examples", "rules_only", "rules_plus_long_templates"]
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
key: "instruction_file_location",
|
|
159
|
+
prompt: "Instruction file location",
|
|
160
|
+
options: ["root_agents_md", "ai_instructions_md", "both"]
|
|
161
|
+
}
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
function printUsage() {
|
|
165
|
+
console.log(`Usage:
|
|
166
|
+
code-standards <command> [options]
|
|
167
|
+
|
|
168
|
+
Commands:
|
|
169
|
+
init [project-name] Initialize a project from templates
|
|
170
|
+
profile Create or update the AI style profile
|
|
171
|
+
|
|
172
|
+
Init options:
|
|
173
|
+
--template <node-lib|node-service>
|
|
174
|
+
--yes
|
|
175
|
+
--target <dir>
|
|
176
|
+
--no-install
|
|
177
|
+
--force
|
|
178
|
+
--with-ai-adapters
|
|
179
|
+
--no-ai-adapters
|
|
180
|
+
--profile <path>
|
|
181
|
+
|
|
182
|
+
Profile options:
|
|
183
|
+
--profile <path>
|
|
184
|
+
--non-interactive
|
|
185
|
+
--force-profile
|
|
186
|
+
|
|
187
|
+
Global:
|
|
188
|
+
-h, --help`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function parseInitArgs(argv) {
|
|
192
|
+
const options = {
|
|
193
|
+
template: undefined,
|
|
194
|
+
yes: false,
|
|
195
|
+
target: undefined,
|
|
196
|
+
install: true,
|
|
197
|
+
force: false,
|
|
198
|
+
withAiAdapters: true,
|
|
199
|
+
projectName: undefined,
|
|
200
|
+
profilePath: undefined,
|
|
201
|
+
help: false
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
205
|
+
const token = argv[i];
|
|
206
|
+
|
|
207
|
+
if (!token.startsWith("-")) {
|
|
208
|
+
if (!options.projectName) {
|
|
209
|
+
options.projectName = token;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
throw new Error(`Unexpected positional argument: ${token}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (token === "--template") {
|
|
217
|
+
const value = argv[i + 1];
|
|
218
|
+
|
|
219
|
+
if (!value || value.startsWith("-")) {
|
|
220
|
+
throw new Error("Missing value for --template");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!TEMPLATE_NAMES.includes(value)) {
|
|
224
|
+
throw new Error(`Invalid template: ${value}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
options.template = value;
|
|
228
|
+
i += 1;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (token === "--target") {
|
|
233
|
+
const value = argv[i + 1];
|
|
234
|
+
|
|
235
|
+
if (!value || value.startsWith("-")) {
|
|
236
|
+
throw new Error("Missing value for --target");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
options.target = value;
|
|
240
|
+
i += 1;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (token === "--profile") {
|
|
245
|
+
const value = argv[i + 1];
|
|
246
|
+
|
|
247
|
+
if (!value || value.startsWith("-")) {
|
|
248
|
+
throw new Error("Missing value for --profile");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
options.profilePath = value;
|
|
252
|
+
i += 1;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (token === "--yes") {
|
|
257
|
+
options.yes = true;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (token === "--no-install") {
|
|
262
|
+
options.install = false;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (token === "--force") {
|
|
267
|
+
options.force = true;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (token === "--with-ai-adapters") {
|
|
272
|
+
options.withAiAdapters = true;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (token === "--no-ai-adapters") {
|
|
277
|
+
options.withAiAdapters = false;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (token === "-h" || token === "--help") {
|
|
282
|
+
options.help = true;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
throw new Error(`Unknown option: ${token}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return options;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function parseProfileArgs(argv) {
|
|
293
|
+
const options = {
|
|
294
|
+
profilePath: undefined,
|
|
295
|
+
nonInteractive: false,
|
|
296
|
+
forceProfile: false,
|
|
297
|
+
help: false
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
301
|
+
const token = argv[i];
|
|
302
|
+
|
|
303
|
+
if (token === "--profile") {
|
|
304
|
+
const value = argv[i + 1];
|
|
305
|
+
|
|
306
|
+
if (!value || value.startsWith("-")) {
|
|
307
|
+
throw new Error("Missing value for --profile");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
options.profilePath = value;
|
|
311
|
+
i += 1;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (token === "--non-interactive") {
|
|
316
|
+
options.nonInteractive = true;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (token === "--force-profile") {
|
|
321
|
+
options.forceProfile = true;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (token === "-h" || token === "--help") {
|
|
326
|
+
options.help = true;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
throw new Error(`Unknown option: ${token}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return options;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function sanitizePackageName(input) {
|
|
337
|
+
return (
|
|
338
|
+
input
|
|
339
|
+
.trim()
|
|
340
|
+
.toLowerCase()
|
|
341
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
342
|
+
.replace(/-+/g, "-")
|
|
343
|
+
.replace(/^-+|-+$/g, "") || "my-project"
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function replaceTokens(content, tokens) {
|
|
348
|
+
let output = content;
|
|
349
|
+
|
|
350
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
351
|
+
output = output.replaceAll(`{{${key}}}`, value);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return output;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function normalizeProfile(rawProfile) {
|
|
358
|
+
const normalized = {};
|
|
359
|
+
|
|
360
|
+
for (const key of PROFILE_KEY_ORDER) {
|
|
361
|
+
normalized[key] = rawProfile[key];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return normalized;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function readJsonFile(filePath) {
|
|
368
|
+
const raw = await readFile(filePath, "utf8");
|
|
369
|
+
return JSON.parse(raw);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function writeJsonFile(filePath, value) {
|
|
373
|
+
const dir = path.dirname(filePath);
|
|
374
|
+
await mkdir(dir, { recursive: true });
|
|
375
|
+
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function pathExists(targetPath) {
|
|
379
|
+
try {
|
|
380
|
+
await access(targetPath, constants.F_OK);
|
|
381
|
+
return true;
|
|
382
|
+
} catch {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function ensureTargetReady(targetPath, force) {
|
|
388
|
+
const exists = await pathExists(targetPath);
|
|
389
|
+
|
|
390
|
+
if (!exists) {
|
|
391
|
+
await mkdir(targetPath, { recursive: true });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const fileStat = await stat(targetPath);
|
|
396
|
+
|
|
397
|
+
if (!fileStat.isDirectory()) {
|
|
398
|
+
throw new Error(`Target exists and is not a directory: ${targetPath}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const entries = await readdir(targetPath);
|
|
402
|
+
|
|
403
|
+
if (entries.length > 0 && !force) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`Target directory is not empty: ${targetPath}. Use --force to continue and overwrite files.`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function copyTemplateDirectory(sourceDir, targetDir, tokens) {
|
|
411
|
+
await mkdir(targetDir, { recursive: true });
|
|
412
|
+
const entries = await readdir(sourceDir, { withFileTypes: true });
|
|
413
|
+
|
|
414
|
+
for (const entry of entries) {
|
|
415
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
416
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
417
|
+
|
|
418
|
+
if (entry.isDirectory()) {
|
|
419
|
+
await copyTemplateDirectory(sourcePath, targetPath, tokens);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (entry.isSymbolicLink()) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const raw = await readFile(sourcePath, "utf8");
|
|
428
|
+
const rendered = replaceTokens(raw, tokens);
|
|
429
|
+
await writeFile(targetPath, rendered, "utf8");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function resolvePackageRoot() {
|
|
434
|
+
const binPath = fileURLToPath(import.meta.url);
|
|
435
|
+
return path.resolve(path.dirname(binPath), "..");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function getBundledProfilePath(packageRoot) {
|
|
439
|
+
return path.join(packageRoot, "profiles", "default.profile.json");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function getProfileSchemaPath(packageRoot) {
|
|
443
|
+
return path.join(packageRoot, "profiles", "schema.json");
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function loadProfileSchema(packageRoot) {
|
|
447
|
+
const schemaPath = getProfileSchemaPath(packageRoot);
|
|
448
|
+
await access(schemaPath, constants.R_OK);
|
|
449
|
+
return readJsonFile(schemaPath);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function validateProfile(profile, schema, sourceLabel) {
|
|
453
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true });
|
|
454
|
+
const validate = ajv.compile(schema);
|
|
455
|
+
const valid = validate(profile);
|
|
456
|
+
|
|
457
|
+
if (valid) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const details = (validate.errors ?? [])
|
|
462
|
+
.map((issue) => `${issue.instancePath || "/"}: ${issue.message ?? "invalid"}`)
|
|
463
|
+
.join("; ");
|
|
464
|
+
|
|
465
|
+
throw new Error(`Invalid profile at ${sourceLabel}: ${details}`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function readAndValidateProfile(profilePath, schema) {
|
|
469
|
+
const profile = await readJsonFile(profilePath);
|
|
470
|
+
validateProfile(profile, schema, profilePath);
|
|
471
|
+
return normalizeProfile(profile);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function runCommand(command, args, cwd) {
|
|
475
|
+
return new Promise((resolve, reject) => {
|
|
476
|
+
const child = spawn(command, args, {
|
|
477
|
+
cwd,
|
|
478
|
+
stdio: "inherit",
|
|
479
|
+
shell: process.platform === "win32"
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
child.on("error", reject);
|
|
483
|
+
child.on("exit", (code) => {
|
|
484
|
+
if (code === 0) {
|
|
485
|
+
resolve();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
reject(new Error(`${command} ${args.join(" ")} exited with code ${code ?? "unknown"}`));
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function askChoice(rl, prompt, options, defaultValue) {
|
|
495
|
+
const defaultIndex = options.indexOf(defaultValue);
|
|
496
|
+
|
|
497
|
+
if (defaultIndex < 0) {
|
|
498
|
+
throw new Error(`Invalid default value for question ${prompt}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
console.log(`\n${prompt}`);
|
|
502
|
+
|
|
503
|
+
for (let index = 0; index < options.length; index += 1) {
|
|
504
|
+
console.log(` ${index + 1}) ${options[index]}`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const answer = await rl.question(`Select option [${defaultIndex + 1}]: `);
|
|
508
|
+
const normalized = answer.trim();
|
|
509
|
+
|
|
510
|
+
if (normalized.length === 0) {
|
|
511
|
+
return defaultValue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const numeric = Number.parseInt(normalized, 10);
|
|
515
|
+
|
|
516
|
+
if (!Number.isNaN(numeric) && numeric >= 1 && numeric <= options.length) {
|
|
517
|
+
return options[numeric - 1];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (options.includes(normalized)) {
|
|
521
|
+
return normalized;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
throw new Error(`Invalid option for ${prompt}: ${answer}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function promptYesNo(rl, prompt, defaultYes = true) {
|
|
528
|
+
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
529
|
+
const answer = (await rl.question(`${prompt} ${suffix}: `)).trim().toLowerCase();
|
|
530
|
+
|
|
531
|
+
if (answer.length === 0) {
|
|
532
|
+
return defaultYes;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (answer === "y" || answer === "yes") {
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (answer === "n" || answer === "no") {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
throw new Error(`Invalid yes/no answer: ${answer}`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function createProfileInteractively(baseProfile) {
|
|
547
|
+
const profile = { ...baseProfile };
|
|
548
|
+
const rl = readline.createInterface({
|
|
549
|
+
input: process.stdin,
|
|
550
|
+
output: process.stdout
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
for (const question of PROFILE_QUESTIONS) {
|
|
555
|
+
profile[question.key] = await askChoice(
|
|
556
|
+
rl,
|
|
557
|
+
question.prompt,
|
|
558
|
+
question.options,
|
|
559
|
+
profile[question.key]
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
} finally {
|
|
563
|
+
rl.close();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return normalizeProfile(profile);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function runProfile(rawOptions) {
|
|
570
|
+
if (rawOptions.help) {
|
|
571
|
+
printUsage();
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const packageRoot = resolvePackageRoot();
|
|
576
|
+
const schema = await loadProfileSchema(packageRoot);
|
|
577
|
+
const defaultProfilePath = getBundledProfilePath(packageRoot);
|
|
578
|
+
const outputPath = rawOptions.profilePath
|
|
579
|
+
? path.resolve(process.cwd(), rawOptions.profilePath)
|
|
580
|
+
: defaultProfilePath;
|
|
581
|
+
const shouldUseNonInteractive = rawOptions.nonInteractive || !process.stdin.isTTY;
|
|
582
|
+
const exists = await pathExists(outputPath);
|
|
583
|
+
|
|
584
|
+
if (exists && !rawOptions.forceProfile) {
|
|
585
|
+
if (shouldUseNonInteractive) {
|
|
586
|
+
throw new Error(`Profile already exists at ${outputPath}. Use --force-profile to overwrite.`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const rl = readline.createInterface({
|
|
590
|
+
input: process.stdin,
|
|
591
|
+
output: process.stdout
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
const shouldOverwrite = await promptYesNo(
|
|
596
|
+
rl,
|
|
597
|
+
`Profile already exists at ${outputPath}. Overwrite?`,
|
|
598
|
+
false
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
if (!shouldOverwrite) {
|
|
602
|
+
console.log("Profile update cancelled.");
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
} finally {
|
|
606
|
+
rl.close();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
let profile = normalizeProfile(DEFAULT_PROFILE);
|
|
611
|
+
|
|
612
|
+
if (shouldUseNonInteractive) {
|
|
613
|
+
validateProfile(profile, schema, "built-in defaults");
|
|
614
|
+
} else {
|
|
615
|
+
profile = await createProfileInteractively(profile);
|
|
616
|
+
validateProfile(profile, schema, "interactive answers");
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
await writeJsonFile(outputPath, profile);
|
|
620
|
+
console.log(`Profile written to ${outputPath}`);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function buildProfileSummary(profile) {
|
|
624
|
+
return [
|
|
625
|
+
`- Paradigm: \`${profile.paradigm}\``,
|
|
626
|
+
`- Function size policy: \`${profile.function_size_policy}\``,
|
|
627
|
+
`- Return policy: \`${profile.return_policy}\``,
|
|
628
|
+
`- Class design: \`${profile.class_design}\``,
|
|
629
|
+
`- Comments policy: \`${profile.comments_policy}\``,
|
|
630
|
+
`- Testing policy: \`${profile.testing_policy}\``,
|
|
631
|
+
`- Architecture: \`${profile.architecture}\``,
|
|
632
|
+
`- Error handling: \`${profile.error_handling}\``,
|
|
633
|
+
`- Async style: \`${profile.async_style}\``,
|
|
634
|
+
`- Class file policy: \`${profile.class_file_policy}\``,
|
|
635
|
+
`- Type contracts: \`${profile.type_contract_policy}\``,
|
|
636
|
+
`- Mutability: \`${profile.mutability}\``,
|
|
637
|
+
`- Comment section blocks: \`${profile.comment_section_blocks.join(" | ")}\``,
|
|
638
|
+
`- Comment section format: \`${profile.comment_section_format}\``,
|
|
639
|
+
`- Empty section blocks required: \`${String(profile.comment_sections_required_when_empty)}\``,
|
|
640
|
+
`- If statements require braces: \`${String(profile.if_requires_braces)}\``,
|
|
641
|
+
`- README style: \`${profile.readme_style}\``,
|
|
642
|
+
`- Rule severity: \`${profile.rule_severity_model}\``,
|
|
643
|
+
`- Language: \`${profile.language}\``,
|
|
644
|
+
`- Examples: \`${profile.examples_density}\``
|
|
645
|
+
].join("\n");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function buildAlternativeRules(profile) {
|
|
649
|
+
const codeRules = [];
|
|
650
|
+
|
|
651
|
+
if (profile.paradigm !== "class-first") {
|
|
652
|
+
codeRules.push(
|
|
653
|
+
"### Paradigm Override (MUST)\n\n" +
|
|
654
|
+
`- The active paradigm is \`${profile.paradigm}\` and MUST be followed consistently.\n` +
|
|
655
|
+
"- Any class usage MUST be explicitly justified in comments."
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (profile.return_policy !== "single_return_strict_no_exceptions") {
|
|
660
|
+
codeRules.push(
|
|
661
|
+
"### Return Policy Override (MUST)\n\n" +
|
|
662
|
+
`- The active return policy is \`${profile.return_policy}\` and MUST be respected for all new functions.`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (profile.function_size_policy !== "max_30_lines_soft") {
|
|
667
|
+
codeRules.push(
|
|
668
|
+
"### Function Size Override (MUST)\n\n" +
|
|
669
|
+
`- The active function-size policy is \`${profile.function_size_policy}\` and MUST be enforced.`
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (profile.error_handling !== "exceptions_with_typed_errors") {
|
|
674
|
+
codeRules.push(
|
|
675
|
+
"### Error Handling Override (MUST)\n\n" +
|
|
676
|
+
`- The active error-handling policy is \`${profile.error_handling}\`.`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (profile.async_style !== "async_await_only") {
|
|
681
|
+
codeRules.push(
|
|
682
|
+
"### Async Style Override (MUST)\n\n" +
|
|
683
|
+
`- The active async policy is \`${profile.async_style}\` and MUST be followed in new code.`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
let architectureRule = "";
|
|
688
|
+
if (profile.architecture !== "feature_folders") {
|
|
689
|
+
architectureRule =
|
|
690
|
+
"### Architecture Override (MUST)\n\n" +
|
|
691
|
+
`- The active architecture is \`${profile.architecture}\` and MUST take precedence.`;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
let testingRule = "";
|
|
695
|
+
if (profile.testing_policy !== "tests_required_for_behavior_change") {
|
|
696
|
+
testingRule =
|
|
697
|
+
"### Testing Override (MUST)\n\n" +
|
|
698
|
+
`- The active testing policy is \`${profile.testing_policy}\`.`;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
codeRules,
|
|
703
|
+
architectureRule,
|
|
704
|
+
testingRule
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function buildRuleSections(packageRoot, profile) {
|
|
709
|
+
const baseRulesDir = path.join(packageRoot, "resources", "ai", "templates", "rules");
|
|
710
|
+
|
|
711
|
+
const readRule = async (fileName) => {
|
|
712
|
+
const fullPath = path.join(baseRulesDir, fileName);
|
|
713
|
+
return readFile(fullPath, "utf8");
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
const classRule = await readRule("class-first.md");
|
|
717
|
+
const functionRule = await readRule("functions.md");
|
|
718
|
+
const returnRule = await readRule("returns.md");
|
|
719
|
+
const controlFlowRule = await readRule("control-flow.md");
|
|
720
|
+
const errorRule = await readRule("errors.md");
|
|
721
|
+
const asyncRule = await readRule("async.md");
|
|
722
|
+
const architectureRule = await readRule("architecture.md");
|
|
723
|
+
const testingRule = await readRule("testing.md");
|
|
724
|
+
const readmeRule = await readRule("readme.md");
|
|
725
|
+
|
|
726
|
+
const alternatives = buildAlternativeRules(profile);
|
|
727
|
+
const codeGenerationRules = [
|
|
728
|
+
classRule,
|
|
729
|
+
functionRule,
|
|
730
|
+
returnRule,
|
|
731
|
+
controlFlowRule,
|
|
732
|
+
errorRule,
|
|
733
|
+
asyncRule
|
|
734
|
+
];
|
|
735
|
+
|
|
736
|
+
if (alternatives.codeRules.length > 0) {
|
|
737
|
+
codeGenerationRules.push(...alternatives.codeRules);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return {
|
|
741
|
+
codeGenerationRules: codeGenerationRules.join("\n\n"),
|
|
742
|
+
architectureRules: alternatives.architectureRule || architectureRule,
|
|
743
|
+
testingReviewRules: `${alternatives.testingRule || testingRule}\n\n${readmeRule}`
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async function renderProjectAgents(packageRoot, targetDir, projectName, profile) {
|
|
748
|
+
const templatePath = path.join(
|
|
749
|
+
packageRoot,
|
|
750
|
+
"resources",
|
|
751
|
+
"ai",
|
|
752
|
+
"templates",
|
|
753
|
+
"agents.project.template.md"
|
|
754
|
+
);
|
|
755
|
+
const template = await readFile(templatePath, "utf8");
|
|
756
|
+
const sections = await buildRuleSections(packageRoot, profile);
|
|
757
|
+
|
|
758
|
+
const rendered = replaceTokens(template, {
|
|
759
|
+
projectName,
|
|
760
|
+
profileSummary: buildProfileSummary(profile),
|
|
761
|
+
codeGenerationRules: sections.codeGenerationRules,
|
|
762
|
+
architectureRules: sections.architectureRules,
|
|
763
|
+
testingReviewRules: sections.testingReviewRules,
|
|
764
|
+
assistantExecutionNotes:
|
|
765
|
+
"All assistant-specific adapter files in `ai/*.md` MUST remain aligned with these rules. " +
|
|
766
|
+
"Before finalizing code generation, assistants MUST run `npm run check`."
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
const agentsTarget = path.join(targetDir, "AGENTS.md");
|
|
770
|
+
await writeFile(agentsTarget, rendered, "utf8");
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function renderAdapterFiles(packageRoot, targetDir, tokens) {
|
|
774
|
+
const adaptersTemplateDir = path.join(packageRoot, "resources", "ai", "templates", "adapters");
|
|
775
|
+
const adaptersTarget = path.join(targetDir, "ai");
|
|
776
|
+
await mkdir(adaptersTarget, { recursive: true });
|
|
777
|
+
|
|
778
|
+
const entries = await readdir(adaptersTemplateDir, { withFileTypes: true });
|
|
779
|
+
|
|
780
|
+
for (const entry of entries) {
|
|
781
|
+
if (!entry.isFile() || !entry.name.endsWith(".template.md")) {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const sourcePath = path.join(adaptersTemplateDir, entry.name);
|
|
786
|
+
const outputName = entry.name.replace(/\.template\.md$/, ".md");
|
|
787
|
+
const targetPath = path.join(adaptersTarget, outputName);
|
|
788
|
+
const raw = await readFile(sourcePath, "utf8");
|
|
789
|
+
await writeFile(targetPath, replaceTokens(raw, tokens), "utf8");
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
async function generateAiInstructions(packageRoot, targetDir, tokens, profile) {
|
|
794
|
+
await renderProjectAgents(packageRoot, targetDir, tokens.projectName, profile);
|
|
795
|
+
await renderAdapterFiles(packageRoot, targetDir, tokens);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async function maybeInitializeProfileInteractively(packageRoot, profilePath) {
|
|
799
|
+
const rl = readline.createInterface({
|
|
800
|
+
input: process.stdin,
|
|
801
|
+
output: process.stdout
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
try {
|
|
805
|
+
const shouldInit = await promptYesNo(
|
|
806
|
+
rl,
|
|
807
|
+
`Profile not found at ${profilePath}. Initialize it with package defaults?`,
|
|
808
|
+
true
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
if (!shouldInit) {
|
|
812
|
+
throw new Error("Profile initialization declined by user.");
|
|
813
|
+
}
|
|
814
|
+
} finally {
|
|
815
|
+
rl.close();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
await mkdir(path.dirname(profilePath), { recursive: true });
|
|
819
|
+
await copyFile(getBundledProfilePath(packageRoot), profilePath);
|
|
820
|
+
console.log(`Profile initialized at ${profilePath}`);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async function resolveProfileForInit(packageRoot, rawOptions, schema) {
|
|
824
|
+
const bundledProfilePath = getBundledProfilePath(packageRoot);
|
|
825
|
+
|
|
826
|
+
if (!rawOptions.profilePath) {
|
|
827
|
+
const bundledExists = await pathExists(bundledProfilePath);
|
|
828
|
+
|
|
829
|
+
if (!bundledExists) {
|
|
830
|
+
if (rawOptions.yes) {
|
|
831
|
+
validateProfile(DEFAULT_PROFILE, schema, "hardcoded defaults");
|
|
832
|
+
return normalizeProfile(DEFAULT_PROFILE);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
await writeJsonFile(bundledProfilePath, normalizeProfile(DEFAULT_PROFILE));
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return readAndValidateProfile(bundledProfilePath, schema);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const requestedPath = path.resolve(process.cwd(), rawOptions.profilePath);
|
|
842
|
+
const requestedExists = await pathExists(requestedPath);
|
|
843
|
+
|
|
844
|
+
if (!requestedExists) {
|
|
845
|
+
if (rawOptions.yes) {
|
|
846
|
+
const bundledExists = await pathExists(bundledProfilePath);
|
|
847
|
+
|
|
848
|
+
if (bundledExists) {
|
|
849
|
+
return readAndValidateProfile(bundledProfilePath, schema);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
validateProfile(DEFAULT_PROFILE, schema, "hardcoded defaults");
|
|
853
|
+
return normalizeProfile(DEFAULT_PROFILE);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
await maybeInitializeProfileInteractively(packageRoot, requestedPath);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return readAndValidateProfile(requestedPath, schema);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
async function promptForMissing(options) {
|
|
863
|
+
const rl = readline.createInterface({
|
|
864
|
+
input: process.stdin,
|
|
865
|
+
output: process.stdout
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
try {
|
|
869
|
+
const resolved = { ...options };
|
|
870
|
+
|
|
871
|
+
if (!resolved.template) {
|
|
872
|
+
const templateAnswer = await rl.question(
|
|
873
|
+
"Choose template (node-lib/node-service) [node-lib]: "
|
|
874
|
+
);
|
|
875
|
+
const normalized = templateAnswer.trim() || "node-lib";
|
|
876
|
+
|
|
877
|
+
if (!TEMPLATE_NAMES.includes(normalized)) {
|
|
878
|
+
throw new Error(`Invalid template: ${normalized}`);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
resolved.template = normalized;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (!resolved.projectName) {
|
|
885
|
+
const nameAnswer = await rl.question("Project name [my-project]: ");
|
|
886
|
+
resolved.projectName = nameAnswer.trim() || "my-project";
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (!resolved.target) {
|
|
890
|
+
const targetDefault = resolved.projectName;
|
|
891
|
+
const targetAnswer = await rl.question(`Target directory [${targetDefault}]: `);
|
|
892
|
+
resolved.target = targetAnswer.trim() || targetDefault;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (options.install) {
|
|
896
|
+
const installAnswer = await rl.question("Install dependencies now? (Y/n): ");
|
|
897
|
+
const normalized = installAnswer.trim().toLowerCase();
|
|
898
|
+
resolved.install = !(normalized === "n" || normalized === "no");
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return resolved;
|
|
902
|
+
} finally {
|
|
903
|
+
rl.close();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
async function validateInitResources(packageRoot, templateName) {
|
|
908
|
+
const templateDir = path.join(packageRoot, "templates", templateName);
|
|
909
|
+
const agentsTemplatePath = path.join(
|
|
910
|
+
packageRoot,
|
|
911
|
+
"resources",
|
|
912
|
+
"ai",
|
|
913
|
+
"templates",
|
|
914
|
+
"agents.project.template.md"
|
|
915
|
+
);
|
|
916
|
+
const adaptersTemplateDir = path.join(packageRoot, "resources", "ai", "templates", "adapters");
|
|
917
|
+
|
|
918
|
+
await access(templateDir, constants.R_OK);
|
|
919
|
+
await access(agentsTemplatePath, constants.R_OK);
|
|
920
|
+
await access(adaptersTemplateDir, constants.R_OK);
|
|
921
|
+
|
|
922
|
+
return { templateDir };
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async function runInit(rawOptions) {
|
|
926
|
+
if (rawOptions.help) {
|
|
927
|
+
printUsage();
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
let options = { ...rawOptions };
|
|
932
|
+
|
|
933
|
+
if (options.yes) {
|
|
934
|
+
options.template ??= "node-lib";
|
|
935
|
+
options.projectName ??= "my-project";
|
|
936
|
+
options.target ??= options.projectName;
|
|
937
|
+
} else {
|
|
938
|
+
options = await promptForMissing(options);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const template = options.template ?? "node-lib";
|
|
942
|
+
|
|
943
|
+
if (!TEMPLATE_NAMES.includes(template)) {
|
|
944
|
+
throw new Error(`Invalid template: ${template}`);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const packageRoot = resolvePackageRoot();
|
|
948
|
+
const schema = await loadProfileSchema(packageRoot);
|
|
949
|
+
const profile = await resolveProfileForInit(packageRoot, options, schema);
|
|
950
|
+
|
|
951
|
+
const projectName = options.projectName ?? path.basename(options.target ?? "my-project");
|
|
952
|
+
const packageName = sanitizePackageName(projectName);
|
|
953
|
+
const targetPath = path.resolve(process.cwd(), options.target ?? projectName);
|
|
954
|
+
|
|
955
|
+
await ensureTargetReady(targetPath, options.force);
|
|
956
|
+
|
|
957
|
+
const { templateDir } = await validateInitResources(packageRoot, template);
|
|
958
|
+
const tokens = {
|
|
959
|
+
projectName,
|
|
960
|
+
packageName,
|
|
961
|
+
year: String(new Date().getFullYear()),
|
|
962
|
+
profileSummary: JSON.stringify(profile)
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
await copyTemplateDirectory(templateDir, targetPath, tokens);
|
|
966
|
+
|
|
967
|
+
if (options.withAiAdapters) {
|
|
968
|
+
await generateAiInstructions(packageRoot, targetPath, tokens, profile);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (options.install) {
|
|
972
|
+
console.log("Installing dependencies...");
|
|
973
|
+
await runCommand("npm", ["install"], targetPath);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
console.log(`Project created at ${targetPath}`);
|
|
977
|
+
console.log("Next steps:");
|
|
978
|
+
console.log(` cd ${targetPath}`);
|
|
979
|
+
console.log(" npm run check");
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
async function main() {
|
|
983
|
+
const argv = process.argv.slice(2);
|
|
984
|
+
|
|
985
|
+
if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
|
|
986
|
+
printUsage();
|
|
987
|
+
process.exit(0);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const [command, ...rest] = argv;
|
|
991
|
+
|
|
992
|
+
try {
|
|
993
|
+
if (command === "init") {
|
|
994
|
+
await runInit(parseInitArgs(rest));
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (command === "profile") {
|
|
999
|
+
await runProfile(parseProfileArgs(rest));
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
throw new Error(`Unknown command: ${command}`);
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1006
|
+
process.exit(1);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
main();
|