ai-ide-config 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/README.md +117 -0
- package/bin/cli.mjs +186 -0
- package/package.json +34 -0
- package/src/init.mjs +195 -0
- package/src/package-manager.mjs +95 -0
- package/src/paths.mjs +20 -0
- package/src/prompt.mjs +49 -0
- package/src/skills.mjs +46 -0
- package/src/stacks.mjs +20 -0
- package/templates/angular/.cursor/rules/angular-20.mdc +135 -0
- package/templates/angular/AGENTS.md +50 -0
- package/templates/angular/pnpm-workspace.yaml +5 -0
- package/templates/shared/.cursor/commands/generate-pr-description.md +70 -0
- package/templates/shared/.cursor/rules/conventional-commits.mdc +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# ai-ide-config
|
|
2
|
+
|
|
3
|
+
Scaffold shared Cursor IDE / agent setup into a project: shared `.cursor` rules/commands, plus stack-specific `AGENTS.md` and stack rules when needed.
|
|
4
|
+
|
|
5
|
+
## Install / run
|
|
6
|
+
|
|
7
|
+
No global install required:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx ai-ide-config init
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
From a local checkout:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd ai-ide-config
|
|
17
|
+
npm link
|
|
18
|
+
cd /path/to/your-project
|
|
19
|
+
npx ai-ide-config init
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Interactive: pick a stack
|
|
26
|
+
npx ai-ide-config init
|
|
27
|
+
|
|
28
|
+
# Non-interactive
|
|
29
|
+
npx ai-ide-config init --stack angular
|
|
30
|
+
npx ai-ide-config init-angular
|
|
31
|
+
|
|
32
|
+
# Options
|
|
33
|
+
npx ai-ide-config init --force # overwrite existing files
|
|
34
|
+
npx ai-ide-config init --dry-run # preview only
|
|
35
|
+
npx ai-ide-config init --skip-skills # skip angular/skills install
|
|
36
|
+
npx ai-ide-config init ./apps/web # target directory
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Existing template files are **skipped** unless you pass `--force`.
|
|
40
|
+
|
|
41
|
+
`.gitignore` is handled specially:
|
|
42
|
+
|
|
43
|
+
- If missing → create it with `.cursor/` and `.agents/`
|
|
44
|
+
- If present → append `.cursor/` and/or `.agents/` only when missing
|
|
45
|
+
|
|
46
|
+
If `package.json` exists, init pins an **exact** `packageManager` / `devEngines.packageManager` to the installed pnpm version. That avoids Corepack failing on ranges like `^11.13.0` from `pnpm init`.
|
|
47
|
+
|
|
48
|
+
After scaffolding, the CLI runs:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pnpx skills add https://github.com/angular/skills
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
(`pnpx` is used instead of `npx` because the skills package requires pnpm via `devEngines`.)
|
|
55
|
+
|
|
56
|
+
Use `--skip-skills` to skip that step. On `--dry-run`, the command is only printed.
|
|
57
|
+
|
|
58
|
+
In CI or non-TTY shells, pass `--stack` (or use `init-<stack>`). Interactive prompts require a terminal.
|
|
59
|
+
|
|
60
|
+
## What gets written
|
|
61
|
+
|
|
62
|
+
| Path | Source |
|
|
63
|
+
|------|--------|
|
|
64
|
+
| `.cursor/rules/conventional-commits.mdc` | shared |
|
|
65
|
+
| `.cursor/commands/generate-pr-description.md` | shared |
|
|
66
|
+
| `.cursor/rules/angular-20.mdc` | angular ([angular.dev](https://angular.dev/assets/context/angular-20.mdc)) |
|
|
67
|
+
| `AGENTS.md` | angular |
|
|
68
|
+
| `pnpm-workspace.yaml` | angular (`blockExoticSubdeps`, `minimumReleaseAge: 4320`) |
|
|
69
|
+
| `.gitignore` | create or append `.cursor/` and `.agents/` |
|
|
70
|
+
|
|
71
|
+
After init, edit `AGENTS.md` placeholders (`<PROJECT_NAME>`, package manager, domains, paths) for the target repo.
|
|
72
|
+
|
|
73
|
+
## Stacks
|
|
74
|
+
|
|
75
|
+
| Id | Label |
|
|
76
|
+
|----|-------|
|
|
77
|
+
| `angular` | Angular |
|
|
78
|
+
|
|
79
|
+
### Adding a stack
|
|
80
|
+
|
|
81
|
+
1. Add `templates/<stack>/AGENTS.md`
|
|
82
|
+
2. Register it in [`src/stacks.mjs`](src/stacks.mjs)
|
|
83
|
+
3. Document it here
|
|
84
|
+
|
|
85
|
+
Shared Cursor rules/commands stay under `templates/shared/`.
|
|
86
|
+
|
|
87
|
+
## Publishing to npm
|
|
88
|
+
|
|
89
|
+
CD publishes to npm when you create a **GitHub Release** (workflow: [`.github/workflows/publish.yml`](.github/workflows/publish.yml)).
|
|
90
|
+
|
|
91
|
+
### One-time setup
|
|
92
|
+
|
|
93
|
+
1. Create an [npmjs.com](https://www.npmjs.com) account and verify your email.
|
|
94
|
+
2. Create the package on npm (first publish can also create it), or claim the name `ai-ide-config`.
|
|
95
|
+
3. In the package settings on npm → **Trusted Publisher**:
|
|
96
|
+
- **Organization or user:** `iraldoad`
|
|
97
|
+
- **Repository:** `ai-ide-config`
|
|
98
|
+
- **Workflow filename:** `publish.yml`
|
|
99
|
+
- Allow **npm publish**
|
|
100
|
+
4. No `NPM_TOKEN` secret is required (OIDC Trusted Publishing).
|
|
101
|
+
|
|
102
|
+
### Release a version
|
|
103
|
+
|
|
104
|
+
1. Bump `version` in `package.json` (e.g. `0.1.1`).
|
|
105
|
+
2. Commit and push.
|
|
106
|
+
3. Create a GitHub Release for that commit (e.g. tag `v0.1.1`).
|
|
107
|
+
4. The workflow runs `npm publish --access public --provenance`.
|
|
108
|
+
|
|
109
|
+
After the first successful publish:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npx ai-ide-config init
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getStack, listStacks, stackIds } from '../src/stacks.mjs';
|
|
5
|
+
import { promptForStack } from '../src/prompt.mjs';
|
|
6
|
+
import { initStack, printReport } from '../src/init.mjs';
|
|
7
|
+
import { runSkillsAdd, SKILLS_ADD_COMMAND } from '../src/skills.mjs';
|
|
8
|
+
|
|
9
|
+
function printUsage() {
|
|
10
|
+
const stacks = listStacks()
|
|
11
|
+
.map((s) => ` init-${s.id}`.padEnd(18) + `# shortcut for init --stack ${s.id}`)
|
|
12
|
+
.join('\n');
|
|
13
|
+
|
|
14
|
+
console.log(`Usage:
|
|
15
|
+
ai-ide-config init [dir] [--stack <name>] [--force] [--dry-run] [--skip-skills]
|
|
16
|
+
ai-ide-config init-<stack> [dir] [--force] [--dry-run] [--skip-skills]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--stack <name> Skip interactive prompt (e.g. angular)
|
|
20
|
+
--force Overwrite existing files
|
|
21
|
+
--dry-run Show what would be written
|
|
22
|
+
--skip-skills Do not run: ${SKILLS_ADD_COMMAND}
|
|
23
|
+
|
|
24
|
+
Stacks:
|
|
25
|
+
${listStacks()
|
|
26
|
+
.map((s) => ` ${s.id.padEnd(12)} ${s.label}`)
|
|
27
|
+
.join('\n')}
|
|
28
|
+
|
|
29
|
+
Shortcuts:
|
|
30
|
+
${stacks}
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string[]} argv
|
|
36
|
+
*/
|
|
37
|
+
function parseArgs(argv) {
|
|
38
|
+
const args = [...argv];
|
|
39
|
+
/** @type {{ command: string | null, targetDir: string, stackId: string | null, force: boolean, dryRun: boolean, skipSkills: boolean, help: boolean }} */
|
|
40
|
+
const result = {
|
|
41
|
+
command: null,
|
|
42
|
+
targetDir: process.cwd(),
|
|
43
|
+
stackId: null,
|
|
44
|
+
force: false,
|
|
45
|
+
dryRun: false,
|
|
46
|
+
skipSkills: false,
|
|
47
|
+
help: false,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const positionals = [];
|
|
51
|
+
|
|
52
|
+
while (args.length > 0) {
|
|
53
|
+
const token = args.shift();
|
|
54
|
+
|
|
55
|
+
if (token === '--help' || token === '-h') {
|
|
56
|
+
result.help = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (token === '--force') {
|
|
60
|
+
result.force = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (token === '--dry-run') {
|
|
64
|
+
result.dryRun = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (token === '--skip-skills') {
|
|
68
|
+
result.skipSkills = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (token === '--stack') {
|
|
72
|
+
const value = args.shift();
|
|
73
|
+
if (!value) {
|
|
74
|
+
throw new Error('--stack requires a value');
|
|
75
|
+
}
|
|
76
|
+
result.stackId = value;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (token.startsWith('-')) {
|
|
80
|
+
throw new Error(`Unknown option: ${token}`);
|
|
81
|
+
}
|
|
82
|
+
positionals.push(token);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
result.command = positionals[0] ?? null;
|
|
86
|
+
if (positionals[1]) {
|
|
87
|
+
result.targetDir = path.resolve(positionals[1]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve stack id from command name like init-angular.
|
|
95
|
+
* @param {string} command
|
|
96
|
+
* @returns {string | null}
|
|
97
|
+
*/
|
|
98
|
+
function stackFromInitAlias(command) {
|
|
99
|
+
const match = /^init-(.+)$/.exec(command);
|
|
100
|
+
if (!match) return null;
|
|
101
|
+
return match[1];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function main() {
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = parseArgs(process.argv.slice(2));
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error(err.message);
|
|
110
|
+
printUsage();
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (parsed.help || !parsed.command) {
|
|
116
|
+
printUsage();
|
|
117
|
+
process.exitCode = parsed.help ? 0 : 1;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let stackId = parsed.stackId;
|
|
122
|
+
|
|
123
|
+
if (parsed.command === 'init') {
|
|
124
|
+
// stackId may already be set via --stack
|
|
125
|
+
} else {
|
|
126
|
+
const aliasStack = stackFromInitAlias(parsed.command);
|
|
127
|
+
if (aliasStack) {
|
|
128
|
+
if (stackId && stackId !== aliasStack) {
|
|
129
|
+
console.error(
|
|
130
|
+
`Conflicting stacks: command is init-${aliasStack} but --stack ${stackId} was passed.`,
|
|
131
|
+
);
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
stackId = aliasStack;
|
|
136
|
+
} else {
|
|
137
|
+
console.error(`Unknown command: ${parsed.command}`);
|
|
138
|
+
printUsage();
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!stackId) {
|
|
145
|
+
try {
|
|
146
|
+
stackId = await promptForStack();
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error(err.message);
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const stack = getStack(stackId);
|
|
155
|
+
if (!stack) {
|
|
156
|
+
console.error(
|
|
157
|
+
`Unknown stack "${stackId}". Available: ${stackIds().join(', ')}`,
|
|
158
|
+
);
|
|
159
|
+
process.exitCode = 1;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const report = initStack({
|
|
165
|
+
stack,
|
|
166
|
+
targetDir: parsed.targetDir,
|
|
167
|
+
force: parsed.force,
|
|
168
|
+
dryRun: parsed.dryRun,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
console.log(
|
|
172
|
+
`${parsed.dryRun ? '[dry-run] ' : ''}Scaffolding ${stack.label} AI IDE config into ${parsed.targetDir}`,
|
|
173
|
+
);
|
|
174
|
+
printReport(report, { dryRun: parsed.dryRun });
|
|
175
|
+
|
|
176
|
+
runSkillsAdd(parsed.targetDir, {
|
|
177
|
+
dryRun: parsed.dryRun,
|
|
178
|
+
skip: parsed.skipSkills,
|
|
179
|
+
});
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.error(err.message);
|
|
182
|
+
process.exitCode = 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-ide-config",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold shared Cursor IDE / agent config (rules, commands, stack AGENTS.md)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ai-ide-config": "./bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"templates",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/iraldoad/ai-ide-config.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/iraldoad/ai-ide-config/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/iraldoad/ai-ide-config#readme",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"cursor",
|
|
28
|
+
"agents",
|
|
29
|
+
"ai",
|
|
30
|
+
"ide",
|
|
31
|
+
"angular"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT"
|
|
34
|
+
}
|
package/src/init.mjs
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { sharedTemplatesDir, stackTemplatesDir } from './paths.mjs';
|
|
4
|
+
import { ensureExactPnpmEngines } from './package-manager.mjs';
|
|
5
|
+
|
|
6
|
+
const GITIGNORE = '.gitignore';
|
|
7
|
+
const GITIGNORE_ENTRIES = ['.cursor/', '.agents/'];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Recursively collect relative file paths under dir.
|
|
11
|
+
* @param {string} dir
|
|
12
|
+
* @param {string} [base]
|
|
13
|
+
* @returns {string[]}
|
|
14
|
+
*/
|
|
15
|
+
function listFiles(dir, base = dir) {
|
|
16
|
+
if (!fs.existsSync(dir)) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
21
|
+
const files = [];
|
|
22
|
+
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const absolute = path.join(dir, entry.name);
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
files.push(...listFiles(absolute, base));
|
|
27
|
+
} else if (entry.isFile()) {
|
|
28
|
+
files.push(path.relative(base, absolute));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return files;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {{ created: string[], skipped: string[], overwritten: string[], updated: string[] }} CopyReport
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether .gitignore already ignores a directory entry (with or without trailing slash).
|
|
41
|
+
* @param {string} content
|
|
42
|
+
* @param {string} entry e.g. ".cursor/" or ".agents/"
|
|
43
|
+
*/
|
|
44
|
+
function hasIgnoreEntry(content, entry) {
|
|
45
|
+
const name = entry.replace(/\/$/, '').replace(/^\./, '\\.');
|
|
46
|
+
return new RegExp(`(?:^|\\n)\\s*${name}\\/?\\s*(?:\\n|$)`).test(content);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create .gitignore with required entries, or append any that are missing.
|
|
51
|
+
* @param {string} targetDir
|
|
52
|
+
* @param {{ dryRun?: boolean }} options
|
|
53
|
+
* @param {CopyReport} report
|
|
54
|
+
*/
|
|
55
|
+
function ensureGitignore(targetDir, options, report) {
|
|
56
|
+
const filePath = path.join(targetDir, GITIGNORE);
|
|
57
|
+
const exists = fs.existsSync(filePath);
|
|
58
|
+
|
|
59
|
+
if (!exists) {
|
|
60
|
+
if (options.dryRun) {
|
|
61
|
+
report.created.push(`${GITIGNORE} (${GITIGNORE_ENTRIES.join(', ')})`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
fs.writeFileSync(filePath, `${GITIGNORE_ENTRIES.join('\n')}\n`, 'utf8');
|
|
65
|
+
report.created.push(GITIGNORE);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
70
|
+
const missing = GITIGNORE_ENTRIES.filter(
|
|
71
|
+
(entry) => !hasIgnoreEntry(content, entry),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (missing.length === 0) {
|
|
75
|
+
report.skipped.push(
|
|
76
|
+
`${GITIGNORE} (${GITIGNORE_ENTRIES.join(', ')} already listed)`,
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (options.dryRun) {
|
|
82
|
+
report.updated.push(`${GITIGNORE} (append ${missing.join(', ')})`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const separator = content.length === 0 || content.endsWith('\n') ? '' : '\n';
|
|
87
|
+
fs.writeFileSync(
|
|
88
|
+
filePath,
|
|
89
|
+
`${content}${separator}${missing.join('\n')}\n`,
|
|
90
|
+
'utf8',
|
|
91
|
+
);
|
|
92
|
+
report.updated.push(`${GITIGNORE} (appended ${missing.join(', ')})`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Copy all files from sourceRoot into targetDir.
|
|
97
|
+
* @param {string} sourceRoot
|
|
98
|
+
* @param {string} targetDir
|
|
99
|
+
* @param {{ force?: boolean, dryRun?: boolean }} options
|
|
100
|
+
* @param {CopyReport} report
|
|
101
|
+
*/
|
|
102
|
+
function copyTree(sourceRoot, targetDir, options, report) {
|
|
103
|
+
const relativeFiles = listFiles(sourceRoot);
|
|
104
|
+
|
|
105
|
+
for (const relative of relativeFiles) {
|
|
106
|
+
const from = path.join(sourceRoot, relative);
|
|
107
|
+
const to = path.join(targetDir, relative);
|
|
108
|
+
const exists = fs.existsSync(to);
|
|
109
|
+
|
|
110
|
+
if (exists && !options.force) {
|
|
111
|
+
report.skipped.push(relative);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (options.dryRun) {
|
|
116
|
+
if (exists) {
|
|
117
|
+
report.overwritten.push(relative);
|
|
118
|
+
} else {
|
|
119
|
+
report.created.push(relative);
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
125
|
+
fs.copyFileSync(from, to);
|
|
126
|
+
|
|
127
|
+
if (exists) {
|
|
128
|
+
report.overwritten.push(relative);
|
|
129
|
+
} else {
|
|
130
|
+
report.created.push(relative);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Scaffold shared .cursor config + stack templates into targetDir.
|
|
137
|
+
* @param {{ stack: { id: string, agentsDir: string }, targetDir: string, force?: boolean, dryRun?: boolean }} opts
|
|
138
|
+
* @returns {CopyReport}
|
|
139
|
+
*/
|
|
140
|
+
export function initStack(opts) {
|
|
141
|
+
const { stack, targetDir, force = false, dryRun = false } = opts;
|
|
142
|
+
const options = { force, dryRun };
|
|
143
|
+
/** @type {CopyReport} */
|
|
144
|
+
const report = {
|
|
145
|
+
created: [],
|
|
146
|
+
skipped: [],
|
|
147
|
+
overwritten: [],
|
|
148
|
+
updated: [],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const shared = sharedTemplatesDir();
|
|
152
|
+
const stackDir = stackTemplatesDir(stack.agentsDir);
|
|
153
|
+
|
|
154
|
+
if (!fs.existsSync(shared)) {
|
|
155
|
+
throw new Error(`Shared templates missing: ${shared}`);
|
|
156
|
+
}
|
|
157
|
+
if (!fs.existsSync(stackDir)) {
|
|
158
|
+
throw new Error(`Stack templates missing for "${stack.id}": ${stackDir}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
copyTree(shared, targetDir, options, report);
|
|
162
|
+
copyTree(stackDir, targetDir, options, report);
|
|
163
|
+
ensureGitignore(targetDir, options, report);
|
|
164
|
+
ensureExactPnpmEngines(targetDir, options, report);
|
|
165
|
+
|
|
166
|
+
return report;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function printReport(report, { dryRun = false } = {}) {
|
|
170
|
+
const prefix = dryRun ? '[dry-run] ' : '';
|
|
171
|
+
|
|
172
|
+
const sections = [
|
|
173
|
+
['Created', report.created],
|
|
174
|
+
['Updated', report.updated],
|
|
175
|
+
['Overwritten', report.overwritten],
|
|
176
|
+
['Skipped (already exists)', report.skipped],
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
for (const [label, files] of sections) {
|
|
180
|
+
if (!files || files.length === 0) continue;
|
|
181
|
+
console.log(`${prefix}${label}:`);
|
|
182
|
+
for (const file of files) {
|
|
183
|
+
console.log(` ${file}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (
|
|
188
|
+
report.created.length === 0 &&
|
|
189
|
+
report.overwritten.length === 0 &&
|
|
190
|
+
report.skipped.length === 0 &&
|
|
191
|
+
(report.updated?.length ?? 0) === 0
|
|
192
|
+
) {
|
|
193
|
+
console.log(`${prefix}Nothing to do.`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve pnpm version without using the target project as cwd.
|
|
8
|
+
* Running inside a project with `devEngines.packageManager.version: "^…"`
|
|
9
|
+
* makes Corepack/pnpm refuse to start.
|
|
10
|
+
* @returns {string | null} installed pnpm version (e.g. "11.13.0")
|
|
11
|
+
*/
|
|
12
|
+
export function detectPnpmVersion() {
|
|
13
|
+
const result = spawnSync('pnpm --version', {
|
|
14
|
+
cwd: os.tmpdir(),
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
shell: true,
|
|
17
|
+
env: process.env,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (result.status !== 0) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const version = (result.stdout ?? '').trim().split(/\s+/)[0];
|
|
25
|
+
return /^\d+\.\d+\.\d+/.test(version) ? version : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* True if version is not an exact x.y.z (e.g. ^11.13.0, >=11).
|
|
30
|
+
* @param {string | undefined} version
|
|
31
|
+
*/
|
|
32
|
+
function isRangeOrInvalid(version) {
|
|
33
|
+
if (!version || typeof version !== 'string') return true;
|
|
34
|
+
return !/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version.trim());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Pin packageManager / devEngines to an exact pnpm version so Corepack/pnpm
|
|
39
|
+
* do not fail on ranges like "^11.13.0" (from `pnpm init`).
|
|
40
|
+
*
|
|
41
|
+
* @param {string} targetDir
|
|
42
|
+
* @param {{ dryRun?: boolean }} options
|
|
43
|
+
* @param {{ created: string[], updated: string[], skipped: string[] }} report
|
|
44
|
+
*/
|
|
45
|
+
export function ensureExactPnpmEngines(targetDir, options, report) {
|
|
46
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
47
|
+
|
|
48
|
+
if (!fs.existsSync(pkgPath)) {
|
|
49
|
+
report.skipped.push('package.json (missing — skipped packageManager pin)');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const version = detectPnpmVersion();
|
|
54
|
+
if (!version) {
|
|
55
|
+
report.skipped.push('package.json (pnpm not found — skipped packageManager pin)');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
60
|
+
const expectedPackageManager = `pnpm@${version}`;
|
|
61
|
+
const currentDevVersion = pkg.devEngines?.packageManager?.version;
|
|
62
|
+
const needsPackageManager = pkg.packageManager !== expectedPackageManager;
|
|
63
|
+
const needsDevEngines =
|
|
64
|
+
!pkg.devEngines?.packageManager ||
|
|
65
|
+
pkg.devEngines.packageManager.name !== 'pnpm' ||
|
|
66
|
+
isRangeOrInvalid(currentDevVersion) ||
|
|
67
|
+
currentDevVersion !== version;
|
|
68
|
+
|
|
69
|
+
if (!needsPackageManager && !needsDevEngines) {
|
|
70
|
+
report.skipped.push('package.json (packageManager already exact)');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (options.dryRun) {
|
|
75
|
+
report.updated.push(
|
|
76
|
+
`package.json (pin packageManager to ${expectedPackageManager})`,
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pkg.packageManager = expectedPackageManager;
|
|
82
|
+
pkg.devEngines = {
|
|
83
|
+
...pkg.devEngines,
|
|
84
|
+
packageManager: {
|
|
85
|
+
name: 'pnpm',
|
|
86
|
+
version,
|
|
87
|
+
onFail: 'download',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
92
|
+
report.updated.push(
|
|
93
|
+
`package.json (pinned packageManager to ${expectedPackageManager})`,
|
|
94
|
+
);
|
|
95
|
+
}
|
package/src/paths.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
|
|
6
|
+
export function packageRoot() {
|
|
7
|
+
return path.resolve(__dirname, '..');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function templatesRoot() {
|
|
11
|
+
return path.join(packageRoot(), 'templates');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function sharedTemplatesDir() {
|
|
15
|
+
return path.join(templatesRoot(), 'shared');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function stackTemplatesDir(agentsDir) {
|
|
19
|
+
return path.join(templatesRoot(), agentsDir);
|
|
20
|
+
}
|
package/src/prompt.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
3
|
+
import { listStacks } from './stacks.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Interactively pick a stack. Throws if stdin is not a TTY.
|
|
7
|
+
* @returns {Promise<string>} stack id
|
|
8
|
+
*/
|
|
9
|
+
export async function promptForStack() {
|
|
10
|
+
const stacks = listStacks();
|
|
11
|
+
|
|
12
|
+
if (!input.isTTY || !output.isTTY) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
'Non-interactive terminal: pass --stack <name> (e.g. --stack angular).',
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const rl = readline.createInterface({ input, output });
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
output.write('Select stack:\n');
|
|
22
|
+
for (let i = 0; i < stacks.length; i++) {
|
|
23
|
+
output.write(` ${i + 1}) ${stacks[i].label} (${stacks[i].id})\n`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
while (true) {
|
|
27
|
+
const answer = (await rl.question(`Enter number [1-${stacks.length}]: `)).trim();
|
|
28
|
+
|
|
29
|
+
// Empty Enter → default to first stack when only one option.
|
|
30
|
+
if (answer === '' && stacks.length === 1) {
|
|
31
|
+
return stacks[0].id;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const index = Number.parseInt(answer, 10);
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
Number.isInteger(index) &&
|
|
38
|
+
index >= 1 &&
|
|
39
|
+
index <= stacks.length
|
|
40
|
+
) {
|
|
41
|
+
return stacks[index - 1].id;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
output.write(`Invalid choice. Enter a number between 1 and ${stacks.length}.\n`);
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
rl.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/skills.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export const SKILLS_ADD_COMMAND =
|
|
4
|
+
'pnpx skills add https://github.com/angular/skills';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Install Angular agent skills via the skills CLI (pnpm/pnpx required by devEngines).
|
|
8
|
+
* @param {string} targetDir
|
|
9
|
+
* @param {{ dryRun?: boolean, skip?: boolean }} [options]
|
|
10
|
+
* @returns {{ skipped: boolean, dryRun?: boolean, status?: number | null }}
|
|
11
|
+
*/
|
|
12
|
+
export function runSkillsAdd(targetDir, options = {}) {
|
|
13
|
+
const { dryRun = false, skip = false } = options;
|
|
14
|
+
|
|
15
|
+
if (skip) {
|
|
16
|
+
console.log('Skipping skills install (--skip-skills).');
|
|
17
|
+
return { skipped: true };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (dryRun) {
|
|
21
|
+
console.log(`[dry-run] Would run: ${SKILLS_ADD_COMMAND}`);
|
|
22
|
+
return { skipped: false, dryRun: true };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(`Running: ${SKILLS_ADD_COMMAND}`);
|
|
26
|
+
|
|
27
|
+
// Single command string + shell avoids DEP0190 (args + shell:true).
|
|
28
|
+
const result = spawnSync(SKILLS_ADD_COMMAND, {
|
|
29
|
+
cwd: targetDir,
|
|
30
|
+
stdio: 'inherit',
|
|
31
|
+
shell: true,
|
|
32
|
+
env: process.env,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (result.error) {
|
|
36
|
+
throw result.error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (result.status !== 0) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`${SKILLS_ADD_COMMAND} failed with exit code ${result.status ?? 'unknown'}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { skipped: false, status: result.status };
|
|
46
|
+
}
|
package/src/stacks.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Known stacks. Add an entry + templates/<id>/AGENTS.md for each new stack. */
|
|
2
|
+
export const STACKS = {
|
|
3
|
+
angular: {
|
|
4
|
+
id: 'angular',
|
|
5
|
+
label: 'Angular',
|
|
6
|
+
agentsDir: 'angular',
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function listStacks() {
|
|
11
|
+
return Object.values(STACKS);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getStack(id) {
|
|
15
|
+
return STACKS[id] ?? null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function stackIds() {
|
|
19
|
+
return Object.keys(STACKS);
|
|
20
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: This rule provides comprehensive best practices and coding standards for Angular development, focusing on modern TypeScript, standalone components, signals, and performance optimizations.
|
|
3
|
+
globs: ["**/*.{ts,html,scss,css}"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Angular Best Practices
|
|
7
|
+
|
|
8
|
+
This project adheres to modern Angular best practices, emphasizing maintainability, performance, accessibility, and scalability.
|
|
9
|
+
|
|
10
|
+
## TypeScript Best Practices
|
|
11
|
+
|
|
12
|
+
* **Strict Type Checking:** Always enable and adhere to strict type checking. This helps catch errors early and improves code quality.
|
|
13
|
+
* **Prefer Type Inference:** Allow TypeScript to infer types when they are obvious from the context. This reduces verbosity while maintaining type safety.
|
|
14
|
+
* **Bad:**
|
|
15
|
+
```typescript
|
|
16
|
+
let name: string = 'Angular';
|
|
17
|
+
```
|
|
18
|
+
* **Good:**
|
|
19
|
+
```typescript
|
|
20
|
+
let name = 'Angular';
|
|
21
|
+
```
|
|
22
|
+
* **Avoid `any`:** Do not use the `any` type unless absolutely necessary as it bypasses type checking. Prefer `unknown` when a type is uncertain and you need to handle it safely.
|
|
23
|
+
|
|
24
|
+
## Angular Best Practices
|
|
25
|
+
|
|
26
|
+
* **Standalone Components:** Always use standalone components, directives, and pipes. Avoid using `NgModules` for new features or refactoring existing ones.
|
|
27
|
+
* **Implicit Standalone:** When creating standalone components, you do not need to explicitly set `standalone: true` inside the `@Component`, `@Directive` and `@Pipe` decorators, as it is implied by default.
|
|
28
|
+
* **Bad:**
|
|
29
|
+
```typescript
|
|
30
|
+
@Component({
|
|
31
|
+
standalone: true,
|
|
32
|
+
// ...
|
|
33
|
+
})
|
|
34
|
+
export class MyComponent {}
|
|
35
|
+
```
|
|
36
|
+
* **Good:**
|
|
37
|
+
```typescript
|
|
38
|
+
@Component({
|
|
39
|
+
// `standalone: true` is implied
|
|
40
|
+
// ...
|
|
41
|
+
})
|
|
42
|
+
export class MyComponent {}
|
|
43
|
+
```
|
|
44
|
+
* **Signals for State Management:** Utilize Angular Signals for reactive state management within components and services.
|
|
45
|
+
* **Lazy Loading:** Implement lazy loading for feature routes to improve initial load times of your application.
|
|
46
|
+
* **NgOptimizedImage:** Use `NgOptimizedImage` for all static images to automatically optimize image loading and performance.
|
|
47
|
+
* **Host bindings:** Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead.
|
|
48
|
+
|
|
49
|
+
## Components
|
|
50
|
+
|
|
51
|
+
* **Single Responsibility:** Keep components small, focused, and responsible for a single piece of functionality.
|
|
52
|
+
* **`input()` and `output()` Functions:** Prefer `input()` and `output()` functions over the `@Input()` and `@Output()` decorators for defining component inputs and outputs.
|
|
53
|
+
* **Old Decorator Syntax:**
|
|
54
|
+
```typescript
|
|
55
|
+
@Input() userId!: string;
|
|
56
|
+
@Output() userSelected = new EventEmitter<string>();
|
|
57
|
+
```
|
|
58
|
+
* **New Function Syntax:**
|
|
59
|
+
```typescript
|
|
60
|
+
import { input, output } from '@angular/core';
|
|
61
|
+
|
|
62
|
+
// ...
|
|
63
|
+
userId = input<string>('');
|
|
64
|
+
userSelected = output<string>();
|
|
65
|
+
```
|
|
66
|
+
* **`computed()` for Derived State:** Use the `computed()` function from `@angular/core` for derived state based on signals.
|
|
67
|
+
* **Inline Templates:** Prefer inline templates (template: `...`) for small components to keep related code together. For larger templates, use external HTML files.
|
|
68
|
+
* **Reactive Forms:** Prefer Reactive forms over Template-driven forms for complex forms, validation, and dynamic controls due to their explicit, immutable, and synchronous nature.
|
|
69
|
+
* **No `ngClass` / `NgClass`:** Do not use the `ngClass` directive. Instead, use native `class` bindings for conditional styling.
|
|
70
|
+
* **Bad:**
|
|
71
|
+
```html
|
|
72
|
+
<section [ngClass]="{'active': isActive}"></section>
|
|
73
|
+
```
|
|
74
|
+
* **Good:**
|
|
75
|
+
```html
|
|
76
|
+
<section [class.active]="isActive"></section>
|
|
77
|
+
<section [class]="{'active': isActive}"></section>
|
|
78
|
+
<section [class]="myClasses"></section>
|
|
79
|
+
```
|
|
80
|
+
* **No `ngStyle` / `NgStyle`:** Do not use the `ngStyle` directive. Instead, use native `style` bindings for conditional inline styles.
|
|
81
|
+
* **Bad:**
|
|
82
|
+
```html
|
|
83
|
+
<section [ngStyle]="{'font-size': fontSize + 'px'}"></section>
|
|
84
|
+
```
|
|
85
|
+
* **Good:**
|
|
86
|
+
```html
|
|
87
|
+
<section [style.font-size.px]="fontSize"></section>
|
|
88
|
+
<section [style]="myStyles"></section>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## State Management
|
|
92
|
+
|
|
93
|
+
* **Signals for Local State:** Use signals for managing local component state.
|
|
94
|
+
* **`computed()` for Derived State:** Leverage `computed()` for any state that can be derived from other signals.
|
|
95
|
+
* **Pure and Predictable Transformations:** Ensure state transformations are pure functions (no side effects) and predictable.
|
|
96
|
+
* **Signal value updates:** Do NOT use `mutate` on signals, use `update` or `set` instead.
|
|
97
|
+
|
|
98
|
+
## Templates
|
|
99
|
+
|
|
100
|
+
* **Simple Templates:** Keep templates as simple as possible, avoiding complex logic directly in the template. Delegate complex logic to the component's TypeScript code.
|
|
101
|
+
* **Native Control Flow:** Use the new built-in control flow syntax (`@if`, `@for`, `@switch`) instead of the older structural directives (`*ngIf`, `*ngFor`, `*ngSwitch`).
|
|
102
|
+
* **Old Syntax:**
|
|
103
|
+
```html
|
|
104
|
+
<section *ngIf="isVisible">Content</section>
|
|
105
|
+
<section *ngFor="let item of items">{{ item }}</section>
|
|
106
|
+
```
|
|
107
|
+
* **New Syntax:**
|
|
108
|
+
```html
|
|
109
|
+
@if (isVisible) {
|
|
110
|
+
<section>Content</section>
|
|
111
|
+
}
|
|
112
|
+
@for (item of items; track item.id) {
|
|
113
|
+
<section>{{ item }}</section>
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
* **Async Pipe:** Use the `async` pipe to handle observables in templates. This automatically subscribes and unsubscribes, preventing memory leaks.
|
|
117
|
+
|
|
118
|
+
## Services
|
|
119
|
+
|
|
120
|
+
* **Single Responsibility:** Design services around a single, well-defined responsibility.
|
|
121
|
+
* **`providedIn: 'root'`:** Use the `providedIn: 'root'` option when declaring injectable services to ensure they are singletons and tree-shakable.
|
|
122
|
+
* **`inject()` Function:** Prefer the `inject()` function over constructor injection when injecting dependencies, especially within `provide` functions, `computed` properties, or outside of constructor context.
|
|
123
|
+
* **Old Constructor Injection:**
|
|
124
|
+
```typescript
|
|
125
|
+
constructor(private myService: MyService) {}
|
|
126
|
+
```
|
|
127
|
+
* **New `inject()` Function:**
|
|
128
|
+
```typescript
|
|
129
|
+
import { inject } from '@angular/core';
|
|
130
|
+
|
|
131
|
+
export class MyComponent {
|
|
132
|
+
private myService = inject(MyService);
|
|
133
|
+
// ...
|
|
134
|
+
}
|
|
135
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# AGENTS.md — Angular
|
|
2
|
+
|
|
3
|
+
Project instructions for coding agents. Prefer the nearest `AGENTS.md` to the file being edited.
|
|
4
|
+
|
|
5
|
+
Angular/TypeScript standards: `.cursor/rules/angular-20.mdc`. Keep this file project-specific — do not duplicate those rules.
|
|
6
|
+
|
|
7
|
+
## At a glance
|
|
8
|
+
|
|
9
|
+
- Project: `<PROJECT_NAME>`
|
|
10
|
+
- Angular: `<VERSION>` (standalone) · SSR: `<yes|no>`
|
|
11
|
+
- Styling: `<Tailwind|SCSS|…>`
|
|
12
|
+
- Package manager: `pnpm` only — never `npm` or `yarn`
|
|
13
|
+
- Domains: `<DOMAIN_LIST>`
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
Inspect `package.json` if a script name differs.
|
|
18
|
+
|
|
19
|
+
| Action | Command |
|
|
20
|
+
|--------|---------|
|
|
21
|
+
| Install | `pnpm install` |
|
|
22
|
+
| Dev | `pnpm start` |
|
|
23
|
+
| Test | `pnpm test` |
|
|
24
|
+
| Lint | `pnpm lint` |
|
|
25
|
+
| Build | `pnpm build` |
|
|
26
|
+
|
|
27
|
+
Run lint/build (and tests when relevant) before committing. Fix failures you introduce.
|
|
28
|
+
|
|
29
|
+
## Hard rules
|
|
30
|
+
|
|
31
|
+
- Do not add dependencies unless asked
|
|
32
|
+
- Prefer existing shared components/services over new ones
|
|
33
|
+
- Match existing folder and file layout
|
|
34
|
+
- Never commit secrets or API keys
|
|
35
|
+
|
|
36
|
+
## Architecture
|
|
37
|
+
|
|
38
|
+
- App: `src/app/`
|
|
39
|
+
- Features: `<PATHS>`
|
|
40
|
+
- Shared UI (prefer these): `<COMPONENTS>`
|
|
41
|
+
- Path aliases: `<e.g. @app/*>`
|
|
42
|
+
- API pattern: `<methods, response shape, error helper>`
|
|
43
|
+
- Models: `<naming / base types>`
|
|
44
|
+
- Guards / interceptors / env: `<PATHS>`
|
|
45
|
+
|
|
46
|
+
## Done checklist
|
|
47
|
+
|
|
48
|
+
- [ ] Lint and build pass
|
|
49
|
+
- [ ] Tests updated when behavior changes
|
|
50
|
+
- [ ] No secrets in the diff
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Generate PR Description
|
|
2
|
+
|
|
3
|
+
Generate a copy-paste PR description for the current branch.
|
|
4
|
+
|
|
5
|
+
## Destination branch
|
|
6
|
+
|
|
7
|
+
Use the destination (base) branch from the text after this command, if provided.
|
|
8
|
+
|
|
9
|
+
Examples:
|
|
10
|
+
- `/generate-pr-description` → base `origin/dev`
|
|
11
|
+
- `/generate-pr-description integration-dev-3` → base `origin/integration-dev-3`
|
|
12
|
+
- `/generate-pr-description latest-release` → base `origin/latest-release`
|
|
13
|
+
|
|
14
|
+
Rules:
|
|
15
|
+
1. If the user provides a branch name (with or without `origin/`), use that as the base.
|
|
16
|
+
2. If none is provided, default to `origin/dev`.
|
|
17
|
+
3. If the user did not provide a base and the default looks wrong for the branch (e.g. release / hotfix naming), ask once which destination branch to use, then continue.
|
|
18
|
+
4. Normalize short names to remote refs when available (`dev` → `origin/dev`).
|
|
19
|
+
|
|
20
|
+
## Steps
|
|
21
|
+
|
|
22
|
+
1. Resolve the destination branch using the rules above.
|
|
23
|
+
2. Gather context with git (do **not** create `pr-diff.txt` or other temp files):
|
|
24
|
+
- `git status -sb`
|
|
25
|
+
- `git log --oneline <base>...HEAD`
|
|
26
|
+
- `git diff --stat <base>...HEAD`
|
|
27
|
+
- `git diff <base>...HEAD`
|
|
28
|
+
3. Prefer the three-dot range (`<base>...HEAD`) so the description matches what GitHub/Azure DevOps shows for the PR.
|
|
29
|
+
4. Infer any Jira key from branch name or commit messages (e.g. `feature-31575`, `CRA10-30756`, `Refs: CRA-10`). If unknown, leave a placeholder.
|
|
30
|
+
5. Write the PR description (max ~300 words). Omit empty optional sections.
|
|
31
|
+
6. Output **only** the markdown below — no preamble, no tool commentary, no offer to create the PR unless asked.
|
|
32
|
+
|
|
33
|
+
## Output format
|
|
34
|
+
|
|
35
|
+
```markdown
|
|
36
|
+
# <concise PR title>
|
|
37
|
+
|
|
38
|
+
## Overview
|
|
39
|
+
<1-3 sentences: what this PR does and why>
|
|
40
|
+
|
|
41
|
+
## Changes
|
|
42
|
+
- <high-level change>
|
|
43
|
+
- <high-level change>
|
|
44
|
+
|
|
45
|
+
## Breaking changes
|
|
46
|
+
None.
|
|
47
|
+
<!-- or a short list -->
|
|
48
|
+
|
|
49
|
+
## Related
|
|
50
|
+
Refs: <JIRA-KEY or leave blank after the colon>
|
|
51
|
+
Base: `<destination-branch>`
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Optional sections — include only when clearly relevant:
|
|
55
|
+
|
|
56
|
+
```markdown
|
|
57
|
+
## Architectural notes
|
|
58
|
+
<short note on design/pattern changes>
|
|
59
|
+
|
|
60
|
+
## Performance
|
|
61
|
+
<short note only if there are real perf/ops improvements>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Writing rules
|
|
65
|
+
|
|
66
|
+
- Focus on why and what, not file-by-file commentary.
|
|
67
|
+
- Prefer product/feature language over implementation trivia.
|
|
68
|
+
- Do not invent features, tests, or breaking changes not supported by the diff/commits.
|
|
69
|
+
- Keep bullets short and specific.
|
|
70
|
+
- Match this repo’s ticket style (Jira), not `#123` GitHub issues, unless the commits clearly use GitHub issue numbers.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Use Conventional Commits format for all git commit messages
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Git commit messages
|
|
7
|
+
|
|
8
|
+
Use Conventional Commits format. Write clear, concise messages in imperative mood (e.g. `add feature`, not `added feature`). Do not capitalize the first letter of the subject. Enclose file names in backticks.
|
|
9
|
+
|
|
10
|
+
## Required structure
|
|
11
|
+
|
|
12
|
+
Every commit message must follow this exact structure:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
<type>(<scope>): <subject>
|
|
16
|
+
|
|
17
|
+
- <bullet 1>
|
|
18
|
+
- <bullet 2>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
1. **Subject line** — one line, imperative mood.
|
|
22
|
+
2. **Blank line**
|
|
23
|
+
3. **Bullets** (optional if a single change fits in the subject) — one concrete change per bullet.
|
|
24
|
+
|
|
25
|
+
## Bullet rules
|
|
26
|
+
|
|
27
|
+
- Start each bullet with a lowercase verb in imperative mood: `fix`, `update`, `remove`, `use`, not `Fixed`, `Updated`, `Simplified`.
|
|
28
|
+
- One file or one logical change per bullet.
|
|
29
|
+
- Keep bullets short — state what changed, not why it is better.
|
|
30
|
+
- Do not append justification phrases like "to enhance readability", "for improved type safety", or "for consistency and clarity".
|
|
31
|
+
|
|
32
|
+
## Do not include
|
|
33
|
+
|
|
34
|
+
- A closing summary paragraph. Never end with lines like "These changes improve code maintainability and performance across multiple components."
|
|
35
|
+
- Past-tense verbs in bullets (`Simplified`, `Updated`, `Refactored`, `Enhanced`, `Adjusted`).
|
|
36
|
+
- Vague bullets that describe intent instead of the change (`Adjusted API calls for consistency` → `align API params in `PendingInvoicesComponent``).
|
|
37
|
+
|
|
38
|
+
## Good example
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
refactor(pending-invoices): streamline country and cash control data handling
|
|
42
|
+
|
|
43
|
+
- simplify country mapping in `CountryCityPostalSuburb`
|
|
44
|
+
- rename API params in `CashControlPullService`
|
|
45
|
+
- use `takeUntilDestroyed` in `PendingInvoicesComponent`
|
|
46
|
+
- store customer id without transformation in `PendingInvoicesResultsComponent`
|
|
47
|
+
- tighten types in `shipment-documents.types.ts`
|
|
48
|
+
- fix shipment address type in `AddressValidationModalComponent`
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Bad example (do not generate this)
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
refactor: streamline country and cash control data handling
|
|
55
|
+
|
|
56
|
+
- Simplified country data mapping in `CountryCityPostalSuburb` to enhance readability and performance.
|
|
57
|
+
- Updated `CashControlPullService` to use more descriptive parameter names for API calls.
|
|
58
|
+
- Refactored `PendingInvoicesComponent` to utilize `takeUntilDestroyed` for better memory management.
|
|
59
|
+
|
|
60
|
+
These changes improve code maintainability and performance across multiple components.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Problems: past-tense bullets, verbose justifications, and a forbidden closing summary.
|