@aliaksei-raketski/pi-angular-developer 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.
Files changed (48) hide show
  1. package/README.md +44 -0
  2. package/overlays/angular-developer/references/docs-helpers.md +36 -0
  3. package/overlays/angular-developer/scripts/get-best-practices.mjs +268 -0
  4. package/overlays/angular-developer/scripts/search-documentation.mjs +396 -0
  5. package/package.json +41 -0
  6. package/scripts/sync-angular-skill.mjs +307 -0
  7. package/skills/angular-developer/SKILL.md +133 -0
  8. package/skills/angular-developer/UPSTREAM.md +9 -0
  9. package/skills/angular-developer/data/best-practices.md +56 -0
  10. package/skills/angular-developer/references/angular-animations.md +160 -0
  11. package/skills/angular-developer/references/angular-aria.md +597 -0
  12. package/skills/angular-developer/references/cli.md +86 -0
  13. package/skills/angular-developer/references/component-harnesses.md +57 -0
  14. package/skills/angular-developer/references/component-styling.md +91 -0
  15. package/skills/angular-developer/references/components.md +117 -0
  16. package/skills/angular-developer/references/creating-services.md +97 -0
  17. package/skills/angular-developer/references/data-resolvers.md +69 -0
  18. package/skills/angular-developer/references/define-routes.md +67 -0
  19. package/skills/angular-developer/references/defining-providers.md +72 -0
  20. package/skills/angular-developer/references/di-fundamentals.md +118 -0
  21. package/skills/angular-developer/references/docs-helpers.md +36 -0
  22. package/skills/angular-developer/references/e2e-testing.md +66 -0
  23. package/skills/angular-developer/references/effects.md +83 -0
  24. package/skills/angular-developer/references/environment-configuration.md +132 -0
  25. package/skills/angular-developer/references/hierarchical-injectors.md +43 -0
  26. package/skills/angular-developer/references/host-elements.md +80 -0
  27. package/skills/angular-developer/references/injection-context.md +63 -0
  28. package/skills/angular-developer/references/inputs.md +101 -0
  29. package/skills/angular-developer/references/linked-signal.md +59 -0
  30. package/skills/angular-developer/references/loading-strategies.md +61 -0
  31. package/skills/angular-developer/references/migrations.md +30 -0
  32. package/skills/angular-developer/references/navigate-to-routes.md +69 -0
  33. package/skills/angular-developer/references/outputs.md +86 -0
  34. package/skills/angular-developer/references/reactive-forms.md +118 -0
  35. package/skills/angular-developer/references/rendering-strategies.md +44 -0
  36. package/skills/angular-developer/references/resource.md +74 -0
  37. package/skills/angular-developer/references/route-animations.md +56 -0
  38. package/skills/angular-developer/references/route-guards.md +52 -0
  39. package/skills/angular-developer/references/router-lifecycle.md +45 -0
  40. package/skills/angular-developer/references/router-testing.md +87 -0
  41. package/skills/angular-developer/references/show-routes-with-outlets.md +68 -0
  42. package/skills/angular-developer/references/signal-forms.md +907 -0
  43. package/skills/angular-developer/references/signals-overview.md +94 -0
  44. package/skills/angular-developer/references/tailwind-css.md +69 -0
  45. package/skills/angular-developer/references/template-driven-forms.md +114 -0
  46. package/skills/angular-developer/references/testing-fundamentals.md +63 -0
  47. package/skills/angular-developer/scripts/get-best-practices.mjs +268 -0
  48. package/skills/angular-developer/scripts/search-documentation.mjs +396 -0
