@chemx/starter-kit 26.9.9-786 → 26.9.11-481

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 (45) hide show
  1. package/cli/audit/ast-visitors.js +243 -0
  2. package/cli/audit/discussion-store.js +39 -0
  3. package/cli/audit/history.d.ts +69 -0
  4. package/cli/audit/history.js +397 -0
  5. package/cli/audit/metrics.js +179 -0
  6. package/cli/audit/prompts.js +174 -0
  7. package/cli/audit/reporter-ascii.d.ts +4 -0
  8. package/cli/audit/reporter-ascii.js +99 -0
  9. package/cli/audit/reporter-grades.js +275 -0
  10. package/cli/audit/reporter-markdown.js +137 -0
  11. package/cli/audit/reporter-utils.js +125 -0
  12. package/cli/audit/reporter.js +497 -0
  13. package/cli/audit/rules-helpers.js +109 -0
  14. package/cli/audit/rules-registry.js +97 -0
  15. package/cli/audit/rules.js +123 -0
  16. package/cli/audit/social-constants.js +4 -0
  17. package/cli/audit/social-gh.js +126 -0
  18. package/cli/audit/social-git.js +214 -0
  19. package/cli/audit/social-http.js +222 -0
  20. package/cli/audit/social-publisher.js +193 -0
  21. package/cli/audit/social.d.ts +69 -0
  22. package/cli/audit/social.js +303 -0
  23. package/cli/audit/types.d.ts +140 -0
  24. package/cli/audit.d.ts +3 -24
  25. package/cli/audit.js +207 -210
  26. package/cli/help.d.ts +1 -0
  27. package/cli/help.js +136 -0
  28. package/cli/index.js +128 -566
  29. package/cli/installer-templates.js +91 -0
  30. package/cli/installer.d.ts +21 -0
  31. package/cli/installer.js +104 -0
  32. package/cli/license.js +320 -0
  33. package/cli/navigator-actions.js +274 -0
  34. package/cli/navigator-banner.js +98 -0
  35. package/cli/navigator-conversion.js +100 -0
  36. package/cli/navigator-grades.js +87 -0
  37. package/cli/navigator-menu.js +127 -0
  38. package/cli/navigator-paged.js +86 -0
  39. package/cli/navigator-share.js +129 -0
  40. package/cli/navigator.d.ts +43 -0
  41. package/cli/navigator.js +126 -0
  42. package/cli/scaffold.js +148 -0
  43. package/cli/terminal.js +126 -0
  44. package/docs/CHANGELOG.md +101 -0
  45. package/package.json +11 -10
