@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,97 @@
1
+ export const PILLARS = {
2
+ PILLAR_1: 'Line Budgets & Monolith Decomposition',
3
+ PILLAR_2: 'Control Flow & Boolean Logic',
4
+ PILLAR_3: 'Reactivity & Composable Contracts',
5
+ PILLAR_4: 'Type Architecture & Data Integrity',
6
+ PILLAR_5: 'Design System & Styling Hygiene',
7
+ PILLAR_6: 'Timers & Macro-Task Discipline',
8
+ PILLAR_7: 'Global Hygiene & Typography'
9
+ };
10
+
11
+ export const RULE_REGISTRY = {
12
+ LINE_BUDGET_FILE: {
13
+ pillar: PILLARS.PILLAR_1,
14
+ severity: 'MEDIUM',
15
+ directive: 'Decompose monolith into domain capsules and molecules'
16
+ },
17
+ LINE_BUDGET_MOLECULE: {
18
+ pillar: PILLARS.PILLAR_1,
19
+ severity: 'MEDIUM',
20
+ directive: 'Split molecule into focused sub-molecules or extract state to hook'
21
+ },
22
+ VIEW_MONOLITH: {
23
+ pillar: PILLARS.PILLAR_1,
24
+ severity: 'HIGH',
25
+ directive: 'Refactor top-level view to a 10 to 20 line Table of Contents'
26
+ },
27
+ CONTROL_FLOW_INLINE_BOOLEAN: {
28
+ pillar: PILLARS.PILLAR_2,
29
+ severity: 'MEDIUM',
30
+ directive: 'Compose booleans into Stage 1 concepts and Stage 2 decision variables'
31
+ },
32
+ CONTROL_FLOW_NESTED_TERNARY: {
33
+ pillar: PILLARS.PILLAR_2,
34
+ severity: 'CRITICAL',
35
+ directive: 'Extract display states into computed descriptor objects or early returns'
36
+ },
37
+ HOOK_SATURATION: {
38
+ pillar: PILLARS.PILLAR_3,
39
+ severity: 'HIGH',
40
+ directive: 'Extract related state and effects into dedicated domain hooks'
41
+ },
42
+ HOOK_RETURN_OVERLOAD: {
43
+ pillar: PILLARS.PILLAR_3,
44
+ severity: 'HIGH',
45
+ directive: 'Limit hook return values to 3 to 5 properties (State + Status + Actions)'
46
+ },
47
+ TIMER_DISCIPLINE: {
48
+ pillar: PILLARS.PILLAR_6,
49
+ severity: 'CRITICAL',
50
+ directive: 'Wrap timers in self-cleaning hooks returning lifecycle cleanup disposers'
51
+ },
52
+ RENDER_HACK_TIMEOUT: {
53
+ pillar: PILLARS.PILLAR_6,
54
+ severity: 'CRITICAL',
55
+ directive: 'Replace render-hack setTimeout(0) with await nextTick() or RAF'
56
+ },
57
+ TYPE_COLOCATION: {
58
+ pillar: PILLARS.PILLAR_4,
59
+ severity: 'MEDIUM',
60
+ directive: 'Define co-located domain interfaces in types/*.d.ts'
61
+ },
62
+ TYPE_MONOLITH: {
63
+ pillar: PILLARS.PILLAR_4,
64
+ severity: 'HIGH',
65
+ directive: 'Decompose root type monolith into co-located capsule types'
66
+ },
67
+ SYNTHETIC_MOCK_DATA: {
68
+ pillar: PILLARS.PILLAR_4,
69
+ severity: 'MEDIUM',
70
+ directive: 'Use live API data or explicit empty states instead of mock placeholders'
71
+ },
72
+ RAW_INLINE_STYLE: {
73
+ pillar: PILLARS.PILLAR_5,
74
+ severity: 'MEDIUM',
75
+ directive: 'Decouple into Level 1 atom props, SCSS mixins, or scoped classes via @apply'
76
+ },
77
+ ICON_SVG_STYLE_LEAK: {
78
+ pillar: PILLARS.PILLAR_5,
79
+ severity: 'LOW',
80
+ directive: 'Avoid text-* utility classes on FontAwesome icons; use color prop or CSS'
81
+ },
82
+ TYPOGRAPHY_EM_DASH: {
83
+ pillar: PILLARS.PILLAR_7,
84
+ severity: 'LOW',
85
+ directive: 'Replace em dashes with hyphens or colons per typography standard'
86
+ },
87
+ UNGUARDED_LOGGING: {
88
+ pillar: PILLARS.PILLAR_7,
89
+ severity: 'LOW',
90
+ directive: 'Route runtime diagnostics through a production-silenced debug proxy'
91
+ },
92
+ SYNTAX_PARSE_ERROR: {
93
+ pillar: PILLARS.PILLAR_1,
94
+ severity: 'CRITICAL',
95
+ directive: 'Fix syntax errors before static analysis'
96
+ }
97
+ };
@@ -0,0 +1,123 @@
1
+ import { parse } from '@babel/parser';
2
+ import traverse from '@babel/traverse';
3
+ import path from 'node:path';
4
+ import { PILLARS, RULE_REGISTRY } from './rules-registry.js';
5
+ import {
6
+ extractParseableCode,
7
+ checkTypographyEmDash,
8
+ checkMockDataPatterns,
9
+ resolveMonolithTier,
10
+ resolveMoleculeTier
11
+ } from './rules-helpers.js';
12
+ import { createAstVisitors } from './ast-visitors.js';
13
+
14
+ export { PILLARS, RULE_REGISTRY };
15
+
16
+ export const auditCode = (content, filePath, relativePath) => {
17
+ const violations = [];
18
+ const lines = content.split('\n');
19
+ const lineCount = lines.length;
20
+ const ext = path.extname(filePath);
21
+ const baseName = path.basename(filePath);
22
+
23
+ const isMolecule = relativePath.includes('molecules') || relativePath.includes('/m-') || baseName.startsWith('m-');
24
+ const isView = relativePath.includes('/views/') || relativePath.includes('/pages/') || /View\.[tj]sx?$/.test(baseName);
25
+ const isRootTypeFile = baseName === 'types.ts' || baseName === 'global.d.ts';
26
+
27
+ // Pillar 1: Sliding Scale Monolith Detection
28
+ if (lineCount > 500) {
29
+ const tier = resolveMonolithTier(lineCount);
30
+ violations.push({
31
+ filePath: relativePath,
32
+ line: 1,
33
+ column: 1,
34
+ hazard: tier.hazard,
35
+ rule: 'LINE_BUDGET_FILE',
36
+ severity: tier.severity,
37
+ pillar: PILLARS.PILLAR_1,
38
+ directive: tier.directive
39
+ });
40
+ } else if (isMolecule && lineCount > 100) {
41
+ const tier = resolveMoleculeTier(lineCount);
42
+ violations.push({
43
+ filePath: relativePath,
44
+ line: 1,
45
+ column: 1,
46
+ hazard: tier.hazard,
47
+ rule: 'LINE_BUDGET_MOLECULE',
48
+ severity: tier.severity,
49
+ pillar: PILLARS.PILLAR_1,
50
+ directive: tier.directive
51
+ });
52
+ } else if (isView && lineCount > 200) {
53
+ const isSevere = lineCount >= 500;
54
+ const severity = isSevere ? 'CRITICAL' : 'MEDIUM';
55
+ const hazardLabel = isSevere
56
+ ? `Extreme view monolith (${lineCount} >= 500 lines)`
57
+ : `View template budget warning (${lineCount} > 200 lines)`;
58
+
59
+ violations.push({
60
+ filePath: relativePath,
61
+ line: 1,
62
+ column: 1,
63
+ hazard: hazardLabel,
64
+ rule: 'VIEW_MONOLITH',
65
+ severity,
66
+ pillar: PILLARS.PILLAR_1,
67
+ directive: 'Refactor top-level view to a 10 to 20 line Table of Contents'
68
+ });
69
+ }
70
+
71
+ // Pillar 4: Type Monolith
72
+ if (isRootTypeFile && lineCount > 150) {
73
+ const meta = RULE_REGISTRY.TYPE_MONOLITH;
74
+ violations.push({
75
+ filePath: relativePath,
76
+ line: 1,
77
+ column: 1,
78
+ hazard: `Root type file monolith detected (${lineCount} > 150 lines)`,
79
+ rule: 'TYPE_MONOLITH',
80
+ severity: meta.severity,
81
+ pillar: meta.pillar,
82
+ directive: meta.directive
83
+ });
84
+ }
85
+
86
+ // Pillar 7: Typography & Mock Data Fast Checks
87
+ checkTypographyEmDash(content, lines, relativePath, violations);
88
+ checkMockDataPatterns(content, lines, relativePath, violations);
89
+
90
+ const codeToParse = extractParseableCode(content, ext);
91
+ if (!codeToParse.trim()) {
92
+ return violations;
93
+ }
94
+
95
+ let ast;
96
+ try {
97
+ ast = parse(codeToParse, {
98
+ sourceType: 'module',
99
+ plugins: ['typescript', 'jsx']
100
+ });
101
+ } catch (err) {
102
+ const errorMsg = err instanceof Error ? err.message : String(err);
103
+ const meta = RULE_REGISTRY.SYNTAX_PARSE_ERROR;
104
+ violations.push({
105
+ filePath: relativePath,
106
+ line: 1,
107
+ column: 1,
108
+ hazard: `Parse error: ${errorMsg}`,
109
+ rule: 'SYNTAX_PARSE_ERROR',
110
+ severity: meta.severity,
111
+ pillar: meta.pillar,
112
+ directive: meta.directive
113
+ });
114
+ return violations;
115
+ }
116
+
117
+ const traverseFn = traverse.default || traverse;
118
+ const visitors = createAstVisitors({ relativePath, violations });
119
+
120
+ traverseFn(ast, visitors);
121
+
122
+ return violations;
123
+ };
@@ -0,0 +1,4 @@
1
+ export const DISCUSSION_CATEGORY = 'npx chemx audit';
2
+ export const DISCUSSION_CATEGORY_SLUG = 'npx-chemx-audit';
3
+ export const ORG_DISCUSSIONS_URL = 'https://github.com/orgs/Chemical-X-Protocol/discussions';
4
+ export const DEFAULT_DISCUSSION_REPO = 'Chemical-X-Protocol/.github';
@@ -0,0 +1,126 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import {
3
+ DISCUSSION_CATEGORY,
4
+ DISCUSSION_CATEGORY_SLUG
5
+ } from './social-constants.js';
6
+
7
+ export const publishDiscussionViaGh = (repo, title, body, category = DISCUSSION_CATEGORY) => {
8
+ try {
9
+ let res = spawnSync(
10
+ 'gh',
11
+ ['discussion', 'create', '-R', repo, '--title', title, '--body', body, '--category', category],
12
+ { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }
13
+ );
14
+ if (res.status === 0) {
15
+ return { success: true, url: (res.stdout || '').trim(), error: null };
16
+ }
17
+
18
+ const errText = (res.stderr || '').trim();
19
+
20
+ if (errText.includes('category') && category !== DISCUSSION_CATEGORY_SLUG) {
21
+ res = spawnSync(
22
+ 'gh',
23
+ ['discussion', 'create', '-R', repo, '--title', title, '--body', body, '--category', DISCUSSION_CATEGORY_SLUG],
24
+ { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }
25
+ );
26
+ if (res.status === 0) {
27
+ return { success: true, url: (res.stdout || '').trim(), error: null };
28
+ }
29
+ }
30
+
31
+ if (errText.includes('category') && category !== 'Audits') {
32
+ res = spawnSync(
33
+ 'gh',
34
+ ['discussion', 'create', '-R', repo, '--title', title, '--body', body, '--category', 'Audits'],
35
+ { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }
36
+ );
37
+ if (res.status === 0) {
38
+ return { success: true, url: (res.stdout || '').trim(), error: null };
39
+ }
40
+ }
41
+
42
+ if (errText.includes('category')) {
43
+ res = spawnSync(
44
+ 'gh',
45
+ ['discussion', 'create', '-R', repo, '--title', title, '--body', body, '--category', 'General'],
46
+ { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }
47
+ );
48
+ if (res.status === 0) {
49
+ return { success: true, url: (res.stdout || '').trim(), error: null };
50
+ }
51
+ }
52
+
53
+ return { success: false, url: null, error: errText };
54
+ } catch (err) {
55
+ return { success: false, url: null, error: err.message };
56
+ }
57
+ };
58
+
59
+ export const viewDiscussionViaGh = (repo, discussionNumber) => {
60
+ try {
61
+ const res = spawnSync(
62
+ 'gh',
63
+ ['discussion', 'view', String(discussionNumber), '-R', repo, '--json', 'id,number,title,body,url'],
64
+ { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }
65
+ );
66
+ if (res.status === 0 && res.stdout) {
67
+ return JSON.parse(res.stdout);
68
+ }
69
+ return null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ };
74
+
75
+ export const postDiscussionCommentViaGh = (repo, discussionNumber, commentBody) => {
76
+ try {
77
+ const res = spawnSync(
78
+ 'gh',
79
+ ['discussion', 'comment', String(discussionNumber), '-R', repo, '--body', commentBody],
80
+ { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }
81
+ );
82
+ return res.status === 0;
83
+ } catch {
84
+ return false;
85
+ }
86
+ };
87
+
88
+ export const editDiscussionViaGh = (repo, discussionNumber, title, body) => {
89
+ try {
90
+ const res = spawnSync(
91
+ 'gh',
92
+ ['discussion', 'edit', String(discussionNumber), '-R', repo, '--title', title, '--body', body],
93
+ { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }
94
+ );
95
+ return res.status === 0;
96
+ } catch {
97
+ return false;
98
+ }
99
+ };
100
+
101
+ export const findExistingDiscussionViaGh = (repo, projectName) => {
102
+ try {
103
+ const res = spawnSync(
104
+ 'gh',
105
+ ['discussion', 'list', '-R', repo, '--limit', '25', '--json', 'id,number,title,body,url'],
106
+ { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }
107
+ );
108
+ if (res.status === 0 && res.stdout) {
109
+ const list = JSON.parse(res.stdout);
110
+ const targetLower = projectName.toLowerCase();
111
+ const match = list.find((d) => {
112
+ const titleLower = (d.title || '').toLowerCase();
113
+ return (
114
+ titleLower.includes(` ${targetLower} :`) ||
115
+ titleLower.includes(` ${targetLower} [`) ||
116
+ titleLower.includes(`: ${targetLower}`) ||
117
+ titleLower.includes(`${targetLower} audit`)
118
+ );
119
+ });
120
+ return match || null;
121
+ }
122
+ return null;
123
+ } catch {
124
+ return null;
125
+ }
126
+ };
@@ -0,0 +1,214 @@
1
+ import fs from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
3
+ import path from 'node:path';
4
+
5
+ export const parseGitRemoteUrl = (url) => {
6
+ if (!url || typeof url !== 'string') return null;
7
+ const clean = url.trim().replace(/^git\+/, '').replace(/\.git$/, '');
8
+
9
+ // Match HTTPS / SSH: https://github.com/owner/repo or ssh://git@github.com/owner/repo
10
+ const httpMatch = clean.match(/^(?:https?|ssh|git):\/\/[^/]+\/([^/]+)\/([^/]+)$/);
11
+ if (httpMatch) {
12
+ const owner = httpMatch[1];
13
+ const repo = httpMatch[2];
14
+ return {
15
+ owner,
16
+ repo,
17
+ nameWithOwner: `${owner}/${repo}`,
18
+ url: `https://github.com/${owner}/${repo}`
19
+ };
20
+ }
21
+
22
+ // Match SCP-like SSH: git@github.com:owner/repo
23
+ const scpMatch = clean.match(/^[\w\-]+@[^:]+:([^/]+)\/([^/]+)$/);
24
+ if (scpMatch) {
25
+ const owner = scpMatch[1];
26
+ const repo = scpMatch[2];
27
+ return {
28
+ owner,
29
+ repo,
30
+ nameWithOwner: `${owner}/${repo}`,
31
+ url: `https://github.com/${owner}/${repo}`
32
+ };
33
+ }
34
+
35
+ return null;
36
+ };
37
+
38
+ export const detectGitRepoInfo = (cwd = process.cwd()) => {
39
+ // 1. Try git remote get-url origin
40
+ try {
41
+ const res = spawnSync('git', ['remote', 'get-url', 'origin'], {
42
+ cwd,
43
+ encoding: 'utf-8',
44
+ stdio: ['ignore', 'pipe', 'ignore']
45
+ });
46
+ if (res.status === 0 && res.stdout) {
47
+ const parsed = parseGitRemoteUrl(res.stdout);
48
+ if (parsed) return parsed;
49
+ }
50
+ } catch {}
51
+
52
+ // 2. Try git config --get remote.origin.url
53
+ try {
54
+ const res = spawnSync('git', ['config', '--get', 'remote.origin.url'], {
55
+ cwd,
56
+ encoding: 'utf-8',
57
+ stdio: ['ignore', 'pipe', 'ignore']
58
+ });
59
+ if (res.status === 0 && res.stdout) {
60
+ const parsed = parseGitRemoteUrl(res.stdout);
61
+ if (parsed) return parsed;
62
+ }
63
+ } catch {}
64
+
65
+ // 3. Try any available remote if origin is not configured
66
+ try {
67
+ const remotesRes = spawnSync('git', ['remote'], {
68
+ cwd,
69
+ encoding: 'utf-8',
70
+ stdio: ['ignore', 'pipe', 'ignore']
71
+ });
72
+ if (remotesRes.status === 0 && remotesRes.stdout) {
73
+ const firstRemote = (remotesRes.stdout.trim().split(/\s+/)[0] || '').trim();
74
+ if (firstRemote) {
75
+ const urlRes = spawnSync('git', ['remote', 'get-url', firstRemote], {
76
+ cwd,
77
+ encoding: 'utf-8',
78
+ stdio: ['ignore', 'pipe', 'ignore']
79
+ });
80
+ if (urlRes.status === 0 && urlRes.stdout) {
81
+ const parsed = parseGitRemoteUrl(urlRes.stdout);
82
+ if (parsed) return parsed;
83
+ }
84
+ }
85
+ }
86
+ } catch {}
87
+
88
+ // 4. Try package.json repository or name field
89
+ try {
90
+ const pkgPath = path.resolve(cwd, 'package.json');
91
+ if (fs.existsSync(pkgPath)) {
92
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
93
+ const repoUrl = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url;
94
+ if (repoUrl) {
95
+ const parsed = parseGitRemoteUrl(repoUrl);
96
+ if (parsed) return parsed;
97
+ }
98
+ if (pkg.name) {
99
+ if (pkg.name.startsWith('@') && pkg.name.includes('/')) {
100
+ const [scope, pkgRepo] = pkg.name.slice(1).split('/');
101
+ return {
102
+ owner: scope,
103
+ repo: pkgRepo,
104
+ nameWithOwner: `${scope}/${pkgRepo}`,
105
+ url: `https://github.com/${scope}/${pkgRepo}`
106
+ };
107
+ }
108
+ return {
109
+ owner: '',
110
+ repo: pkg.name,
111
+ nameWithOwner: pkg.name,
112
+ url: ''
113
+ };
114
+ }
115
+ }
116
+ } catch {}
117
+
118
+ // 5. Try git rev-parse toplevel folder name
119
+ try {
120
+ const topRes = spawnSync('git', ['rev-parse', '--show-toplevel'], {
121
+ cwd,
122
+ encoding: 'utf-8',
123
+ stdio: ['ignore', 'pipe', 'ignore']
124
+ });
125
+ if (topRes.status === 0 && topRes.stdout) {
126
+ const topDir = topRes.stdout.trim();
127
+ const repoName = path.basename(topDir);
128
+ if (repoName) {
129
+ return {
130
+ owner: '',
131
+ repo: repoName,
132
+ nameWithOwner: repoName,
133
+ url: ''
134
+ };
135
+ }
136
+ }
137
+ } catch {}
138
+
139
+ const fallback = path.basename(cwd) || 'Codebase';
140
+ return {
141
+ owner: '',
142
+ repo: fallback,
143
+ nameWithOwner: fallback,
144
+ url: ''
145
+ };
146
+ };
147
+
148
+ export const detectGitHubUser = (preferredUser) => {
149
+ if (preferredUser && typeof preferredUser === 'string') {
150
+ const trimmed = preferredUser.trim();
151
+ if (trimmed && trimmed !== 'Architect') return trimmed;
152
+ }
153
+
154
+ // 1. Try gh api if authenticated with valid token
155
+ try {
156
+ const res = spawnSync('gh', ['api', 'user', '-q', '.login'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
157
+ if (res.status === 0) {
158
+ const user = (res.stdout || '').trim();
159
+ if (user && /^[a-zA-Z0-9_\-]+$/.test(user)) return user;
160
+ }
161
+ } catch {}
162
+
163
+ // 2. Try gh auth status account name (persists in config even if token is temporarily expired)
164
+ try {
165
+ const statusRes = spawnSync('gh', ['auth', 'status'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
166
+ const output = `${statusRes.stdout || ''} ${statusRes.stderr || ''}`.trim();
167
+ const match = output.match(/account\s+([a-zA-Z0-9_\-]+)/i) || output.match(/Logged in to [^\s]+ account ([a-zA-Z0-9_\-]+)/i);
168
+ if (match && match[1]) {
169
+ const user = match[1].trim();
170
+ if (user && user !== 'default') return user;
171
+ }
172
+ } catch {}
173
+
174
+ // 3. Try git config github.user
175
+ try {
176
+ const ghUser = spawnSync('git', ['config', 'github.user'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
177
+ const u = (ghUser.stdout || '').trim();
178
+ if (u && /^[a-zA-Z0-9_\-]+$/.test(u)) return u;
179
+ } catch {}
180
+
181
+ // 4. Try git config user.name
182
+ try {
183
+ const gitRes = spawnSync('git', ['config', 'user.name'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
184
+ const name = (gitRes.stdout || '').trim();
185
+ if (name && /^[a-zA-Z0-9_\-]+$/.test(name)) return name;
186
+ } catch {}
187
+
188
+ return 'Architect';
189
+ };
190
+
191
+ export const copyToClipboard = (text) => {
192
+ try {
193
+ if (process.platform === 'darwin') {
194
+ const res = spawnSync('pbcopy', [], { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
195
+ return res.status === 0;
196
+ }
197
+ if (process.platform === 'win32') {
198
+ const res = spawnSync('clip', [], { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
199
+ return res.status === 0;
200
+ }
201
+ // Linux: try wl-copy then xclip
202
+ const wl = spawnSync('which', ['wl-copy'], { stdio: 'ignore' });
203
+ if (wl.status === 0) {
204
+ const res = spawnSync('wl-copy', [], { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
205
+ if (res.status === 0) return true;
206
+ }
207
+ const xc = spawnSync('which', ['xclip'], { stdio: 'ignore' });
208
+ if (xc.status === 0) {
209
+ const res = spawnSync('xclip', ['-selection', 'clipboard'], { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
210
+ if (res.status === 0) return true;
211
+ }
212
+ } catch {}
213
+ return false;
214
+ };