@42ws/cli 0.1.13 → 0.1.15

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.
Files changed (132) hide show
  1. package/README.md +9 -225
  2. package/cli.js +39 -0
  3. package/package.json +5 -21
  4. package/bin/cli.js +0 -10
  5. package/dist/commands/chatlog.d.ts +0 -10
  6. package/dist/commands/chatlog.d.ts.map +0 -1
  7. package/dist/commands/chatlog.js +0 -103
  8. package/dist/commands/chatlog.js.map +0 -1
  9. package/dist/commands/collections.d.ts +0 -39
  10. package/dist/commands/collections.d.ts.map +0 -1
  11. package/dist/commands/collections.js +0 -1186
  12. package/dist/commands/collections.js.map +0 -1
  13. package/dist/commands/context.d.ts +0 -12
  14. package/dist/commands/context.d.ts.map +0 -1
  15. package/dist/commands/context.js +0 -271
  16. package/dist/commands/context.js.map +0 -1
  17. package/dist/commands/dev.d.ts +0 -13
  18. package/dist/commands/dev.d.ts.map +0 -1
  19. package/dist/commands/dev.js +0 -363
  20. package/dist/commands/dev.js.map +0 -1
  21. package/dist/commands/domains.d.ts +0 -10
  22. package/dist/commands/domains.d.ts.map +0 -1
  23. package/dist/commands/domains.js +0 -179
  24. package/dist/commands/domains.js.map +0 -1
  25. package/dist/commands/editlog.d.ts +0 -12
  26. package/dist/commands/editlog.d.ts.map +0 -1
  27. package/dist/commands/editlog.js +0 -188
  28. package/dist/commands/editlog.js.map +0 -1
  29. package/dist/commands/forms.d.ts +0 -23
  30. package/dist/commands/forms.d.ts.map +0 -1
  31. package/dist/commands/forms.js +0 -558
  32. package/dist/commands/forms.js.map +0 -1
  33. package/dist/commands/init.d.ts +0 -18
  34. package/dist/commands/init.d.ts.map +0 -1
  35. package/dist/commands/init.js +0 -197
  36. package/dist/commands/init.js.map +0 -1
  37. package/dist/commands/login.d.ts +0 -7
  38. package/dist/commands/login.d.ts.map +0 -1
  39. package/dist/commands/login.js +0 -110
  40. package/dist/commands/login.js.map +0 -1
  41. package/dist/commands/logout.d.ts +0 -2
  42. package/dist/commands/logout.d.ts.map +0 -1
  43. package/dist/commands/logout.js +0 -31
  44. package/dist/commands/logout.js.map +0 -1
  45. package/dist/commands/open.d.ts +0 -6
  46. package/dist/commands/open.d.ts.map +0 -1
  47. package/dist/commands/open.js +0 -54
  48. package/dist/commands/open.js.map +0 -1
  49. package/dist/commands/prompts.d.ts +0 -7
  50. package/dist/commands/prompts.d.ts.map +0 -1
  51. package/dist/commands/prompts.js +0 -55
  52. package/dist/commands/prompts.js.map +0 -1
  53. package/dist/commands/publish.d.ts +0 -19
  54. package/dist/commands/publish.d.ts.map +0 -1
  55. package/dist/commands/publish.js +0 -437
  56. package/dist/commands/publish.js.map +0 -1
  57. package/dist/commands/pull.d.ts +0 -7
  58. package/dist/commands/pull.d.ts.map +0 -1
  59. package/dist/commands/pull.js +0 -157
  60. package/dist/commands/pull.js.map +0 -1
  61. package/dist/commands/sites.d.ts +0 -13
  62. package/dist/commands/sites.d.ts.map +0 -1
  63. package/dist/commands/sites.js +0 -316
  64. package/dist/commands/sites.js.map +0 -1
  65. package/dist/commands/upgrade.d.ts +0 -2
  66. package/dist/commands/upgrade.d.ts.map +0 -1
  67. package/dist/commands/upgrade.js +0 -15
  68. package/dist/commands/upgrade.js.map +0 -1
  69. package/dist/commands/whoami.d.ts +0 -4
  70. package/dist/commands/whoami.d.ts.map +0 -1
  71. package/dist/commands/whoami.js +0 -98
  72. package/dist/commands/whoami.js.map +0 -1
  73. package/dist/index.d.ts +0 -2
  74. package/dist/index.d.ts.map +0 -1
  75. package/dist/index.js +0 -428
  76. package/dist/index.js.map +0 -1
  77. package/dist/lib/api.d.ts +0 -12
  78. package/dist/lib/api.d.ts.map +0 -1
  79. package/dist/lib/api.js +0 -121
  80. package/dist/lib/api.js.map +0 -1
  81. package/dist/lib/config.d.ts +0 -27
  82. package/dist/lib/config.d.ts.map +0 -1
  83. package/dist/lib/config.js +0 -65
  84. package/dist/lib/config.js.map +0 -1
  85. package/dist/lib/detect.d.ts +0 -9
  86. package/dist/lib/detect.d.ts.map +0 -1
  87. package/dist/lib/detect.js +0 -112
  88. package/dist/lib/detect.js.map +0 -1
  89. package/dist/lib/files.d.ts +0 -16
  90. package/dist/lib/files.d.ts.map +0 -1
  91. package/dist/lib/files.js +0 -200
  92. package/dist/lib/files.js.map +0 -1
  93. package/dist/lib/html.d.ts +0 -8
  94. package/dist/lib/html.d.ts.map +0 -1
  95. package/dist/lib/html.js +0 -63
  96. package/dist/lib/html.js.map +0 -1
  97. package/dist/lib/i18n.d.ts +0 -6
  98. package/dist/lib/i18n.d.ts.map +0 -1
  99. package/dist/lib/i18n.js +0 -256
  100. package/dist/lib/i18n.js.map +0 -1
  101. package/dist/lib/import-prompts.d.ts +0 -46
  102. package/dist/lib/import-prompts.d.ts.map +0 -1
  103. package/dist/lib/import-prompts.js +0 -279
  104. package/dist/lib/import-prompts.js.map +0 -1
  105. package/dist/lib/inspector-client.d.ts +0 -3
  106. package/dist/lib/inspector-client.d.ts.map +0 -1
  107. package/dist/lib/inspector-client.js +0 -253
  108. package/dist/lib/inspector-client.js.map +0 -1
  109. package/dist/lib/output.d.ts +0 -11
  110. package/dist/lib/output.d.ts.map +0 -1
  111. package/dist/lib/output.js +0 -49
  112. package/dist/lib/output.js.map +0 -1
  113. package/dist/lib/project.d.ts +0 -25
  114. package/dist/lib/project.d.ts.map +0 -1
  115. package/dist/lib/project.js +0 -62
  116. package/dist/lib/project.js.map +0 -1
  117. package/dist/lib/suggest.d.ts +0 -12
  118. package/dist/lib/suggest.d.ts.map +0 -1
  119. package/dist/lib/suggest.js +0 -36
  120. package/dist/lib/suggest.js.map +0 -1
  121. package/dist/lib/update-check.d.ts +0 -22
  122. package/dist/lib/update-check.d.ts.map +0 -1
  123. package/dist/lib/update-check.js +0 -182
  124. package/dist/lib/update-check.js.map +0 -1
  125. package/dist/run.d.ts +0 -2
  126. package/dist/run.d.ts.map +0 -1
  127. package/dist/run.js +0 -17
  128. package/dist/run.js.map +0 -1
  129. package/dist/version.d.ts +0 -2
  130. package/dist/version.d.ts.map +0 -1
  131. package/dist/version.js +0 -3
  132. package/dist/version.js.map +0 -1
