wiki-plugin-shoppe 0.0.6 → 0.0.7

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/CLAUDE.md CHANGED
@@ -31,8 +31,14 @@ The first 3 emoji are fixed per wiki instance (`SHOPPE_BASE_EMOJI`, default `
31
31
  my-shoppe.zip
32
32
  manifest.json ← required: { uuid, emojicode, name }
33
33
  books/
34
- My Novel.epub
35
- Technical Guide.pdf
34
+ My Novel/ ← subfolder per book
35
+ my-novel.epub
36
+ cover.jpg
37
+ info.json ← { "title": "…", "description": "…", "price": 0 }
38
+ Technical Guide/
39
+ guide.pdf
40
+ cover.jpg
41
+ info.json
36
42
  music/
37
43
  My Album/ ← album = subfolder
38
44
  cover.jpg
@@ -40,8 +46,13 @@ my-shoppe.zip
40
46
  02-track.mp3
41
47
  Standalone Track.mp3 ← standalone track = file directly in music/
42
48
  posts/
43
- 2025-01-hello-world.md
44
- 2025-02-another-post.md
49
+ 01-Hello World/ ← numeric prefix determines table of contents order
50
+ post.md ← the post content (required)
51
+ cover.jpg ← optional cover image
52
+ screenshot.png ← any assets referenced in the markdown
53
+ info.json ← optional: { "title": "…", "description": "…" }
54
+ 02-Another Post/
55
+ post.md
45
56
  albums/
46
57
  Vacation 2025/ ← photo album = subfolder
47
58
  photo1.jpg
package/client/shoppe.js CHANGED
@@ -64,13 +64,24 @@
64
64
  <div class="sw-step-body"><strong>Build your shoppe folder</strong> with this structure, then zip the whole thing:
65
65
  <div class="sw-tree">my-shoppe.zip
66
66
  manifest.json ← { "uuid": "…", "emojicode": "…", "name": "My Shoppe" }
67
- books/ ← .epub .pdf .mobi
67
+ books/
68
+ My Novel/ ← subfolder per book
69
+ my-novel.epub
70
+ cover.jpg
71
+ info.json ← { "title": "…", "description": "…", "price": 0 }
68
72
  music/
69
73
  My Album/ ← subfolder = album (add cover.jpg inside)
70
74
  cover.jpg
71
75
  01-track.mp3
72
76
  standalone.mp3 ← file directly here = single track
73
- posts/ ← .md files
77
+ posts/
78
+ 01-Hello World/ ← number prefix sets table of contents order
79
+ post.md ← the post content
80
+ cover.jpg ← optional cover image
81
+ screenshot.png ← any other assets referenced in the markdown
82
+ info.json ← optional: { "title": "…", "description": "…" }
83
+ 02-Another Post/
84
+ post.md
74
85
  albums/
75
86
  Vacation 2025/ ← subfolder of images = photo album
76
87
  photo1.jpg
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-plugin-shoppe",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Multi-tenant digital goods shoppe for federated wiki, powered by Sanora",
5
5
  "keywords": [
6
6
  "wiki",
package/server/server.js CHANGED
@@ -266,19 +266,41 @@ async function processArchive(zipPath) {
266
266
  const results = { books: [], music: [], posts: [], albums: [], products: [] };
267
267
 
268
268
  // ---- books/ ----
269
+ // Each book is a subfolder containing the book file, cover.jpg, and info.json
269
270
  const booksDir = path.join(tmpDir, 'books');
270
271
  if (fs.existsSync(booksDir)) {
271
- for (const file of fs.readdirSync(booksDir)) {
272
- if (!BOOK_EXTS.has(path.extname(file).toLowerCase())) continue;
273
- const title = path.basename(file, path.extname(file));
272
+ for (const entry of fs.readdirSync(booksDir)) {
273
+ const entryPath = path.join(booksDir, entry);
274
+ if (!fs.statSync(entryPath).isDirectory()) continue;
274
275
  try {
275
- const buf = fs.readFileSync(path.join(booksDir, file));
276
- await sanoraCreateProduct(tenant, title, 'book', `Book: ${title}`, 0, 0, 'book');
277
- await sanoraUploadArtifact(tenant, title, buf, file, 'ebook');
278
- results.books.push({ title });
276
+ const infoPath = path.join(entryPath, 'info.json');
277
+ const info = fs.existsSync(infoPath)
278
+ ? JSON.parse(fs.readFileSync(infoPath, 'utf8'))
279
+ : {};
280
+ const title = info.title || entry;
281
+ const description = info.description || '';
282
+ const price = info.price || 0;
283
+
284
+ await sanoraCreateProduct(tenant, title, 'book', description, price, 0, 'book');
285
+
286
+ // Cover image
287
+ const covers = fs.readdirSync(entryPath).filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()));
288
+ if (covers.length > 0) {
289
+ const coverBuf = fs.readFileSync(path.join(entryPath, covers[0]));
290
+ await sanoraUploadImage(tenant, title, coverBuf, covers[0]);
291
+ }
292
+
293
+ // Book file
294
+ const bookFiles = fs.readdirSync(entryPath).filter(f => BOOK_EXTS.has(path.extname(f).toLowerCase()));
295
+ if (bookFiles.length > 0) {
296
+ const buf = fs.readFileSync(path.join(entryPath, bookFiles[0]));
297
+ await sanoraUploadArtifact(tenant, title, buf, bookFiles[0], 'ebook');
298
+ }
299
+
300
+ results.books.push({ title, price });
279
301
  console.log(`[shoppe] 📚 book: ${title}`);
280
302
  } catch (err) {
281
- console.warn(`[shoppe] ⚠️ book ${file}: ${err.message}`);
303
+ console.warn(`[shoppe] ⚠️ book ${entry}: ${err.message}`);
282
304
  }
283
305
  }
284
306
  }
@@ -328,20 +350,66 @@ async function processArchive(zipPath) {
328
350
  }
329
351
 
330
352
  // ---- posts/ ----
353
+ // Each post is a numbered subfolder: "01-My Title/" containing post.md,
354
+ // optional assets (images etc.), and optional info.json for metadata overrides.
355
+ // Folders are sorted by their numeric prefix to build the table of contents.
331
356
  const postsDir = path.join(tmpDir, 'posts');
332
357
  if (fs.existsSync(postsDir)) {
333
- for (const file of fs.readdirSync(postsDir)) {
334
- if (!file.endsWith('.md')) continue;
335
- const title = path.basename(file, '.md');
358
+ const postFolders = fs.readdirSync(postsDir)
359
+ .filter(f => fs.statSync(path.join(postsDir, f)).isDirectory())
360
+ .sort(); // lexicographic sort respects numeric prefixes (01-, 02-, …)
361
+
362
+ for (let order = 0; order < postFolders.length; order++) {
363
+ const entry = postFolders[order];
364
+ const entryPath = path.join(postsDir, entry);
336
365
  try {
337
- const buf = fs.readFileSync(path.join(postsDir, file));
338
- const firstLine = buf.toString('utf8').split('\n')[0].replace(/^#+\s*/, '');
339
- await sanoraCreateProduct(tenant, title, 'post', firstLine || title, 0, 0, 'post,blog');
340
- await sanoraUploadArtifact(tenant, title, buf, file, 'text');
341
- results.posts.push({ title });
342
- console.log(`[shoppe] 📝 post: ${title}`);
366
+ // Metadata: info.json overrides folder name and md heading
367
+ const infoPath = path.join(entryPath, 'info.json');
368
+ const info = fs.existsSync(infoPath)
369
+ ? JSON.parse(fs.readFileSync(infoPath, 'utf8'))
370
+ : {};
371
+
372
+ // Find the .md file
373
+ const mdFiles = fs.readdirSync(entryPath).filter(f => f.endsWith('.md'));
374
+ if (mdFiles.length === 0) {
375
+ console.warn(`[shoppe] ⚠️ post ${entry}: no .md file found, skipping`);
376
+ continue;
377
+ }
378
+ const mdBuf = fs.readFileSync(path.join(entryPath, mdFiles[0]));
379
+ const mdContent = mdBuf.toString('utf8');
380
+
381
+ // Derive title: info.json > folder name stripped of numeric prefix
382
+ const folderTitle = entry.replace(/^\d+-/, '');
383
+ const firstHeading = mdContent.split('\n')[0].replace(/^#+\s*/, '');
384
+ const title = info.title || folderTitle;
385
+ const description = info.description || firstHeading || title;
386
+
387
+ await sanoraCreateProduct(tenant, title, 'post', description, 0, 0, `post,blog,order:${order}`);
388
+
389
+ // Upload the markdown as the main artifact
390
+ await sanoraUploadArtifact(tenant, title, mdBuf, mdFiles[0], 'text');
391
+
392
+ // Upload cover image if present
393
+ const covers = fs.readdirSync(entryPath).filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()));
394
+ if (covers.length > 0) {
395
+ const coverBuf = fs.readFileSync(path.join(entryPath, covers[0]));
396
+ await sanoraUploadImage(tenant, title, coverBuf, covers[0]);
397
+ }
398
+
399
+ // Upload remaining assets (images not used as cover)
400
+ const assets = fs.readdirSync(entryPath).filter(f =>
401
+ !f.endsWith('.md') && f !== 'info.json' && f !== covers[0] &&
402
+ IMAGE_EXTS.has(path.extname(f).toLowerCase())
403
+ );
404
+ for (const asset of assets) {
405
+ const buf = fs.readFileSync(path.join(entryPath, asset));
406
+ await sanoraUploadArtifact(tenant, title, buf, asset, 'image');
407
+ }
408
+
409
+ results.posts.push({ title, order });
410
+ console.log(`[shoppe] 📝 post [${order + 1}]: ${title}`);
343
411
  } catch (err) {
344
- console.warn(`[shoppe] ⚠️ post ${file}: ${err.message}`);
412
+ console.warn(`[shoppe] ⚠️ post ${entry}: ${err.message}`);
345
413
  }
346
414
  }
347
415
  }