@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,86 @@
1
+ import {
2
+ hasGum,
3
+ gumChoose,
4
+ promptQuestion,
5
+ stripAnsi
6
+ } from './terminal.js';
7
+ import { copyToClipboard } from './audit.js';
8
+
9
+ const PROMPT_MARKER = 'Copy and paste the block below directly into Cursor / Claude / Windsurf:';
10
+
11
+ export const extractPromptFromContent = (content) => {
12
+ if (!content?.includes(PROMPT_MARKER)) return null;
13
+ const clean = stripAnsi(content);
14
+ const markerIdx = clean.indexOf(PROMPT_MARKER);
15
+ if (markerIdx === -1) return null;
16
+
17
+ const afterMarker = clean.slice(markerIdx + PROMPT_MARKER.length);
18
+ const borderStartIdx = afterMarker.indexOf('├');
19
+ const sliceFrom = borderStartIdx !== -1 ? afterMarker.slice(borderStartIdx) : afterMarker;
20
+
21
+ const lines = sliceFrom.split('\n');
22
+ const promptLines = [];
23
+ let recording = false;
24
+
25
+ for (const line of lines) {
26
+ if (line.includes('├')) {
27
+ recording = true;
28
+ continue;
29
+ }
30
+ if (line.includes('└')) break;
31
+ if (recording) {
32
+ promptLines.push(line.replace(/^\s*│\s?/, ''));
33
+ }
34
+ }
35
+
36
+ const result = promptLines.join('\n').trim();
37
+ return result.length > 0 ? result : null;
38
+ };
39
+
40
+ export const showPagedContent = async (content, promptText = null) => {
41
+ if (process.stdout.isTTY) console.clear();
42
+ process.stdout.write(content + '\n\n');
43
+
44
+ const effectivePrompt = (promptText && promptText.trim().length > 0)
45
+ ? promptText.trim()
46
+ : extractPromptFromContent(content);
47
+
48
+ if (!effectivePrompt) {
49
+ if (hasGum()) gumChoose(['<-- Back to Audit Dashboard']);
50
+ else await promptQuestion('Press Enter to return to menu...');
51
+ return;
52
+ }
53
+
54
+ while (true) {
55
+ if (hasGum()) {
56
+ const choice = gumChoose(['1. 📋 Copy AI Agent Prompt to Clipboard', '2. <-- Back to Audit Dashboard']);
57
+ if (choice?.startsWith('1.')) {
58
+ const success = copyToClipboard(effectivePrompt);
59
+ if (success) {
60
+ process.stdout.write('\n\x1b[1m\x1b[32m✔ AI Agent refactoring prompt copied to clipboard!\x1b[0m\n');
61
+ process.stdout.write('\x1b[36mPaste directly into Cursor, Claude, or Windsurf to begin automated refactoring.\x1b[0m\n\n');
62
+ } else {
63
+ process.stdout.write('\n\x1b[33m⚠ Could not access system clipboard.\x1b[0m\n\n');
64
+ }
65
+ gumChoose(['<-- Back to Audit Dashboard']);
66
+ break;
67
+ }
68
+ break;
69
+ }
70
+
71
+ process.stdout.write('\n\x1b[1mOptions:\x1b[0m\n [c] 📋 Copy AI Agent Prompt to Clipboard\n [Enter] <-- Back to Audit Dashboard\n');
72
+ const input = await promptQuestion('Select option [Enter]: ');
73
+ if (input.trim().toLowerCase() === 'c') {
74
+ const success = copyToClipboard(effectivePrompt);
75
+ if (success) {
76
+ process.stdout.write('\n\x1b[1m\x1b[32m✔ AI Agent refactoring prompt copied to clipboard!\x1b[0m\n');
77
+ process.stdout.write('\x1b[36mPaste directly into Cursor, Claude, or Windsurf to begin automated refactoring.\x1b[0m\n\n');
78
+ } else {
79
+ process.stdout.write('\n\x1b[33m⚠ Could not access system clipboard.\x1b[0m\n\n');
80
+ }
81
+ await promptQuestion('Press Enter to return to Audit Dashboard...');
82
+ break;
83
+ }
84
+ break;
85
+ }
86
+ };
@@ -0,0 +1,129 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import {
5
+ hasGum,
6
+ gumChoose,
7
+ gumInput,
8
+ promptQuestion,
9
+ openBrowser
10
+ } from './terminal.js';
11
+ import {
12
+ detectGitRepoInfo,
13
+ detectGitHubUser,
14
+ getAuditBaseline,
15
+ createSnapshotFromReport,
16
+ getAuditHistory,
17
+ generateTransformationDiscussionContent,
18
+ generateDiscussionContent,
19
+ publishDiscussion,
20
+ publishOrUpdateDiscussion,
21
+ getStoredDiscussion,
22
+ copyToClipboard,
23
+ ORG_DISCUSSIONS_URL,
24
+ DISCUSSION_CATEGORY,
25
+ DEFAULT_DISCUSSION_REPO,
26
+ DISCUSSION_CATEGORY_SLUG
27
+ } from './audit.js';
28
+
29
+ export const handleShareToDiscussions = async (report) => {
30
+ const repoInfo = detectGitRepoInfo();
31
+ const detectedUser = detectGitHubUser();
32
+ const defaultProject = repoInfo.nameWithOwner || path.basename(process.cwd());
33
+
34
+ const baseline = getAuditBaseline();
35
+ const currentSnapshot = createSnapshotFromReport(report);
36
+ const history = getAuditHistory();
37
+
38
+ const isScoreDifferent = baseline && baseline.health.score !== currentSnapshot.health.score;
39
+ const isViolationsDifferent = baseline && baseline.violations.total !== currentSnapshot.violations.total;
40
+ const hasTransformationHistory = Boolean(baseline && (history.length > 1 || isScoreDifferent || isViolationsDifferent));
41
+
42
+ let shareType = 'single';
43
+ if (hasTransformationHistory) {
44
+ if (hasGum()) {
45
+ const choice = gumChoose([
46
+ '1. 🚀 Post Transformation Showcase (Before vs. After Delta)',
47
+ '2. 📋 Post Single Audit Scorecard (Current Snapshot Only)'
48
+ ]);
49
+ if (choice?.includes('1.')) shareType = 'transformation';
50
+ } else {
51
+ process.stdout.write('\nSelect Discussion Post Format:\n');
52
+ process.stdout.write(' [1] 🚀 Post Transformation Showcase (Before vs. After Delta)\n');
53
+ process.stdout.write(' [2] 📋 Post Single Audit Scorecard (Current Snapshot Only)\n');
54
+ const choice = await promptQuestion('Choice [1]: ');
55
+ if (!choice || choice.trim() === '1') shareType = 'transformation';
56
+ }
57
+ }
58
+
59
+ let detectedSite = '';
60
+ try {
61
+ const pkgPath = path.resolve(process.cwd(), 'package.json');
62
+ if (fs.existsSync(pkgPath)) {
63
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
64
+ detectedSite = pkg.homepage || pkg.website || '';
65
+ }
66
+ } catch {}
67
+
68
+ const stored = getStoredDiscussion();
69
+ if (!detectedSite && stored?.website) {
70
+ detectedSite = stored.website;
71
+ }
72
+
73
+ let user = detectedUser;
74
+ let projectName = defaultProject;
75
+ let liveUrl = detectedSite;
76
+ const modeLabel = shareType === 'transformation' ? 'Transformation Showcase (Delta)' : 'Single Audit Scorecard';
77
+
78
+ if (hasGum()) {
79
+ spawnSync('gum', ['style', '--border=rounded', '--border-foreground=81', '--padding=0 2', '--bold',
80
+ `Post Audit to GitHub Discussions\nBoard: ${ORG_DISCUSSIONS_URL}\nCategory: ${DISCUSSION_CATEGORY}\nMode: ${modeLabel}`
81
+ ], { stdio: 'inherit' });
82
+
83
+ user = gumInput('Your GitHub username:', detectedUser) || detectedUser;
84
+ projectName = gumInput('Project name for audit post:', defaultProject) || defaultProject;
85
+ liveUrl = gumInput('Website URL (optional):', detectedSite) || detectedSite;
86
+ } else {
87
+ process.stdout.write(`\n\x1b[1m\x1b[38;2;98;201;255mPost Audit to GitHub Discussions (${DISCUSSION_CATEGORY})\x1b[0m\n`);
88
+ user = (await promptQuestion(`Your GitHub username [@${detectedUser}]: `)) || detectedUser;
89
+ projectName = (await promptQuestion(`Project name for audit post [${defaultProject}]: `)) || defaultProject;
90
+ liveUrl = (await promptQuestion(`Website URL (optional) [${detectedSite}]: `)) || detectedSite;
91
+ }
92
+
93
+ const isTransformationPost = shareType === 'transformation' && Boolean(baseline);
94
+ const { title, category, categorySlug, body } = isTransformationPost
95
+ ? generateTransformationDiscussionContent(baseline, currentSnapshot, user, projectName, repoInfo.url, liveUrl)
96
+ : generateDiscussionContent(report, user, projectName, repoInfo.url, liveUrl);
97
+
98
+ process.stdout.write('\nAttempting publish to GitHub Discussions...\n');
99
+ const pubResult = await publishOrUpdateDiscussion(DEFAULT_DISCUSSION_REPO, title, body, category, {
100
+ projectName,
101
+ website: liveUrl
102
+ });
103
+
104
+ if (pubResult.success && pubResult.url) {
105
+ if (pubResult.updated) {
106
+ process.stdout.write(`\n\x1b[1m\x1b[32m✔ Successfully updated discussion topic #${pubResult.discussionNumber}!\x1b[0m\n`);
107
+ process.stdout.write(' \x1b[33mPrevious audit report was archived as a comment in the thread.\x1b[0m\n');
108
+ process.stdout.write(`Discussion URL: \x1b[36m${pubResult.url}\x1b[0m\n\n`);
109
+ } else {
110
+ process.stdout.write(`\n\x1b[1m\x1b[32m✔ Successfully published discussion topic!\x1b[0m\nDiscussion URL: \x1b[36m${pubResult.url}\x1b[0m\n\n`);
111
+ }
112
+ openBrowser(pubResult.url);
113
+ } else {
114
+ copyToClipboard(body);
115
+ const targetSlug = categorySlug || DISCUSSION_CATEGORY_SLUG || 'npx-chemx-audit';
116
+ const fallbackUrl = pubResult.discussionNumber
117
+ ? `https://github.com/${DEFAULT_DISCUSSION_REPO}/discussions/${pubResult.discussionNumber}`
118
+ : `${ORG_DISCUSSIONS_URL}/new?category=${encodeURIComponent(targetSlug)}&title=${encodeURIComponent(title)}`;
119
+ process.stdout.write('\n\x1b[32m✔ Formatted audit report copied to your system clipboard!\x1b[0m\n');
120
+ process.stdout.write(`Opening GitHub Discussions in default browser:\n \x1b[36m${fallbackUrl}\x1b[0m\n\n`);
121
+ openBrowser(fallbackUrl);
122
+ }
123
+
124
+ if (hasGum()) {
125
+ gumChoose(['<-- Back to Audit Dashboard']);
126
+ } else {
127
+ await promptQuestion('Press Enter to return to menu...');
128
+ }
129
+ };
@@ -0,0 +1,43 @@
1
+ import type { AuditReport } from './audit/types';
2
+
3
+ export declare function resolveGradeColor(grade: string): string;
4
+ export declare function resolveGradeBadge(grade: string, width?: number): string;
5
+ export declare function resolveHealthHearts(grade: string, score?: number, isAnsi?: boolean): string;
6
+ export declare function resolveCriticalGrade(count: number): string;
7
+ export declare function resolveHighMedGrade(count: number): string;
8
+ export declare function resolveLowGrade(count: number): string;
9
+ export declare function resolvePillarGrade(pillars: Record<string, unknown>): string;
10
+ export declare function resolveHotspotGrade(hotspots: readonly { readonly lineCount: number }[]): string;
11
+ export declare function resolveContextGrade(riskTier: string): string;
12
+
13
+ export interface NavigatorMenuItem {
14
+ key?: string;
15
+ grade?: string;
16
+ tag: string;
17
+ label: string;
18
+ index?: number;
19
+ action: () => Promise<void>;
20
+ }
21
+
22
+ export declare function formatButtonTag(label: string, color?: string, width?: number): string;
23
+ export declare function buildNavigatorMenu(
24
+ topActions: readonly NavigatorMenuItem[],
25
+ activeGrades: readonly NavigatorMenuItem[],
26
+ midActions: readonly NavigatorMenuItem[],
27
+ bottomActions: readonly NavigatorMenuItem[]
28
+ ): { menuItems: NavigatorMenuItem[]; menuOptions: string[] };
29
+
30
+ export declare function extractPromptFromContent(content: string): string | null;
31
+ export declare function showPagedContent(content: string, promptText?: string | null): Promise<void>;
32
+ export declare function showConversionMenu(onScaffold?: (() => Promise<void>) | null): Promise<void>;
33
+ export declare function handleShareToDiscussions(report: AuditReport): Promise<void>;
34
+ export declare function renderDashboardBanner(
35
+ health: AuditReport['health'],
36
+ metrics: AuditReport['metrics'],
37
+ violations: AuditReport['violations'],
38
+ critical: readonly unknown[],
39
+ highMediumCount: number,
40
+ low: readonly unknown[],
41
+ contextAnalysis?: AuditReport['contextAnalysis'] | null
42
+ ): void;
43
+ export declare function runInteractiveAuditNavigator(report: AuditReport, onScaffold?: (() => Promise<void>) | null): Promise<void>;
@@ -0,0 +1,126 @@
1
+ import {
2
+ hasGum,
3
+ gumChoose,
4
+ promptQuestion,
5
+ stripAnsi,
6
+ confirmAction
7
+ } from "./terminal.js";
8
+ import { saveAuditSnapshot, groupViolationsBySeverity } from "./audit.js";
9
+ import { areGuardrailsInstalled } from "./installer.js";
10
+ import { renderDashboardBanner } from "./navigator-banner.js";
11
+ import {
12
+ formatButtonTag,
13
+ buildNavigatorMenu,
14
+ matchGumChoice,
15
+ matchFallbackChoice
16
+ } from "./navigator-menu.js";
17
+ import {
18
+ buildActiveGrades,
19
+ buildDashboardActionGroups
20
+ } from "./navigator-actions.js";
21
+ import { handleShareToDiscussions } from "./navigator-share.js";
22
+
23
+ export {
24
+ resolveGradeColor,
25
+ resolveGradeBadge,
26
+ resolveHealthHearts,
27
+ resolveCriticalGrade,
28
+ resolveHighMedGrade,
29
+ resolveLowGrade,
30
+ resolvePillarGrade,
31
+ resolveHotspotGrade,
32
+ resolveContextGrade
33
+ } from "./navigator-grades.js";
34
+ export { extractPromptFromContent, showPagedContent } from "./navigator-paged.js";
35
+ export { showConversionMenu } from "./navigator-conversion.js";
36
+ export { handleShareToDiscussions } from "./navigator-share.js";
37
+ export { formatButtonTag, buildNavigatorMenu } from "./navigator-menu.js";
38
+ export { renderDashboardBanner } from "./navigator-banner.js";
39
+
40
+ export const runInteractiveAuditNavigator = async (report, onScaffold = null) => {
41
+ saveAuditSnapshot(report);
42
+ const { metrics, health, violations, contextAnalysis } = report;
43
+ const { critical, high, medium, low } = groupViolationsBySeverity(violations);
44
+ const highMediumCount = high.length + medium.length;
45
+
46
+ const activeGrades = buildActiveGrades(report);
47
+ const actions = buildDashboardActionGroups({ report, onScaffold });
48
+
49
+ renderDashboardBanner(health, metrics, violations, critical, highMediumCount, low, contextAnalysis);
50
+
51
+ const shouldPublish = await confirmAction(
52
+ "Publish audit report and promote your project to our GitHub Discussions Audits Forum?",
53
+ "Publish Report",
54
+ "Skip to Menu",
55
+ true
56
+ );
57
+
58
+ if (shouldPublish) {
59
+ await handleShareToDiscussions(report);
60
+ }
61
+
62
+ while (true) {
63
+ renderDashboardBanner(health, metrics, violations, critical, highMediumCount, low, contextAnalysis);
64
+
65
+ const guardrailsInstalled = areGuardrailsInstalled(process.cwd());
66
+ const topActions = [];
67
+ if (!guardrailsInstalled) {
68
+ topActions.push(actions.installAction);
69
+ }
70
+ topActions.push(actions.upgradeAction, actions.reportAction);
71
+
72
+ const midActions = [actions.shareAction, actions.progressAction, actions.exportAction];
73
+ const bottomActions = [];
74
+ if (actions.shouldShowPromptAction) {
75
+ bottomActions.push(actions.copyPromptAction);
76
+ }
77
+ bottomActions.push(actions.exitAction);
78
+
79
+ const { menuItems, menuOptions } = buildNavigatorMenu(
80
+ topActions,
81
+ activeGrades,
82
+ midActions,
83
+ bottomActions
84
+ );
85
+
86
+ if (hasGum()) {
87
+ const choice = gumChoose(menuOptions, "Select an audit section or grade to inspect:");
88
+ const cleanChoice = stripAnsi(choice || "").trim();
89
+
90
+ const isSeparator = !choice || cleanChoice.includes("───") || cleanChoice.includes("---");
91
+ if (isSeparator) {
92
+ continue;
93
+ }
94
+
95
+ const match = matchGumChoice(cleanChoice, menuItems);
96
+ const shouldExit = !match || match.key === "exit";
97
+ if (shouldExit) {
98
+ break;
99
+ }
100
+
101
+ await match.action();
102
+ } else {
103
+ for (const opt of menuOptions) {
104
+ process.stdout.write(` ${opt}\n`);
105
+ }
106
+ process.stdout.write("\n");
107
+
108
+ const exitIndex = actions.exitAction.index;
109
+ const promptMsg = `Select option [1-${exitIndex}] (default: ${exitIndex}): `;
110
+ const selection = await promptQuestion(promptMsg);
111
+ const effective = selection.trim().toLowerCase() || String(exitIndex);
112
+
113
+ const isSep = effective.includes("---") || effective.includes("───");
114
+ if (isSep) continue;
115
+
116
+ const match = matchFallbackChoice(effective, menuItems, exitIndex);
117
+ const shouldExit = !match || match.key === "exit";
118
+ if (shouldExit) {
119
+ break;
120
+ }
121
+
122
+ await match.action();
123
+ await promptQuestion("Press Enter to return to menu...");
124
+ }
125
+ }
126
+ };
@@ -0,0 +1,148 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { hasGum, gumInput, promptQuestion, renderBanner } from './terminal.js';
4
+ import { obtainLicenseKey, fetchStarterKitFiles, checkOrPromptEvaluation } from './license.js';
5
+
6
+ export const runScaffold = async (projectName, rawArgs = [], onRunAudit = null) => {
7
+ renderBanner('Chemical X: Molecular Architecture Scaffolder (npm create chemx)');
8
+
9
+ const licenseKey = await obtainLicenseKey(rawArgs, onRunAudit);
10
+ if (!licenseKey) {
11
+ process.stderr.write('\x1b[31m✕ Valid license key is required to scaffold blueprints.\x1b[0m\n');
12
+ process.exit(1);
13
+ }
14
+
15
+ let targetName = projectName;
16
+ if (!targetName) {
17
+ targetName = hasGum()
18
+ ? gumInput('Project directory name:', 'my-molecular-app')
19
+ : await promptQuestion('Project directory name [my-molecular-app]: ');
20
+ }
21
+
22
+ const finalDirName = targetName.trim() || 'my-molecular-app';
23
+ const targetDir = path.resolve(process.cwd(), finalDirName);
24
+
25
+ const dirExists = fs.existsSync(targetDir);
26
+ const isDirNonEmpty = dirExists && fs.readdirSync(targetDir).length > 0;
27
+ if (isDirNonEmpty) {
28
+ process.stderr.write(`\x1b[31m✕ Error: Directory '${finalDirName}' already exists and is not empty.\x1b[0m\n`);
29
+ process.exit(1);
30
+ }
31
+
32
+ const files = await fetchStarterKitFiles(licenseKey);
33
+
34
+ process.stdout.write(`Scaffolding Molecular Architecture into: \x1b[36m${finalDirName}/\x1b[0m\n`);
35
+ fs.mkdirSync(targetDir, { recursive: true });
36
+
37
+ for (const [relPath, content] of Object.entries(files)) {
38
+ const fullPath = path.join(targetDir, relPath);
39
+ const dirName = path.dirname(fullPath);
40
+ if (!fs.existsSync(dirName)) {
41
+ fs.mkdirSync(dirName, { recursive: true });
42
+ }
43
+ fs.writeFileSync(fullPath, content, 'utf-8');
44
+ process.stdout.write(` \x1b[32m✔\x1b[0m ${relPath}\n`);
45
+ }
46
+
47
+ const cursorRulesPath = path.join(targetDir, '.cursorrules');
48
+ if (!fs.existsSync(cursorRulesPath)) {
49
+ const rules = '# Chemical X Molecular Architecture Directives\nStrictly follow AGENTS.md rules. Never exceed 500 lines per file. All molecule capsules must stay under 100 lines.\n';
50
+ fs.writeFileSync(cursorRulesPath, rules, 'utf-8');
51
+ process.stdout.write(' \x1b[32m✔\x1b[0m .cursorrules\n');
52
+ }
53
+
54
+ process.stdout.write(
55
+ `\n\x1b[1m\x1b[32m✔ Molecular Architecture project created successfully at ${finalDirName}!\x1b[0m\n\n`
56
+ );
57
+ process.stdout.write('Next Steps:\n');
58
+ process.stdout.write(` 1. cd ${finalDirName}\n`);
59
+ process.stdout.write(' 2. Review AGENTS.md for line budgets and architecture standards\n');
60
+ process.stdout.write(' 3. Run npx chemx generate m-<feature> to create capsules\n');
61
+ process.stdout.write(' 4. Run npx chemx audit to scan for line budget compliance\n\n');
62
+ };
63
+
64
+ export const runInit = async (targetSubDir = 'src/chemical-x', rawArgs = [], onRunAudit = null) => {
65
+ renderBanner('Chemical X: In-Repo Capsule Drop-in');
66
+
67
+ const targetDir = path.resolve(process.cwd(), targetSubDir);
68
+ const licenseKey = await obtainLicenseKey(rawArgs, onRunAudit);
69
+ if (!licenseKey) {
70
+ process.stderr.write('\x1b[31m✕ Valid license key is required.\x1b[0m\n');
71
+ process.exit(1);
72
+ }
73
+
74
+ const files = await fetchStarterKitFiles(licenseKey);
75
+
76
+ process.stdout.write(`Unpacking blueprints and hooks into: \x1b[36m${targetSubDir}/\x1b[0m\n`);
77
+
78
+ let count = 0;
79
+ for (const [relPath, content] of Object.entries(files)) {
80
+ const fullPath = path.join(targetDir, relPath);
81
+ const dirName = path.dirname(fullPath);
82
+ if (!fs.existsSync(dirName)) {
83
+ fs.mkdirSync(dirName, { recursive: true });
84
+ }
85
+ fs.writeFileSync(fullPath, content, 'utf-8');
86
+ process.stdout.write(` \x1b[32m✔\x1b[0m ${relPath}\n`);
87
+ count++;
88
+ }
89
+
90
+ process.stdout.write(
91
+ `\n\x1b[1m\x1b[32m✔ Successfully installed ${count} Chemical X assets into ${targetSubDir}!\x1b[0m\n\n`
92
+ );
93
+ };
94
+
95
+ export const runGenerateCapsule = async (capsuleName) => {
96
+ await checkOrPromptEvaluation('generate capsule');
97
+
98
+ const normalizedName = capsuleName.startsWith('m-') ? capsuleName : `m-${capsuleName}`;
99
+ const targetDir = path.resolve(process.cwd(), normalizedName);
100
+
101
+ if (fs.existsSync(targetDir)) {
102
+ process.stderr.write(`\x1b[31m✕ Error: Directory ${normalizedName} already exists.\x1b[0m\n`);
103
+ process.exit(1);
104
+ }
105
+
106
+ fs.mkdirSync(targetDir, { recursive: true });
107
+
108
+ const pascalName = normalizedName
109
+ .split('-')
110
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
111
+ .join('');
112
+
113
+ const componentCode = `import React from 'react';
114
+ import type { ${pascalName}Props } from './types';
115
+
116
+ export const ${pascalName}: React.FC<${pascalName}Props> = ({ label }) => {
117
+ return (
118
+ <div className="${normalizedName}">
119
+ <span>{label}</span>
120
+ </div>
121
+ );
122
+ };
123
+
124
+ export default ${pascalName};
125
+ `;
126
+
127
+ const typesCode = `export interface ${pascalName}Props {
128
+ readonly label: string;
129
+ }
130
+ `;
131
+
132
+ const indexCode = `export { ${pascalName} } from './${normalizedName}';
133
+ export type { ${pascalName}Props } from './types';
134
+ `;
135
+
136
+ fs.writeFileSync(path.join(targetDir, `${normalizedName}.tsx`), componentCode, 'utf-8');
137
+ fs.writeFileSync(path.join(targetDir, 'types.d.ts'), typesCode, 'utf-8');
138
+ fs.writeFileSync(path.join(targetDir, 'index.ts'), indexCode, 'utf-8');
139
+
140
+ process.stdout.write(
141
+ `\x1b[32m✔ Successfully generated crystalline capsule:\x1b[0m ${normalizedName}/\n`
142
+ );
143
+ process.stdout.write(` - ${normalizedName}/${normalizedName}.tsx (< 50 lines)\n`);
144
+ process.stdout.write(` - ${normalizedName}/types.d.ts\n`);
145
+ process.stdout.write(` - ${normalizedName}/index.ts\n\n`);
146
+ };
147
+
148
+ export { printHelp } from './help.js';
@@ -0,0 +1,126 @@
1
+ import readline from "node:readline";
2
+ import { spawnSync } from "node:child_process";
3
+
4
+ export const openBrowser = (url) => {
5
+ const platform = process.platform;
6
+ try {
7
+ if (platform === "darwin") {
8
+ spawnSync("open", [url], { stdio: "ignore" });
9
+ } else if (platform === "win32") {
10
+ spawnSync("cmd.exe", ["/c", "start", '""', url], { stdio: "ignore" });
11
+ } else {
12
+ spawnSync("xdg-open", [url], { stdio: "ignore" });
13
+ }
14
+ } catch {}
15
+ };
16
+
17
+ export const hasGum = () => {
18
+ try {
19
+ return spawnSync("which", ["gum"], { stdio: "ignore" }).status === 0;
20
+ } catch {
21
+ return false;
22
+ }
23
+ };
24
+
25
+ export const gumChoose = (options, header = "") => {
26
+ const args = ["choose"];
27
+ if (header) {
28
+ args.push(`--header=${header}`, "--header.foreground=81");
29
+ }
30
+ const headerPadding = header ? 3 : 1;
31
+ const listHeight = options.length + headerPadding;
32
+ args.push("--cursor.foreground=81", `--height=${listHeight}`, ...options);
33
+ const res = spawnSync("gum", args, { encoding: "utf-8", stdio: ["inherit", "pipe", "inherit"] });
34
+ return (res.stdout || "").trim();
35
+ };
36
+
37
+ export const gumInput = (promptText, placeholder = "", isPassword = false) => {
38
+ const args = ["input", `--prompt=${promptText} `, `--placeholder=${placeholder}`];
39
+ if (isPassword) {
40
+ args.push("--password");
41
+ }
42
+ const res = spawnSync("gum", args, { encoding: "utf-8", stdio: ["inherit", "pipe", "inherit"] });
43
+ return (res.stdout || "").trim();
44
+ };
45
+
46
+ export const promptQuestion = (query) => {
47
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
48
+ return new Promise((resolve) => {
49
+ rl.question(query, (answer) => {
50
+ rl.close();
51
+ resolve(answer.trim());
52
+ });
53
+ });
54
+ };
55
+
56
+ export const stripAnsi = (text) => text.replace(/\x1b\[[0-9;]*m/g, "");
57
+
58
+ export const renderBanner = (title = "Chemical X Protocol: Molecular Architecture") => {
59
+ if (hasGum()) {
60
+ spawnSync(
61
+ "gum",
62
+ [
63
+ "style",
64
+ "--border=normal",
65
+ "--margin=1",
66
+ "--padding=1 2",
67
+ "--border-foreground=45",
68
+ "--foreground=81",
69
+ "--bold",
70
+ ` ${title}\n Zero-Context-Rot Scaffolding & Engineering Directives`
71
+ ],
72
+ { stdio: "inherit" }
73
+ );
74
+ } else {
75
+ process.stdout.write(
76
+ "\n\x1b[38;2;98;201;255m=====================================================\x1b[0m\n"
77
+ );
78
+ process.stdout.write(`\x1b[1m\x1b[38;2;98;201;255m ${title}\x1b[0m\n`);
79
+ process.stdout.write(" Zero-Context-Rot Scaffolding & Engineering Directives\n");
80
+ process.stdout.write(
81
+ "\x1b[38;2;98;201;255m=====================================================\x1b[0m\n\n"
82
+ );
83
+ }
84
+ };
85
+
86
+ export const gumConfirm = (
87
+ promptText = "Publish audit report and promote your project to our GitHub Discussions Audits Forum?",
88
+ affirmative = "Publish Report",
89
+ negative = "Skip to Menu",
90
+ defaultVal = true
91
+ ) => {
92
+ const args = [
93
+ "confirm",
94
+ promptText,
95
+ `--default=${defaultVal}`,
96
+ `--affirmative=${affirmative}`,
97
+ `--negative=${negative}`,
98
+ "--prompt.foreground=81",
99
+ "--selected.background=81",
100
+ "--selected.foreground=0"
101
+ ];
102
+ const res = spawnSync("gum", args, { stdio: "inherit" });
103
+ return res.status === 0;
104
+ };
105
+
106
+ export const promptConfirm = async (
107
+ query = "Publish audit report and promote your project to our GitHub Discussions Audits Forum?",
108
+ defaultVal = true
109
+ ) => {
110
+ const suffix = defaultVal ? " [Y/n]: " : " [y/N]: ";
111
+ const answer = await promptQuestion(`${query}${suffix}`);
112
+ if (!answer) return defaultVal;
113
+ return /^(y|yes)$/i.test(answer);
114
+ };
115
+
116
+ export const confirmAction = async (
117
+ promptText = "Publish audit report and promote your project to our GitHub Discussions Audits Forum?",
118
+ affirmative = "Publish Report",
119
+ negative = "Skip to Menu",
120
+ defaultVal = true
121
+ ) => {
122
+ if (hasGum()) {
123
+ return gumConfirm(promptText, affirmative, negative, defaultVal);
124
+ }
125
+ return promptConfirm(promptText, defaultVal);
126
+ };