@@ -1,1186 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { resolve, join } from 'node:path';
3
- import { tmpdir } from 'node:os';
4
- import { randomBytes } from 'node:crypto';
5
- import { createInterface } from 'node:readline';
6
- import { loadConfig, deriveDomains } from '../lib/config.js';
7
- import { apiRequest, ApiError, uploadFile } from '../lib/api.js';
8
- import { info, success, successMessage, error, warn, isJsonMode } from '../lib/output.js';
9
- import { t } from '../lib/i18n.js';
10
- import { buildCollectionsImportPrompt } from '../lib/import-prompts.js';
11
- import { findLocalPageFile } from '../lib/project.js';
12
- const COLLECTIONS_HELP = `
13
- 42ws collections — Manage CMS collections
14
-
15
- Usage:
16
-
17
- 📖 Read (副作用なし):
18
- 42ws collections list List your collections
19
- 42ws collections get <id> [opts] Show a collection (full JSON, or one --field)
20
- 42ws collections entries list <coll-id> List entries
21
-
22
- ✏️ Modify:
23
- 42ws collections import [options] Fetch a source and print an AI generation prompt
24
- (the AI then calls \`collections create\`)
25
- 42ws collections create [options] Create a collection from a JSON file (use after import)
26
- 42ws collections set <id> [opts] Update fields (pageShell / templates / themeColor)
27
- 42ws collections republish <id> Re-render all published entries + index (use after template/render changes)
28
- 42ws collections entries add <coll-id> ... Add a new entry (--upsert で冪等更新)
29
- 42ws collections entries set <entry-id> ... Update an entry
30
-
31
- ⚠️ Destructive:
32
- 42ws collections delete <id> Delete a collection (cascades all entries)
33
- 42ws collections entries delete <entry-id> Delete an entry
34
-
35
- Get options:
36
- --field <name> Output only this field (pageShell / indexTemplate / detailTemplate / themeColor)
37
- --out <path> Write to a file instead of stdout
38
-
39
- Set options:
40
- --page-shell <path> Replace pageShell with the contents of a file
41
- --page-shell-inline <text> Replace pageShell with inline HTML
42
- --index-template <path> Replace indexTemplate with the contents of a file
43
- --index-template-inline <text> Replace indexTemplate with inline HTML
44
- --detail-template <path> Replace detailTemplate with the contents of a file
45
- --detail-template-inline <text> Replace detailTemplate with inline HTML
46
- --theme-color <#hex> Update theme color
47
- --name <text> / --slug <text> Update name / slug (slug change re-renders all entries)
48
- --from-json <path> Apply multiple fields from a JSON file
49
- --republish After update, also re-render entries + index (set + republish in one go)
50
-
51
- Entries:
52
- 42ws collections entries list <coll-id> List entries in a collection
53
- 42ws collections entries add <coll-id> ... Add a new entry (see entries --help)
54
- 42ws collections entries delete <entry-id> Delete an entry
55
-
56
- Import sources (one required):
57
- --site <name> --page <html> Import from a 42ws-hosted site page (recommended)
58
- --url <url> Import from any URL
59
- --file <path> Import from a local HTML file
60
-
61
- Import options:
62
- --name <name> Collection name override (forwarded to \`create\`)
63
- --slug <slug> URL slug override (forwarded)
64
- --inject After create, rewrite the source HTML to embed
65
- the collection (forwarded)
66
- --json Output result as JSON (suppresses prompt)
67
-
68
- Create options (after the AI returns a JSON file):
69
- --from-json <path> Required. Collection definition JSON.
70
- Top-level: name, themeColor, fields?, indexTemplate, detailTemplate,
71
- replaceTargetSelector?, detailRemoveSelectors?
72
- fields? = カスタムスキーマ [{key,type,label,required?,repeat?,options?}]
73
- --name <name> Override JSON's name
74
- --slug <slug> URL slug (default: derived from name)
75
- --site <name> --page <html> Link to a 42ws site page (also enables pageShell extraction)
76
- --file <path> Pair with --inject to rewrite a local HTML file
77
- --inject Rewrite the source HTML to embed the collection
78
- (--site mode: re-publishes the page)
79
- (--file mode: overwrites the local file)
80
-
81
- Notes:
82
- - import → create の 2 段構えは AI ベンダー依存をサーバから外すための分離
83
- です。AI (Claude Code / Codex 等)が import の出力を読み、テンプレ生成
84
- JSON を一時ファイルに書いて \`collections create\` を呼び出します。
85
- - 1 サイトに複数のコレクションを作成できます。--inject は --page で
86
- 指定したページの該当セレクタを置き換えます。
87
- `.trim();
88
- export async function collections(args) {
89
- const sub = args.subcommand;
90
- if (!sub || sub === '--help' || sub === 'help') {
91
- process.stdout.write(COLLECTIONS_HELP + '\n');
92
- return;
93
- }
94
- if (sub === 'import')
95
- return collectionsImport(args);
96
- if (sub === 'create')
97
- return collectionsCreate(args);
98
- if (sub === 'list')
99
- return collectionsList();
100
- if (sub === 'delete')
101
- return collectionsDelete(args);
102
- if (sub === 'republish')
103
- return collectionsRepublish(args);
104
- if (sub === 'get')
105
- return collectionsGet(args);
106
- if (sub === 'set')
107
- return collectionsSet(args);
108
- if (sub === 'entries')
109
- return collectionsEntries(args);
110
- process.stderr.write(`Unknown collections subcommand: ${sub}\n\n${COLLECTIONS_HELP}\n`);
111
- process.exit(1);
112
- }
113
- const ENTRIES_HELP = `
114
- 42ws collections entries — Manage entries in a collection
115
-
116
- Usage:
117
- 42ws collections entries list <coll-id>
118
- 42ws collections entries add <coll-id> --title <text> [options]
119
- 42ws collections entries set <entry-id> [options]
120
- 42ws collections entries delete <entry-id>
121
-
122
- Add options:
123
- --title <text> Required. Entry title
124
- --slug <text> URL slug (default: derived from title)
125
- --body <text> Body text (HTML or Markdown)
126
- --body-file <path> Read body from a local file
127
- --main-image <path> Local image to upload as the main image
128
- --sub-images <p1,p2> Comma-separated local image paths for sub images
129
- --tags <t1,t2> Comma-separated classification labels (category = one tag)
130
- --data <json> Custom field values as a JSON object (collection.fields に準拠)
131
- --data-file <path> Read custom field values from a JSON file
132
- --status draft|published Default: draft
133
- --published-at <text> Display date for entry. Anything Date() can parse
134
- becomes YYYY-MM-DD; non-date strings are kept as-is.
135
- Examples: "2025", "2025-03-01", "2025/03/01"
136
- --from-json <path> Read all entry fields from a JSON file
137
- (image paths inside JSON are uploaded automatically)
138
- --upsert Slug 衝突時に既存 entry を上書き更新する。
139
- ネットワーク不達からの安全リトライ用 (冪等)。
140
- 応答に upserted: true が立つ。
141
- --json Output result as JSON
142
-
143
- Set options (any subset; behaves like Add but for an existing entry):
144
- --title / --slug / --body / --body-file / --status / --published-at
145
- --main-image <path> / --sub-images <p1,p2,...>
146
- --data <json> / --data-file <path> Replace custom field values
147
- --from-json <path> Apply multiple fields from a JSON file
148
-
149
- Notes:
150
- - 画像はサイト連携済みコレクションのみアップロード可能
151
- (連携先サイト上に配置されます)
152
- - http(s)://… で始まる画像値はアップロードせずそのまま使用します
153
- `.trim();
154
- async function collectionsEntries(args) {
155
- const action = args.positional[0];
156
- if (!action || action === '--help' || action === 'help') {
157
- process.stdout.write(ENTRIES_HELP + '\n');
158
- return;
159
- }
160
- if (action === 'list')
161
- return entriesList(args);
162
- if (action === 'add')
163
- return entriesAdd(args);
164
- if (action === 'set')
165
- return entriesSet(args);
166
- if (action === 'delete')
167
- return entriesDelete(args);
168
- process.stderr.write(`Unknown entries action: ${action}\n\n${ENTRIES_HELP}\n`);
169
- process.exit(1);
170
- }
171
- async function entriesList(args) {
172
- const config = loadConfig();
173
- if (!config.token) {
174
- error('NOT_LOGGED_IN', t('notLoggedIn'), t('notLoggedIn.fix'));
175
- process.exit(1);
176
- }
177
- const collId = args.positional[1];
178
- if (!collId) {
179
- error('ID_REQUIRED', t('delete.idRequired.usage', { usage: 'collections entries list <coll-id>' }), t('delete.idRequired.fix', { listCmd: 'collections list' }));
180
- process.exit(1);
181
- }
182
- let result;
183
- try {
184
- result = await apiRequest(config, 'GET', `/v1/collections/${collId}/entries`);
185
- }
186
- catch (e) {
187
- if (e instanceof ApiError)
188
- error(e.code, e.message, e.suggestedFix);
189
- else
190
- error('FETCH_FAILED', String(e), t('tryAgain'));
191
- process.exit(1);
192
- }
193
- const items = result.entries ?? [];
194
- if (isJsonMode()) {
195
- success({ entries: items });
196
- return;
197
- }
198
- if (items.length === 0) {
199
- info(t('entries.list.empty'));
200
- return;
201
- }
202
- const COL_ID = 22;
203
- const COL_TITLE = 30;
204
- const COL_SLUG = 20;
205
- const header = 'ID'.padEnd(COL_ID) + 'TITLE'.padEnd(COL_TITLE) + 'SLUG'.padEnd(COL_SLUG) + 'STATUS';
206
- process.stdout.write(header + '\n');
207
- for (const e of items) {
208
- const id = (e.id || '').padEnd(COL_ID);
209
- const title = truncate(e.title || '', COL_TITLE - 2).padEnd(COL_TITLE);
210
- const slug = truncate(e.slug || '', COL_SLUG - 2).padEnd(COL_SLUG);
211
- process.stdout.write(id + title + slug + (e.status || '') + '\n');
212
- }
213
- }
214
- function readFromJson(path) {
215
- const raw = readFileSync(resolve(process.cwd(), path), 'utf-8');
216
- return JSON.parse(raw);
217
- }
218
- // --data (JSON 文字列) / --data-file (パス) からカスタムフィールド値を読む。
219
- function parseDataArg(args) {
220
- let raw;
221
- if (args.dataFile)
222
- raw = readFileSync(resolve(process.cwd(), args.dataFile), 'utf-8');
223
- else if (args.data)
224
- raw = args.data;
225
- if (raw === undefined)
226
- return undefined;
227
- const parsed = JSON.parse(raw);
228
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
229
- throw new Error('--data / --data-file は JSON オブジェクトである必要があります');
230
- }
231
- return parsed;
232
- }
233
- function isHttpUrl(s) {
234
- return /^https?:\/\//i.test(s);
235
- }
236
- function inferContentType(p) {
237
- const ext = p.toLowerCase().split('.').pop() ?? '';
238
- switch (ext) {
239
- case 'jpg':
240
- case 'jpeg': return 'image/jpeg';
241
- case 'png': return 'image/png';
242
- case 'gif': return 'image/gif';
243
- case 'webp': return 'image/webp';
244
- case 'svg': return 'image/svg+xml';
245
- case 'avif': return 'image/avif';
246
- default: return 'application/octet-stream';
247
- }
248
- }
249
- async function uploadLocalImage(config, collId, localPath, role) {
250
- const abs = resolve(process.cwd(), localPath);
251
- if (!existsSync(abs))
252
- throw new ApiError('FILE_NOT_FOUND', t('entries.add.fileNotFound', { path: abs }), '');
253
- const filename = abs.split('/').pop() ?? 'image.bin';
254
- const contentType = inferContentType(filename);
255
- const { uploadUrl, publicUrl } = await apiRequest(config, 'POST', `/v1/collections/${collId}/upload-url`, { filename, contentType, role });
256
- const body = readFileSync(abs);
257
- const startedAt = Date.now();
258
- await uploadFile(uploadUrl, body, contentType);
259
- const ms = Date.now() - startedAt;
260
- info(t('entries.add.uploaded', { size: formatBytes(body.length), ms }));
261
- return publicUrl;
262
- }
263
- function formatBytes(n) {
264
- if (n < 1024)
265
- return `${n}B`;
266
- if (n < 1024 * 1024)
267
- return `${(n / 1024).toFixed(1)}KB`;
268
- return `${(n / (1024 * 1024)).toFixed(2)}MB`;
269
- }
270
- async function entriesAdd(args) {
271
- if (args.help) {
272
- process.stdout.write(ENTRIES_HELP + '\n');
273
- return;
274
- }
275
- const config = loadConfig();
276
- if (!config.token) {
277
- error('NOT_LOGGED_IN', t('notLoggedIn'), t('notLoggedIn.fix'));
278
- process.exit(1);
279
- }
280
- const collId = args.positional[1];
281
- if (!collId) {
282
- error('ID_REQUIRED', t('entries.add.collIdRequired'), t('delete.idRequired.fix', { listCmd: 'collections list' }));
283
- process.exit(1);
284
- }
285
- let payload;
286
- try {
287
- if (args.fromJson) {
288
- payload = readFromJson(args.fromJson);
289
- const dataOverride = parseDataArg(args);
290
- if (dataOverride)
291
- payload.data = { ...(payload.data ?? {}), ...dataOverride };
292
- }
293
- else {
294
- if (!args.title) {
295
- error('TITLE_REQUIRED', t('entries.add.titleRequired'), t('entries.add.titleRequired.fix'));
296
- process.exit(1);
297
- }
298
- payload = {
299
- title: args.title,
300
- slug: args.slug,
301
- body: args.body ?? (args.bodyFile ? readFileSync(resolve(process.cwd(), args.bodyFile), 'utf-8') : undefined),
302
- mainImage: args.mainImage,
303
- subImages: args.subImages ? args.subImages.split(',').map((s) => s.trim()).filter(Boolean) : undefined,
304
- status: args.status,
305
- publishedAt: args.publishedAt,
306
- tags: args.tags ? args.tags.split(',').map((s) => s.trim()).filter(Boolean) : undefined,
307
- data: parseDataArg(args),
308
- };
309
- }
310
- }
311
- catch (e) {
312
- error('INPUT_ERROR', e instanceof Error ? e.message : String(e), '');
313
- process.exit(1);
314
- }
315
- // 画像のアップロード(ローカルパスのみ。http(s) はそのまま)
316
- try {
317
- if (payload.mainImage && !isHttpUrl(payload.mainImage)) {
318
- info(t('entries.add.uploading.main', { path: payload.mainImage }));
319
- payload.mainImage = await uploadLocalImage(config, collId, payload.mainImage, 'main');
320
- }
321
- if (payload.subImages && payload.subImages.length > 0) {
322
- const uploaded = [];
323
- for (const p of payload.subImages) {
324
- if (isHttpUrl(p)) {
325
- uploaded.push(p);
326
- continue;
327
- }
328
- info(t('entries.add.uploading.sub', { path: p }));
329
- uploaded.push(await uploadLocalImage(config, collId, p, 'sub'));
330
- }
331
- payload.subImages = uploaded;
332
- }
333
- }
334
- catch (e) {
335
- if (e instanceof ApiError)
336
- error(e.code, e.message, e.suggestedFix);
337
- else
338
- error('UPLOAD_FAILED', String(e), t('tryAgain'));
339
- process.exit(1);
340
- }
341
- // --upsert: slug 衝突時に既存 entry を上書き更新する (リトライ冪等性)
342
- const requestBody = { ...payload };
343
- if (args.upsert)
344
- requestBody.upsert = true;
345
- let created;
346
- try {
347
- created = await apiRequest(config, 'POST', `/v1/collections/${collId}/entries`, requestBody);
348
- }
349
- catch (e) {
350
- if (e instanceof ApiError)
351
- error(e.code, e.message, e.suggestedFix, e.extra);
352
- else
353
- error('CREATE_FAILED', String(e), t('tryAgain'));
354
- process.exit(1);
355
- }
356
- if (created.warnings && created.warnings.length > 0) {
357
- warn('サーバからの注意:');
358
- for (const w of created.warnings)
359
- warn(` - ${w}`);
360
- }
361
- if (isJsonMode())
362
- success({ entry: created.entry, upserted: created.upserted ?? false, ...(created.warnings ? { warnings: created.warnings } : {}) });
363
- else {
364
- if (created.upserted)
365
- successMessage(`✓ 既存 entry を更新しました (--upsert): ${created.entry.id}`);
366
- else
367
- successMessage(t('entries.add.created', { id: created.entry.id, status: created.entry.status }));
368
- }
369
- }
370
- async function entriesSet(args) {
371
- const config = loadConfig();
372
- if (!config.token) {
373
- error('NOT_LOGGED_IN', t('notLoggedIn'), t('notLoggedIn.fix'));
374
- process.exit(1);
375
- }
376
- const id = args.positional[1];
377
- if (!id) {
378
- error('ID_REQUIRED', t('delete.idRequired.usage', { usage: 'collections entries set <entry-id> [options]' }), t('delete.idRequired.fix', { listCmd: 'collections entries list <coll-id>' }));
379
- process.exit(1);
380
- }
381
- // 画像アップロードに collectionId が必要なケースを最小化するため先に entry を取得
382
- let existing;
383
- try {
384
- existing = await apiRequest(config, 'GET', `/v1/entries/${id}`);
385
- }
386
- catch (e) {
387
- if (e instanceof ApiError)
388
- error(e.code, e.message, e.suggestedFix);
389
- else
390
- error('FETCH_FAILED', String(e), t('tryAgain'));
391
- process.exit(1);
392
- }
393
- const collId = existing.entry.collectionId;
394
- const updates = {};
395
- if (args.fromJson) {
396
- const obj = JSON.parse(readFileSync(resolve(process.cwd(), args.fromJson), 'utf-8'));
397
- Object.assign(updates, obj);
398
- }
399
- if (args.title !== undefined)
400
- updates.title = args.title;
401
- if (args.slug !== undefined)
402
- updates.slug = args.slug;
403
- if (args.body !== undefined)
404
- updates.body = args.body;
405
- if (args.bodyFile)
406
- updates.body = readFileSync(resolve(process.cwd(), args.bodyFile), 'utf-8');
407
- if (args.status !== undefined)
408
- updates.status = args.status;
409
- if (args.publishedAt !== undefined)
410
- updates.publishedAt = args.publishedAt;
411
- if (args.mainImage !== undefined)
412
- updates.mainImage = args.mainImage;
413
- if (args.subImages !== undefined) {
414
- updates.subImages = args.subImages.split(',').map((s) => s.trim()).filter(Boolean);
415
- }
416
- if (args.tags !== undefined) {
417
- updates.tags = args.tags.split(',').map((s) => s.trim()).filter(Boolean);
418
- }
419
- const dataUpdate = parseDataArg(args);
420
- if (dataUpdate !== undefined)
421
- updates.data = dataUpdate;
422
- // 画像は ローカルパスならアップロード(http(s) はそのまま)
423
- try {
424
- if (updates.mainImage && !isHttpUrl(updates.mainImage)) {
425
- info(t('entries.add.uploading.main', { path: updates.mainImage }));
426
- updates.mainImage = await uploadLocalImage(config, collId, updates.mainImage, 'main');
427
- }
428
- if (updates.subImages && updates.subImages.length > 0) {
429
- const out = [];
430
- for (const p of updates.subImages) {
431
- if (isHttpUrl(p)) {
432
- out.push(p);
433
- continue;
434
- }
435
- info(t('entries.add.uploading.sub', { path: p }));
436
- out.push(await uploadLocalImage(config, collId, p, 'sub'));
437
- }
438
- updates.subImages = out;
439
- }
440
- }
441
- catch (e) {
442
- if (e instanceof ApiError)
443
- error(e.code, e.message, e.suggestedFix);
444
- else
445
- error('UPLOAD_FAILED', String(e), t('tryAgain'));
446
- process.exit(1);
447
- }
448
- if (Object.keys(updates).length === 0) {
449
- error('NO_UPDATES', '更新するフィールドがありません。', '--title / --body / --main-image / --sub-images / --status / --published-at / --from-json のいずれかを指定してください。');
450
- process.exit(1);
451
- }
452
- let result;
453
- try {
454
- result = await apiRequest(config, 'PATCH', `/v1/entries/${id}`, updates);
455
- }
456
- catch (e) {
457
- if (e instanceof ApiError)
458
- error(e.code, e.message, e.suggestedFix);
459
- else
460
- error('UPDATE_FAILED', String(e), t('tryAgain'));
461
- process.exit(1);
462
- }
463
- if (isJsonMode())
464
- success({ entry: result.entry });
465
- else
466
- successMessage(`更新しました: ${id} (fields: ${Object.keys(updates).join(', ')})`);
467
- }
468
- async function entriesDelete(args) {
469
- const config = loadConfig();
470
- if (!config.token) {
471
- error('NOT_LOGGED_IN', t('notLoggedIn'), t('notLoggedIn.fix'));
472
- process.exit(1);
473
- }
474
- const id = args.positional[1];
475
- if (!id) {
476
- error('ID_REQUIRED', t('delete.idRequired.usage', { usage: 'collections entries delete <entry-id>' }), t('delete.idRequired.fix', { listCmd: 'collections entries list <coll-id>' }));
477
- process.exit(1);
478
- }
479
- const interactive = process.stdin.isTTY && !isJsonMode();
480
- if (!args.force) {
481
- if (!interactive) {
482
- error('CONFIRMATION_REQUIRED', t('delete.confirmRequired'), t('delete.confirmRequired.fix'));
483
- process.exit(1);
484
- }
485
- const ans = await prompt(t('delete.confirmPrompt.entry', { id }));
486
- if (ans.trim() !== id) {
487
- process.stderr.write(t('delete.cancelled.idMismatch') + '\n');
488
- process.exit(0);
489
- }
490
- }
491
- try {
492
- await apiRequest(config, 'DELETE', `/v1/entries/${id}`);
493
- }
494
- catch (e) {
495
- if (e instanceof ApiError)
496
- error(e.code, e.message, e.suggestedFix);
497
- else
498
- error('DELETE_FAILED', String(e), t('tryAgain'));
499
- process.exit(1);
500
- }
501
- if (isJsonMode())
502
- success({ deleted: true, id });
503
- else
504
- successMessage(t('delete.deleted', { id }));
505
- }
506
- function prompt(q) {
507
- return new Promise((res) => {
508
- const rl = createInterface({ input: process.stdin, output: process.stderr });
509
- rl.question(q, (a) => { rl.close(); res(a.trim()); });
510
- });
511
- }
512
- function truncate(s, max) {
513
- return s.length <= max ? s : s.slice(0, Math.max(0, max - 1)) + '…';
514
- }
515
- async function collectionsList() {
516
- const config = loadConfig();
517
- if (!config.token) {
518
- error('NOT_LOGGED_IN', t('notLoggedIn'), t('notLoggedIn.fix'));
519
- process.exit(1);
520
- }
521
- let result;
522
- try {
523
- result = await apiRequest(config, 'GET', '/v1/collections');
524
- }
525
- catch (e) {
526
- if (e instanceof ApiError)
527
- error(e.code, e.message, e.suggestedFix);
528
- else
529
- error('FETCH_FAILED', String(e), t('tryAgain'));
530
- process.exit(1);
531
- }
532
- const items = result.collections ?? [];
533
- if (isJsonMode()) {
534
- success({ collections: items });
535
- return;
536
- }
537
- if (items.length === 0) {
538
- info(t('collections.list.empty'));
539
- return;
540
- }
541
- const COL_ID = 18;
542
- const COL_NAME = 24;
543
- const COL_SLUG = 18;
544
- const header = 'ID'.padEnd(COL_ID) + 'NAME'.padEnd(COL_NAME) + 'SLUG'.padEnd(COL_SLUG) + 'LINKED';
545
- process.stdout.write(header + '\n');
546
- for (const c of items) {
547
- const id = (c.id || '').padEnd(COL_ID);
548
- const name = truncate(c.name || '', COL_NAME - 2).padEnd(COL_NAME);
549
- const slug = truncate(c.slug || '', COL_SLUG - 2).padEnd(COL_SLUG);
550
- const linked = c.linkedSite ? `${c.linkedSite}/${c.linkedPage || 'index.html'}` : t('linked.orphan');
551
- process.stdout.write(id + name + slug + linked + '\n');
552
- }
553
- }
554
- const EDITABLE_COLL_FIELDS = ['name', 'slug', 'indexTemplate', 'detailTemplate', 'themeColor', 'pageShell', 'linkedSite', 'linkedPage', 'originalSourceHtml', 'fields'];
555
- async function collectionsGet(args) {
556
- const config = loadConfig();
557
- if (!config.token) {
558
- error('NOT_LOGGED_IN', t('notLoggedIn'), t('notLoggedIn.fix'));
559
- process.exit(1);
560
- }
561
- const id = args.positional[0];
562
- if (!id) {
563
- error('ID_REQUIRED', t('delete.idRequired.usage', { usage: 'collections get <id> [--field <name>] [--out <path>]' }), t('delete.idRequired.fix', { listCmd: 'collections list' }));
564
- process.exit(1);
565
- }
566
- let result;
567
- try {
568
- result = await apiRequest(config, 'GET', `/v1/collections/${id}`);
569
- }
570
- catch (e) {
571
- if (e instanceof ApiError)
572
- error(e.code, e.message, e.suggestedFix);
573
- else
574
- error('FETCH_FAILED', String(e), t('tryAgain'));
575
- process.exit(1);
576
- }
577
- const coll = result.collection;
578
- if (args.field) {
579
- // kebab-case (例: --field index-template) を camelCase (indexTemplate) にも当ててみる。
580
- // どちらにも値があれば camelCase 優先。
581
- const camel = args.field.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
582
- const v = coll[camel] ?? coll[args.field];
583
- if (v === undefined) {
584
- const keys = Object.keys(coll).join(', ');
585
- error('FIELD_NOT_FOUND', `フィールド "${args.field}" がコレクションに存在しません`, `指定可能: ${keys}`);
586
- process.exit(1);
587
- }
588
- const text = v == null ? '' : (typeof v === 'string' ? v : JSON.stringify(v, null, 2));
589
- if (args.out) {
590
- writeFileSync(resolve(process.cwd(), args.out), text);
591
- info(`書き出しました: ${args.out} (${text.length} 文字)`);
592
- }
593
- else {
594
- process.stdout.write(text + (text.endsWith('\n') ? '' : '\n'));
595
- }
596
- return;
597
- }
598
- if (isJsonMode()) {
599
- success({ collection: coll });
600
- return;
601
- }
602
- if (args.out) {
603
- writeFileSync(resolve(process.cwd(), args.out), JSON.stringify(coll, null, 2));
604
- info(`書き出しました: ${args.out}`);
605
- return;
606
- }
607
- process.stdout.write(JSON.stringify(coll, null, 2) + '\n');
608
- }
609
- async function collectionsSet(args) {
610
- const config = loadConfig();
611
- if (!config.token) {
612
- error('NOT_LOGGED_IN', t('notLoggedIn'), t('notLoggedIn.fix'));
613
- process.exit(1);
614
- }
615
- const id = args.positional[0];
616
- if (!id) {
617
- error('ID_REQUIRED', t('delete.idRequired.usage', { usage: 'collections set <id> [opts]' }), t('delete.idRequired.fix', { listCmd: 'collections list' }));
618
- process.exit(1);
619
- }
620
- const updates = {};
621
- // --xxx-template はファイルパスを期待。存在しなければ「inline で渡したのでは?」と明示エラーで返す。
622
- // --xxx-template-inline は文字列をそのまま採用。
623
- const readFileOrFail = (path, flagName) => {
624
- const abs = resolve(process.cwd(), path);
625
- if (!existsSync(abs)) {
626
- error('FILE_NOT_FOUND', `--${flagName} はファイルパスを期待しますが見つかりません: ${path}`, `インライン文字列を渡したい場合は --${flagName}-inline "<text>" を使ってください。`);
627
- process.exit(1);
628
- }
629
- return readFileSync(abs, 'utf-8');
630
- };
631
- if (args.pageShellFile)
632
- updates.pageShell = readFileOrFail(args.pageShellFile, 'page-shell');
633
- if (args.pageShellInline !== undefined)
634
- updates.pageShell = args.pageShellInline;
635
- if (args.indexTemplateFile)
636
- updates.indexTemplate = readFileOrFail(args.indexTemplateFile, 'index-template');
637
- if (args.indexTemplateInline !== undefined)
638
- updates.indexTemplate = args.indexTemplateInline;
639
- if (args.detailTemplateFile)
640
- updates.detailTemplate = readFileOrFail(args.detailTemplateFile, 'detail-template');
641
- if (args.detailTemplateInline !== undefined)
642
- updates.detailTemplate = args.detailTemplateInline;
643
- if (args.themeColor !== undefined)
644
- updates.themeColor = args.themeColor;
645
- if (args.name !== undefined)
646
- updates.name = args.name;
647
- if (args.slug !== undefined)
648
- updates.slug = args.slug;
649
- if (args.fromJson) {
650
- const obj = JSON.parse(readFileSync(resolve(process.cwd(), args.fromJson), 'utf-8'));
651
- for (const k of EDITABLE_COLL_FIELDS) {
652
- if (obj[k] !== undefined)
653
- updates[k] = obj[k];
654
- }
655
- }
656
- if (Object.keys(updates).length === 0) {
657
- error('NO_UPDATES', '更新するフィールドがありません。', '--page-shell / --index-template / --detail-template / --theme-color / --name / --slug / --from-json のいずれかを指定してください。');
658
- process.exit(1);
659
- }
660
- let result;
661
- try {
662
- result = await apiRequest(config, 'PATCH', `/v1/collections/${id}`, updates);
663
- }
664
- catch (e) {
665
- if (e instanceof ApiError)
666
- error(e.code, e.message, e.suggestedFix, e.extra);
667
- else
668
- error('UPDATE_FAILED', String(e), t('tryAgain'));
669
- process.exit(1);
670
- }
671
- // テンプレ検証 warning (二重ネスト等) や render warning がサーバから返ってきたら必ず表示。
672
- // 「保存はできた・でも論理バグは気づける」を一貫させる (create だけでなく set でも)。
673
- if (result.warnings && result.warnings.length > 0) {
674
- warn('テンプレ検証/再 render の警告 (保存は成功):');
675
- for (const w of result.warnings)
676
- warn(` - ${w}`);
677
- }
678
- // --republish オプションで一気に再 render
679
- let republishWarnings;
680
- if (args.republish) {
681
- info(t('collections.republish.starting', { id }));
682
- try {
683
- const r = await apiRequest(config, 'POST', `/v1/collections/${id}/republish`);
684
- republishWarnings = r.warnings;
685
- if (!isJsonMode())
686
- info(t('collections.republish.done', { id, count: r.entries }));
687
- if (r.warnings && r.warnings.length > 0) {
688
- warn('republish 中の警告:');
689
- for (const w of r.warnings)
690
- warn(` - ${w}`);
691
- }
692
- }
693
- catch (e) {
694
- if (e instanceof ApiError)
695
- warn(`republish failed: ${e.message}`);
696
- else
697
- warn(`republish failed: ${String(e)}`);
698
- }
699
- }
700
- if (isJsonMode()) {
701
- success({
702
- collection: result.collection,
703
- ...(result.warnings ? { warnings: result.warnings } : {}),
704
- ...(republishWarnings ? { republishWarnings } : {}),
705
- });
706
- }
707
- else {
708
- successMessage(`更新しました: ${id} (fields: ${Object.keys(updates).join(', ')})`);
709
- }
710
- }
711
- async function collectionsRepublish(args) {
712
- const config = loadConfig();
713
- if (!config.token) {
714
- error('NOT_LOGGED_IN', t('notLoggedIn'), t('notLoggedIn.fix'));
715
- process.exit(1);
716
- }
717
- const id = args.positional[0];
718
- if (!id) {
719
- error('ID_REQUIRED', t('delete.idRequired.usage', { usage: 'collections republish <id>' }), t('delete.idRequired.fix', { listCmd: 'collections list' }));
720
- process.exit(1);
721
- }
722
- info(t('collections.republish.starting', { id }));
723
- let result;
724
- try {
725
- result = await apiRequest(config, 'POST', `/v1/collections/${id}/republish`);
726
- }
727
- catch (e) {
728
- if (e instanceof ApiError)
729
- error(e.code, e.message, e.suggestedFix);
730
- else
731
- error('REPUBLISH_FAILED', String(e), t('tryAgain'));
732
- process.exit(1);
733
- }
734
- if (isJsonMode())
735
- success({ id, ...result });
736
- else {
737
- successMessage(t('collections.republish.done', { id, count: result.entries }));
738
- if (result.warnings && result.warnings.length > 0) {
739
- warn('render 中の警告:');
740
- for (const w of result.warnings)
741
- warn(` - ${w}`);
742
- }
743
- const broken = result.brokenImages ?? [];
744
- if (broken.length > 0) {
745
- warn(t('collections.republish.brokenImages', { count: broken.length }));
746
- for (const b of broken.slice(0, 20)) {
747
- warn(` - entry ${b.slug} (${b.entryId}): ${b.url}`);
748
- }
749
- if (broken.length > 20)
750
- warn(` ... and ${broken.length - 20} more`);
751
- warn(t('collections.republish.brokenImages.fix'));
752
- }
753
- }
754
- }
755
- async function collectionsDelete(args) {
756
- const config = loadConfig();
757
- if (!config.token) {
758
- error('NOT_LOGGED_IN', t('notLoggedIn'), t('notLoggedIn.fix'));
759
- process.exit(1);
760
- }
761
- const id = args.positional[0];
762
- if (!id) {
763
- error('ID_REQUIRED', t('delete.idRequired.usage', { usage: 'collections delete <id>' }), t('delete.idRequired.fix', { listCmd: 'collections list' }));
764
- process.exit(1);
765
- }
766
- // 削除前に linked 情報を取得して embed 残存の警告に使う
767
- let linkedSite;
768
- let linkedPage;
769
- try {
770
- const all = await apiRequest(config, 'GET', '/v1/collections');
771
- const target = (all.collections ?? []).find((c) => c.id === id);
772
- linkedSite = target?.linkedSite;
773
- linkedPage = target?.linkedPage;
774
- }
775
- catch {
776
- // ignore
777
- }
778
- const interactive = process.stdin.isTTY && !isJsonMode();
779
- if (!args.force) {
780
- if (!interactive) {
781
- error('CONFIRMATION_REQUIRED', t('delete.confirmRequired'), t('delete.confirmRequired.fix'));
782
- process.exit(1);
783
- }
784
- const ans = await prompt(t('delete.confirmPrompt.collection', { id }));
785
- if (ans.trim() !== id) {
786
- process.stderr.write(t('delete.cancelled.idMismatch') + '\n');
787
- process.exit(0);
788
- }
789
- }
790
- try {
791
- await apiRequest(config, 'DELETE', `/v1/collections/${id}`);
792
- }
793
- catch (e) {
794
- if (e instanceof ApiError)
795
- error(e.code, e.message, e.suggestedFix);
796
- else
797
- error('DELETE_FAILED', String(e), t('tryAgain'));
798
- process.exit(1);
799
- }
800
- if (isJsonMode()) {
801
- success({ deleted: true, id, linkedSite, linkedPage });
802
- }
803
- else {
804
- successMessage(t('delete.deleted', { id }));
805
- if (linkedSite) {
806
- warn(t('delete.embedLeftover.warn', { site: linkedSite, page: linkedPage || 'index.html' }));
807
- }
808
- }
809
- }
810
- // AI が返した CSS セレクタで要素全体を置換/除去する。
811
- // 対応: `#id`, `tag#id`, `.class`, `tag.class`, `tag` のみ。複雑なセレクタは未対応で fallback する。
812
- // 同名タグのネスト ('<div>...<div>...</div>...</div>') を正しく数えるため、
813
- // 開始タグ位置を見つけてから depth カウントで対応する終了タグを探す。
814
- function replaceElement(html, selector, replacement) {
815
- const parts = parseSimpleSelector(selector);
816
- if (!parts)
817
- return null;
818
- const { tag, id, cls } = parts;
819
- const tagPattern = tag ?? '\\w+';
820
- let attrPattern = '';
821
- if (id)
822
- attrPattern += `[^>]*\\bid=["']${escapeRegExp(id)}["']`;
823
- if (cls)
824
- attrPattern += `[^>]*\\bclass=["'][^"']*\\b${escapeRegExp(cls)}\\b[^"']*["']`;
825
- const openRe = new RegExp(`<(${tagPattern})(${attrPattern || '[^>]*'})>`, 'i');
826
- const m = openRe.exec(html);
827
- if (!m)
828
- return null;
829
- const matchedTag = m[1];
830
- const openStart = m.index;
831
- const openEnd = openStart + m[0].length;
832
- const closeEnd = findMatchingClose(html, openEnd, matchedTag);
833
- if (closeEnd === -1)
834
- return null;
835
- return html.slice(0, openStart) + replacement + html.slice(closeEnd);
836
- }
837
- function findMatchingClose(html, fromIdx, tag) {
838
- // 同タグの開始/終了を順序通りに走査して depth を数える。
839
- // self-closing (<br/>) や void 要素は対象外(DIV/SECTION/UL 等のコンテナを想定)。
840
- const openRe = new RegExp(`<${tag}\\b[^>]*?(/?)>`, 'gi');
841
- const closeRe = new RegExp(`</${tag}\\s*>`, 'gi');
842
- let depth = 1;
843
- let pos = fromIdx;
844
- while (depth > 0) {
845
- openRe.lastIndex = pos;
846
- closeRe.lastIndex = pos;
847
- const oNext = openRe.exec(html);
848
- const cNext = closeRe.exec(html);
849
- if (!cNext)
850
- return -1;
851
- if (oNext && oNext.index < cNext.index) {
852
- // self-closing なら depth を増やさない
853
- if (oNext[1] !== '/')
854
- depth++;
855
- pos = oNext.index + oNext[0].length;
856
- }
857
- else {
858
- depth--;
859
- if (depth === 0)
860
- return cNext.index + cNext[0].length;
861
- pos = cNext.index + cNext[0].length;
862
- }
863
- }
864
- return -1;
865
- }
866
- function parseSimpleSelector(sel) {
867
- const trimmed = sel.trim();
868
- // ID: #foo or tag#foo
869
- const idMatch = trimmed.match(/^([a-zA-Z]\w*)?#([\w-]+)$/);
870
- if (idMatch)
871
- return { tag: idMatch[1], id: idMatch[2] };
872
- // class: .foo or tag.foo
873
- const clsMatch = trimmed.match(/^([a-zA-Z]\w*)?\.([\w-]+)$/);
874
- if (clsMatch)
875
- return { tag: clsMatch[1], cls: clsMatch[2] };
876
- // tag only
877
- const tagMatch = trimmed.match(/^([a-zA-Z]\w*)$/);
878
- if (tagMatch)
879
- return { tag: tagMatch[1] };
880
- return null;
881
- }
882
- function escapeRegExp(s) {
883
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
884
- }
885
- function injectEmbedIntoHtml(html, snippet, selector) {
886
- // selector が指定された場合はその範囲だけを置換する。外れても <main> 全置換に
887
- // フォールバックしない(hero やフィルタ UI ごと消す事故を防ぐ)。
888
- if (selector) {
889
- return replaceElement(html, selector, snippet);
890
- }
891
- // selector 未指定のときだけ <main> / </body> へのヒューリスティック挿入を使う
892
- const mainRe = /(<main\b[^>]*>)([\s\S]*?)(<\/main>)/i;
893
- if (mainRe.test(html)) {
894
- return html.replace(mainRe, (_m, open, _i, close) => `${open}\n${snippet}\n${close}`);
895
- }
896
- const bodyRe = /<\/body>/i;
897
- if (bodyRe.test(html)) {
898
- return html.replace(bodyRe, `${snippet}\n</body>`);
899
- }
900
- return null;
901
- }
902
- function buildPageShell(html, selector, removeSelectors) {
903
- let working = html;
904
- // detailRemoveSelectors: 該当要素を削除(best-effort)
905
- for (const sel of removeSelectors ?? []) {
906
- const removed = replaceElement(working, sel, '');
907
- if (removed !== null)
908
- working = removed;
909
- }
910
- // 一覧領域を CMS_CONTENT プレースホルダに。
911
- // selector 指定があって外れたら <main> 全置換にフォールバックしない(chrome を壊さない)。
912
- if (selector) {
913
- return replaceElement(working, selector, '<!--CMS_CONTENT-->') ?? undefined;
914
- }
915
- const mainRe = /(<main\b[^>]*>)([\s\S]*?)(<\/main>)/i;
916
- if (mainRe.test(working)) {
917
- return working.replace(mainRe, (_m, open, _i, close) => `${open}<!--CMS_CONTENT-->${close}`);
918
- }
919
- const bodyRe = /<\/body>/i;
920
- if (bodyRe.test(working)) {
921
- return working.replace(bodyRe, '<!--CMS_CONTENT--></body>');
922
- }
923
- return undefined;
924
- }
925
- function buildEmbedSnippet(collectionId) {
926
- const cfg = loadConfig();
927
- const { webBase } = deriveDomains(cfg.apiUrl);
928
- return `<div data-cms-target="${collectionId}"></div>\n<script src="${webBase}/embed/collection.js" data-collection-id="${collectionId}" data-api-base="${cfg.apiUrl}" data-target='[data-cms-target="${collectionId}"]'></script>`;
929
- }
930
- async function fetchCollectionsImportSource(args) {
931
- const config = loadConfig();
932
- if (args.site) {
933
- if (!args.page) {
934
- error('PAGE_REQUIRED', '--site と一緒に --page <html> を指定してください。', '例: --site mysite --page news.html');
935
- process.exit(1);
936
- }
937
- let src;
938
- try {
939
- src = await apiRequest(config, 'GET', `/v1/sites/${args.site}/source?page=${encodeURIComponent(args.page)}`);
940
- }
941
- catch (e) {
942
- if (e instanceof ApiError)
943
- error(e.code, e.message, e.suggestedFix);
944
- else
945
- error('FETCH_FAILED', String(e), t('tryAgain'));
946
- process.exit(1);
947
- }
948
- return { html: src.html, source: { kind: 'site', site: args.site, page: args.page } };
949
- }
950
- if (args.url) {
951
- let res;
952
- try {
953
- res = await fetch(args.url, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; 42ws-cli)' } });
954
- }
955
- catch (e) {
956
- error('FETCH_FAILED', `URL取得に失敗しました: ${e.message}`, t('tryAgain'));
957
- process.exit(1);
958
- }
959
- if (!res.ok) {
960
- error('FETCH_FAILED', `URL取得に失敗しました (${res.status})`, t('tryAgain'));
961
- process.exit(1);
962
- }
963
- const html = await res.text();
964
- return { html, source: { kind: 'url', url: args.url } };
965
- }
966
- const filePath = resolve(process.cwd(), args.file);
967
- if (!existsSync(filePath)) {
968
- error('FILE_NOT_FOUND', `ファイルが見つかりません: ${filePath}`, '');
969
- process.exit(1);
970
- }
971
- return { html: readFileSync(filePath, 'utf-8'), source: { kind: 'file', path: args.file } };
972
- }
973
- async function collectionsImport(args) {
974
- if (args.help) {
975
- process.stdout.write(COLLECTIONS_HELP + '\n');
976
- return;
977
- }
978
- const config = loadConfig();
979
- if (!config.token) {
980
- error('NOT_LOGGED_IN', t('notLoggedIn'), t('notLoggedIn.fix'));
981
- process.exit(1);
982
- }
983
- const sourceCount = [args.site, args.url, args.file].filter(Boolean).length;
984
- if (sourceCount === 0) {
985
- error('SOURCE_REQUIRED', '取り込みソースを指定してください。', '--site <name> --page <html> / --url <url> / --file <path> のいずれか1つを指定。');
986
- process.exit(1);
987
- }
988
- if (sourceCount > 1) {
989
- error('MULTIPLE_SOURCES', '--site / --url / --file は同時に指定できません。', 'いずれか1つを選んでください。');
990
- process.exit(1);
991
- }
992
- const { html, source } = await fetchCollectionsImportSource(args);
993
- const baseUrl = args.site && args.page
994
- ? `https://${args.site}.${deriveDomains(config.apiUrl).sitesHost}/${args.page}`
995
- : undefined;
996
- const suggestedJsonPath = join(tmpdir(), `42ws-collection-${randomBytes(4).toString('hex')}.json`);
997
- const prompt = buildCollectionsImportPrompt({
998
- html,
999
- source,
1000
- baseUrl,
1001
- name: args.name,
1002
- slug: args.slug,
1003
- inject: !!args.inject,
1004
- suggestedJsonPath,
1005
- });
1006
- if (isJsonMode()) {
1007
- success({ prompt, source, name: args.name, slug: args.slug });
1008
- return;
1009
- }
1010
- process.stdout.write(prompt);
1011
- }
1012
- async function collectionsCreate(args) {
1013
- if (args.help) {
1014
- process.stdout.write(COLLECTIONS_HELP + '\n');
1015
- return;
1016
- }
1017
- const config = loadConfig();
1018
- if (!config.token) {
1019
- error('NOT_LOGGED_IN', t('notLoggedIn'), t('notLoggedIn.fix'));
1020
- process.exit(1);
1021
- }
1022
- if (!args.fromJson) {
1023
- error('FROM_JSON_REQUIRED', '--from-json <path> を指定してください。', 'AI が出力した JSON ファイルのパス。');
1024
- process.exit(1);
1025
- }
1026
- const jsonPath = resolve(process.cwd(), args.fromJson);
1027
- if (!existsSync(jsonPath)) {
1028
- error('FILE_NOT_FOUND', `ファイルが見つかりません: ${jsonPath}`, '');
1029
- process.exit(1);
1030
- }
1031
- let parsed;
1032
- try {
1033
- parsed = JSON.parse(readFileSync(jsonPath, 'utf-8'));
1034
- }
1035
- catch (e) {
1036
- error('JSON_PARSE_ERROR', `JSON の解析に失敗しました: ${e.message}`, 'スキーマは name / themeColor / fields? / indexTemplate / detailTemplate / replaceTargetSelector? / detailRemoveSelectors?');
1037
- process.exit(1);
1038
- }
1039
- if (!parsed.indexTemplate || !parsed.detailTemplate) {
1040
- error('TEMPLATES_REQUIRED', 'indexTemplate と detailTemplate は必須です。', '出力 JSON にこの 2 つが含まれているか確認してください。');
1041
- process.exit(1);
1042
- }
1043
- const linkedSite = args.site;
1044
- const linkedPage = args.site ? args.page : undefined;
1045
- if (args.site && !args.page) {
1046
- error('PAGE_REQUIRED', '--site と一緒に --page <html> を指定してください。', '例: --site mysite --page news.html');
1047
- process.exit(1);
1048
- }
1049
- // --inject 用 + pageShell 抽出のために元ソースを取得。
1050
- // **ローカル優先**: --site --page でもまずローカル (42ws.json output / dist 等) を試す。
1051
- // 見つかればローカルから読み、後段の inject もローカルファイルへ。
1052
- // 無ければ remote の /v1/sites/{site}/source から取得。
1053
- let originalHtml;
1054
- let localPath;
1055
- if (args.site && args.page) {
1056
- localPath = findLocalPageFile(args.page, { explicitFile: args.file });
1057
- if (localPath) {
1058
- originalHtml = readFileSync(localPath, 'utf-8');
1059
- }
1060
- else {
1061
- try {
1062
- const src = await apiRequest(config, 'GET', `/v1/sites/${args.site}/source?page=${encodeURIComponent(args.page)}`);
1063
- originalHtml = src.html;
1064
- }
1065
- catch (e) {
1066
- if (e instanceof ApiError)
1067
- warn(`元ソース取得に失敗: ${e.message}`);
1068
- else
1069
- warn(`元ソース取得に失敗: ${String(e)}`);
1070
- }
1071
- }
1072
- }
1073
- else if (args.file) {
1074
- const fp = resolve(process.cwd(), args.file);
1075
- if (existsSync(fp)) {
1076
- originalHtml = readFileSync(fp, 'utf-8');
1077
- localPath = fp;
1078
- }
1079
- else if (args.inject)
1080
- warn(`ファイルが見つかりません(--inject スキップ): ${fp}`);
1081
- }
1082
- // pageShell は AI が返した replaceTargetSelector / detailRemoveSelectors を使って CLI 側で抽出
1083
- const pageShell = originalHtml
1084
- ? buildPageShell(originalHtml, parsed.replaceTargetSelector, parsed.detailRemoveSelectors)
1085
- : undefined;
1086
- const name = args.name || parsed.name || 'コレクション';
1087
- let created;
1088
- try {
1089
- created = await apiRequest(config, 'POST', '/v1/collections', {
1090
- name,
1091
- slug: args.slug,
1092
- indexTemplate: parsed.indexTemplate,
1093
- detailTemplate: parsed.detailTemplate,
1094
- themeColor: parsed.themeColor,
1095
- ...(Array.isArray(parsed.fields) && parsed.fields.length > 0 ? { fields: parsed.fields } : {}),
1096
- pageShell,
1097
- originalSourceHtml: originalHtml,
1098
- ...(linkedSite ? { linkedSite, linkedPage } : {}),
1099
- });
1100
- }
1101
- catch (e) {
1102
- if (e instanceof ApiError)
1103
- error(e.code, e.message, e.suggestedFix, e.extra);
1104
- else
1105
- error('CREATE_FAILED', String(e), t('tryAgain'));
1106
- process.exit(1);
1107
- }
1108
- if (created.warnings && created.warnings.length > 0) {
1109
- warn('テンプレ検証の警告 (作成は成功):');
1110
- for (const w of created.warnings)
1111
- warn(` - ${w}`);
1112
- }
1113
- const coll = created.collection;
1114
- const embedSnippet = buildEmbedSnippet(coll.id);
1115
- let injected = false;
1116
- let injectReason;
1117
- let injectedToLocal;
1118
- if (args.inject) {
1119
- if (!originalHtml) {
1120
- injectReason = '元ソースが取得できなかったため --inject をスキップ (--site --page か --file を指定してください)';
1121
- warn(injectReason);
1122
- }
1123
- else {
1124
- const newHtml = injectEmbedIntoHtml(originalHtml, embedSnippet, parsed.replaceTargetSelector);
1125
- if (!newHtml) {
1126
- const sel = parsed.replaceTargetSelector ? `(対象セレクタ: ${parsed.replaceTargetSelector})` : '(replaceTargetSelector が未指定で、<main> / </body> もマッチしません)';
1127
- injectReason = `元ソースに置換ターゲットが見つかりません ${sel}`;
1128
- warn(`${injectReason}。--inject はスキップしました(ページは変更していません)。`);
1129
- info(`手動で一覧部分を次のスニペットに置き換えてください:\n${embedSnippet}`);
1130
- }
1131
- else if (localPath) {
1132
- // ローカル優先: ローカルファイルに書き戻す → 次の publish で remote 反映 (一方向)
1133
- writeFileSync(localPath, newHtml, 'utf-8');
1134
- injectedToLocal = localPath;
1135
- injected = true;
1136
- info(`ローカルファイルを書き換えました: ${localPath}`);
1137
- }
1138
- else if (args.site && args.page) {
1139
- // ローカル無し → remote 直接更新 (pull で同期案内)
1140
- try {
1141
- await apiRequest(config, 'POST', `/v1/sites/${args.site}/save-and-publish`, { html: newHtml, page: args.page });
1142
- info(`サイトを再公開しました: ${args.site}/${args.page} (リモート直接更新)`);
1143
- injected = true;
1144
- }
1145
- catch (e) {
1146
- injectReason = `サイト再公開に失敗: ${e instanceof ApiError ? e.message : String(e)}`;
1147
- warn(injectReason);
1148
- }
1149
- }
1150
- }
1151
- }
1152
- if (parsed.warnings && parsed.warnings.length > 0) {
1153
- warn('AI からの注意点:');
1154
- for (const w of parsed.warnings)
1155
- warn(` - ${w}`);
1156
- }
1157
- if (isJsonMode()) {
1158
- success({
1159
- collection: { id: coll.id, name: coll.name, slug: coll.slug, linkedSite: coll.linkedSite, linkedPage: coll.linkedPage },
1160
- embedSnippet,
1161
- injected,
1162
- ...(injectedToLocal ? { injectedToLocal } : {}),
1163
- ...(injectReason ? { injectReason } : {}),
1164
- });
1165
- }
1166
- else {
1167
- successMessage(`✓ コレクション作成: ${coll.name} (${coll.id})`);
1168
- successMessage(`管理画面: ${deriveDomains(config.apiUrl).webBase}/collections/${coll.id}`);
1169
- if (!injected) {
1170
- info('');
1171
- info('埋め込みコード:');
1172
- process.stdout.write(embedSnippet + '\n');
1173
- }
1174
- else if (injectedToLocal) {
1175
- info('');
1176
- info(`📦 次に publish して反映してください:`);
1177
- info(` 42ws publish${linkedSite ? ` --site ${linkedSite}` : ''}`);
1178
- }
1179
- else if (linkedSite && linkedPage) {
1180
- info('');
1181
- info(`⚠️ ローカルに ${linkedPage} が見当たらなかったため、リモートを直接更新しました。`);
1182
- info(` ローカル側も同期しておいてください: 42ws pull --site ${linkedSite} --dir <出力先>`);
1183
- }
1184
- }
1185
- }
1186
- //# sourceMappingURL=collections.js.map