@@ -0,0 +1,222 @@
1
+ import {
2
+ DISCUSSION_CATEGORY,
3
+ DISCUSSION_CATEGORY_SLUG
4
+ } from './social-constants.js';
5
+
6
+ export const findMatchedCategory = (categories, categoryName) => {
7
+ const exactName = categories.find((c) => c.name === categoryName);
8
+ if (exactName) return exactName;
9
+
10
+ const exactSlug = categories.find((c) => c.slug === categoryName);
11
+ if (exactSlug) return exactSlug;
12
+
13
+ const defaultSlug = categories.find((c) => c.slug === DISCUSSION_CATEGORY_SLUG);
14
+ if (defaultSlug) return defaultSlug;
15
+
16
+ const targetLower = categoryName.toLowerCase();
17
+ const lowerName = categories.find((c) => c.name.toLowerCase() === targetLower);
18
+ if (lowerName) return lowerName;
19
+
20
+ const lowerSlug = categories.find((c) => c.slug.toLowerCase() === targetLower);
21
+ if (lowerSlug) return lowerSlug;
22
+
23
+ const chemxName = categories.find((c) => c.name.toLowerCase().includes('chemx'));
24
+ if (chemxName) return chemxName;
25
+
26
+ const chemxSlug = categories.find((c) => c.slug.toLowerCase().includes('chemx'));
27
+ if (chemxSlug) return chemxSlug;
28
+
29
+ const auditName = categories.find((c) => c.name.toLowerCase().includes('audit'));
30
+ if (auditName) return auditName;
31
+
32
+ const auditSlug = categories.find((c) => c.slug.toLowerCase().includes('audit'));
33
+ if (auditSlug) return auditSlug;
34
+
35
+ const generalName = categories.find((c) => c.name.toLowerCase().includes('general'));
36
+ if (generalName) return generalName;
37
+
38
+ return categories[0];
39
+ };
40
+
41
+ export const publishDiscussionViaHttp = async (token, repo, title, body, categoryName = DISCUSSION_CATEGORY) => {
42
+ if (!token) return { success: false, url: null, id: null, number: null, error: 'No GitHub token provided' };
43
+
44
+ const [owner, name] = repo.split('/');
45
+ if (!owner || !name) return { success: false, url: null, id: null, number: null, error: `Invalid repository format: ${repo}` };
46
+
47
+ try {
48
+ const metaQuery = `
49
+ query($owner: String!, $name: String!) {
50
+ repository(owner: $owner, name: $name) {
51
+ id
52
+ discussionCategories(first: 25) {
53
+ nodes {
54
+ id
55
+ name
56
+ slug
57
+ emoji
58
+ }
59
+ }
60
+ }
61
+ }
62
+ `;
63
+
64
+ const metaRes = await fetch('https://api.github.com/graphql', {
65
+ method: 'POST',
66
+ headers: {
67
+ 'Authorization': `Bearer ${token}`,
68
+ 'Content-Type': 'application/json',
69
+ 'User-Agent': 'Chemical-X-Audit-CLI'
70
+ },
71
+ body: JSON.stringify({ query: metaQuery, variables: { owner, name } })
72
+ });
73
+
74
+ const metaJson = await metaRes.json();
75
+ if (metaJson.errors) {
76
+ return { success: false, url: null, id: null, number: null, error: metaJson.errors[0]?.message || 'GraphQL error fetching categories' };
77
+ }
78
+
79
+ const repositoryId = metaJson.data?.repository?.id;
80
+ const categories = metaJson.data?.repository?.discussionCategories?.nodes || [];
81
+
82
+ if (!repositoryId || categories.length === 0) {
83
+ return { success: false, url: null, id: null, number: null, error: `Repository ${repo} has no discussions enabled` };
84
+ }
85
+
86
+ const matchedCat = findMatchedCategory(categories, categoryName);
87
+
88
+ const mutation = `
89
+ mutation($repositoryId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
90
+ createDiscussion(input: { repositoryId: $repositoryId, categoryId: $categoryId, title: $title, body: $body }) {
91
+ discussion {
92
+ id
93
+ number
94
+ url
95
+ }
96
+ }
97
+ }
98
+ `;
99
+
100
+ const postRes = await fetch('https://api.github.com/graphql', {
101
+ method: 'POST',
102
+ headers: {
103
+ 'Authorization': `Bearer ${token}`,
104
+ 'Content-Type': 'application/json',
105
+ 'User-Agent': 'Chemical-X-Audit-CLI'
106
+ },
107
+ body: JSON.stringify({
108
+ query: mutation,
109
+ variables: { repositoryId, categoryId: matchedCat.id, title, body }
110
+ })
111
+ });
112
+
113
+ const postJson = await postRes.json();
114
+ if (postJson.errors) {
115
+ return { success: false, url: null, id: null, number: null, error: postJson.errors[0]?.message || 'Failed creating discussion' };
116
+ }
117
+
118
+ const disc = postJson.data?.createDiscussion?.discussion;
119
+ const url = disc?.url;
120
+ return {
121
+ success: Boolean(url),
122
+ url: url || null,
123
+ id: disc?.id || null,
124
+ number: disc?.number || null,
125
+ error: null
126
+ };
127
+ } catch (err) {
128
+ return { success: false, url: null, id: null, number: null, error: err.message };
129
+ }
130
+ };
131
+
132
+ export const viewDiscussionViaHttp = async (token, repo, discussionNumber) => {
133
+ const [owner, name] = repo.split('/');
134
+ if (!owner || !name) return null;
135
+
136
+ try {
137
+ const query = `
138
+ query($owner: String!, $name: String!, $number: Int!) {
139
+ repository(owner: $owner, name: $name) {
140
+ discussion(number: $number) {
141
+ id
142
+ number
143
+ title
144
+ body
145
+ url
146
+ }
147
+ }
148
+ }
149
+ `;
150
+ const res = await fetch('https://api.github.com/graphql', {
151
+ method: 'POST',
152
+ headers: {
153
+ 'Authorization': `Bearer ${token}`,
154
+ 'Content-Type': 'application/json',
155
+ 'User-Agent': 'Chemical-X-Audit-CLI'
156
+ },
157
+ body: JSON.stringify({ query, variables: { owner, name, number: Number(discussionNumber) } })
158
+ });
159
+ const json = await res.json();
160
+ return json.data?.repository?.discussion || null;
161
+ } catch {
162
+ return null;
163
+ }
164
+ };
165
+
166
+ export const postDiscussionCommentViaHttp = async (token, discussionId, commentBody) => {
167
+ try {
168
+ const mutation = `
169
+ mutation($discussionId: ID!, $body: String!) {
170
+ addDiscussionComment(input: { discussionId: $discussionId, body: $body }) {
171
+ comment {
172
+ id
173
+ url
174
+ }
175
+ }
176
+ }
177
+ `;
178
+ const res = await fetch('https://api.github.com/graphql', {
179
+ method: 'POST',
180
+ headers: {
181
+ 'Authorization': `Bearer ${token}`,
182
+ 'Content-Type': 'application/json',
183
+ 'User-Agent': 'Chemical-X-Audit-CLI'
184
+ },
185
+ body: JSON.stringify({ query: mutation, variables: { discussionId, body: commentBody } })
186
+ });
187
+ const json = await res.json();
188
+ return Boolean(json.data?.addDiscussionComment?.comment?.id);
189
+ } catch {
190
+ return false;
191
+ }
192
+ };
193
+
194
+ export const editDiscussionViaHttp = async (token, discussionId, title, body) => {
195
+ try {
196
+ const mutation = `
197
+ mutation($discussionId: ID!, $title: String!, $body: String!) {
198
+ updateDiscussion(input: { discussionId: $discussionId, title: $title, body: $body }) {
199
+ discussion {
200
+ id
201
+ number
202
+ url
203
+ title
204
+ }
205
+ }
206
+ }
207
+ `;
208
+ const res = await fetch('https://api.github.com/graphql', {
209
+ method: 'POST',
210
+ headers: {
211
+ 'Authorization': `Bearer ${token}`,
212
+ 'Content-Type': 'application/json',
213
+ 'User-Agent': 'Chemical-X-Audit-CLI'
214
+ },
215
+ body: JSON.stringify({ query: mutation, variables: { discussionId, title, body } })
216
+ });
217
+ const json = await res.json();
218
+ return Boolean(json.data?.updateDiscussion?.discussion?.id);
219
+ } catch {
220
+ return false;
221
+ }
222
+ };
@@ -0,0 +1,193 @@
1
+ import {
2
+ DISCUSSION_CATEGORY,
3
+ DISCUSSION_CATEGORY_SLUG,
4
+ ORG_DISCUSSIONS_URL,
5
+ DEFAULT_DISCUSSION_REPO
6
+ } from './social-constants.js';
7
+
8
+ import {
9
+ getStoredDiscussion,
10
+ saveStoredDiscussion,
11
+ clearStoredDiscussion
12
+ } from './discussion-store.js';
13
+
14
+ import {
15
+ findMatchedCategory,
16
+ publishDiscussionViaHttp,
17
+ viewDiscussionViaHttp,
18
+ postDiscussionCommentViaHttp,
19
+ editDiscussionViaHttp
20
+ } from './social-http.js';
21
+
22
+ import {
23
+ publishDiscussionViaGh,
24
+ viewDiscussionViaGh,
25
+ postDiscussionCommentViaGh,
26
+ editDiscussionViaGh,
27
+ findExistingDiscussionViaGh
28
+ } from './social-gh.js';
29
+
30
+ export {
31
+ DISCUSSION_CATEGORY,
32
+ DISCUSSION_CATEGORY_SLUG,
33
+ ORG_DISCUSSIONS_URL,
34
+ DEFAULT_DISCUSSION_REPO
35
+ } from './social-constants.js';
36
+
37
+ export {
38
+ getStoredDiscussion,
39
+ saveStoredDiscussion,
40
+ clearStoredDiscussion
41
+ } from './discussion-store.js';
42
+
43
+ export {
44
+ findMatchedCategory,
45
+ publishDiscussionViaHttp,
46
+ viewDiscussionViaHttp,
47
+ postDiscussionCommentViaHttp,
48
+ editDiscussionViaHttp
49
+ } from './social-http.js';
50
+
51
+ export {
52
+ publishDiscussionViaGh,
53
+ viewDiscussionViaGh,
54
+ postDiscussionCommentViaGh,
55
+ editDiscussionViaGh,
56
+ findExistingDiscussionViaGh
57
+ } from './social-gh.js';
58
+
59
+ export const formatArchiveComment = (previousTitle, previousBody) => {
60
+ const archiveTimestamp = new Date().toUTCString();
61
+ return [
62
+ '## 📜 Previous Audit Snapshot (Archived)',
63
+ '',
64
+ `> **Archived on:** ${archiveTimestamp}`,
65
+ `> **Previous Status:** ${previousTitle || 'Prior Report'}`,
66
+ '',
67
+ '---',
68
+ '',
69
+ previousBody
70
+ ].join('\n');
71
+ };
72
+
73
+ export const publishDiscussion = async (repo, title, body, category = DISCUSSION_CATEGORY) => {
74
+ const envToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
75
+ if (envToken) {
76
+ const httpResult = await publishDiscussionViaHttp(envToken, repo, title, body, category);
77
+ if (httpResult.success) {
78
+ return httpResult;
79
+ }
80
+ }
81
+
82
+ return publishDiscussionViaGh(repo, title, body, category);
83
+ };
84
+
85
+ export const publishOrUpdateDiscussion = async (
86
+ repo = DEFAULT_DISCUSSION_REPO,
87
+ title,
88
+ body,
89
+ category = DISCUSSION_CATEGORY,
90
+ options = {}
91
+ ) => {
92
+ const envToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
93
+ const stored = getStoredDiscussion();
94
+ const targetRepo = repo || DEFAULT_DISCUSSION_REPO;
95
+
96
+ let existingDiscussion = null;
97
+
98
+ if (stored && stored.number && (!stored.repo || stored.repo === targetRepo)) {
99
+ if (envToken) {
100
+ existingDiscussion = await viewDiscussionViaHttp(envToken, targetRepo, stored.number);
101
+ }
102
+ if (!existingDiscussion) {
103
+ existingDiscussion = viewDiscussionViaGh(targetRepo, stored.number);
104
+ }
105
+ }
106
+
107
+ if (!existingDiscussion && options.projectName) {
108
+ existingDiscussion = findExistingDiscussionViaGh(targetRepo, options.projectName);
109
+ }
110
+
111
+ if (existingDiscussion && existingDiscussion.number) {
112
+ const discussionNumber = existingDiscussion.number;
113
+ const discussionId = existingDiscussion.id || stored?.discussionId;
114
+ const previousBody = existingDiscussion.body || '';
115
+
116
+ const hasPreviousBody = previousBody.trim().length > 0;
117
+ if (hasPreviousBody) {
118
+ const archiveComment = formatArchiveComment(existingDiscussion.title, previousBody);
119
+
120
+ if (envToken && discussionId) {
121
+ await postDiscussionCommentViaHttp(envToken, discussionId, archiveComment);
122
+ } else {
123
+ postDiscussionCommentViaGh(targetRepo, discussionNumber, archiveComment);
124
+ }
125
+ }
126
+
127
+ let editSuccess = false;
128
+ if (envToken && discussionId) {
129
+ editSuccess = await editDiscussionViaHttp(envToken, discussionId, title, body);
130
+ }
131
+ if (!editSuccess) {
132
+ editSuccess = editDiscussionViaGh(targetRepo, discussionNumber, title, body);
133
+ }
134
+
135
+ const fallbackUrl = `https://github.com/${targetRepo}/discussions/${discussionNumber}`;
136
+ const discussionUrl = existingDiscussion.url || stored?.url || fallbackUrl;
137
+
138
+ if (editSuccess) {
139
+ saveStoredDiscussion({
140
+ repo: targetRepo,
141
+ number: discussionNumber,
142
+ discussionId,
143
+ url: discussionUrl,
144
+ projectName: options.projectName || stored?.projectName || '',
145
+ website: options.website || stored?.website || '',
146
+ title,
147
+ lastPublishedAt: new Date().toISOString()
148
+ });
149
+
150
+ return {
151
+ success: true,
152
+ url: discussionUrl,
153
+ updated: true,
154
+ discussionNumber,
155
+ error: null
156
+ };
157
+ }
158
+ }
159
+
160
+ const pubResult = await publishDiscussion(targetRepo, title, body, category);
161
+ if (pubResult.success && pubResult.url) {
162
+ const match = pubResult.url.match(/\/discussions\/(\d+)/);
163
+ const parsedNumber = match ? parseInt(match[1], 10) : null;
164
+ const discussionNumber = pubResult.number || parsedNumber;
165
+
166
+ saveStoredDiscussion({
167
+ repo: targetRepo,
168
+ number: discussionNumber,
169
+ discussionId: pubResult.id || null,
170
+ url: pubResult.url,
171
+ projectName: options.projectName || '',
172
+ website: options.website || '',
173
+ title,
174
+ lastPublishedAt: new Date().toISOString()
175
+ });
176
+
177
+ return {
178
+ success: true,
179
+ url: pubResult.url,
180
+ updated: false,
181
+ discussionNumber,
182
+ error: null
183
+ };
184
+ }
185
+
186
+ return {
187
+ success: false,
188
+ url: null,
189
+ updated: false,
190
+ discussionNumber: null,
191
+ error: pubResult.error
192
+ };
193
+ };
@@ -0,0 +1,69 @@
1
+ import type { AuditReport } from './types';
2
+ import type { AuditSnapshot } from './history';
3
+
4
+ export interface DiscussionContent {
5
+ readonly title: string;
6
+ readonly category: string;
7
+ readonly categorySlug: string;
8
+ readonly body: string;
9
+ }
10
+
11
+ export interface GitRepoInfo {
12
+ readonly owner: string;
13
+ readonly repo: string;
14
+ readonly nameWithOwner: string;
15
+ readonly url: string;
16
+ }
17
+
18
+ export declare const DISCUSSION_CATEGORY: string;
19
+ export declare const DISCUSSION_CATEGORY_SLUG: string;
20
+ export declare const ORG_DISCUSSIONS_URL: string;
21
+ export declare const DEFAULT_DISCUSSION_REPO: string;
22
+
23
+ export declare function parseGitRemoteUrl(url: string): GitRepoInfo | null;
24
+ export declare function detectGitRepoInfo(cwd?: string): GitRepoInfo;
25
+ export declare function detectGitHubUser(preferredUser?: string): string;
26
+ export declare function copyToClipboard(text: string): boolean;
27
+ export declare function resolveBadgeColor(score: number): string;
28
+ export declare function resolveHotspotTierText(lineCount: number): string;
29
+ export declare function resolvePillarProgressionBadge(isImproved: boolean, beforeStatus: string, afterStatus: string): string;
30
+ export declare function formatWebUrl(url: string): string;
31
+ export declare function resolveExcessCostPerPass(tokensObj?: any, fallbackCostPerMillion?: number): number;
32
+ export declare function resolveMonthlyWastePerDev(tokensObj?: any, fallbackCostPerMillion?: number): number;
33
+ export declare function resolveWeeklyWastePerDev(tokensObj?: any, fallbackCostPerMillion?: number): number;
34
+ export declare function generateDiscussionContent(report: AuditReport, username: string, projectName?: string, repoUrl?: string, liveUrl?: string): DiscussionContent;
35
+ export declare function generateTransformationDiscussionContent(beforeSnapshot: AuditSnapshot, afterSnapshot: AuditSnapshot, username: string, projectName?: string, repoUrl?: string, liveUrl?: string): DiscussionContent;
36
+ export declare function publishDiscussionViaGh(repo: string, title: string, body: string, category?: string): { success: boolean; url: string | null; error?: string | null };
37
+ export declare function publishDiscussionViaHttp(token: string, repo: string, title: string, body: string, categoryName?: string): Promise<{ success: boolean; url: string | null; error?: string | null }>;
38
+ export declare function publishDiscussion(repo: string, title: string, body: string, category?: string): Promise<{ success: boolean; url: string | null; error?: string | null }>;
39
+
40
+ export interface StoredDiscussion {
41
+ readonly repo: string;
42
+ readonly number: number | null;
43
+ readonly discussionId?: string | null;
44
+ readonly url: string;
45
+ readonly projectName?: string;
46
+ readonly website?: string;
47
+ readonly title: string;
48
+ readonly lastPublishedAt: string;
49
+ }
50
+
51
+ export interface DiscussionPublishResult {
52
+ readonly success: boolean;
53
+ readonly url: string | null;
54
+ readonly updated?: boolean;
55
+ readonly discussionNumber?: number | null;
56
+ readonly error?: string | null;
57
+ }
58
+
59
+ export declare function getStoredDiscussion(cwd?: string): StoredDiscussion | null;
60
+ export declare function saveStoredDiscussion(data: StoredDiscussion, cwd?: string): boolean;
61
+ export declare function clearStoredDiscussion(cwd?: string): boolean;
62
+ export declare function formatArchiveComment(previousTitle: string, previousBody: string): string;
63
+ export declare function publishOrUpdateDiscussion(
64
+ repo?: string,
65
+ title?: string,
66
+ body?: string,
67
+ category?: string,
68
+ options?: { readonly projectName?: string; readonly website?: string }
69
+ ): Promise<DiscussionPublishResult>;