wiki-plugin-shoppe 0.0.7 → 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 +9 -2
- package/client/shoppe.js +9 -3
- package/package.json +1 -1
- package/server/server.js +133 -47
package/CLAUDE.md
CHANGED
|
@@ -51,8 +51,15 @@ my-shoppe.zip
|
|
|
51
51
|
cover.jpg ← optional cover image
|
|
52
52
|
screenshot.png ← any assets referenced in the markdown
|
|
53
53
|
info.json ← optional: { "title": "…", "description": "…" }
|
|
54
|
-
02-
|
|
55
|
-
|
|
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
|
|
56
63
|
albums/
|
|
57
64
|
Vacation 2025/ ← photo album = subfolder
|
|
58
65
|
photo1.jpg
|
package/client/shoppe.js
CHANGED
|
@@ -78,10 +78,16 @@
|
|
|
78
78
|
01-Hello World/ ← number prefix sets table of contents order
|
|
79
79
|
post.md ← the post content
|
|
80
80
|
cover.jpg ← optional cover image
|
|
81
|
-
screenshot.png ← any
|
|
81
|
+
screenshot.png ← any assets referenced in the markdown
|
|
82
82
|
info.json ← optional: { "title": "…", "description": "…" }
|
|
83
|
-
02-
|
|
84
|
-
|
|
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
|
|
85
91
|
albums/
|
|
86
92
|
Vacation 2025/ ← subfolder of images = photo album
|
|
87
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
|
}
|
|
@@ -267,7 +275,7 @@ async function processArchive(zipPath) {
|
|
|
267
275
|
|
|
268
276
|
// ---- books/ ----
|
|
269
277
|
// Each book is a subfolder containing the book file, cover.jpg, and info.json
|
|
270
|
-
const booksDir = path.join(
|
|
278
|
+
const booksDir = path.join(root, 'books');
|
|
271
279
|
if (fs.existsSync(booksDir)) {
|
|
272
280
|
for (const entry of fs.readdirSync(booksDir)) {
|
|
273
281
|
const entryPath = path.join(booksDir, entry);
|
|
@@ -307,7 +315,7 @@ async function processArchive(zipPath) {
|
|
|
307
315
|
|
|
308
316
|
// ---- music/ ----
|
|
309
317
|
// Albums are subfolders; standalone files are individual tracks
|
|
310
|
-
const musicDir = path.join(
|
|
318
|
+
const musicDir = path.join(root, 'music');
|
|
311
319
|
if (fs.existsSync(musicDir)) {
|
|
312
320
|
for (const entry of fs.readdirSync(musicDir)) {
|
|
313
321
|
const entryPath = path.join(musicDir, entry);
|
|
@@ -353,7 +361,7 @@ async function processArchive(zipPath) {
|
|
|
353
361
|
// Each post is a numbered subfolder: "01-My Title/" containing post.md,
|
|
354
362
|
// optional assets (images etc.), and optional info.json for metadata overrides.
|
|
355
363
|
// Folders are sorted by their numeric prefix to build the table of contents.
|
|
356
|
-
const postsDir = path.join(
|
|
364
|
+
const postsDir = path.join(root, 'posts');
|
|
357
365
|
if (fs.existsSync(postsDir)) {
|
|
358
366
|
const postFolders = fs.readdirSync(postsDir)
|
|
359
367
|
.filter(f => fs.statSync(path.join(postsDir, f)).isDirectory())
|
|
@@ -362,61 +370,139 @@ async function processArchive(zipPath) {
|
|
|
362
370
|
for (let order = 0; order < postFolders.length; order++) {
|
|
363
371
|
const entry = postFolders[order];
|
|
364
372
|
const entryPath = path.join(postsDir, entry);
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
+
}
|
|
371
410
|
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
+
}
|
|
377
460
|
}
|
|
378
|
-
const mdBuf = fs.readFileSync(path.join(entryPath, mdFiles[0]));
|
|
379
|
-
const mdContent = mdBuf.toString('utf8');
|
|
380
461
|
|
|
381
|
-
|
|
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;
|
|
462
|
+
results.posts.push({ title: seriesTitle, order, parts: subDirs.length });
|
|
386
463
|
|
|
387
|
-
|
|
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;
|
|
388
475
|
|
|
389
|
-
|
|
390
|
-
|
|
476
|
+
await sanoraCreateProduct(tenant, title, 'post', description, 0, 0, `post,blog,order:${order}`);
|
|
477
|
+
await sanoraUploadArtifact(tenant, title, mdBuf, mdFiles[0], 'text');
|
|
391
478
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
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
|
+
}
|
|
398
484
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
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
|
+
}
|
|
408
493
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
+
}
|
|
413
499
|
}
|
|
414
500
|
}
|
|
415
501
|
}
|
|
416
502
|
|
|
417
503
|
// ---- albums/ ----
|
|
418
504
|
// Each subfolder is a photo album
|
|
419
|
-
const albumsDir = path.join(
|
|
505
|
+
const albumsDir = path.join(root, 'albums');
|
|
420
506
|
if (fs.existsSync(albumsDir)) {
|
|
421
507
|
for (const entry of fs.readdirSync(albumsDir)) {
|
|
422
508
|
const entryPath = path.join(albumsDir, entry);
|
|
@@ -442,7 +528,7 @@ async function processArchive(zipPath) {
|
|
|
442
528
|
|
|
443
529
|
// ---- products/ ----
|
|
444
530
|
// Each subfolder is a physical product with cover.jpg + info.json
|
|
445
|
-
const productsDir = path.join(
|
|
531
|
+
const productsDir = path.join(root, 'products');
|
|
446
532
|
if (fs.existsSync(productsDir)) {
|
|
447
533
|
for (const entry of fs.readdirSync(productsDir)) {
|
|
448
534
|
const entryPath = path.join(productsDir, entry);
|