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 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,5 @@
1
+ packages:
2
+ - '.'
3
+
4
+ blockExoticSubdeps: true
5
+ minimumReleaseAge: 4320
@@ -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.