draftgo-cli 1.0.8 → 1.0.10
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 +7 -7
- package/package.json +1 -1
- package/resources/skill/manifest.json +1 -1
- package/src/cli.js +3 -0
- package/src/commands/components.js +302 -101
- package/src/commands/help.js +4 -4
package/README.md
CHANGED
|
@@ -111,19 +111,19 @@ draftgo conflict resolve <type> <id>
|
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
113
|
draftgo components search <query> --output json
|
|
114
|
-
draftgo components show <library/component> --output json
|
|
114
|
+
draftgo components show <id|library/component|query> --output json
|
|
115
115
|
draftgo components expand --page <id> --instance <data-dg-instance>
|
|
116
116
|
draftgo verify pages <id>
|
|
117
117
|
|
|
118
118
|
# 开发组件:commit 保存草稿,publish 显式上线
|
|
119
|
-
draftgo components checkout <library/component>
|
|
120
|
-
draftgo components diff <library/component>
|
|
121
|
-
draftgo components verify <library/component>
|
|
122
|
-
draftgo components commit <library/component>
|
|
123
|
-
draftgo components publish <library/component>
|
|
119
|
+
draftgo components checkout <id|library/component>
|
|
120
|
+
draftgo components diff <id|library/component>
|
|
121
|
+
draftgo components verify <id|library/component>
|
|
122
|
+
draftgo components commit <id|library/component>
|
|
123
|
+
draftgo components publish <id|library/component>
|
|
124
124
|
|
|
125
125
|
# 库管理和标准 ZIP 迁移
|
|
126
|
-
draftgo components libraries list|show|create|update|delete
|
|
126
|
+
draftgo components libraries list|search|show|create|update|delete
|
|
127
127
|
draftgo components import <archive.zip> --dry-run
|
|
128
128
|
draftgo components export <library> --file <archive.zip>
|
|
129
129
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "draftgo-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Install and manage the DraftGo skill across AI coding agents (Claude Code, Codex, Cursor, Windsurf, Antigravity, Copilot, Gemini, Kiro, Pi, ZCode).",
|
|
5
5
|
"bin": {
|
|
6
6
|
"draftgo": "bin/draftgo.js"
|
package/src/cli.js
CHANGED
|
@@ -9,6 +9,11 @@ const log = require('../logger');
|
|
|
9
9
|
const { loadProjectConfig } = require('../projectConfig');
|
|
10
10
|
const { loadManifest, getEntry, absolutePath } = require('../worktree/manifest');
|
|
11
11
|
|
|
12
|
+
const COMPONENT_PAGE_SIZE = 100;
|
|
13
|
+
const MAX_COMPONENT_PAGES = 1000;
|
|
14
|
+
const DEFINITION_FIELDS = ['html', 'css', 'js', 'assets', 'props', 'slots', 'css_variables', 'events', 'dependencies', 'examples', 'usage', 'root_tag', 'meta'];
|
|
15
|
+
const METADATA_FIELDS = ['name', 'slug', 'category', 'description', 'tags', 'status'];
|
|
16
|
+
|
|
12
17
|
async function request(config, method, route, body) {
|
|
13
18
|
const multipart = typeof FormData !== 'undefined' && body instanceof FormData;
|
|
14
19
|
const response = await fetch(`${config.server}/api/${route.replace(/^\//, '')}`, {
|
|
@@ -25,14 +30,71 @@ function componentName(item) {
|
|
|
25
30
|
return item.full_name || `${item.library_slug}/${item.slug}`;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
function componentItems(result) {
|
|
34
|
+
if (Array.isArray(result)) return result;
|
|
35
|
+
return Array.isArray(result?.items) ? result.items : [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function listAllComponents(config, filters = {}) {
|
|
39
|
+
const items = [];
|
|
40
|
+
const seenPages = new Set();
|
|
41
|
+
for (let page = 1; page <= MAX_COMPONENT_PAGES; page += 1) {
|
|
42
|
+
const query = new URLSearchParams({ page: String(page), page_size: String(COMPONENT_PAGE_SIZE) });
|
|
43
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
44
|
+
if (value !== undefined && value !== null && value !== '') query.set(key, String(value));
|
|
45
|
+
}
|
|
46
|
+
const result = await request(config, 'GET', `components?${query}`);
|
|
47
|
+
const pageItems = componentItems(result);
|
|
48
|
+
const signature = pageItems.map(item => String(item.id ?? componentName(item))).join('\u0000');
|
|
49
|
+
if (page > 1 && pageItems.length && seenPages.has(signature)) {
|
|
50
|
+
throw new Error(`DraftGo component catalog repeated page ${page}; refusing an incomplete result.`);
|
|
51
|
+
}
|
|
52
|
+
if (pageItems.length) seenPages.add(signature);
|
|
53
|
+
items.push(...pageItems);
|
|
54
|
+
const hasTotal = result?.total !== undefined && result?.total !== null
|
|
55
|
+
&& Number.isFinite(Number(result.total)) && Number(result.total) >= 0;
|
|
56
|
+
if (hasTotal) {
|
|
57
|
+
if (items.length >= Number(result.total)) return items;
|
|
58
|
+
if (!pageItems.length) throw new Error(`DraftGo component catalog ended before its reported total on page ${page}.`);
|
|
59
|
+
} else if (pageItems.length < COMPONENT_PAGE_SIZE) return items;
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`DraftGo component catalog exceeded ${MAX_COMPONENT_PAGES} pages.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ambiguousComponent(selector, items) {
|
|
65
|
+
const candidates = items.map(item => `${componentName(item)} (id ${item.id})`).join(', ');
|
|
66
|
+
return new Error(`Component selector is ambiguous: ${selector}. Use an ID or full library/component name. Candidates: ${candidates}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function findComponent(config, selector) {
|
|
70
|
+
const value = String(selector || '').trim();
|
|
71
|
+
if (!value) throw new Error('Component selector must be an ID or component name.');
|
|
72
|
+
if (/^[1-9]\d*$/.test(value)) {
|
|
73
|
+
try {
|
|
74
|
+
return await request(config, 'GET', `components/${value}`);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new Error(`Component not found: ${value}`, { cause: error });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (value.includes('/')) {
|
|
81
|
+
const parts = value.split('/');
|
|
82
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) throw new Error('Component name must be <library/component>.');
|
|
83
|
+
const [library, slug] = parts;
|
|
84
|
+
const items = await listAllComponents(config, { library, q: slug });
|
|
85
|
+
const exact = items.filter(item => componentName(item) === value
|
|
86
|
+
|| (item.library_slug === library && item.slug === slug));
|
|
87
|
+
if (!exact.length) throw new Error(`Component not found: ${value}`);
|
|
88
|
+
if (exact.length > 1) throw ambiguousComponent(value, exact);
|
|
89
|
+
return request(config, 'GET', `components/${exact[0].id}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const items = await listAllComponents(config, { q: value });
|
|
93
|
+
const exact = items.filter(item => item.slug === value || item.name === value);
|
|
94
|
+
const candidates = exact.length ? exact : items;
|
|
95
|
+
if (!candidates.length) throw new Error(`Component not found: ${value}`);
|
|
96
|
+
if (candidates.length > 1) throw ambiguousComponent(value, candidates);
|
|
97
|
+
return request(config, 'GET', `components/${candidates[0].id}`);
|
|
36
98
|
}
|
|
37
99
|
|
|
38
100
|
function readJSON(projectDir, file) {
|
|
@@ -57,6 +119,21 @@ function definitionHash(definition) {
|
|
|
57
119
|
return crypto.createHash('sha256').update(JSON.stringify(normalizeDefinition(definition))).digest('hex');
|
|
58
120
|
}
|
|
59
121
|
|
|
122
|
+
function metadataFrom(item = {}) {
|
|
123
|
+
return Object.fromEntries(METADATA_FIELDS.map(key => [key, key === 'tags' ? (item[key] || []) : (item[key] ?? '')]));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function changedFields(left, right, fields) {
|
|
127
|
+
return fields.filter(key => JSON.stringify(left?.[key] ?? null) !== JSON.stringify(right?.[key] ?? null));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function localChanges(definition, metadata, base) {
|
|
131
|
+
return [
|
|
132
|
+
...changedFields(normalizeDefinition(definition), normalizeDefinition(base?.draft), DEFINITION_FIELDS),
|
|
133
|
+
...changedFields(metadataFrom(metadata), metadataFrom(base), METADATA_FIELDS).map(key => `metadata.${key}`),
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
60
137
|
function componentDir(projectDir, name) {
|
|
61
138
|
const [library, slug] = String(name).split('/');
|
|
62
139
|
if (!library || !slug || !/^[a-z0-9-]+$/.test(library) || !/^[a-z0-9-]+$/.test(slug)) {
|
|
@@ -80,20 +157,41 @@ function localDefinition(directory) {
|
|
|
80
157
|
});
|
|
81
158
|
}
|
|
82
159
|
|
|
83
|
-
function
|
|
160
|
+
function atomicWriteFile(file, value) {
|
|
161
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
162
|
+
const suffix = `${process.pid}-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
163
|
+
const temporary = `${file}.tmp-${suffix}`;
|
|
164
|
+
const backup = `${file}.bak-${suffix}`;
|
|
165
|
+
fs.writeFileSync(temporary, value);
|
|
166
|
+
let moved = false;
|
|
167
|
+
try {
|
|
168
|
+
if (fs.existsSync(file)) {
|
|
169
|
+
fs.renameSync(file, backup);
|
|
170
|
+
moved = true;
|
|
171
|
+
}
|
|
172
|
+
fs.renameSync(temporary, file);
|
|
173
|
+
if (moved) fs.rmSync(backup, { force: true });
|
|
174
|
+
} catch (error) {
|
|
175
|
+
fs.rmSync(temporary, { force: true });
|
|
176
|
+
if (moved && !fs.existsSync(file) && fs.existsSync(backup)) fs.renameSync(backup, file);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function writeComponentFiles(directory, item, server) {
|
|
84
182
|
fs.mkdirSync(directory, { recursive: true });
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
183
|
+
atomicWriteFile(path.join(directory, 'component.html'), item.draft.html || '');
|
|
184
|
+
atomicWriteFile(path.join(directory, 'component.css'), item.draft.css || '');
|
|
185
|
+
atomicWriteFile(path.join(directory, 'component.js'), item.draft.js || '');
|
|
88
186
|
const metadata = {
|
|
89
187
|
id: item.id, library_id: item.library_id, library_slug: item.library_slug,
|
|
90
188
|
name: item.name, slug: item.slug, category: item.category || '', description: item.description || '',
|
|
91
189
|
tags: item.tags || [], status: item.status || 'active', published_revision: item.published_revision || 0,
|
|
92
|
-
remote_draft_hash: definitionHash(item.draft), updated_at: item.updated_at,
|
|
190
|
+
version: item.version, remote_draft_hash: definitionHash(item.draft), updated_at: item.updated_at,
|
|
93
191
|
...contractFrom(item.draft),
|
|
94
192
|
};
|
|
95
|
-
|
|
96
|
-
|
|
193
|
+
atomicWriteFile(path.join(directory, 'component.json'), `${JSON.stringify(metadata, null, 2)}\n`);
|
|
194
|
+
atomicWriteFile(path.join(directory, '.base.json'), `${JSON.stringify({ ...item, server }, null, 2)}\n`);
|
|
97
195
|
}
|
|
98
196
|
|
|
99
197
|
function output(value, flags, title) {
|
|
@@ -105,67 +203,125 @@ function output(value, flags, title) {
|
|
|
105
203
|
async function search(projectDir, query, flags) {
|
|
106
204
|
if (!query) throw new Error('Usage: draftgo components search <query>');
|
|
107
205
|
const config = loadProjectConfig(projectDir);
|
|
108
|
-
|
|
109
|
-
|
|
206
|
+
let items;
|
|
207
|
+
if (/^[1-9]\d*$/.test(query) || query.includes('/')) {
|
|
208
|
+
items = [await findComponent(config, query)];
|
|
209
|
+
} else {
|
|
210
|
+
items = await listAllComponents(config, { q: query });
|
|
211
|
+
}
|
|
212
|
+
output(items, flags, `DraftGo components: ${query}`);
|
|
110
213
|
return 0;
|
|
111
214
|
}
|
|
112
215
|
|
|
113
|
-
async function show(projectDir,
|
|
114
|
-
if (!
|
|
216
|
+
async function show(projectDir, selector, flags) {
|
|
217
|
+
if (!selector) throw new Error('Usage: draftgo components show <id|library/component|query>');
|
|
115
218
|
const config = loadProjectConfig(projectDir);
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
const item = (result.items || []).find(component => `${component.library_slug}/${component.slug}` === name || component.slug === slug);
|
|
119
|
-
if (!item) throw new Error(`Component not found: ${name}`);
|
|
120
|
-
output(item, flags, `DraftGo component ${name}`);
|
|
219
|
+
const item = await findComponent(config, selector);
|
|
220
|
+
output(item, flags, `DraftGo component ${componentName(item)}`);
|
|
121
221
|
return 0;
|
|
122
222
|
}
|
|
123
223
|
|
|
124
224
|
async function list(projectDir, flags) {
|
|
125
225
|
const config = loadProjectConfig(projectDir);
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
if (flags.status) query.set('status', String(flags.status));
|
|
129
|
-
const result = await request(config, 'GET', `components?${query}`);
|
|
130
|
-
output(result.items || result, flags, 'DraftGo components');
|
|
226
|
+
const items = await listAllComponents(config, { library: flags.library, status: flags.status, category: flags.category });
|
|
227
|
+
output(items, flags, 'DraftGo components');
|
|
131
228
|
return 0;
|
|
132
229
|
}
|
|
133
230
|
|
|
231
|
+
async function listAllLibraries(config, filters = {}) {
|
|
232
|
+
const items = [];
|
|
233
|
+
const seenPages = new Set();
|
|
234
|
+
for (let page = 1; page <= MAX_COMPONENT_PAGES; page += 1) {
|
|
235
|
+
const query = new URLSearchParams({ page: String(page), page_size: String(COMPONENT_PAGE_SIZE) });
|
|
236
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
237
|
+
if (value !== undefined && value !== null && value !== '') query.set(key, String(value));
|
|
238
|
+
}
|
|
239
|
+
const result = await request(config, 'GET', `component-libraries?${query}`);
|
|
240
|
+
const pageItems = componentItems(result);
|
|
241
|
+
const signature = pageItems.map(item => String(item.id ?? item.slug)).join('\u0000');
|
|
242
|
+
if (page > 1 && pageItems.length && seenPages.has(signature)) {
|
|
243
|
+
throw new Error(`DraftGo component library catalog repeated page ${page}; refusing an incomplete result.`);
|
|
244
|
+
}
|
|
245
|
+
if (pageItems.length) seenPages.add(signature);
|
|
246
|
+
items.push(...pageItems);
|
|
247
|
+
const hasTotal = result?.total !== undefined && result?.total !== null
|
|
248
|
+
&& Number.isFinite(Number(result.total)) && Number(result.total) >= 0;
|
|
249
|
+
if (hasTotal) {
|
|
250
|
+
if (items.length >= Number(result.total)) return items;
|
|
251
|
+
if (!pageItems.length) throw new Error(`DraftGo component library catalog ended before its reported total on page ${page}.`);
|
|
252
|
+
} else if (pageItems.length < COMPONENT_PAGE_SIZE) return items;
|
|
253
|
+
}
|
|
254
|
+
throw new Error(`DraftGo component library catalog exceeded ${MAX_COMPONENT_PAGES} pages.`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function ambiguousLibrary(selector, items) {
|
|
258
|
+
const candidates = items.map(item => `${item.slug} (${item.name}, id ${item.id})`).join(', ');
|
|
259
|
+
return new Error(`Component library selector is ambiguous: ${selector}. Use an ID or exact slug. Candidates: ${candidates}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function findLibrary(config, selector) {
|
|
263
|
+
const value = String(selector || '').trim();
|
|
264
|
+
if (!value) throw new Error('Component library selector must be an ID, slug, or name.');
|
|
265
|
+
if (/^[1-9]\d*$/.test(value)) {
|
|
266
|
+
try { return await request(config, 'GET', `component-libraries/${value}`); } catch (error) {
|
|
267
|
+
throw new Error(`Component library not found: ${value}`, { cause: error });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const items = await listAllLibraries(config, { q: value });
|
|
271
|
+
const slugMatches = items.filter(item => item.slug === value);
|
|
272
|
+
if (slugMatches.length === 1) return request(config, 'GET', `component-libraries/${slugMatches[0].id}`);
|
|
273
|
+
if (slugMatches.length > 1) throw ambiguousLibrary(value, slugMatches);
|
|
274
|
+
const nameMatches = items.filter(item => item.name === value);
|
|
275
|
+
const candidates = nameMatches.length ? nameMatches : items;
|
|
276
|
+
if (!candidates.length) throw new Error(`Component library not found: ${value}`);
|
|
277
|
+
if (candidates.length > 1) throw ambiguousLibrary(value, candidates);
|
|
278
|
+
return request(config, 'GET', `component-libraries/${candidates[0].id}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
134
281
|
async function libraries(projectDir, positional, flags) {
|
|
135
282
|
const action = String(positional[0] || 'list').toLowerCase();
|
|
136
283
|
const config = loadProjectConfig(projectDir);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
if (!
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (action === 'show') {
|
|
284
|
+
if (action === 'list') { output(await listAllLibraries(config, { status: flags.status }), flags, 'DraftGo component libraries'); return 0; }
|
|
285
|
+
if (action === 'search') {
|
|
286
|
+
const query = positional.slice(1).join(' ').trim();
|
|
287
|
+
if (!query) throw new Error('Usage: draftgo components libraries search <query>');
|
|
288
|
+
output(await listAllLibraries(config, { q: query, status: flags.status }), flags, `DraftGo component libraries: ${query}`);
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
if (action === 'show') {
|
|
292
|
+
const item = await findLibrary(config, positional[1]);
|
|
293
|
+
output(item, flags, `Component library ${item.slug}`);
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
145
296
|
if (action === 'delete') {
|
|
146
|
-
const item = await
|
|
297
|
+
const item = await findLibrary(config, positional[1]);
|
|
147
298
|
output(await request(config, 'DELETE', `component-libraries/${item.id}`), flags, 'Component library deleted');
|
|
148
299
|
return 0;
|
|
149
300
|
}
|
|
150
301
|
if (action === 'create' || action === 'update') {
|
|
151
|
-
const current = action === 'update' ? await
|
|
152
|
-
const
|
|
302
|
+
const current = action === 'update' ? await findLibrary(config, positional[1]) : {};
|
|
303
|
+
const source = { ...current, ...readJSON(projectDir, flags.file) };
|
|
153
304
|
for (const [flag, key] of [['name', 'name'], ['slug', 'slug'], ['description', 'description'], ['source-type', 'source_type'], ['status', 'status']]) {
|
|
154
|
-
if (flags[flag] !== undefined)
|
|
305
|
+
if (flags[flag] !== undefined) source[key] = flags[flag];
|
|
155
306
|
}
|
|
307
|
+
if (flags['common-css'] !== undefined) source.common_css = flags['common-css'];
|
|
308
|
+
if (flags['common-js'] !== undefined) source.common_js = flags['common-js'];
|
|
309
|
+
if (flags.assets !== undefined) source.assets = csv(flags.assets) || [];
|
|
310
|
+
const input = Object.fromEntries(['name', 'slug', 'description', 'source_type', 'common_css', 'common_js', 'assets', 'status']
|
|
311
|
+
.map(key => [key, source[key] ?? (key === 'assets' ? [] : '')]));
|
|
312
|
+
if (action === 'update') input.expected_version = current.version;
|
|
156
313
|
const result = await request(config, action === 'create' ? 'POST' : 'PUT',
|
|
157
314
|
action === 'create' ? 'component-libraries' : `component-libraries/${current.id}`, input);
|
|
158
315
|
output(result, flags, `Component library ${action}d`);
|
|
159
316
|
return 0;
|
|
160
317
|
}
|
|
161
|
-
throw new Error('Usage: draftgo components libraries list|show|create|update|delete');
|
|
318
|
+
throw new Error('Usage: draftgo components libraries list|search|show|create|update|delete');
|
|
162
319
|
}
|
|
163
320
|
|
|
164
321
|
async function create(projectDir, flags) {
|
|
165
322
|
const config = loadProjectConfig(projectDir);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (!library) throw new Error('Usage: draftgo components create --library <id|slug> --name <name> --slug <slug> [--file input.json]');
|
|
323
|
+
if (!flags.library) throw new Error('Usage: draftgo components create --library <id|slug> --name <name> --slug <slug> [--file input.json]');
|
|
324
|
+
const library = await findLibrary(config, flags.library);
|
|
169
325
|
const fileInput = readJSON(projectDir, flags.file);
|
|
170
326
|
const input = {
|
|
171
327
|
...fileInput, library_id: library.id,
|
|
@@ -190,9 +346,8 @@ async function remove(projectDir, name, flags) {
|
|
|
190
346
|
async function copy(projectDir, source, flags) {
|
|
191
347
|
const config = loadProjectConfig(projectDir);
|
|
192
348
|
const item = await findComponent(config, source);
|
|
193
|
-
|
|
194
|
-
const library =
|
|
195
|
-
if (!library || !flags.slug) throw new Error('Usage: draftgo components copy <library/component> --slug <new-slug> [--library slug] [--name name]');
|
|
349
|
+
if (!flags.slug) throw new Error('Usage: draftgo components copy <id|library/component> --slug <new-slug> [--library id|slug] [--name name]');
|
|
350
|
+
const library = await findLibrary(config, flags.library || item.library_slug);
|
|
196
351
|
const created = await request(config, 'POST', 'components', {
|
|
197
352
|
library_id: library.id, name: flags.name || `${item.name} Copy`, slug: flags.slug,
|
|
198
353
|
category: item.category, description: item.description, tags: item.tags, draft: item.draft, status: item.status,
|
|
@@ -207,11 +362,12 @@ async function checkout(projectDir, name, flags) {
|
|
|
207
362
|
const directory = componentDir(projectDir, componentName(item));
|
|
208
363
|
if (fs.existsSync(path.join(directory, '.base.json')) && !flags.force) {
|
|
209
364
|
const base = JSON.parse(fs.readFileSync(path.join(directory, '.base.json'), 'utf8'));
|
|
210
|
-
|
|
365
|
+
const metadata = JSON.parse(fs.readFileSync(path.join(directory, 'component.json'), 'utf8'));
|
|
366
|
+
if (localChanges(localDefinition(directory), metadata, base).length) {
|
|
211
367
|
throw new Error(`Local changes present for ${name}; commit them or use --force.`);
|
|
212
368
|
}
|
|
213
369
|
}
|
|
214
|
-
writeComponentFiles(directory, item);
|
|
370
|
+
writeComponentFiles(directory, item, config.server);
|
|
215
371
|
output({ component: componentName(item), directory: path.relative(projectDir, directory).replace(/\\/g, '/') }, flags, 'Component checked out');
|
|
216
372
|
return 0;
|
|
217
373
|
}
|
|
@@ -227,24 +383,46 @@ function localState(projectDir, name) {
|
|
|
227
383
|
};
|
|
228
384
|
}
|
|
229
385
|
|
|
386
|
+
function assertLocalState(state, remote, config) {
|
|
387
|
+
if (state.base.server && String(state.base.server).replace(/\/$/, '') !== String(config.server).replace(/\/$/, '')) {
|
|
388
|
+
throw new Error(`Component checkout belongs to ${state.base.server}; checkout again before using ${config.server}.`);
|
|
389
|
+
}
|
|
390
|
+
for (const key of ['id', 'library_id', 'library_slug', 'slug']) {
|
|
391
|
+
if (String(state.metadata[key] ?? '') !== String(state.base[key] ?? '')) {
|
|
392
|
+
throw new Error(`component.json cannot change component identity field ${key}; create or copy a component instead.`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (String(remote.id) !== String(state.base.id)) throw new Error('Local component base does not match the selected remote component.');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function selectedState(projectDir, config, selector) {
|
|
399
|
+
const remote = await findComponent(config, selector);
|
|
400
|
+
const name = componentName(remote);
|
|
401
|
+
const state = localState(projectDir, name);
|
|
402
|
+
assertLocalState(state, remote, config);
|
|
403
|
+
return { remote, name, state };
|
|
404
|
+
}
|
|
405
|
+
|
|
230
406
|
async function diff(projectDir, name, flags) {
|
|
231
407
|
const config = loadProjectConfig(projectDir);
|
|
232
|
-
const
|
|
233
|
-
const remote =
|
|
234
|
-
const
|
|
235
|
-
const changed = (left, right) => {
|
|
236
|
-
const normalizedLeft = normalizeDefinition(left);
|
|
237
|
-
const normalizedRight = normalizeDefinition(right);
|
|
238
|
-
return sections.filter(key => JSON.stringify(normalizedLeft[key]) !== JSON.stringify(normalizedRight[key]));
|
|
239
|
-
};
|
|
408
|
+
const selected = await selectedState(projectDir, config, name);
|
|
409
|
+
const { state, remote } = selected;
|
|
410
|
+
const changed = (left, right) => changedFields(normalizeDefinition(left), normalizeDefinition(right), DEFINITION_FIELDS);
|
|
240
411
|
const result = {
|
|
241
|
-
component: name,
|
|
242
|
-
|
|
412
|
+
component: selected.name,
|
|
413
|
+
local_vs_base: localChanges(state.definition, state.metadata, state.base),
|
|
414
|
+
local_vs_remote_draft: [
|
|
415
|
+
...changed(state.definition, remote.draft),
|
|
416
|
+
...changedFields(metadataFrom(state.metadata), metadataFrom(remote), METADATA_FIELDS).map(key => `metadata.${key}`),
|
|
417
|
+
],
|
|
243
418
|
local_vs_published: changed(state.definition, remote.published),
|
|
244
|
-
base_remote_changed:
|
|
419
|
+
base_remote_changed: state.base.version !== remote.version
|
|
420
|
+
|| localChanges(remote.draft, remote, state.base).length > 0,
|
|
421
|
+
base_version: state.base.version,
|
|
422
|
+
remote_version: remote.version,
|
|
245
423
|
published_revision: remote.published_revision || 0,
|
|
246
424
|
};
|
|
247
|
-
output(result, flags, `Component diff ${name}`);
|
|
425
|
+
output(result, flags, `Component diff ${selected.name}`);
|
|
248
426
|
return 0;
|
|
249
427
|
}
|
|
250
428
|
|
|
@@ -271,54 +449,64 @@ function verifyDefinition(definition) {
|
|
|
271
449
|
|
|
272
450
|
async function verify(projectDir, name, flags) {
|
|
273
451
|
const config = loadProjectConfig(projectDir);
|
|
274
|
-
const
|
|
452
|
+
const selected = await selectedState(projectDir, config, name);
|
|
453
|
+
const { state } = selected;
|
|
275
454
|
const errors = verifyDefinition(state.definition);
|
|
276
455
|
for (const dependency of state.definition.dependencies || []) {
|
|
277
456
|
try { await findComponent(config, dependency); } catch { errors.push(`Dependency not found: ${dependency}`); }
|
|
278
457
|
}
|
|
279
|
-
const result = { component: name, valid: errors.length === 0, errors };
|
|
280
|
-
output(result, flags, `Component verify ${name}`);
|
|
458
|
+
const result = { component: selected.name, valid: errors.length === 0, errors };
|
|
459
|
+
output(result, flags, `Component verify ${selected.name}`);
|
|
281
460
|
if (errors.length) throw new Error(`Component verification failed with ${errors.length} error(s).`);
|
|
282
461
|
return 0;
|
|
283
462
|
}
|
|
284
463
|
|
|
285
464
|
async function commit(projectDir, name, flags) {
|
|
286
465
|
const config = loadProjectConfig(projectDir);
|
|
287
|
-
const
|
|
466
|
+
const selected = await selectedState(projectDir, config, name);
|
|
467
|
+
const { state, remote } = selected;
|
|
288
468
|
const errors = verifyDefinition(state.definition);
|
|
289
469
|
for (const dependency of state.definition.dependencies || []) {
|
|
290
470
|
try { await findComponent(config, dependency); } catch { errors.push(`Dependency not found: ${dependency}`); }
|
|
291
471
|
}
|
|
292
472
|
if (errors.length) throw new Error(`Component verification failed: ${errors.join(' ')}`);
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
473
|
+
if (!Number.isInteger(remote.version) || remote.version < 1 || !Number.isInteger(state.base.version) || state.base.version < 1) {
|
|
474
|
+
throw new Error(`Component ${selected.name} is missing a valid version; run components checkout again.`);
|
|
475
|
+
}
|
|
476
|
+
if (remote.version !== state.base.version || localChanges(remote.draft, remote, state.base).length) {
|
|
477
|
+
throw new Error(`Remote component changed after checkout for ${selected.name}; run components checkout after preserving local changes.`);
|
|
478
|
+
}
|
|
479
|
+
if (!localChanges(state.definition, state.metadata, remote).length) {
|
|
480
|
+
writeComponentFiles(state.directory, remote, config.server);
|
|
481
|
+
output({ component: selected.name, status: 'unchanged' }, flags, 'Component draft unchanged');
|
|
301
482
|
return 0;
|
|
302
483
|
}
|
|
303
484
|
const metadata = state.metadata;
|
|
304
485
|
const result = await request(config, 'PUT', `components/${remote.id}`, {
|
|
305
486
|
library_id: remote.library_id, name: metadata.name, slug: metadata.slug, category: metadata.category || '',
|
|
306
487
|
description: metadata.description || '', tags: metadata.tags || [], status: metadata.status || 'active', draft: state.definition,
|
|
488
|
+
expected_version: remote.version,
|
|
307
489
|
});
|
|
308
|
-
writeComponentFiles(state.directory, result);
|
|
490
|
+
writeComponentFiles(state.directory, result, config.server);
|
|
309
491
|
output(result, flags, 'Component draft committed');
|
|
310
492
|
return 0;
|
|
311
493
|
}
|
|
312
494
|
|
|
313
495
|
async function publish(projectDir, name, flags) {
|
|
314
496
|
const config = loadProjectConfig(projectDir);
|
|
315
|
-
const
|
|
316
|
-
const state =
|
|
317
|
-
if (
|
|
497
|
+
const selected = await selectedState(projectDir, config, name);
|
|
498
|
+
const { state, remote } = selected;
|
|
499
|
+
if (!Number.isInteger(remote.version) || remote.version < 1 || !Number.isInteger(state.base.version) || state.base.version < 1) {
|
|
500
|
+
throw new Error(`Component ${selected.name} is missing a valid version; run components checkout again.`);
|
|
501
|
+
}
|
|
502
|
+
if (remote.version !== state.base.version || localChanges(remote.draft, remote, state.base).length) {
|
|
503
|
+
throw new Error(`Remote component changed after checkout for ${selected.name}; checkout again before publish.`);
|
|
504
|
+
}
|
|
505
|
+
if (localChanges(state.definition, state.metadata, remote).length) {
|
|
318
506
|
throw new Error('Local component differs from the remote draft; commit before publish.');
|
|
319
507
|
}
|
|
320
|
-
const result = await request(config, 'POST', `components/${remote.id}/publish`, {});
|
|
321
|
-
writeComponentFiles(state.directory, result);
|
|
508
|
+
const result = await request(config, 'POST', `components/${remote.id}/publish`, { expected_version: remote.version });
|
|
509
|
+
writeComponentFiles(state.directory, result, config.server);
|
|
322
510
|
output(result, flags, 'Component published');
|
|
323
511
|
return 0;
|
|
324
512
|
}
|
|
@@ -337,15 +525,8 @@ async function importZIP(projectDir, file, flags) {
|
|
|
337
525
|
async function exportZIP(projectDir, librarySlug, flags) {
|
|
338
526
|
if (!librarySlug) throw new Error('Usage: draftgo components export <library> --file <zip-file>');
|
|
339
527
|
const config = loadProjectConfig(projectDir);
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
if (!library) throw new Error(`Component library not found: ${librarySlug}`);
|
|
343
|
-
const components = [];
|
|
344
|
-
for (let page = 1; ; page += 1) {
|
|
345
|
-
const result = await request(config, 'GET', `components?library=${encodeURIComponent(library.slug)}&page=${page}&page_size=100`);
|
|
346
|
-
components.push(...(result.items || []));
|
|
347
|
-
if ((result.items || []).length < 100) break;
|
|
348
|
-
}
|
|
528
|
+
const library = await findLibrary(config, librarySlug);
|
|
529
|
+
const components = await listAllComponents(config, { library: library.slug });
|
|
349
530
|
const zip = new AdmZip();
|
|
350
531
|
const manifest = {
|
|
351
532
|
library: { name: library.name, slug: library.slug, description: library.description, source_type: 'imported', common_css: 'library/common.css', common_js: 'library/common.js', assets: [], status: library.status },
|
|
@@ -482,7 +663,10 @@ function expandHtml(source, instance, component, runtimeCatalog = null) {
|
|
|
482
663
|
.map(item => item.definition?.css || '')
|
|
483
664
|
.filter(Boolean)
|
|
484
665
|
.join('\n');
|
|
485
|
-
const assets =
|
|
666
|
+
const assets = [...new Set([
|
|
667
|
+
...libraries.flatMap(library => library.assets || []),
|
|
668
|
+
...(runtimeCatalog?.components || []).flatMap(item => item.definition?.assets || []),
|
|
669
|
+
].filter(Boolean))];
|
|
486
670
|
const assetLinks = assets.map(value => {
|
|
487
671
|
const url = String(value).replace(/&/g, '&').replace(/"/g, '"');
|
|
488
672
|
if (/\.css(?:[?#]|$)/i.test(url)) return `<link rel="stylesheet" href="${url}" data-dg-expanded-asset>`;
|
|
@@ -490,6 +674,10 @@ function expandHtml(source, instance, component, runtimeCatalog = null) {
|
|
|
490
674
|
return `<link rel="preload" href="${url}" as="fetch" data-dg-expanded-asset>`;
|
|
491
675
|
}).join('');
|
|
492
676
|
const libraryJS = libraries.map(library => library.common_js || '').filter(Boolean).map((value, index) => `<script data-dg-expanded-library="${index}">${escapeScript(value)}</script>`).join('');
|
|
677
|
+
// Component JS is a host-scoped mount function in the Page runtime, not a
|
|
678
|
+
// registration script. Dependency definitions without a concrete expanded
|
|
679
|
+
// host must not be executed globally; their CSS and external assets are the
|
|
680
|
+
// dependency-level resources that apply to this expanded instance.
|
|
493
681
|
const combinedCSS = [libraryCSS, dependencyCSS, css].filter(Boolean).join('\n');
|
|
494
682
|
const style = combinedCSS ? `<style data-dg-expanded="${instance}">${combinedCSS}</style>` : '';
|
|
495
683
|
const propsJSON = JSON.stringify(props).replace(/</g, '\\u003c').replace(/<\/script/gi, '<\\/script');
|
|
@@ -515,18 +703,31 @@ async function expand(projectDir, flags) {
|
|
|
515
703
|
if (!target) throw new Error(`data-dg-instance not found: ${instance}`);
|
|
516
704
|
const use = target.attrs.find(attr => attr.name === 'data-dg-use')?.value;
|
|
517
705
|
if (!use) throw new Error('The selected instance does not have data-dg-use.');
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
if (!
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
706
|
+
const remote = await findComponent(config, use);
|
|
707
|
+
if (componentName(remote) !== use) throw new Error(`Component reference did not resolve exactly: ${use}`);
|
|
708
|
+
const runtimeCatalog = await request(config, 'POST', 'component-runtime/resolve', { components: [use] });
|
|
709
|
+
if (!runtimeCatalog || !Array.isArray(runtimeCatalog.components) || !Array.isArray(runtimeCatalog.libraries)) {
|
|
710
|
+
throw new Error(`DraftGo runtime returned an incomplete component catalog for ${use}.`);
|
|
711
|
+
}
|
|
712
|
+
if (Array.isArray(runtimeCatalog.missing) && runtimeCatalog.missing.length) {
|
|
713
|
+
throw new Error(`Unable to expand ${use}; missing runtime dependencies: ${runtimeCatalog.missing.join(', ')}`);
|
|
714
|
+
}
|
|
715
|
+
const byName = new Map(runtimeCatalog.components.map(item => [item.name, item]));
|
|
716
|
+
const runtimeComponent = byName.get(use);
|
|
717
|
+
if (!runtimeComponent?.definition) throw new Error(`DraftGo runtime did not return the exact published component ${use}.`);
|
|
718
|
+
for (const item of runtimeCatalog.components) {
|
|
719
|
+
for (const dependency of item.definition?.dependencies || []) {
|
|
720
|
+
if (!byName.has(dependency)) throw new Error(`Unable to expand ${use}; runtime dependency is missing: ${dependency}`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const librarySlugs = new Set(runtimeCatalog.libraries.map(item => item.slug));
|
|
724
|
+
for (const item of runtimeCatalog.components) {
|
|
725
|
+
const library = String(item.name || '').split('/')[0];
|
|
726
|
+
if (!library || !librarySlugs.has(library)) throw new Error(`Unable to expand ${use}; runtime library is missing: ${library || '(unknown)'}`);
|
|
528
727
|
}
|
|
529
|
-
|
|
728
|
+
const component = { ...remote, published: runtimeComponent.definition, published_revision: runtimeComponent.revision };
|
|
729
|
+
const expanded = expandHtml(source, instance, component, runtimeCatalog);
|
|
730
|
+
atomicWriteFile(file, expanded);
|
|
530
731
|
output({ page: String(pageID), instance, component: use, file: path.relative(projectDir, file).replace(/\\/g, '/') }, flags, 'Component expanded');
|
|
531
732
|
return 0;
|
|
532
733
|
}
|
package/src/commands/help.js
CHANGED
|
@@ -54,15 +54,15 @@ Usage:
|
|
|
54
54
|
--stat/--summary omit the full patch.
|
|
55
55
|
draftgo commit <type> <id...> Validate and upload complete checked-out bodies/files.
|
|
56
56
|
draftgo components search <query> Search the live component catalog.
|
|
57
|
-
draftgo components show <library/component>
|
|
57
|
+
draftgo components show <id|library/component|query>
|
|
58
58
|
Show HTML, props, slots, CSS variables and revision.
|
|
59
59
|
draftgo components expand --page <id> --instance <data-dg-instance>
|
|
60
60
|
Expand one component in a checked-out Page worktree.
|
|
61
|
-
draftgo components libraries list|show|create|update|delete
|
|
62
|
-
Manage component libraries.
|
|
61
|
+
draftgo components libraries list|search|show|create|update|delete
|
|
62
|
+
Manage component libraries.
|
|
63
63
|
draftgo components list|create|copy|delete
|
|
64
64
|
Manage component catalog entries.
|
|
65
|
-
draftgo components checkout|diff|verify|commit|publish <library/component>
|
|
65
|
+
draftgo components checkout|diff|verify|commit|publish <id|library/component>
|
|
66
66
|
Develop a component draft locally, then publish explicitly.
|
|
67
67
|
draftgo components import <zip> [--dry-run]
|
|
68
68
|
draftgo components export <library> [--file <zip>]
|