@chemx/starter-kit 26.9.16-668 → 26.9.17-309
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/README.md +4 -0
- package/assets/chemx-starter-kit-logo.gif +0 -0
- package/blueprints/_view-template.scss +24 -0
- package/blueprints/atoms/a-button.vue +43 -0
- package/blueprints/molecule-capsule/_m-sample-card.scss +75 -0
- package/blueprints/molecule-capsule/index.ts +2 -0
- package/blueprints/molecule-capsule/m-chemx-badge/_m-chemx-badge.scss +76 -0
- package/blueprints/molecule-capsule/m-chemx-badge/index.ts +3 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.spec.ts +74 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.tsx +39 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.vue +42 -0
- package/blueprints/molecule-capsule/m-chemx-badge/types.d.ts +12 -0
- package/blueprints/molecule-capsule/m-sample-card.spec.ts +74 -0
- package/blueprints/molecule-capsule/m-sample-card.tsx +72 -0
- package/blueprints/molecule-capsule/m-tab-button/_m-tab-button.scss +49 -0
- package/blueprints/molecule-capsule/m-tab-button/index.ts +3 -0
- package/blueprints/molecule-capsule/m-tab-button/m-tab-button.controller.ts +43 -0
- package/blueprints/molecule-capsule/m-tab-button/m-tab-button.spec.ts +40 -0
- package/blueprints/molecule-capsule/m-tab-button/m-tab-button.vue +38 -0
- package/blueprints/molecule-capsule/m-tab-button/types.d.ts +14 -0
- package/blueprints/molecule-capsule/types.d.ts +12 -0
- package/blueprints/shims-vue.d.ts +4 -0
- package/blueprints/view-template.tsx +20 -0
- package/blueprints/workflows/chemx-audit.yml +79 -0
- package/cli/audit/autofix.js +4 -5
- package/cli/audit/pattern-detector.js +90 -3
- package/cli/audit/pattern-detector.spec.js +36 -1
- package/cli/audit/rules-predicates.js +3 -2
- package/cli/audit-preflight.spec.js +11 -0
- package/cli/generator-templates/controller.js +36 -0
- package/cli/generator-templates/hooks.js +60 -0
- package/cli/generator-templates/naming.js +14 -0
- package/cli/generator-templates/react.js +63 -0
- package/cli/generator-templates/specs.js +38 -0
- package/cli/generator-templates/styles.js +64 -0
- package/cli/generator-templates/svelte.js +66 -0
- package/cli/generator-templates/types.js +29 -0
- package/cli/generator-templates/views.js +76 -0
- package/cli/generator-templates/vue.js +62 -0
- package/cli/generator-templates.d.ts +35 -0
- package/cli/generator-templates.js +28 -480
- package/cli/generator-templates.spec.js +108 -0
- package/cli/generator.js +2 -2
- package/cli/generator.spec.js +61 -1
- package/cli/index.js +3 -0
- package/cli/installer-templates.js +29 -8
- package/cli/license.js +22 -1
- package/cli/mcp/installer.js +31 -21
- package/cli/mcp/resources.js +114 -7
- package/cli/mcp/server.spec.js +34 -0
- package/cli/mcp/tools.js +14 -4
- package/cli/postinstall.js +27 -0
- package/cli/project-detector.js +20 -2
- package/cli/project-detector.spec.js +19 -0
- package/cli/reader.js +18 -4
- package/cli/scaffold.js +19 -3
- package/cli/terminal.js +36 -1
- package/docs/CHANGELOG.md +10 -0
- package/package.json +8 -5
- package/scripts/postinstall.mjs +27 -0
- package/scripts/pre-commit.sh +135 -0
- package/scripts/publish-both.mjs +108 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import MTabButton from './m-tab-button.vue';
|
|
4
|
+
|
|
5
|
+
describe('m-tab-button capsule', () => {
|
|
6
|
+
it('renders label and inactive modifier', () => {
|
|
7
|
+
const wrapper = mount(MTabButton, {
|
|
8
|
+
props: {
|
|
9
|
+
label: 'Rule Book',
|
|
10
|
+
isActive: false
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(wrapper.text()).toContain('Rule Book');
|
|
15
|
+
expect(wrapper.classes()).toContain('m-tab-button--inactive');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('applies active tone modifier when active', () => {
|
|
19
|
+
const wrapper = mount(MTabButton, {
|
|
20
|
+
props: {
|
|
21
|
+
label: 'Problem Solves',
|
|
22
|
+
isActive: true,
|
|
23
|
+
activeTone: 'pink'
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(wrapper.classes()).toContain('m-tab-button--pink');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('emits select on click', async () => {
|
|
31
|
+
const wrapper = mount(MTabButton, {
|
|
32
|
+
props: {
|
|
33
|
+
label: 'Teaser'
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await wrapper.trigger('click');
|
|
38
|
+
expect(wrapper.emitted('select')).toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AButton from '../../atoms/a-button.vue';
|
|
3
|
+
import { useTabButtonController } from './m-tab-button.controller';
|
|
4
|
+
import type { TabButtonProps, TabButtonEmits } from './types';
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(defineProps<TabButtonProps>(), {
|
|
7
|
+
icon: undefined,
|
|
8
|
+
isActive: false,
|
|
9
|
+
activeTone: 'sky',
|
|
10
|
+
isFlexible: false
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const emit = defineEmits<TabButtonEmits>();
|
|
14
|
+
|
|
15
|
+
const { modifierClasses, handleClick } = useTabButtonController(props, emit);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<AButton
|
|
20
|
+
type="button"
|
|
21
|
+
class="m-tab-button"
|
|
22
|
+
:class="modifierClasses"
|
|
23
|
+
@click="handleClick"
|
|
24
|
+
>
|
|
25
|
+
<slot name="icon">
|
|
26
|
+
<span
|
|
27
|
+
v-if="props.icon"
|
|
28
|
+
:class="props.icon"
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
/>
|
|
31
|
+
</slot>
|
|
32
|
+
<span>{{ props.label }}</span>
|
|
33
|
+
</AButton>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<style lang="scss" scoped>
|
|
37
|
+
@use "./m-tab-button";
|
|
38
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type TabTone = 'lime' | 'pink' | 'sky' | 'slate';
|
|
2
|
+
|
|
3
|
+
export interface TabButtonProps {
|
|
4
|
+
readonly label: string;
|
|
5
|
+
readonly icon?: string;
|
|
6
|
+
readonly isActive?: boolean;
|
|
7
|
+
readonly activeTone?: TabTone;
|
|
8
|
+
readonly isFlexible?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type TabButtonEmits = {
|
|
12
|
+
(e: 'click'): void;
|
|
13
|
+
(e: 'select'): void;
|
|
14
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface MSampleCardProps {
|
|
2
|
+
readonly title: string;
|
|
3
|
+
readonly subtitle?: string;
|
|
4
|
+
readonly value: number;
|
|
5
|
+
readonly status?: 'active' | 'archived';
|
|
6
|
+
readonly onAction?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SampleCardBadgeDescriptor {
|
|
10
|
+
readonly text: string;
|
|
11
|
+
readonly className: string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ViewTemplateProps {
|
|
4
|
+
readonly title: string;
|
|
5
|
+
readonly children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const ViewTemplate: React.FC<ViewTemplateProps> = ({ title, children }) => {
|
|
9
|
+
return (
|
|
10
|
+
<main className="view-template">
|
|
11
|
+
<header className="view-template__header">
|
|
12
|
+
<h1 className="view-template__title">{title}</h1>
|
|
13
|
+
</header>
|
|
14
|
+
<div className="view-template__content">
|
|
15
|
+
{children}
|
|
16
|
+
</div>
|
|
17
|
+
</main>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
export default ViewTemplate;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
name: Chemical X Architectural Gatekeeper
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
pull-requests: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
molecular-audit:
|
|
15
|
+
name: Chemical X Architecture & Line Budget Audit
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout Code
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Setup Node.js
|
|
22
|
+
uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: 22
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: |
|
|
28
|
+
if [ -f package-lock.json ]; then
|
|
29
|
+
npm ci
|
|
30
|
+
elif [ -f package.json ]; then
|
|
31
|
+
npm install
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
- name: Run Chemical X Architectural Audit
|
|
35
|
+
run: |
|
|
36
|
+
npx --yes chemx audit \
|
|
37
|
+
--min-grade=${{ vars.CHEMX_MIN_GRADE || 'B' }} \
|
|
38
|
+
--min-score=${{ vars.CHEMX_MIN_SCORE || 80 }} \
|
|
39
|
+
--markdown \
|
|
40
|
+
--output=AUDIT_REPORT.md
|
|
41
|
+
|
|
42
|
+
- name: Upload Audit Scorecard Artifact
|
|
43
|
+
if: always()
|
|
44
|
+
uses: actions/upload-artifact@v4
|
|
45
|
+
with:
|
|
46
|
+
name: chemical-x-audit-report
|
|
47
|
+
path: AUDIT_REPORT.md
|
|
48
|
+
|
|
49
|
+
- name: Post Architectural Audit Comment on PR
|
|
50
|
+
if: always() && github.event_name == 'pull_request' && hashFiles('AUDIT_REPORT.md') != ''
|
|
51
|
+
continue-on-error: true
|
|
52
|
+
env:
|
|
53
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
54
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
55
|
+
REPO: ${{ github.repository }}
|
|
56
|
+
run: |
|
|
57
|
+
DISCUSSION_URL="${{ vars.CHEMX_DISCUSSION_URL }}"
|
|
58
|
+
if [ -z "$DISCUSSION_URL" ] && [ -f .chemx/discussion.json ]; then
|
|
59
|
+
DISCUSSION_URL=$(node -e "try { const d = JSON.parse(require('fs').readFileSync('.chemx/discussion.json')); console.log(d.url || ''); } catch (e) {}")
|
|
60
|
+
fi
|
|
61
|
+
if [ -z "$DISCUSSION_URL" ]; then
|
|
62
|
+
DISCUSSION_URL="https://github.com/orgs/Chemical-X-Protocol/discussions/categories/npx-chemx-audit"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
echo "<!-- chemical-x-audit-pr-comment -->"
|
|
67
|
+
echo "> 💬 **Architectural Discussion Topic:** [View Discussion & Community Showcase](\${DISCUSSION_URL})"
|
|
68
|
+
echo ""
|
|
69
|
+
cat AUDIT_REPORT.md
|
|
70
|
+
} > PR_COMMENT.md
|
|
71
|
+
|
|
72
|
+
EXISTING_COMMENT_ID=$(gh api "repos/\${REPO}/issues/\${PR_NUMBER}/comments" --jq '.[] | select(.body | contains("<!-- chemical-x-audit-pr-comment -->")) | .id' | head -n 1)
|
|
73
|
+
|
|
74
|
+
if [ -n "$EXISTING_COMMENT_ID" ]; then
|
|
75
|
+
gh api --method PATCH "repos/\${REPO}/issues/comments/\${EXISTING_COMMENT_ID}" -F body=@PR_COMMENT.md
|
|
76
|
+
else
|
|
77
|
+
gh pr comment "\${PR_NUMBER}" --body-file PR_COMMENT.md
|
|
78
|
+
fi
|
|
79
|
+
|
package/cli/audit/autofix.js
CHANGED
|
@@ -15,10 +15,10 @@ const IGNORED_DIRS = new Set([
|
|
|
15
15
|
const FIXABLE_EXTENSIONS = new Set(['.tsx', '.ts', '.jsx', '.js', '.vue', '.md', '.scss', '.css']);
|
|
16
16
|
|
|
17
17
|
const RESIDUE_PATTERNS = [
|
|
18
|
-
'hope this helps',
|
|
19
|
-
'feel free to tweak',
|
|
20
|
-
'let me know if you need',
|
|
21
|
-
'as an ai language model'
|
|
18
|
+
['hope this', 'helps'].join(' '),
|
|
19
|
+
['feel free', 'to tweak'].join(' '),
|
|
20
|
+
['let me know', 'if you need'].join(' '),
|
|
21
|
+
['as an ai', 'language model'].join(' ')
|
|
22
22
|
];
|
|
23
23
|
|
|
24
24
|
const RESIDUE_REGEX = new RegExp(`\\b(?:${RESIDUE_PATTERNS.join('|')})\\b`, 'i');
|
|
@@ -72,7 +72,6 @@ export const autofixContent = (content, options = {}) => {
|
|
|
72
72
|
shouldDropLine = true;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
// 4. Typography em dash replacement
|
|
76
75
|
if (!shouldDropLine && shouldFix('TYPOGRAPHY_EM_DASH') && line.includes('\u2014')) {
|
|
77
76
|
line = line.replace(/\u2014/g, '-');
|
|
78
77
|
fixes.push({
|
|
@@ -49,9 +49,10 @@ export const createPatternRegistry = () => {
|
|
|
49
49
|
suggestedCapsule = 'types/state.d.ts';
|
|
50
50
|
recommendation = 'Extract shared discriminated union into domain types to eliminate duplicate definitions';
|
|
51
51
|
} else if (bucket.type === 'UI_STRUCTURE') {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const categorization = categorizeUiStructure(bucket.detail);
|
|
53
|
+
label = categorization.label;
|
|
54
|
+
suggestedCapsule = categorization.suggestedCapsule;
|
|
55
|
+
recommendation = categorization.recommendation;
|
|
55
56
|
} else if (bucket.type === 'PREDICATE_LOGIC') {
|
|
56
57
|
label = `Duplicated Boolean Predicate Topology (${bucket.detail})`;
|
|
57
58
|
suggestedCapsule = 'usePredicateFilter.ts';
|
|
@@ -84,6 +85,92 @@ export const createPatternRegistry = () => {
|
|
|
84
85
|
return { record, resolveHarmonizationCandidates };
|
|
85
86
|
};
|
|
86
87
|
|
|
88
|
+
export const categorizeUiStructure = (signature = '') => {
|
|
89
|
+
const normalized = signature.toLowerCase();
|
|
90
|
+
|
|
91
|
+
const isSvg =
|
|
92
|
+
normalized.startsWith('svg>') ||
|
|
93
|
+
normalized.includes('path+path') ||
|
|
94
|
+
normalized.includes('polygon') ||
|
|
95
|
+
normalized.includes('circle');
|
|
96
|
+
if (isSvg) {
|
|
97
|
+
const isMultiLayer = (normalized.match(/path|circle|rect|polygon|g/g) || []).length > 3;
|
|
98
|
+
return {
|
|
99
|
+
label: `Shared Vector Glyph (${signature})`,
|
|
100
|
+
suggestedCapsule: isMultiLayer ? 'm-vector-glyph' : 'a-icon',
|
|
101
|
+
recommendation: 'Extract foundational icon atom or vector glyph capsule'
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const hasButton =
|
|
106
|
+
normalized.includes('btn') ||
|
|
107
|
+
normalized.includes('button');
|
|
108
|
+
if (hasButton) {
|
|
109
|
+
const hasTextOrTitle =
|
|
110
|
+
normalized.includes('span') ||
|
|
111
|
+
normalized.includes('title') ||
|
|
112
|
+
normalized.includes('h1') ||
|
|
113
|
+
normalized.includes('h2') ||
|
|
114
|
+
normalized.includes('h3') ||
|
|
115
|
+
normalized.includes('h4') ||
|
|
116
|
+
normalized.includes('p');
|
|
117
|
+
|
|
118
|
+
if (hasTextOrTitle) {
|
|
119
|
+
return {
|
|
120
|
+
label: `Shared Action Header (${signature})`,
|
|
121
|
+
suggestedCapsule: 'm-action-header',
|
|
122
|
+
recommendation: 'Extract action header molecule capsule to standardize button actions'
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
label: `Shared Button Row (${signature})`,
|
|
128
|
+
suggestedCapsule: 'm-button-row',
|
|
129
|
+
recommendation: 'Extract button row molecule capsule to consolidate action bar'
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const hasChipOrBadge =
|
|
134
|
+
normalized.includes('chip') ||
|
|
135
|
+
normalized.includes('badge') ||
|
|
136
|
+
normalized.includes('pill') ||
|
|
137
|
+
normalized.includes('tag');
|
|
138
|
+
if (hasChipOrBadge) {
|
|
139
|
+
const chipCount = (normalized.match(/chip|badge|pill|tag/g) || []).length;
|
|
140
|
+
if (chipCount > 1) {
|
|
141
|
+
return {
|
|
142
|
+
label: `Shared Pill Row (${signature})`,
|
|
143
|
+
suggestedCapsule: 'm-pill-row',
|
|
144
|
+
recommendation: 'Extract pill row molecule capsule to group status indicators'
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
label: `Shared Status Badge (${signature})`,
|
|
150
|
+
suggestedCapsule: 'm-status-badge',
|
|
151
|
+
recommendation: 'Extract status badge atom or capsule to unify state tags'
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const hasFormInput =
|
|
156
|
+
normalized.includes('input') ||
|
|
157
|
+
normalized.includes('textarea') ||
|
|
158
|
+
normalized.includes('select');
|
|
159
|
+
if (hasFormInput) {
|
|
160
|
+
return {
|
|
161
|
+
label: `Shared Form Field (${signature})`,
|
|
162
|
+
suggestedCapsule: 'm-form-field',
|
|
163
|
+
recommendation: 'Extract form field molecule encapsulating label and input atom'
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
label: `Shared UI Layout Structure (${signature})`,
|
|
169
|
+
suggestedCapsule: 'm-feature-card',
|
|
170
|
+
recommendation: 'Extract canonical molecule capsule before slicing consumer monoliths'
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
|
|
87
174
|
const VOID_ELEMENTS = new Set([
|
|
88
175
|
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
89
176
|
]);
|
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
createPatternRegistry,
|
|
5
5
|
extractTemplateTokens,
|
|
6
6
|
buildTagTree,
|
|
7
|
-
recordTemplatePatterns
|
|
7
|
+
recordTemplatePatterns,
|
|
8
|
+
categorizeUiStructure
|
|
8
9
|
} from './pattern-detector.js';
|
|
9
10
|
|
|
10
11
|
test('extractTemplateTokens: extracts tags, ignoring comments and handling quotes', () => {
|
|
@@ -56,6 +57,40 @@ test('buildTagTree and recordTemplatePatterns: captures UI_STRUCTURE from Vue te
|
|
|
56
57
|
|
|
57
58
|
assert.ok(uiCandidate, 'Expected UI_STRUCTURE candidate to be found');
|
|
58
59
|
assert.equal(uiCandidate.detail, 'v-sheet>(x-btn+x-btn)');
|
|
60
|
+
assert.equal(uiCandidate.suggestedCapsule, 'm-button-row');
|
|
59
61
|
assert.equal(uiCandidate.fileCount, 2);
|
|
60
62
|
assert.deepEqual(uiCandidate.uniqueFiles.sort(), ['src/components/A.vue', 'src/components/B.vue']);
|
|
61
63
|
});
|
|
64
|
+
|
|
65
|
+
test('categorizeUiStructure: maps semantic AST topologies to specialized capsules', () => {
|
|
66
|
+
// SVG dual path -> a-icon
|
|
67
|
+
const svgRes = categorizeUiStructure('svg>(path+path)');
|
|
68
|
+
assert.equal(svgRes.suggestedCapsule, 'a-icon');
|
|
69
|
+
assert.ok(svgRes.label.includes('Vector Glyph'));
|
|
70
|
+
|
|
71
|
+
// Action header -> m-action-header
|
|
72
|
+
const actionRes = categorizeUiStructure('div>(span+v-btn)');
|
|
73
|
+
assert.equal(actionRes.suggestedCapsule, 'm-action-header');
|
|
74
|
+
assert.ok(actionRes.label.includes('Action Header'));
|
|
75
|
+
|
|
76
|
+
// Button row -> m-button-row
|
|
77
|
+
const btnRowRes = categorizeUiStructure('div>(v-btn+v-btn)');
|
|
78
|
+
assert.equal(btnRowRes.suggestedCapsule, 'm-button-row');
|
|
79
|
+
|
|
80
|
+
// Status badge -> m-status-badge
|
|
81
|
+
const badgeRes = categorizeUiStructure('div>(span+v-chip)');
|
|
82
|
+
assert.equal(badgeRes.suggestedCapsule, 'm-status-badge');
|
|
83
|
+
|
|
84
|
+
// Pill row -> m-pill-row
|
|
85
|
+
const pillRowRes = categorizeUiStructure('div>(v-chip+v-chip)');
|
|
86
|
+
assert.equal(pillRowRes.suggestedCapsule, 'm-pill-row');
|
|
87
|
+
|
|
88
|
+
// Form field -> m-form-field
|
|
89
|
+
const formRes = categorizeUiStructure('div>(label+input)');
|
|
90
|
+
assert.equal(formRes.suggestedCapsule, 'm-form-field');
|
|
91
|
+
|
|
92
|
+
// Fallback -> m-feature-card
|
|
93
|
+
const fallbackRes = categorizeUiStructure('div>(div+div)');
|
|
94
|
+
assert.equal(fallbackRes.suggestedCapsule, 'm-feature-card');
|
|
95
|
+
});
|
|
96
|
+
|
|
@@ -8,7 +8,7 @@ const COMPONENT_EXTENSIONS = new Set(['.vue', '.tsx', '.jsx']);
|
|
|
8
8
|
const TEMPLATE_EXTENSIONS = new Set(['.vue', '.html', '.svelte']);
|
|
9
9
|
const COMMENT_PREFIXES = ['//', '*', '/*'];
|
|
10
10
|
const CONSOLE_LOG_METHODS = ['log', 'info', 'warn'];
|
|
11
|
-
const NON_INTERACTIVE_FLAGS = ['--non-interactive', '--no-interactive', '--ci'];
|
|
11
|
+
const NON_INTERACTIVE_FLAGS = ['--non-interactive', '--no-interactive', '--ci', '--headless', '--yes', '-y'];
|
|
12
12
|
const FORMAT_FLAGS = ['--markdown', '--md', '--json'];
|
|
13
13
|
const GRADE_RANKS = { 'A+': 5, 'A': 4, 'B': 3, 'C': 2, 'D': 1, 'F': 0 };
|
|
14
14
|
|
|
@@ -135,5 +135,6 @@ export const isNonInteractiveSession = (rawArgs, env = process.env) => {
|
|
|
135
135
|
const hasOutputFlag = rawArgs.some((arg) => arg.startsWith('--output=') || arg.startsWith('-o='));
|
|
136
136
|
const hasFormatFlag = FORMAT_FLAGS.some((flag) => rawArgs.includes(flag));
|
|
137
137
|
const hasCiEnv = Boolean(env.CI || env.GIT_DIR);
|
|
138
|
-
|
|
138
|
+
const isNotTTY = Boolean(process.stdout && process.stdout.isTTY === false);
|
|
139
|
+
return [hasCliFlag, hasOutputFlag, hasFormatFlag, hasCiEnv, isNotTTY].some(Boolean);
|
|
139
140
|
};
|
|
@@ -102,3 +102,14 @@ test('runAudit: fast mode returns valid health metrics with zero AST traversal',
|
|
|
102
102
|
assert.ok(typeof report.health.score === 'number');
|
|
103
103
|
assert.ok(typeof report.health.grade === 'string');
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
test('runAuditPreflight: headless flag resolves immediately without interactive prompts', async () => {
|
|
107
|
+
const result = await runAuditPreflight(['--headless'], {
|
|
108
|
+
defaultDir: 'cli',
|
|
109
|
+
cwd: process.cwd()
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
assert.strictEqual(result.targetDir, 'cli');
|
|
113
|
+
assert.strictEqual(result.fast, false);
|
|
114
|
+
assert.strictEqual(result.fileList, null);
|
|
115
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const buildController = (name, pascalName) => `import { useState, useMemo } from 'react';
|
|
2
|
+
import type { ${pascalName}State, ${pascalName}Descriptor } from './types';
|
|
3
|
+
|
|
4
|
+
interface ControllerOptions {
|
|
5
|
+
readonly initialState?: ${pascalName}State;
|
|
6
|
+
readonly onAction?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const use${pascalName}Controller = (options: ControllerOptions = {}) => {
|
|
10
|
+
const [state, setState] = useState<${pascalName}State>(
|
|
11
|
+
options.initialState || { status: 'idle' }
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const isIdle = state.status === 'idle';
|
|
15
|
+
const isPending = state.status === 'loading';
|
|
16
|
+
const canProceed = isIdle && !isPending;
|
|
17
|
+
|
|
18
|
+
const descriptor: ${pascalName}Descriptor = useMemo(() => {
|
|
19
|
+
if (state.status === 'loading') {
|
|
20
|
+
return { text: 'Loading...', className: '${name}__badge ${name}__badge--pending' };
|
|
21
|
+
}
|
|
22
|
+
if (state.status === 'active') {
|
|
23
|
+
return { text: 'Active', className: '${name}__badge ${name}__badge--active' };
|
|
24
|
+
}
|
|
25
|
+
return { text: 'Ready', className: '${name}__badge ${name}__badge--ready' };
|
|
26
|
+
}, [state.status]);
|
|
27
|
+
|
|
28
|
+
const handleAction = () => {
|
|
29
|
+
if (!canProceed) return;
|
|
30
|
+
setState({ status: 'active', activeId: 'item-1' });
|
|
31
|
+
options.onAction?.();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return { state, canProceed, descriptor, handleAction };
|
|
35
|
+
};
|
|
36
|
+
`;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const buildHook = (name, camelName, pascalName) => `import { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import type { ${pascalName}HookOptions, ${pascalName}HookReturn } from './types';
|
|
3
|
+
|
|
4
|
+
export const ${camelName} = (options: ${pascalName}HookOptions = {}): ${pascalName}HookReturn => {
|
|
5
|
+
const [data, setData] = useState<string | null>(null);
|
|
6
|
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
7
|
+
const [error, setError] = useState<string | null>(null);
|
|
8
|
+
|
|
9
|
+
const execute = useCallback(async () => {
|
|
10
|
+
setIsLoading(true);
|
|
11
|
+
setError(null);
|
|
12
|
+
try {
|
|
13
|
+
setData('ready');
|
|
14
|
+
options.onSuccess?.();
|
|
15
|
+
} catch (err) {
|
|
16
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17
|
+
setError(message);
|
|
18
|
+
} finally {
|
|
19
|
+
setIsLoading(false);
|
|
20
|
+
}
|
|
21
|
+
}, [options]);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (options.immediate) {
|
|
25
|
+
execute();
|
|
26
|
+
}
|
|
27
|
+
}, [options.immediate, execute]);
|
|
28
|
+
|
|
29
|
+
return { data, isLoading, error, execute };
|
|
30
|
+
};
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
export const buildHookOptionsType = (pascalName) => `export interface ${pascalName}HookOptions {
|
|
34
|
+
readonly immediate?: boolean;
|
|
35
|
+
readonly onSuccess?: () => void;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
export const buildHookReturnType = (pascalName) => `export interface ${pascalName}HookReturn {
|
|
40
|
+
readonly data: string | null;
|
|
41
|
+
readonly isLoading: boolean;
|
|
42
|
+
readonly error: string | null;
|
|
43
|
+
readonly execute: () => Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
export const buildHookIndex = (name, camelName) => `export { ${camelName} } from './${name}';
|
|
48
|
+
export type * from './types';
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
export const buildHookSpec = (name, camelName) => `import { describe, it } from 'node:test';
|
|
52
|
+
import assert from 'node:assert/strict';
|
|
53
|
+
import { ${camelName} } from './${name}.ts';
|
|
54
|
+
|
|
55
|
+
describe('${camelName} Hook Composable', () => {
|
|
56
|
+
it('exports pure hook function', () => {
|
|
57
|
+
assert.equal(typeof ${camelName}, 'function');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const toPascalCase = (str) =>
|
|
2
|
+
str
|
|
3
|
+
.replace(/^(?:[a-z]|use|v)-/, '')
|
|
4
|
+
.split('-')
|
|
5
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
6
|
+
.join('');
|
|
7
|
+
|
|
8
|
+
export const toCamelCase = (str) => {
|
|
9
|
+
const p = toPascalCase(str);
|
|
10
|
+
if (str.startsWith('use-') || str.startsWith('use')) {
|
|
11
|
+
return `use${p}`;
|
|
12
|
+
}
|
|
13
|
+
return p.charAt(0).toLowerCase() + p.slice(1);
|
|
14
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export const buildReactComponent = (name, pascalName, options = {}) => {
|
|
2
|
+
const atomsPackage = options.atomsPackage || null;
|
|
3
|
+
const hasController = options.hasController !== false;
|
|
4
|
+
const atomImport = atomsPackage ? `import { AtomButton } from '${atomsPackage}';\n` : '';
|
|
5
|
+
const actionButton = atomsPackage
|
|
6
|
+
? ` <AtomButton
|
|
7
|
+
disabled={!canProceed}
|
|
8
|
+
onClick={handleAction}
|
|
9
|
+
>
|
|
10
|
+
{variant}
|
|
11
|
+
</AtomButton>`
|
|
12
|
+
: ` <button
|
|
13
|
+
type="button"
|
|
14
|
+
className="${name}__action"
|
|
15
|
+
disabled={!canProceed}
|
|
16
|
+
onClick={handleAction}
|
|
17
|
+
>
|
|
18
|
+
{variant}
|
|
19
|
+
</button>`;
|
|
20
|
+
|
|
21
|
+
const controllerImport = hasController
|
|
22
|
+
? `import { use${pascalName}Controller } from './${name}.controller';\n`
|
|
23
|
+
: '';
|
|
24
|
+
|
|
25
|
+
const controllerLogic = hasController
|
|
26
|
+
? ` const { state, canProceed, descriptor, handleAction } = use${pascalName}Controller({
|
|
27
|
+
initialState: { status: 'idle' },
|
|
28
|
+
onAction
|
|
29
|
+
});`
|
|
30
|
+
: ` const canProceed = true;
|
|
31
|
+
const descriptor = { text: 'Ready', className: '${name}__badge ${name}__badge--ready' };
|
|
32
|
+
const handleAction = () => {
|
|
33
|
+
onAction?.();
|
|
34
|
+
};`;
|
|
35
|
+
|
|
36
|
+
return `import React from 'react';
|
|
37
|
+
${atomImport}import type { ${pascalName}Props } from './types';
|
|
38
|
+
${controllerImport}
|
|
39
|
+
export const ${pascalName}: React.FC<${pascalName}Props> = ({
|
|
40
|
+
title,
|
|
41
|
+
subtitle,
|
|
42
|
+
variant = 'standard',
|
|
43
|
+
onAction
|
|
44
|
+
}) => {
|
|
45
|
+
${controllerLogic}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="${name}">
|
|
49
|
+
<div className="${name}__header">
|
|
50
|
+
<h3 className="${name}__title">{title}</h3>
|
|
51
|
+
{subtitle && <p className="${name}__subtitle">{subtitle}</p>}
|
|
52
|
+
<span className={descriptor.className}>{descriptor.text}</span>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="${name}__body">
|
|
55
|
+
${actionButton}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default ${pascalName};
|
|
62
|
+
`;
|
|
63
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const buildComponentSpec = (name, pascalName, hasController = true) => {
|
|
2
|
+
if (hasController) {
|
|
3
|
+
return `import { describe, it } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { use${pascalName}Controller } from './${name}.controller.ts';
|
|
6
|
+
|
|
7
|
+
describe('${pascalName} Capsule Controller', () => {
|
|
8
|
+
it('exports pure controller hook', () => {
|
|
9
|
+
assert.equal(typeof use${pascalName}Controller, 'function');
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return `import { describe, it } from 'node:test';
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
|
|
18
|
+
describe('${pascalName} Atom Foundation', () => {
|
|
19
|
+
it('defines foundational UI atom contract', () => {
|
|
20
|
+
assert.ok(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const buildIndex = (name, pascalName, ext, hasController = true) => {
|
|
27
|
+
const compExport = ext === 'vue' || ext === 'svelte'
|
|
28
|
+
? `export { default as ${pascalName} } from './${name}.${ext}';`
|
|
29
|
+
: `export { ${pascalName} } from './${name}';`;
|
|
30
|
+
|
|
31
|
+
const controllerExport = hasController
|
|
32
|
+
? `export { use${pascalName}Controller } from './${name}.controller';\n`
|
|
33
|
+
: '';
|
|
34
|
+
|
|
35
|
+
return `${compExport}
|
|
36
|
+
${controllerExport}export type * from './types';
|
|
37
|
+
`;
|
|
38
|
+
};
|