wiki-plugin-shoppe 0.0.6 → 0.0.8
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 +22 -4
- package/client/shoppe.js +19 -2
- package/package.json +1 -1
- package/server/server.js +181 -27
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
|
|
35
|
-
|
|
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,20 @@ my-shoppe.zip
|
|
|
40
46
|
02-track.mp3
|
|
41
47
|
Standalone Track.mp3 ← standalone track = file directly in music/
|
|
42
48
|
posts/
|
|
43
|
-
|
|
44
|
-
|
|
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-My Series/ ← subdirectories = multi-part series
|
|
55
|
+
cover.jpg ← optional series cover
|
|
56
|
+
intro.md ← optional series intro
|
|
57
|
+
info.json ← optional: { "title": "…", "description": "…" }
|
|
58
|
+
01-Part One/
|
|
59
|
+
post.md
|
|
60
|
+
diagram.png
|
|
61
|
+
02-Part Two/
|
|
62
|
+
post.md
|
|
45
63
|
albums/
|
|
46
64
|
Vacation 2025/ ← photo album = subfolder
|
|
47
65
|
photo1.jpg
|
package/client/shoppe.js
CHANGED
|
@@ -64,13 +64,30 @@
|
|
|
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/
|
|
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/
|
|
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 assets referenced in the markdown
|
|
82
|
+
info.json ← optional: { "title": "…", "description": "…" }
|
|
83
|
+
02-My Series/ ← subdirectories = multi-part series
|
|
84
|
+
cover.jpg ← optional series cover
|
|
85
|
+
info.json ← optional: { "title": "…", "description": "…" }
|
|
86
|
+
01-Part One/
|
|
87
|
+
post.md
|
|
88
|
+
diagram.png
|
|
89
|
+
02-Part Two/
|
|
90
|
+
post.md
|
|
74
91
|
albums/
|
|
75
92
|
Vacation 2025/ ← subfolder of images = photo album
|
|
76
93
|
photo1.jpg
|
package/package.json
CHANGED
package/server/server.js
CHANGED
|
@@ -246,8 +246,16 @@ async function processArchive(zipPath) {
|
|
|
246
246
|
zip.extractAllTo(tmpDir, true);
|
|
247
247
|
|
|
248
248
|
try {
|
|
249
|
-
//
|
|
250
|
-
|
|
249
|
+
// Find manifest.json — handle zips that wrap everything in a top-level folder
|
|
250
|
+
let root = tmpDir;
|
|
251
|
+
let manifestPath = path.join(tmpDir, 'manifest.json');
|
|
252
|
+
if (!fs.existsSync(manifestPath)) {
|
|
253
|
+
const entries = fs.readdirSync(tmpDir);
|
|
254
|
+
if (entries.length === 1 && fs.statSync(path.join(tmpDir, entries[0])).isDirectory()) {
|
|
255
|
+
root = path.join(tmpDir, entries[0]);
|
|
256
|
+
manifestPath = path.join(root, 'manifest.json');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
251
259
|
if (!fs.existsSync(manifestPath)) {
|
|
252
260
|
throw new Error('Archive is missing manifest.json');
|
|
253
261
|
}
|
|
@@ -266,26 +274,48 @@ async function processArchive(zipPath) {
|
|
|
266
274
|
const results = { books: [], music: [], posts: [], albums: [], products: [] };
|
|
267
275
|
|
|
268
276
|
// ---- books/ ----
|
|
269
|
-
|
|
277
|
+
// Each book is a subfolder containing the book file, cover.jpg, and info.json
|
|
278
|
+
const booksDir = path.join(root, 'books');
|
|
270
279
|
if (fs.existsSync(booksDir)) {
|
|
271
|
-
for (const
|
|
272
|
-
|
|
273
|
-
|
|
280
|
+
for (const entry of fs.readdirSync(booksDir)) {
|
|
281
|
+
const entryPath = path.join(booksDir, entry);
|
|
282
|
+
if (!fs.statSync(entryPath).isDirectory()) continue;
|
|
274
283
|
try {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
284
|
+
const infoPath = path.join(entryPath, 'info.json');
|
|
285
|
+
const info = fs.existsSync(infoPath)
|
|
286
|
+
? JSON.parse(fs.readFileSync(infoPath, 'utf8'))
|
|
287
|
+
: {};
|
|
288
|
+
const title = info.title || entry;
|
|
289
|
+
const description = info.description || '';
|
|
290
|
+
const price = info.price || 0;
|
|
291
|
+
|
|
292
|
+
await sanoraCreateProduct(tenant, title, 'book', description, price, 0, 'book');
|
|
293
|
+
|
|
294
|
+
// Cover image
|
|
295
|
+
const covers = fs.readdirSync(entryPath).filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()));
|
|
296
|
+
if (covers.length > 0) {
|
|
297
|
+
const coverBuf = fs.readFileSync(path.join(entryPath, covers[0]));
|
|
298
|
+
await sanoraUploadImage(tenant, title, coverBuf, covers[0]);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Book file
|
|
302
|
+
const bookFiles = fs.readdirSync(entryPath).filter(f => BOOK_EXTS.has(path.extname(f).toLowerCase()));
|
|
303
|
+
if (bookFiles.length > 0) {
|
|
304
|
+
const buf = fs.readFileSync(path.join(entryPath, bookFiles[0]));
|
|
305
|
+
await sanoraUploadArtifact(tenant, title, buf, bookFiles[0], 'ebook');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
results.books.push({ title, price });
|
|
279
309
|
console.log(`[shoppe] 📚 book: ${title}`);
|
|
280
310
|
} catch (err) {
|
|
281
|
-
console.warn(`[shoppe] ⚠️ book ${
|
|
311
|
+
console.warn(`[shoppe] ⚠️ book ${entry}: ${err.message}`);
|
|
282
312
|
}
|
|
283
313
|
}
|
|
284
314
|
}
|
|
285
315
|
|
|
286
316
|
// ---- music/ ----
|
|
287
317
|
// Albums are subfolders; standalone files are individual tracks
|
|
288
|
-
const musicDir = path.join(
|
|
318
|
+
const musicDir = path.join(root, 'music');
|
|
289
319
|
if (fs.existsSync(musicDir)) {
|
|
290
320
|
for (const entry of fs.readdirSync(musicDir)) {
|
|
291
321
|
const entryPath = path.join(musicDir, entry);
|
|
@@ -328,27 +358,151 @@ async function processArchive(zipPath) {
|
|
|
328
358
|
}
|
|
329
359
|
|
|
330
360
|
// ---- posts/ ----
|
|
331
|
-
|
|
361
|
+
// Each post is a numbered subfolder: "01-My Title/" containing post.md,
|
|
362
|
+
// optional assets (images etc.), and optional info.json for metadata overrides.
|
|
363
|
+
// Folders are sorted by their numeric prefix to build the table of contents.
|
|
364
|
+
const postsDir = path.join(root, 'posts');
|
|
332
365
|
if (fs.existsSync(postsDir)) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
366
|
+
const postFolders = fs.readdirSync(postsDir)
|
|
367
|
+
.filter(f => fs.statSync(path.join(postsDir, f)).isDirectory())
|
|
368
|
+
.sort(); // lexicographic sort respects numeric prefixes (01-, 02-, …)
|
|
369
|
+
|
|
370
|
+
for (let order = 0; order < postFolders.length; order++) {
|
|
371
|
+
const entry = postFolders[order];
|
|
372
|
+
const entryPath = path.join(postsDir, entry);
|
|
373
|
+
const folderTitle = entry.replace(/^\d+-/, '');
|
|
374
|
+
|
|
375
|
+
const infoPath = path.join(entryPath, 'info.json');
|
|
376
|
+
const info = fs.existsSync(infoPath)
|
|
377
|
+
? JSON.parse(fs.readFileSync(infoPath, 'utf8'))
|
|
378
|
+
: {};
|
|
379
|
+
const seriesTitle = info.title || folderTitle;
|
|
380
|
+
|
|
381
|
+
// Check if this is a multi-part series (has numbered subdirectories)
|
|
382
|
+
const subDirs = fs.readdirSync(entryPath)
|
|
383
|
+
.filter(f => fs.statSync(path.join(entryPath, f)).isDirectory())
|
|
384
|
+
.sort();
|
|
385
|
+
const mdFiles = fs.readdirSync(entryPath).filter(f => f.endsWith('.md'));
|
|
386
|
+
const isSeries = subDirs.length > 0;
|
|
387
|
+
|
|
388
|
+
if (isSeries) {
|
|
389
|
+
// Register the series itself as a parent product
|
|
390
|
+
try {
|
|
391
|
+
const description = info.description || `A ${subDirs.length}-part series`;
|
|
392
|
+
await sanoraCreateProduct(tenant, seriesTitle, 'post-series', description, 0, 0, `post,series,order:${order}`);
|
|
393
|
+
|
|
394
|
+
const covers = fs.readdirSync(entryPath).filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()));
|
|
395
|
+
if (covers.length > 0) {
|
|
396
|
+
const coverBuf = fs.readFileSync(path.join(entryPath, covers[0]));
|
|
397
|
+
await sanoraUploadImage(tenant, seriesTitle, coverBuf, covers[0]);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Optional series-level intro .md
|
|
401
|
+
if (mdFiles.length > 0) {
|
|
402
|
+
const mdBuf = fs.readFileSync(path.join(entryPath, mdFiles[0]));
|
|
403
|
+
await sanoraUploadArtifact(tenant, seriesTitle, mdBuf, mdFiles[0], 'text');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
console.log(`[shoppe] 📝 series [${order + 1}]: ${seriesTitle} (${subDirs.length} parts)`);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
console.warn(`[shoppe] ⚠️ series ${entry}: ${err.message}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Register each part
|
|
412
|
+
for (let partIndex = 0; partIndex < subDirs.length; partIndex++) {
|
|
413
|
+
const partEntry = subDirs[partIndex];
|
|
414
|
+
const partPath = path.join(entryPath, partEntry);
|
|
415
|
+
const partFolderTitle = partEntry.replace(/^\d+-/, '');
|
|
416
|
+
|
|
417
|
+
const partInfoPath = path.join(partPath, 'info.json');
|
|
418
|
+
const partInfo = fs.existsSync(partInfoPath)
|
|
419
|
+
? JSON.parse(fs.readFileSync(partInfoPath, 'utf8'))
|
|
420
|
+
: {};
|
|
421
|
+
const partTitle = partInfo.title || partFolderTitle;
|
|
422
|
+
// Sanora product title must be unique — namespace under series
|
|
423
|
+
const productTitle = `${seriesTitle}: ${partTitle}`;
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const partMdFiles = fs.readdirSync(partPath).filter(f => f.endsWith('.md'));
|
|
427
|
+
if (partMdFiles.length === 0) {
|
|
428
|
+
console.warn(`[shoppe] ⚠️ part ${partEntry}: no .md file, skipping`);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const mdBuf = fs.readFileSync(path.join(partPath, partMdFiles[0]));
|
|
433
|
+
const firstHeading = mdBuf.toString('utf8').split('\n')[0].replace(/^#+\s*/, '');
|
|
434
|
+
const description = partInfo.description || firstHeading || partTitle;
|
|
435
|
+
|
|
436
|
+
await sanoraCreateProduct(tenant, productTitle, 'post', description, 0, 0,
|
|
437
|
+
`post,blog,series:${seriesTitle},part:${partIndex + 1},order:${order}`);
|
|
438
|
+
|
|
439
|
+
await sanoraUploadArtifact(tenant, productTitle, mdBuf, partMdFiles[0], 'text');
|
|
440
|
+
|
|
441
|
+
const partCovers = fs.readdirSync(partPath).filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()));
|
|
442
|
+
if (partCovers.length > 0) {
|
|
443
|
+
const coverBuf = fs.readFileSync(path.join(partPath, partCovers[0]));
|
|
444
|
+
await sanoraUploadImage(tenant, productTitle, coverBuf, partCovers[0]);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const partAssets = fs.readdirSync(partPath).filter(f =>
|
|
448
|
+
!f.endsWith('.md') && f !== 'info.json' && f !== partCovers[0] &&
|
|
449
|
+
IMAGE_EXTS.has(path.extname(f).toLowerCase())
|
|
450
|
+
);
|
|
451
|
+
for (const asset of partAssets) {
|
|
452
|
+
const buf = fs.readFileSync(path.join(partPath, asset));
|
|
453
|
+
await sanoraUploadArtifact(tenant, productTitle, buf, asset, 'image');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
console.log(`[shoppe] part ${partIndex + 1}: ${partTitle}`);
|
|
457
|
+
} catch (err) {
|
|
458
|
+
console.warn(`[shoppe] ⚠️ part ${partEntry}: ${err.message}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
results.posts.push({ title: seriesTitle, order, parts: subDirs.length });
|
|
463
|
+
|
|
464
|
+
} else {
|
|
465
|
+
// Single post
|
|
466
|
+
try {
|
|
467
|
+
if (mdFiles.length === 0) {
|
|
468
|
+
console.warn(`[shoppe] ⚠️ post ${entry}: no .md file found, skipping`);
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const mdBuf = fs.readFileSync(path.join(entryPath, mdFiles[0]));
|
|
472
|
+
const firstHeading = mdBuf.toString('utf8').split('\n')[0].replace(/^#+\s*/, '');
|
|
473
|
+
const title = info.title || folderTitle;
|
|
474
|
+
const description = info.description || firstHeading || title;
|
|
475
|
+
|
|
476
|
+
await sanoraCreateProduct(tenant, title, 'post', description, 0, 0, `post,blog,order:${order}`);
|
|
477
|
+
await sanoraUploadArtifact(tenant, title, mdBuf, mdFiles[0], 'text');
|
|
478
|
+
|
|
479
|
+
const covers = fs.readdirSync(entryPath).filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()));
|
|
480
|
+
if (covers.length > 0) {
|
|
481
|
+
const coverBuf = fs.readFileSync(path.join(entryPath, covers[0]));
|
|
482
|
+
await sanoraUploadImage(tenant, title, coverBuf, covers[0]);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const assets = fs.readdirSync(entryPath).filter(f =>
|
|
486
|
+
!f.endsWith('.md') && f !== 'info.json' && f !== covers[0] &&
|
|
487
|
+
IMAGE_EXTS.has(path.extname(f).toLowerCase())
|
|
488
|
+
);
|
|
489
|
+
for (const asset of assets) {
|
|
490
|
+
const buf = fs.readFileSync(path.join(entryPath, asset));
|
|
491
|
+
await sanoraUploadArtifact(tenant, title, buf, asset, 'image');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
results.posts.push({ title, order });
|
|
495
|
+
console.log(`[shoppe] 📝 post [${order + 1}]: ${title}`);
|
|
496
|
+
} catch (err) {
|
|
497
|
+
console.warn(`[shoppe] ⚠️ post ${entry}: ${err.message}`);
|
|
498
|
+
}
|
|
345
499
|
}
|
|
346
500
|
}
|
|
347
501
|
}
|
|
348
502
|
|
|
349
503
|
// ---- albums/ ----
|
|
350
504
|
// Each subfolder is a photo album
|
|
351
|
-
const albumsDir = path.join(
|
|
505
|
+
const albumsDir = path.join(root, 'albums');
|
|
352
506
|
if (fs.existsSync(albumsDir)) {
|
|
353
507
|
for (const entry of fs.readdirSync(albumsDir)) {
|
|
354
508
|
const entryPath = path.join(albumsDir, entry);
|
|
@@ -374,7 +528,7 @@ async function processArchive(zipPath) {
|
|
|
374
528
|
|
|
375
529
|
// ---- products/ ----
|
|
376
530
|
// Each subfolder is a physical product with cover.jpg + info.json
|
|
377
|
-
const productsDir = path.join(
|
|
531
|
+
const productsDir = path.join(root, 'products');
|
|
378
532
|
if (fs.existsSync(productsDir)) {
|
|
379
533
|
for (const entry of fs.readdirSync(productsDir)) {
|
|
380
534
|
const entryPath = path.join(productsDir, entry);
|