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 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.8",
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"
@@ -2,7 +2,7 @@
2
2
  "schema_version": "1.0",
3
3
  "id": "draftgo",
4
4
  "name": "DraftGo 开发助手",
5
- "version": "1.0.8",
5
+ "version": "1.0.10",
6
6
  "entry": "SKILL.md",
7
7
  "description": "以 Skill/reference 任务路由、MCP 实时发现、长正文 checkout/commit、统一验证和完成日志为边界的 DraftGo 工作流。",
8
8
  "license": "MIT",
package/src/cli.js CHANGED
@@ -47,6 +47,9 @@ const VALUE_FLAGS = new Set([
47
47
  'description',
48
48
  'tags',
49
49
  'source-type',
50
+ 'common-css',
51
+ 'common-js',
52
+ 'assets',
50
53
  'status',
51
54
  'module',
52
55
  'resource-type',
@@ -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
- async function findComponent(config, name) {
29
- if (!name) throw new Error('Component name must be <library/component>.');
30
- const [library, slug] = String(name).split('/');
31
- if (!library || !slug) throw new Error('Component name must be <library/component>.');
32
- const result = await request(config, 'GET', `components?library=${encodeURIComponent(library)}&q=${encodeURIComponent(slug)}&page_size=100`);
33
- const item = (result.items || []).find(value => componentName(value) === name || (value.library_slug === library && value.slug === slug));
34
- if (!item) throw new Error(`Component not found: ${name}`);
35
- return request(config, 'GET', `components/${item.id}`);
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 writeComponentFiles(directory, item) {
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
- fs.writeFileSync(path.join(directory, 'component.html'), item.draft.html || '', 'utf8');
86
- fs.writeFileSync(path.join(directory, 'component.css'), item.draft.css || '', 'utf8');
87
- fs.writeFileSync(path.join(directory, 'component.js'), item.draft.js || '', 'utf8');
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
- fs.writeFileSync(path.join(directory, 'component.json'), `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
96
- fs.writeFileSync(path.join(directory, '.base.json'), `${JSON.stringify(item, null, 2)}\n`, 'utf8');
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
- const result = await request(config, 'GET', `components?q=${encodeURIComponent(query)}&page_size=100`);
109
- output(result.items || result, flags, `DraftGo components: ${query}`);
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, name, flags) {
114
- if (!name) throw new Error('Usage: draftgo components show <library/component>');
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 slug = String(name).split('/').pop();
117
- const result = await request(config, 'GET', `components?q=${encodeURIComponent(slug)}&page_size=100`);
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 query = new URLSearchParams({ page_size: '100' });
127
- if (flags.library) query.set('library', String(flags.library));
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
- const items = async () => (await request(config, 'GET', 'component-libraries')).items || [];
138
- const find = async value => {
139
- const item = (await items()).find(candidate => String(candidate.id) === String(value) || candidate.slug === value);
140
- if (!item) throw new Error(`Component library not found: ${value}`);
141
- return item;
142
- };
143
- if (action === 'list') { output(await items(), flags, 'DraftGo component libraries'); return 0; }
144
- if (action === 'show') { output(await find(positional[1]), flags, `Component library ${positional[1]}`); return 0; }
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 find(positional[1]);
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 find(positional[1]) : {};
152
- const input = { ...current, ...readJSON(projectDir, flags.file) };
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) input[key] = flags[flag];
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
- const library = (await request(config, 'GET', 'component-libraries')).items
167
- .find(item => String(item.id) === String(flags.library) || item.slug === flags.library);
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
- const librariesResult = await request(config, 'GET', 'component-libraries');
194
- const library = librariesResult.items.find(value => value.slug === (flags.library || item.library_slug));
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
- if (definitionHash(localDefinition(directory)) !== definitionHash(base.draft)) {
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 state = localState(projectDir, name);
233
- const remote = await findComponent(config, name);
234
- const sections = ['html', 'css', 'js', 'assets', 'props', 'slots', 'css_variables', 'events', 'dependencies', 'examples', 'usage', 'root_tag', 'meta'];
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, local_vs_base: changed(state.definition, state.base.draft),
242
- local_vs_remote_draft: changed(state.definition, remote.draft),
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: definitionHash(state.base.draft) !== definitionHash(remote.draft),
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 state = localState(projectDir, name);
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 state = localState(projectDir, name);
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
- const remote = await findComponent(config, name);
294
- if (definitionHash(remote.draft) !== definitionHash(state.base.draft)) {
295
- throw new Error(`Remote draft changed after checkout for ${name}; run components checkout after preserving local changes.`);
296
- }
297
- if (definitionHash(state.definition) === definitionHash(remote.draft)
298
- && ['name', 'slug', 'category', 'description', 'tags', 'status'].every(key => JSON.stringify(state.metadata[key] ?? null) === JSON.stringify(remote[key] ?? null))) {
299
- writeComponentFiles(state.directory, remote);
300
- output({ component: name, status: 'unchanged' }, flags, 'Component draft unchanged');
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 remote = await findComponent(config, name);
316
- const state = localState(projectDir, name);
317
- if (definitionHash(state.definition) !== definitionHash(remote.draft)) {
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 librariesResult = await request(config, 'GET', 'component-libraries');
341
- const library = librariesResult.items.find(item => item.slug === librarySlug || String(item.id) === String(librarySlug));
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 = libraries.flatMap(library => library.assets || []).filter(Boolean);
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, '&amp;').replace(/"/g, '&quot;');
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 slug = use.split('/').pop();
519
- const result = await request(config, 'GET', `components?q=${encodeURIComponent(slug)}&page_size=100`);
520
- const component = (result.items || []).find(item => item.full_name === use || `${item.library_slug}/${item.slug}` === use || item.slug === slug);
521
- if (!component) throw new Error(`Component not found: ${use}`);
522
- let runtimeCatalog = null;
523
- try {
524
- runtimeCatalog = await request(config, 'POST', 'component-runtime/resolve', { components: [use] });
525
- } catch {
526
- // Older servers may not expose runtime resolve yet; the component can
527
- // still be expanded with its own definition as a compatibility fallback.
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
- fs.writeFileSync(file, expandHtml(source, instance, component, runtimeCatalog), 'utf8');
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
  }
@@ -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>]