@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,396 @@
1
+ #!/usr/bin/env node
2
+
3
+ const APP_ID = 'L1XWT2UJ7F';
4
+ const API_KEY = 'dfca7ed184db27927a512e5c6668b968';
5
+ const ENDPOINT = `https://${APP_ID}-dsn.algolia.net/1/indexes/*/queries`;
6
+
7
+ const SEARCH_ATTRIBUTES = [
8
+ 'hierarchy.lvl0',
9
+ 'hierarchy.lvl1',
10
+ 'hierarchy.lvl2',
11
+ 'hierarchy.lvl3',
12
+ 'hierarchy.lvl4',
13
+ 'hierarchy.lvl5',
14
+ 'hierarchy.lvl6',
15
+ 'content',
16
+ 'type',
17
+ 'url',
18
+ ];
19
+
20
+ const MIN_VERSION = 17;
21
+ const KNOWN_LATEST_VERSION = 22;
22
+ const DEFAULT_LIMIT = 10;
23
+ const MAX_LIMIT = 20;
24
+ const CONTENT_MAX_LENGTH = 20_000;
25
+
26
+ main().catch((error) => {
27
+ console.error(`Fatal: ${error instanceof Error ? error.message : String(error)}`);
28
+ process.exit(1);
29
+ });
30
+
31
+ async function main() {
32
+ const parsed = parseArguments(process.argv.slice(2));
33
+
34
+ if (parsed.help) {
35
+ printHelp();
36
+ process.exit(0);
37
+ }
38
+
39
+ if (parsed.errors.length > 0) {
40
+ console.error(parsed.errors.join('\n'));
41
+ console.error('Use --help for usage.');
42
+ process.exit(2);
43
+ }
44
+
45
+ const query = parsed.query.trim();
46
+ if (!query) {
47
+ console.error('Error: query is required.');
48
+ console.error('Use --help for usage.');
49
+ process.exit(2);
50
+ }
51
+
52
+ const version = parsed.version ?? KNOWN_LATEST_VERSION;
53
+ const result = await searchWithFallback(query, version, parsed.limit, parsed.includeTopContent);
54
+
55
+ const payload = {
56
+ searchedVersion: result.version,
57
+ results: result.results,
58
+ };
59
+
60
+ if (parsed.json) {
61
+ const output = parsed.pretty
62
+ ? JSON.stringify(payload, null, 2)
63
+ : JSON.stringify(payload);
64
+ process.stdout.write(output);
65
+ return;
66
+ }
67
+
68
+ printMarkdown(query, result.version, result.results);
69
+ }
70
+
71
+ function parseArguments(argv) {
72
+ const parsed = {
73
+ queryParts: [],
74
+ version: null,
75
+ limit: DEFAULT_LIMIT,
76
+ includeTopContent: false,
77
+ json: false,
78
+ pretty: false,
79
+ help: false,
80
+ errors: [],
81
+ };
82
+
83
+ for (let i = 0; i < argv.length; i++) {
84
+ const arg = argv[i];
85
+
86
+ if (!arg.startsWith('--')) {
87
+ parsed.queryParts.push(arg);
88
+ continue;
89
+ }
90
+
91
+ if (arg === '--include-top-content') {
92
+ parsed.includeTopContent = true;
93
+ continue;
94
+ }
95
+
96
+ if (arg === '--json') {
97
+ parsed.json = true;
98
+ continue;
99
+ }
100
+
101
+ if (arg === '--pretty') {
102
+ parsed.pretty = true;
103
+ continue;
104
+ }
105
+
106
+ if (arg === '--help') {
107
+ parsed.help = true;
108
+ continue;
109
+ }
110
+
111
+ if (arg === '--version' || arg === '--limit') {
112
+ const value = argv[i + 1];
113
+ if (!value || value.startsWith('--')) {
114
+ parsed.errors.push(`Option ${arg} requires a value.`);
115
+ continue;
116
+ }
117
+
118
+ if (arg === '--version') {
119
+ const parsedVersion = Number.parseInt(value, 10);
120
+ if (!Number.isInteger(parsedVersion) || parsedVersion < MIN_VERSION) {
121
+ parsed.errors.push(`Option --version must be an integer >= ${MIN_VERSION}.`);
122
+ } else {
123
+ parsed.version = parsedVersion;
124
+ }
125
+ }
126
+
127
+ if (arg === '--limit') {
128
+ const parsedLimit = Number.parseInt(value, 10);
129
+ if (!Number.isInteger(parsedLimit) || parsedLimit < 1) {
130
+ parsed.errors.push('Option --limit must be an integer >= 1.');
131
+ } else {
132
+ parsed.limit = clampLimit(parsedLimit);
133
+ }
134
+ }
135
+
136
+ i += 1;
137
+ continue;
138
+ }
139
+
140
+ if (arg.startsWith('--version=')) {
141
+ const raw = arg.slice('--version='.length);
142
+ const parsedVersion = Number.parseInt(raw, 10);
143
+ if (!Number.isInteger(parsedVersion) || parsedVersion < MIN_VERSION) {
144
+ parsed.errors.push(`Option --version must be an integer >= ${MIN_VERSION}.`);
145
+ } else {
146
+ parsed.version = parsedVersion;
147
+ }
148
+ continue;
149
+ }
150
+
151
+ if (arg.startsWith('--limit=')) {
152
+ const raw = arg.slice('--limit='.length);
153
+ const parsedLimit = Number.parseInt(raw, 10);
154
+ if (!Number.isInteger(parsedLimit) || parsedLimit < 1) {
155
+ parsed.errors.push('Option --limit must be an integer >= 1.');
156
+ } else {
157
+ parsed.limit = clampLimit(parsedLimit);
158
+ }
159
+ continue;
160
+ }
161
+
162
+ parsed.errors.push(`Unknown option: ${arg}`);
163
+ }
164
+
165
+ return {
166
+ ...parsed,
167
+ query: parsed.queryParts.join(' '),
168
+ };
169
+ }
170
+
171
+ async function searchWithFallback(query, requestedVersion, limit, includeTopContent) {
172
+ try {
173
+ const result = await queryDocs(requestedVersion, query, limit);
174
+
175
+ if (includeTopContent) {
176
+ await enrichTopResultContent(result.hits);
177
+ }
178
+
179
+ return {version: requestedVersion, results: result.hits};
180
+ } catch (error) {
181
+ if (requestedVersion > KNOWN_LATEST_VERSION) {
182
+ console.error(
183
+ `Warning: angular_v${requestedVersion} search unavailable (${error instanceof Error ? error.message : String(error)}). Falling back to angular_v${KNOWN_LATEST_VERSION}.`,
184
+ );
185
+ const fallback = await queryDocs(KNOWN_LATEST_VERSION, query, limit);
186
+
187
+ if (includeTopContent) {
188
+ await enrichTopResultContent(fallback.hits);
189
+ }
190
+
191
+ return {version: KNOWN_LATEST_VERSION, results: fallback.hits};
192
+ }
193
+
194
+ throw error;
195
+ }
196
+ }
197
+
198
+ async function queryDocs(version, query, limit) {
199
+ const params = new URLSearchParams({
200
+ query,
201
+ attributesToRetrieve: JSON.stringify(SEARCH_ATTRIBUTES),
202
+ hitsPerPage: String(clampLimit(limit)),
203
+ }).toString();
204
+
205
+ const response = await fetch(ENDPOINT, {
206
+ method: 'POST',
207
+ headers: {
208
+ 'Content-Type': 'application/json',
209
+ 'X-Algolia-Application-Id': APP_ID,
210
+ 'X-Algolia-API-Key': API_KEY,
211
+ },
212
+ body: JSON.stringify({
213
+ requests: [
214
+ {
215
+ indexName: `angular_v${version}`,
216
+ params,
217
+ },
218
+ ],
219
+ }),
220
+ });
221
+
222
+ if (!response.ok) {
223
+ const message = response.statusText || String(response.status);
224
+ const responseText = await safeText(response);
225
+ throw new Error(`HTTP ${response.status} ${message}: ${responseText}`);
226
+ }
227
+
228
+ const data = await response.json();
229
+ const result = data?.results?.[0];
230
+
231
+ if (!result || !Array.isArray(result.hits)) {
232
+ throw new Error(`Algolia response missing hits for angular_v${version}.`);
233
+ }
234
+
235
+ return {
236
+ hits: result.hits.map((hit) => formatHit(hit)),
237
+ };
238
+ }
239
+
240
+ function formatHit(hit) {
241
+ const hierarchy = hit?.hierarchy ?? {};
242
+ const levels = [
243
+ hierarchy.lvl0,
244
+ hierarchy.lvl1,
245
+ hierarchy.lvl2,
246
+ hierarchy.lvl3,
247
+ hierarchy.lvl4,
248
+ hierarchy.lvl5,
249
+ hierarchy.lvl6,
250
+ ].map((value) => (typeof value === 'string' ? value.trim() : ''));
251
+
252
+ let title = 'Result';
253
+ let titleIndex = -1;
254
+
255
+ for (let i = levels.length - 1; i >= 0; i--) {
256
+ if (levels[i]) {
257
+ title = levels[i];
258
+ titleIndex = i;
259
+ break;
260
+ }
261
+ }
262
+
263
+ const breadcrumb = levels.slice(0, titleIndex).filter(Boolean).join(' > ');
264
+
265
+ return {
266
+ title,
267
+ breadcrumb,
268
+ url: typeof hit?.url === 'string' ? hit.url : '',
269
+ };
270
+ }
271
+
272
+ async function enrichTopResultContent(results) {
273
+ if (!results || !results.length) {
274
+ return;
275
+ }
276
+
277
+ const top = results[0];
278
+ if (!top.url) {
279
+ return;
280
+ }
281
+
282
+ let url;
283
+ try {
284
+ url = new URL(top.url);
285
+ } catch {
286
+ return;
287
+ }
288
+
289
+ if (url.host !== 'angular.dev' && !url.host.endsWith('.angular.dev')) {
290
+ return;
291
+ }
292
+
293
+ try {
294
+ const response = await fetch(top.url);
295
+ if (!response.ok) {
296
+ throw new Error(`${response.status} ${response.statusText}`);
297
+ }
298
+
299
+ const html = await response.text();
300
+ top.content = extractMainContent(html);
301
+ } catch (error) {
302
+ console.error(
303
+ `Warning: failed to fetch top result page ${top.url}: ${error instanceof Error ? error.message : String(error)}.`,
304
+ );
305
+ }
306
+ }
307
+
308
+ function extractMainContent(html) {
309
+ const mainMatch = html.match(/<main[\s\S]*?>([\s\S]*?)<\/main>/i);
310
+ const source = mainMatch ? mainMatch[1] : html;
311
+
312
+ const withNoScript = source
313
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ')
314
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ');
315
+
316
+ const stripped = withNoScript
317
+ .replace(/<[^>]*>/g, ' ')
318
+ .replace(/&amp;/g, '&')
319
+ .replace(/&lt;/g, '<')
320
+ .replace(/&gt;/g, '>')
321
+ .replace(/&quot;/g, '"')
322
+ .replace(/&#39;/g, "'")
323
+ .replace(/\s+/g, ' ')
324
+ .trim();
325
+
326
+ if (stripped.length > CONTENT_MAX_LENGTH) {
327
+ return `${stripped.slice(0, CONTENT_MAX_LENGTH)}...`;
328
+ }
329
+
330
+ return stripped;
331
+ }
332
+
333
+ function clampLimit(value) {
334
+ if (!Number.isInteger(value) || value < 1) {
335
+ return DEFAULT_LIMIT;
336
+ }
337
+
338
+ if (value > MAX_LIMIT) {
339
+ return MAX_LIMIT;
340
+ }
341
+
342
+ return value;
343
+ }
344
+
345
+ async function safeText(response) {
346
+ try {
347
+ return await response.text();
348
+ } catch {
349
+ return '';
350
+ }
351
+ }
352
+
353
+ function printMarkdown(query, version, results) {
354
+ console.log(`# Angular documentation search: ${query}`);
355
+ console.log(`Version: angular_v${version}`);
356
+ console.log('');
357
+
358
+ if (!results.length) {
359
+ console.log('No results found.');
360
+ return;
361
+ }
362
+
363
+ for (const item of results) {
364
+ console.log(`## ${item.title}`);
365
+ if (item.breadcrumb) {
366
+ console.log(`Breadcrumb: ${item.breadcrumb}`);
367
+ }
368
+ if (item.url) {
369
+ console.log(item.url);
370
+ }
371
+ if (item.content) {
372
+ console.log('');
373
+ console.log(item.content);
374
+ }
375
+ console.log('');
376
+ }
377
+ }
378
+
379
+ function printHelp() {
380
+ console.log(`
381
+ Usage:
382
+ node scripts/search-documentation.mjs "query terms" [--version 22] [--limit 10] [--include-top-content] [--json] [--pretty]
383
+
384
+ Options:
385
+ --version <major> Docs major version (minimum ${MIN_VERSION}, default ${KNOWN_LATEST_VERSION}).
386
+ --limit <n> Max number of results (default 10, clamped to 1-20).
387
+ --include-top-content Fetch and include the top angular.dev page content.
388
+ --json Output JSON only.
389
+ --pretty Pretty-print JSON output.
390
+ --help Show this help.
391
+
392
+ Examples:
393
+ node scripts/search-documentation.mjs "signals resource" --version 22 --limit 5
394
+ node scripts/search-documentation.mjs "signal forms validation" --version 22 --include-top-content
395
+ `);
396
+ }