@@ -0,0 +1,94 @@
1
+ # Angular Signals Overview
2
+
3
+ Signals are the foundation of reactivity in modern Angular applications. A **signal** is a wrapper around a value that notifies interested consumers when that value changes.
4
+
5
+ ## Writable Signals (`signal`)
6
+
7
+ Use `signal()` to create state that can be directly updated.
8
+
9
+ ```ts
10
+ import {signal} from '@angular/core';
11
+
12
+ // Create a writable signal
13
+ const count = signal(0);
14
+
15
+ // Read the value (always requires calling the getter function)
16
+ console.log(count());
17
+
18
+ // Update the value directly
19
+ count.set(3);
20
+
21
+ // Update based on the previous value
22
+ count.update((value) => value + 1);
23
+ ```
24
+
25
+ ### Exposing as Readonly
26
+
27
+ When exposing state from a service, it is a best practice to expose a readonly version to prevent external mutation.
28
+
29
+ ```ts
30
+ private readonly _count = signal(0);
31
+ // Consumers can read this, but cannot call .set() or .update()
32
+ readonly count = this._count.asReadonly();
33
+ ```
34
+
35
+ ## Computed Signals (`computed`)
36
+
37
+ Use `computed()` to create read-only signals that derive their value from other signals.
38
+
39
+ - **Lazily Evaluated**: The derivation function doesn't run until the computed signal is read.
40
+ - **Memoized**: The result is cached. It only recalculates when one of the signals it depends on changes.
41
+ - **Dynamic Dependencies**: Only the signals _actually read_ during the derivation are tracked.
42
+
43
+ ```ts
44
+ import {signal, computed} from '@angular/core';
45
+
46
+ const count = signal(0);
47
+ const doubleCount = computed(() => count() * 2);
48
+
49
+ // doubleCount automatically updates when count changes.
50
+ ```
51
+
52
+ ## Reactive Contexts
53
+
54
+ A **reactive context** is a runtime state where Angular monitors signal reads to establish a dependency.
55
+
56
+ Angular automatically enters a reactive context when evaluating:
57
+
58
+ - `computed` signals
59
+ - `effect` callbacks
60
+ - `linkedSignal` computations
61
+ - Component templates
62
+
63
+ ### Untracked Reads (`untracked`)
64
+
65
+ If you need to read a signal inside a reactive context _without_ creating a dependency (so that the context doesn't re-run when the signal changes), use `untracked()`.
66
+
67
+ ```ts
68
+ import {effect, untracked} from '@angular/core';
69
+
70
+ effect(() => {
71
+ // This effect only runs when currentUser changes.
72
+ // It does NOT run when counter changes, even though counter is read here.
73
+ console.log(`User: ${currentUser()}, Count: ${untracked(counter)}`);
74
+ });
75
+ ```
76
+
77
+ ### Async Operations in Reactive Contexts
78
+
79
+ The reactive context is only active for **synchronous** code. Signal reads after an `await` will not be tracked. **Always read signals before asynchronous boundaries.**
80
+
81
+ ```ts
82
+ // ❌ INCORRECT: theme() is not tracked because it is read after await
83
+ effect(async () => {
84
+ const data = await fetchUserData();
85
+ console.log(theme());
86
+ });
87
+
88
+ // ✅ CORRECT: Read the signal before the await
89
+ effect(async () => {
90
+ const currentTheme = theme();
91
+ const data = await fetchUserData();
92
+ console.log(currentTheme);
93
+ });
94
+ ```
@@ -0,0 +1,69 @@
1
+ # Using Tailwind CSS with Angular
2
+
3
+ Tailwind CSS is a utility-first CSS framework that integrates seamlessly with Angular.
4
+
5
+ **CRITICAL AGENT GUIDANCE: ALWAYS focus on Tailwind CSS v4 practices. DO NOT revert to old Tailwind v3 patterns (like creating `tailwind.config.js` with `@tailwind` directives) as this will break the application build. Modern Angular projects use Tailwind v4.**
6
+
7
+ ## Automated Setup (Recommended)
8
+
9
+ The easiest way to add Tailwind CSS to an Angular project is via the Angular CLI:
10
+
11
+ ```shell
12
+ ng add tailwindcss
13
+ ```
14
+
15
+ This will automatically:
16
+
17
+ 1. Install `tailwindcss` and peer dependencies.
18
+ 2. Configure the project to use Tailwind CSS.
19
+ 3. Add the proper import to your global styles.
20
+
21
+ ## Manual Setup (Tailwind v4)
22
+
23
+ If setting up manually, use the following Tailwind v4 pattern:
24
+
25
+ ### 1. Install Dependencies
26
+
27
+ Install Tailwind CSS and PostCSS:
28
+
29
+ ```shell
30
+ npm install tailwindcss @tailwindcss/postcss postcss
31
+ ```
32
+
33
+ ### 2. Configure PostCSS
34
+
35
+ Create a `.postcssrc.json` file in the project root:
36
+
37
+ ```json
38
+ {
39
+ "plugins": {
40
+ "@tailwindcss/postcss": {}
41
+ }
42
+ }
43
+ ```
44
+
45
+ _(Do NOT create a `tailwind.config.js` file! Configuration in v4 is handled through CSS variables)._
46
+
47
+ ### 3. Import Tailwind CSS
48
+
49
+ In your global styles file (e.g., `src/styles.css`), add the standard v4 import:
50
+
51
+ ```css
52
+ @import 'tailwindcss';
53
+ ```
54
+
55
+ _(If using SCSS, use `@use 'tailwindcss';` instead)._
56
+
57
+ ### 4. Use Utility Classes
58
+
59
+ You can now use Tailwind classes directly in your component templates:
60
+
61
+ ```html
62
+ <h1 class="text-3xl font-bold underline">Hello world!</h1>
63
+ ```
64
+
65
+ ## Summary for AI Agents
66
+
67
+ - **Do not use `@tailwind base; @tailwind components; @tailwind utilities;`**. Use `@import 'tailwindcss';`.
68
+ - **Do not create `tailwind.config.js`**. Configuration is managed directly in CSS via theme variables or using PostCSS configurations.
69
+ - Stick strictly to v4 syntax and workflows.
@@ -0,0 +1,114 @@
1
+ # Template-Driven Forms
2
+
3
+ Template-driven forms use two-way data binding (`[(ngModel)]`) to update the data model in the component as changes are made in the template and vice versa. They are ideal for simple forms and use directives in the HTML template to manage form state and validation.
4
+
5
+ ## Core Directives
6
+
7
+ Template-driven forms rely on the `FormsModule` which provides these key directives:
8
+
9
+ - `NgModel`: Reconciles value changes in the form element with the data model (`[(ngModel)]`).
10
+ - `NgForm`: Automatically creates a top-level `FormGroup` bound to the `<form>` tag.
11
+ - `NgModelGroup`: Creates a nested `FormGroup` bound to a DOM element.
12
+
13
+ ## Setup
14
+
15
+ First, import `FormsModule` into your component or module.
16
+
17
+ ```ts
18
+ import {Component} from '@angular/core';
19
+ import {FormsModule} from '@angular/forms';
20
+
21
+ @Component({
22
+ selector: 'app-user-form',
23
+ imports: [FormsModule],
24
+ templateUrl: './user-form.component.html',
25
+ })
26
+ export class UserForm {
27
+ user = {name: '', role: 'Guest'};
28
+
29
+ onSubmit() {
30
+ console.log('Form submitted!', this.user);
31
+ }
32
+ }
33
+ ```
34
+
35
+ ## Building the Form Template
36
+
37
+ ### Two-Way Binding with `[(ngModel)]`
38
+
39
+ Use `[(ngModel)]` on input elements. **Every element using `[(ngModel)]` MUST have a `name` attribute.** Angular uses the `name` attribute to register the control with the parent `NgForm`.
40
+
41
+ ```html
42
+ <form #userForm="ngForm" (ngSubmit)="onSubmit()">
43
+ <!-- Basic Input -->
44
+ <div>
45
+ <label for="name">Name:</label>
46
+ <input type="text" id="name" required [(ngModel)]="user.name" name="name" #nameCtrl="ngModel" />
47
+ </div>
48
+
49
+ <!-- Select Box -->
50
+ <div>
51
+ <label for="role">Role:</label>
52
+ <select id="role" [(ngModel)]="user.role" name="role">
53
+ <option value="Admin">Admin</option>
54
+ <option value="Guest">Guest</option>
55
+ </select>
56
+ </div>
57
+
58
+ <!-- Submit Button (disabled if form is invalid) -->
59
+ <button type="submit" [disabled]="!userForm.form.valid">Submit</button>
60
+ </form>
61
+ ```
62
+
63
+ ## Form and Control State
64
+
65
+ Angular automatically applies CSS classes to controls and forms based on their state:
66
+
67
+ | State | Class if True | Class if False |
68
+ | :------------- | :-------------------------------- | :------------- |
69
+ | Visited | `ng-touched` | `ng-untouched` |
70
+ | Value Changed | `ng-dirty` | `ng-pristine` |
71
+ | Value is Valid | `ng-valid` | `ng-invalid` |
72
+ | Form Submitted | `ng-submitted` (on `<form>` only) | - |
73
+
74
+ You can use these classes to provide visual feedback in your CSS:
75
+
76
+ ```css
77
+ .ng-valid[required],
78
+ .ng-valid.required {
79
+ border-left: 5px solid #42a948; /* green */
80
+ }
81
+ .ng-invalid:not(form) {
82
+ border-left: 5px solid #a94442; /* red */
83
+ }
84
+ ```
85
+
86
+ ## Validation and Error Messages
87
+
88
+ To display error messages conditionally, export the `ngModel` directive to a template reference variable (e.g., `#nameCtrl="ngModel"`).
89
+
90
+ ```html
91
+ <input type="text" id="name" required [(ngModel)]="user.name" name="name" #nameCtrl="ngModel" />
92
+
93
+ <!-- Show error only if the control is invalid AND (touched OR dirty) -->
94
+ @if (nameCtrl.invalid && (nameCtrl.dirty || nameCtrl.touched)) {
95
+ <div class="alert alert-danger">
96
+ @if (nameCtrl.errors?.['required']) {
97
+ <div>Name is required.</div>
98
+ }
99
+ </div>
100
+ }
101
+ ```
102
+
103
+ ## Submitting the Form
104
+
105
+ 1. Use the `(ngSubmit)` event on the `<form>` element.
106
+ 2. Bind the submit button's disabled state to the overall form validity using the `NgForm` template reference variable (e.g., `[disabled]="!userForm.form.valid"`).
107
+
108
+ ## Resetting the Form
109
+
110
+ To programmatically reset the form to its pristine state (clearing values and validation flags), use the `reset()` method on the `NgForm` instance.
111
+
112
+ ```html
113
+ <button type="button" (click)="userForm.reset()">Reset</button>
114
+ ```
@@ -0,0 +1,63 @@
1
+ # Testing Fundamentals
2
+
3
+ This guide covers the fundamental principles and practices for writing unit tests in this repository, which uses Vitest as the test runner.
4
+
5
+ ## Core Philosophy: Zoneless & Async-First
6
+
7
+ This project follows a modern, zoneless testing approach. State changes schedule updates asynchronously, and tests must account for this.
8
+
9
+ **Do NOT** use `fixture.detectChanges()` to manually trigger updates.
10
+ **ALWAYS** use the "Act, Wait, Assert" pattern:
11
+
12
+ 1. **Act:** Update state or perform an action (e.g., set a component input, click a button).
13
+ 2. **Wait:** Use `await fixture.whenStable()` to allow the framework to process the scheduled update and render the changes.
14
+ 3. **Assert:** Verify the outcome.
15
+
16
+ ### Basic Test Structure Example
17
+
18
+ ```ts
19
+ import {ComponentFixture, TestBed} from '@angular/core/testing';
20
+ import {MyComponent} from './my.component';
21
+
22
+ describe('MyComponent', () => {
23
+ let component: MyComponent;
24
+ let fixture: ComponentFixture<MyComponent>;
25
+ let h1: HTMLElement;
26
+
27
+ beforeEach(() => {
28
+ TestBed.configureTestingModule({});
29
+
30
+ // Create the component fixture
31
+ fixture = TestBed.createComponent(MyComponent);
32
+ component = fixture.componentInstance;
33
+ h1 = fixture.nativeElement.querySelector('h1');
34
+ });
35
+
36
+ it('should display the default title', async () => {
37
+ // ACT: (Implicit) Component is created with default state.
38
+ // WAIT for initial data binding.
39
+ await fixture.whenStable();
40
+ // ASSERT the initial state.
41
+ expect(h1.textContent).toContain('Default Title');
42
+ });
43
+
44
+ it('should display a different title after a change', async () => {
45
+ // ACT: Change the component's title property.
46
+ component.title.set('New Test Title');
47
+
48
+ // WAIT for the asynchronous update to complete.
49
+ await fixture.whenStable();
50
+
51
+ // ASSERT the DOM has been updated.
52
+ expect(h1.textContent).toContain('New Test Title');
53
+ });
54
+ });
55
+ ```
56
+
57
+ ## TestBed and ComponentFixture
58
+
59
+ - **`TestBed`**: The primary utility for creating a test-specific Angular module. Use `TestBed.configureTestingModule({...})` in your `beforeEach` to declare components, provide services, and set up imports needed for your test.
60
+ - **`ComponentFixture`**: A handle on the created component instance and its environment.
61
+ - `fixture.componentInstance`: Access the component's class instance.
62
+ - `fixture.nativeElement`: Access the component's root DOM element.
63
+ - `fixture.debugElement`: An Angular-specific wrapper around the `nativeElement` that provides safer, platform-agnostic ways to query the DOM (e.g., `debugElement.query(By.css('p'))`).
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import {createRequire} from 'node:module';
7
+ import {pathToFileURL, fileURLToPath} from 'node:url';
8
+
9
+ const MAX_BEST_PRACTICES_BYTES = 1_048_576;
10
+ const BUILTIN_FALLBACK = `Bundled Angular best-practices guidance is unavailable in this workspace.\n\nTo get version-aware guidance, run this script from an Angular workspace with @angular/core installed and accessible via Node resolution.`;
11
+
12
+ main().catch((error) => {
13
+ console.error(`Fatal: ${error instanceof Error ? error.message : String(error)}`);
14
+ process.exit(1);
15
+ });
16
+
17
+ async function main() {
18
+ const parsed = parseArguments(process.argv.slice(2));
19
+
20
+ if (parsed.help) {
21
+ printHelp();
22
+ process.exit(0);
23
+ }
24
+
25
+ if (parsed.errors.length > 0) {
26
+ console.error(parsed.errors.join('\n'));
27
+ console.error('Use --help for usage.');
28
+ process.exit(2);
29
+ }
30
+
31
+ const workspacePath = parsed.workspacePath || null;
32
+ const source = await resolveSource(workspacePath);
33
+
34
+ const payload = {
35
+ source: source.source,
36
+ content: source.content,
37
+ };
38
+
39
+ if (parsed.json) {
40
+ const json = parsed.pretty
41
+ ? JSON.stringify(payload, null, 2)
42
+ : JSON.stringify(payload);
43
+ process.stdout.write(json);
44
+ return;
45
+ }
46
+
47
+ console.log(`<!-- Source: ${payload.source} -->`);
48
+ console.log('');
49
+ console.log((payload.content || '').trimEnd());
50
+ }
51
+
52
+ function parseArguments(argv) {
53
+ const result = {
54
+ workspacePath: null,
55
+ json: false,
56
+ pretty: false,
57
+ help: false,
58
+ errors: [],
59
+ };
60
+
61
+ const positional = [];
62
+
63
+ for (let i = 0; i < argv.length; i++) {
64
+ const arg = argv[i];
65
+
66
+ if (!arg.startsWith('--')) {
67
+ positional.push(arg);
68
+ continue;
69
+ }
70
+
71
+ if (arg === '--json') {
72
+ result.json = true;
73
+ continue;
74
+ }
75
+
76
+ if (arg === '--pretty') {
77
+ result.pretty = true;
78
+ continue;
79
+ }
80
+
81
+ if (arg === '--help') {
82
+ result.help = true;
83
+ continue;
84
+ }
85
+
86
+ if (arg.startsWith('--')) {
87
+ result.errors.push(`Unknown option: ${arg}`);
88
+ continue;
89
+ }
90
+ }
91
+
92
+ if (positional.length > 0) {
93
+ result.workspacePath = positional[0];
94
+ }
95
+
96
+ return result;
97
+ }
98
+
99
+ async function resolveSource(workspacePath) {
100
+ if (workspacePath) {
101
+ try {
102
+ const workspaceRoot = await resolveWorkspaceFromInput(path.resolve(workspacePath));
103
+
104
+ if (!workspaceRoot) {
105
+ console.error(`Warning: could not locate an Angular workspace at "${workspacePath}". Falling back to bundled best-practices.md.`);
106
+ } else {
107
+ try {
108
+ const source = await loadWorkspaceBestPractices(workspaceRoot);
109
+ return source;
110
+ } catch (error) {
111
+ console.error(
112
+ `Warning: failed to read workspace-specific best practices from ${workspaceRoot}: ${error instanceof Error ? error.message : String(error)}. Falling back to bundled best-practices.md.`,
113
+ );
114
+ }
115
+ }
116
+ } catch (error) {
117
+ console.error(
118
+ `Warning: failed to locate workspace from ${workspacePath}: ${error instanceof Error ? error.message : String(error)}. Falling back to bundled best-practices.md.`,
119
+ );
120
+ }
121
+ } else {
122
+ const workspaceRoot = await findWorkspaceFromCwd(process.cwd());
123
+
124
+ if (workspaceRoot) {
125
+ try {
126
+ const source = await loadWorkspaceBestPractices(workspaceRoot);
127
+ return source;
128
+ } catch (error) {
129
+ console.error(
130
+ `Warning: failed to read workspace-specific best practices from ${workspaceRoot}: ${error instanceof Error ? error.message : String(error)}. Falling back to bundled best-practices.md.`,
131
+ );
132
+ }
133
+ }
134
+ }
135
+
136
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
137
+ const fallbackPath = path.join(path.dirname(scriptDir), 'data', 'best-practices.md');
138
+ try {
139
+ const content = await fs.readFile(fallbackPath, 'utf8');
140
+ return {source: 'bundled best-practices fallback', content};
141
+ } catch (error) {
142
+ console.error(`Warning: bundled fallback not found at ${fallbackPath}: ${error instanceof Error ? error.message : String(error)}.`);
143
+ return {source: 'built-in fallback', content: BUILTIN_FALLBACK};
144
+ }
145
+ }
146
+
147
+ async function loadWorkspaceBestPractices(workspaceRoot) {
148
+ const packageJsonPath = path.join(workspaceRoot, 'package.json');
149
+ const probePath = path.join(workspaceRoot, '.pi-scripts-probe.mjs');
150
+ const requireBase = (await fileExists(packageJsonPath)) ? packageJsonPath : probePath;
151
+ const require = createRequire(pathToFileURL(requireBase).href);
152
+
153
+ const corePackageJsonPath = require.resolve('@angular/core/package.json');
154
+ const corePackageJson = JSON.parse(await fs.readFile(corePackageJsonPath, 'utf8'));
155
+
156
+ const bestPracticesMeta = corePackageJson.angular?.bestPractices;
157
+
158
+ if (!bestPracticesMeta || bestPracticesMeta.format !== 'markdown' || typeof bestPracticesMeta.path !== 'string') {
159
+ throw new Error('Unsupported Angular best-practices metadata format.');
160
+ }
161
+
162
+ const packageDirectory = path.dirname(corePackageJsonPath);
163
+ const bestPracticesPath = path.resolve(packageDirectory, bestPracticesMeta.path);
164
+
165
+ const rel = path.relative(packageDirectory, bestPracticesPath);
166
+ if (
167
+ rel.startsWith('..') ||
168
+ rel.startsWith(path.sep) ||
169
+ path.isAbsolute(bestPracticesMeta.path)
170
+ ) {
171
+ throw new Error('Best-practices path does not stay within @angular/core package directory.');
172
+ }
173
+
174
+ const stats = await fs.stat(bestPracticesPath);
175
+ if (!stats.isFile()) {
176
+ throw new Error(`Best-practices path is not a file: ${bestPracticesPath}`);
177
+ }
178
+
179
+ if (stats.size > MAX_BEST_PRACTICES_BYTES) {
180
+ throw new Error(`Best-practices file is too large (${stats.size} bytes).`);
181
+ }
182
+
183
+ const content = await fs.readFile(bestPracticesPath, 'utf8');
184
+ if (!content.trim()) {
185
+ throw new Error(`Best-practices file is empty: ${bestPracticesPath}`);
186
+ }
187
+
188
+ return {
189
+ source: `framework version ${corePackageJson.version}`,
190
+ content,
191
+ };
192
+ }
193
+
194
+ async function resolveWorkspaceFromInput(inputPath) {
195
+ const stats = await fs.stat(inputPath);
196
+
197
+ if (stats.isFile()) {
198
+ if (path.basename(inputPath) === 'angular.json') {
199
+ return path.dirname(inputPath);
200
+ }
201
+
202
+ throw new Error('Provided file path is not angular.json.');
203
+ }
204
+
205
+ if (stats.isDirectory()) {
206
+ const candidate = path.join(inputPath, 'angular.json');
207
+ if (await isFile(candidate)) {
208
+ return inputPath;
209
+ }
210
+ throw new Error('No angular.json found at the provided directory.');
211
+ }
212
+
213
+ throw new Error('Provided workspace path is neither a file nor directory.');
214
+ }
215
+
216
+ async function findWorkspaceFromCwd(startDir) {
217
+ let current = path.resolve(startDir);
218
+
219
+ for (;;) {
220
+ const candidate = path.join(current, 'angular.json');
221
+ if (await isFile(candidate)) {
222
+ return current;
223
+ }
224
+
225
+ const parent = path.dirname(current);
226
+ if (parent === current) {
227
+ return null;
228
+ }
229
+
230
+ current = parent;
231
+ }
232
+ }
233
+
234
+ async function isFile(candidate) {
235
+ try {
236
+ return (await fs.stat(candidate)).isFile();
237
+ } catch {
238
+ return false;
239
+ }
240
+ }
241
+
242
+ async function fileExists(candidate) {
243
+ try {
244
+ await fs.access(candidate);
245
+ return true;
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+
251
+ function printHelp() {
252
+ console.log(`
253
+ Usage:
254
+ node scripts/get-best-practices.mjs [workspacePath] [--json] [--pretty] [--help]
255
+
256
+ Arguments:
257
+ workspacePath Optional Angular workspace directory or angular.json file path.
258
+
259
+ Options:
260
+ --json Output JSON only.
261
+ --pretty Pretty-print JSON output.
262
+ --help Show this help.
263
+
264
+ Output:
265
+ Default: markdown with a source comment on top.
266
+ JSON: { source, content }
267
+ `);
268
+ }