@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.
- package/cli/audit/ast-visitors.js +243 -0
- package/cli/audit/discussion-store.js +39 -0
- package/cli/audit/history.d.ts +69 -0
- package/cli/audit/history.js +397 -0
- package/cli/audit/metrics.js +179 -0
- package/cli/audit/prompts.js +174 -0
- package/cli/audit/reporter-ascii.d.ts +4 -0
- package/cli/audit/reporter-ascii.js +99 -0
- package/cli/audit/reporter-grades.js +275 -0
- package/cli/audit/reporter-markdown.js +137 -0
- package/cli/audit/reporter-utils.js +125 -0
- package/cli/audit/reporter.js +497 -0
- package/cli/audit/rules-helpers.js +109 -0
- package/cli/audit/rules-registry.js +97 -0
- package/cli/audit/rules.js +123 -0
- package/cli/audit/social-constants.js +4 -0
- package/cli/audit/social-gh.js +126 -0
- package/cli/audit/social-git.js +214 -0
- package/cli/audit/social-http.js +222 -0
- package/cli/audit/social-publisher.js +193 -0
- package/cli/audit/social.d.ts +69 -0
- package/cli/audit/social.js +303 -0
- package/cli/audit/types.d.ts +140 -0
- package/cli/audit.d.ts +3 -24
- package/cli/audit.js +207 -210
- package/cli/help.d.ts +1 -0
- package/cli/help.js +136 -0
- package/cli/index.js +128 -566
- package/cli/installer-templates.js +91 -0
- package/cli/installer.d.ts +21 -0
- package/cli/installer.js +104 -0
- package/cli/license.js +320 -0
- package/cli/navigator-actions.js +274 -0
- package/cli/navigator-banner.js +98 -0
- package/cli/navigator-conversion.js +100 -0
- package/cli/navigator-grades.js +87 -0
- package/cli/navigator-menu.js +127 -0
- package/cli/navigator-paged.js +86 -0
- package/cli/navigator-share.js +129 -0
- package/cli/navigator.d.ts +43 -0
- package/cli/navigator.js +126 -0
- package/cli/scaffold.js +148 -0
- package/cli/terminal.js +126 -0
- package/docs/CHANGELOG.md +101 -0
- 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
|
+
};
|