@vc-shell/vc-app-skill 2.0.0-alpha.25 → 2.0.0-alpha.26

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/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ # [2.0.0-alpha.26](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.25...v2.0.0-alpha.26) (2026-03-25)
2
+
3
+ **Note:** Version bump only for package @vc-shell/vc-app-skill
4
+
1
5
  # [2.0.0-alpha.25](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.24...v2.0.0-alpha.25) (2026-03-25)
2
6
 
3
7
  **Note:** Version bump only for package @vc-shell/vc-app-skill
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ // ─── Configuration ───────────────────────────────────────────────────────────
8
+ const ROOT = path.resolve(__dirname, '..', '..', '..');
9
+ const FRAMEWORK_DIR = path.join(ROOT, 'framework');
10
+
11
+ const STORYBOOK_BASE = 'https://vc-shell-storybook.govirto.com';
12
+
13
+ // Resolve vc-docs path from --docs-path arg or VC_DOCS_PATH env or default
14
+ const docsPathArg = process.argv.find((a) => a.startsWith('--docs-path='));
15
+ const VC_DOCS_PATH =
16
+ (docsPathArg && docsPathArg.split('=')[1]) ||
17
+ process.env.VC_DOCS_PATH ||
18
+ path.resolve(ROOT, '..', 'vc-docs');
19
+
20
+ const MKDOCS_YML = path.join(VC_DOCS_PATH, 'platform', 'developer-guide', 'mkdocs.yml');
21
+ const DOCS_OUT = path.join(VC_DOCS_PATH, 'platform', 'developer-guide', 'docs', 'custom-apps-development', 'vc-shell');
22
+
23
+ if (!fs.existsSync(VC_DOCS_PATH)) {
24
+ console.error(`vc-docs repo not found at: ${VC_DOCS_PATH}`);
25
+ console.error('Use --docs-path=/path/to/vc-docs or set VC_DOCS_PATH env var');
26
+ process.exit(1);
27
+ }
28
+
29
+ // ─── Path mapping ────────────────────────────────────────────────────────────
30
+ // framework relative path → vc-docs relative path (under DOCS_OUT) + nav category
31
+ function mapPath(relPath) {
32
+ const parts = relPath.replace(/\.docs\.md$/, '').split(path.sep);
33
+
34
+ // core/composables/{name}/index → Essentials/composables/{name}.md
35
+ // core/composables/{name}.docs.md (flat) → Essentials/composables/{name}.md
36
+ if (parts[0] === 'core' && parts[1] === 'composables') {
37
+ const name = parts.length >= 3 ? parts[2] : parts[1];
38
+ return { outPath: `Essentials/composables/${name}.md`, navCategory: 'Composables' };
39
+ }
40
+
41
+ // core/plugins/{name}/* → Essentials/plugins/{name}.md
42
+ if (parts[0] === 'core' && parts[1] === 'plugins') {
43
+ const name = parts[2];
44
+ return { outPath: `Essentials/plugins/${name}.md`, navCategory: 'Plugins' };
45
+ }
46
+
47
+ // core/api/* → Essentials/API-Integration/{name}.md
48
+ if (parts[0] === 'core' && parts[1] === 'api') {
49
+ const name = parts[parts.length - 1];
50
+ return { outPath: `Essentials/API-Integration/${name}.md`, navCategory: null };
51
+ }
52
+
53
+ // core/blade-navigation/* → Essentials/composables/{name}.md
54
+ if (parts[0] === 'core' && parts[1] === 'blade-navigation') {
55
+ const name = parts[parts.length - 1];
56
+ return { outPath: `Essentials/composables/${name}.md`, navCategory: 'Composables' };
57
+ }
58
+
59
+ // core/notifications/* → Essentials/shared/components/notifications.md
60
+ if (parts[0] === 'core' && parts[1] === 'notifications') {
61
+ return { outPath: 'Essentials/shared/components/notifications.md', navCategory: null };
62
+ }
63
+
64
+ // ui/components/{tier}/{name}/* → Essentials/ui-components/{name}.md
65
+ // Skip internal composable docs (table-composables) — only sync the main component doc
66
+ if (parts[0] === 'ui' && parts[1] === 'components') {
67
+ const tier = parts[2]; // atoms, molecules, organisms
68
+ const compDir = parts[3]; // vc-badge, vc-select, etc.
69
+ const fileName = parts[parts.length - 1];
70
+ if (compDir) {
71
+ // Skip sub-directory docs (e.g. composables/table-composables.docs.md) — main doc covers these
72
+ if (parts.length > 5) return null;
73
+ // vc-table dir contains VcDataTable — use the actual component name
74
+ const outName = compDir === 'vc-table' ? 'vc-data-table' : compDir;
75
+ return { outPath: `Essentials/ui-components/${outName}.md`, navCategory: 'UI Components', tier };
76
+ }
77
+ }
78
+
79
+ // ui/composables/* → skip (internal table composables)
80
+ if (parts[0] === 'ui' && parts[1] === 'composables') {
81
+ return null;
82
+ }
83
+
84
+ // shell/components/{name}/* → Essentials/shared/components/{name}.md
85
+ if (parts[0] === 'shell' && parts[1] === 'components') {
86
+ const name = parts[2];
87
+ return { outPath: `Essentials/shared/components/${name}.md`, navCategory: null };
88
+ }
89
+
90
+ // shell/dashboard/{name}/* → Essentials/shared/components/{name}.md
91
+ if (parts[0] === 'shell' && parts[1] === 'dashboard') {
92
+ const name = parts[2];
93
+ return { outPath: `Essentials/shared/components/${name}.md`, navCategory: null };
94
+ }
95
+
96
+ // Anything else we don't map (core/types, core/utilities, etc.)
97
+ return null;
98
+ }
99
+
100
+ // ─── Storybook helpers ───────────────────────────────────────────────────────
101
+ function findStorybookTitle(compDir) {
102
+ const storiesGlob = fs.readdirSync(compDir).find((f) => f.endsWith('.stories.ts'));
103
+ if (!storiesGlob) return null;
104
+
105
+ const content = fs.readFileSync(path.join(compDir, storiesGlob), 'utf-8');
106
+ const match = content.match(/title:\s*["']([^"']+)["']/);
107
+ return match ? match[1] : null;
108
+ }
109
+
110
+ function storybookTitleToId(title) {
111
+ // "Data Display/VcBadge" → "data-display-vcbadge"
112
+ return title
113
+ .toLowerCase()
114
+ .replace(/\s+/g, '-')
115
+ .replace(/\//g, '-');
116
+ }
117
+
118
+ function buildStorybookSection(title, componentName) {
119
+ const id = storybookTitleToId(title);
120
+ const displayName = componentName.split('-').map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('');
121
+ return [
122
+ '',
123
+ '## Storybook',
124
+ '',
125
+ `[${displayName} Storybook](${STORYBOOK_BASE}/?path=/docs/${id}--docs)`,
126
+ '',
127
+ '<iframe',
128
+ ` src="${STORYBOOK_BASE}/iframe.html?id=${id}--docs&viewMode=story&shortcuts=false&singleStory=true"`,
129
+ ' width="1000"',
130
+ ' height="500"',
131
+ '></iframe>',
132
+ '',
133
+ ].join('\n');
134
+ }
135
+
136
+ // ─── Content transformation ──────────────────────────────────────────────────
137
+ function transformContent(content, mapping, frameworkRelPath) {
138
+ let result = content;
139
+
140
+ // If UI component, inject Storybook iframe after first heading
141
+ if (mapping.navCategory === 'UI Components' && mapping.tier) {
142
+ const compName = path.basename(mapping.outPath, '.md');
143
+ const compDir = path.join(FRAMEWORK_DIR, path.dirname(frameworkRelPath));
144
+ const storybookTitle = findStorybookTitle(compDir);
145
+
146
+ if (storybookTitle) {
147
+ const storySection = buildStorybookSection(storybookTitle, compName);
148
+ // Insert after first # heading line
149
+ result = result.replace(/^(# .+\n)/, `$1${storySection}\n`);
150
+ }
151
+ }
152
+
153
+ return result;
154
+ }
155
+
156
+ // ─── Find all docs ───────────────────────────────────────────────────────────
157
+ function findDocsFiles(dir, base) {
158
+ const results = [];
159
+ if (!fs.existsSync(dir)) return results;
160
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
161
+ const fullPath = path.join(dir, entry.name);
162
+ if (entry.isDirectory()) {
163
+ results.push(...findDocsFiles(fullPath, base));
164
+ } else if (entry.name.endsWith('.docs.md')) {
165
+ results.push(path.relative(base, fullPath));
166
+ }
167
+ }
168
+ return results;
169
+ }
170
+
171
+ // ─── Nav update helpers ──────────────────────────────────────────────────────
172
+ function extractTitle(content) {
173
+ const match = content.match(/^# (.+)$/m);
174
+ return match ? match[1] : null;
175
+ }
176
+
177
+ function updateNavSection(mkdocsContent, category, entries) {
178
+ // entries: [{title, path}]
179
+ // We update the specific nav category section in mkdocs.yml
180
+ // This is a targeted update — find the category line and replace its children
181
+
182
+ const lines = mkdocsContent.split('\n');
183
+ const result = [];
184
+ let inCategory = false;
185
+ let categoryIndent = 0;
186
+ let replaced = false;
187
+
188
+ for (let i = 0; i < lines.length; i++) {
189
+ const line = lines[i];
190
+ const trimmed = line.trimStart();
191
+
192
+ // Find the category header (e.g., "- Composables:" or "- UI Components:")
193
+ if (!replaced && trimmed === `- ${category}:`) {
194
+ categoryIndent = line.length - trimmed.length;
195
+ inCategory = true;
196
+ result.push(line);
197
+
198
+ // Add all entries with proper indentation
199
+ const childIndent = ' '.repeat(categoryIndent + 6); // 6 more for child items
200
+ const sortedEntries = entries.sort((a, b) => a.title.localeCompare(b.title));
201
+ for (const entry of sortedEntries) {
202
+ result.push(`${childIndent}- ${entry.title}: ${entry.navPath}`);
203
+ }
204
+ replaced = true;
205
+ continue;
206
+ }
207
+
208
+ // Skip old children of this category
209
+ if (inCategory) {
210
+ const lineIndent = line.length - line.trimStart().length;
211
+ if (line.trim() === '' || lineIndent > categoryIndent) {
212
+ // Still inside category children, skip
213
+ if (line.trim().startsWith('-') && lineIndent > categoryIndent) {
214
+ continue;
215
+ }
216
+ }
217
+ if (line.trim() !== '' && (line.length - line.trimStart().length) <= categoryIndent) {
218
+ inCategory = false;
219
+ // This line belongs to next section, keep it
220
+ result.push(line);
221
+ continue;
222
+ }
223
+ if (line.trim() === '') {
224
+ continue;
225
+ }
226
+ continue;
227
+ }
228
+
229
+ result.push(line);
230
+ }
231
+
232
+ return result.join('\n');
233
+ }
234
+
235
+ // ─── Main ────────────────────────────────────────────────────────────────────
236
+ const frameworkDocs = findDocsFiles(FRAMEWORK_DIR, FRAMEWORK_DIR);
237
+
238
+ let synced = 0;
239
+ let skipped = 0;
240
+ let unmapped = 0;
241
+
242
+ // Track nav entries per category
243
+ const navEntries = {
244
+ Composables: [],
245
+ Plugins: [],
246
+ 'UI Components': [],
247
+ };
248
+
249
+ // Pre-populate nav entries from existing mkdocs.yml to preserve manual entries
250
+ // (we only update categories we manage)
251
+
252
+ const NAV_PREFIX = 'custom-apps-development/vc-shell/';
253
+
254
+ for (const relPath of frameworkDocs) {
255
+ const mapping = mapPath(relPath);
256
+ if (!mapping) {
257
+ unmapped++;
258
+ continue;
259
+ }
260
+
261
+ const src = path.join(FRAMEWORK_DIR, relPath);
262
+ const dest = path.join(DOCS_OUT, mapping.outPath);
263
+ const srcContent = fs.readFileSync(src, 'utf-8');
264
+
265
+ // Transform content
266
+ const transformed = transformContent(srcContent, mapping, relPath);
267
+
268
+ // Check if dest is identical
269
+ if (fs.existsSync(dest)) {
270
+ const destContent = fs.readFileSync(dest, 'utf-8');
271
+ if (destContent === transformed) {
272
+ skipped++;
273
+ // Still collect nav entry
274
+ if (mapping.navCategory && navEntries[mapping.navCategory]) {
275
+ const title = extractTitle(srcContent) || path.basename(mapping.outPath, '.md');
276
+ navEntries[mapping.navCategory].push({
277
+ title,
278
+ navPath: NAV_PREFIX + mapping.outPath,
279
+ });
280
+ }
281
+ continue;
282
+ }
283
+ }
284
+
285
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
286
+ fs.writeFileSync(dest, transformed);
287
+ console.log(` synced: ${relPath} → ${mapping.outPath}`);
288
+ synced++;
289
+
290
+ // Collect nav entry
291
+ if (mapping.navCategory && navEntries[mapping.navCategory]) {
292
+ const title = extractTitle(srcContent) || path.basename(mapping.outPath, '.md');
293
+ navEntries[mapping.navCategory].push({
294
+ title,
295
+ navPath: NAV_PREFIX + mapping.outPath,
296
+ });
297
+ }
298
+ }
299
+
300
+ // Update mkdocs.yml nav sections
301
+ if (fs.existsSync(MKDOCS_YML)) {
302
+ let mkdocsContent = fs.readFileSync(MKDOCS_YML, 'utf-8');
303
+ let navUpdated = false;
304
+
305
+ for (const [category, entries] of Object.entries(navEntries)) {
306
+ if (entries.length === 0) continue;
307
+
308
+ const before = mkdocsContent;
309
+ mkdocsContent = updateNavSection(mkdocsContent, category, entries);
310
+ if (mkdocsContent !== before) navUpdated = true;
311
+ }
312
+
313
+ if (navUpdated) {
314
+ fs.writeFileSync(MKDOCS_YML, mkdocsContent);
315
+ console.log(`\n mkdocs.yml nav updated`);
316
+ }
317
+ }
318
+
319
+ // ─── Mark orphaned docs ──────────────────────────────────────────────────────
320
+ // Docs in vc-docs that no longer have a source in framework get a deprecation banner
321
+ const DEPRECATION_BANNER = [
322
+ '!!! warning "Deprecated"',
323
+ ' This API has been removed from the current version of the framework.',
324
+ ' This page is kept for reference only. Do not use in new code.',
325
+ '',
326
+ ].join('\n');
327
+
328
+ const managedDirs = [
329
+ path.join(DOCS_OUT, 'Essentials', 'composables'),
330
+ path.join(DOCS_OUT, 'Essentials', 'plugins'),
331
+ path.join(DOCS_OUT, 'Essentials', 'ui-components'),
332
+ ];
333
+
334
+ // Build set of all output paths we just synced
335
+ const syncedOutPaths = new Set();
336
+ for (const relPath of frameworkDocs) {
337
+ const mapping = mapPath(relPath);
338
+ if (mapping) syncedOutPaths.add(path.join(DOCS_OUT, mapping.outPath));
339
+ }
340
+
341
+ let deprecated = 0;
342
+ for (const dir of managedDirs) {
343
+ if (!fs.existsSync(dir)) continue;
344
+ for (const file of fs.readdirSync(dir)) {
345
+ if (!file.endsWith('.md')) continue;
346
+ const filePath = path.join(dir, file);
347
+ if (syncedOutPaths.has(filePath)) continue;
348
+
349
+ // File exists in vc-docs but not in framework — mark as deprecated
350
+ const content = fs.readFileSync(filePath, 'utf-8');
351
+ if (content.includes('This API has been removed')) continue; // already marked
352
+
353
+ fs.writeFileSync(filePath, DEPRECATION_BANNER + '\n' + content);
354
+ const relFile = path.relative(DOCS_OUT, filePath);
355
+ console.log(` deprecated: ${relFile}`);
356
+ deprecated++;
357
+ }
358
+ }
359
+
360
+ console.log(`\nSync to vc-docs complete: ${synced} synced, ${skipped} unchanged, ${unmapped} unmapped, ${deprecated} deprecated`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vc-shell/vc-app-skill",
3
- "version": "2.0.0-alpha.25",
3
+ "version": "2.0.0-alpha.26",
4
4
  "description": "AI coding skill for scaffolding and generating VirtoCommerce Shell applications. Works with Claude Code, OpenCode, Gemini, Codex, Cursor.",
5
5
  "bin": "./bin/install.cjs",
6
6
  "files": [
@@ -20,7 +20,8 @@
20
20
  ],
21
21
  "scripts": {
22
22
  "sync-docs": "node bin/sync-docs.cjs",
23
- "knowledge-stats": "node bin/knowledge-stats.cjs"
23
+ "knowledge-stats": "node bin/knowledge-stats.cjs",
24
+ "sync-to-docs": "node bin/sync-to-docs.cjs"
24
25
  },
25
26
  "publishConfig": {
26
27
  "access": "public",
package/runtime/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.0-alpha.25
1
+ 2.0.0-alpha.26
@@ -1 +1 @@
1
- Synced from framework at commit 99a2f022b on 2026-03-25T11:57:14.169Z
1
+ Synced from framework at commit 6f3f68723 on 2026-03-25T13:28:17.111Z
@@ -286,5 +286,5 @@ currentPage.value = Math.min(currentPage.value, totalPages.value);
286
286
 
287
287
  ## Related Components
288
288
 
289
- - [VcDataTable](../../organisms/vc-table/) -- Data table that commonly pairs with pagination for large datasets
289
+ - [VcDataTable](../../organisms/vc-table) -- Data table that commonly pairs with pagination for large datasets
290
290
  - [VcSelect](../vc-select/) -- Can be used alongside pagination for a "rows per page" selector
@@ -24,8 +24,8 @@ Columns are defined as `<VcColumn>` child components -- no configuration objects
24
24
  |----------|-----------|
25
25
  | Tabular data with sorting, selection, pagination | **VcDataTable** |
26
26
  | Simple short list without table features | `v-for` with custom markup |
27
- | Image/card grid layout | [VcGallery](../vc-gallery/) |
28
- | Key-value detail display | [VcField](../../molecules/vc-field/) or [VcCard](../../atoms/vc-card/) |
27
+ | Image/card grid layout | [VcGallery](../vc-gallery) |
28
+ | Key-value detail display | [VcField](../../molecules/vc-field) or [VcCard](../../atoms/vc-card) |
29
29
 
30
30
  Use VcDataTable whenever you need structured rows and columns with any combination of sorting, filtering, inline editing, or column management. **Do not use** VcDataTable for simple lists of 5-10 items that need no table features -- a plain `v-for` loop is lighter. For thumbnail/card grids, prefer VcGallery.
31
31