wiki-plugin-shoppe 0.0.7 → 0.0.9
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 +139 -51
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
|
@@ -9,9 +9,10 @@ const sessionless = require('sessionless-node');
|
|
|
9
9
|
|
|
10
10
|
const SHOPPE_BASE_EMOJI = process.env.SHOPPE_BASE_EMOJI || '🛍️🎨🎁';
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
12
|
+
const DATA_DIR = path.join(process.env.HOME || '/root', '.shoppe');
|
|
13
|
+
const TENANTS_FILE = path.join(DATA_DIR, 'tenants.json');
|
|
14
|
+
const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
|
|
15
|
+
const TMP_DIR = '/tmp/shoppe-uploads';
|
|
15
16
|
|
|
16
17
|
// ============================================================
|
|
17
18
|
// CONFIG (allyabase URL, etc.)
|
|
@@ -246,8 +247,16 @@ async function processArchive(zipPath) {
|
|
|
246
247
|
zip.extractAllTo(tmpDir, true);
|
|
247
248
|
|
|
248
249
|
try {
|
|
249
|
-
//
|
|
250
|
-
|
|
250
|
+
// Find manifest.json — handle zips that wrap everything in a top-level folder
|
|
251
|
+
let root = tmpDir;
|
|
252
|
+
let manifestPath = path.join(tmpDir, 'manifest.json');
|
|
253
|
+
if (!fs.existsSync(manifestPath)) {
|
|
254
|
+
const entries = fs.readdirSync(tmpDir);
|
|
255
|
+
if (entries.length === 1 && fs.statSync(path.join(tmpDir, entries[0])).isDirectory()) {
|
|
256
|
+
root = path.join(tmpDir, entries[0]);
|
|
257
|
+
manifestPath = path.join(root, 'manifest.json');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
251
260
|
if (!fs.existsSync(manifestPath)) {
|
|
252
261
|
throw new Error('Archive is missing manifest.json');
|
|
253
262
|
}
|
|
@@ -267,7 +276,7 @@ async function processArchive(zipPath) {
|
|
|
267
276
|
|
|
268
277
|
// ---- books/ ----
|
|
269
278
|
// Each book is a subfolder containing the book file, cover.jpg, and info.json
|
|
270
|
-
const booksDir = path.join(
|
|
279
|
+
const booksDir = path.join(root, 'books');
|
|
271
280
|
if (fs.existsSync(booksDir)) {
|
|
272
281
|
for (const entry of fs.readdirSync(booksDir)) {
|
|
273
282
|
const entryPath = path.join(booksDir, entry);
|
|
@@ -307,7 +316,7 @@ async function processArchive(zipPath) {
|
|
|
307
316
|
|
|
308
317
|
// ---- music/ ----
|
|
309
318
|
// Albums are subfolders; standalone files are individual tracks
|
|
310
|
-
const musicDir = path.join(
|
|
319
|
+
const musicDir = path.join(root, 'music');
|
|
311
320
|
if (fs.existsSync(musicDir)) {
|
|
312
321
|
for (const entry of fs.readdirSync(musicDir)) {
|
|
313
322
|
const entryPath = path.join(musicDir, entry);
|
|
@@ -353,7 +362,7 @@ async function processArchive(zipPath) {
|
|
|
353
362
|
// Each post is a numbered subfolder: "01-My Title/" containing post.md,
|
|
354
363
|
// optional assets (images etc.), and optional info.json for metadata overrides.
|
|
355
364
|
// Folders are sorted by their numeric prefix to build the table of contents.
|
|
356
|
-
const postsDir = path.join(
|
|
365
|
+
const postsDir = path.join(root, 'posts');
|
|
357
366
|
if (fs.existsSync(postsDir)) {
|
|
358
367
|
const postFolders = fs.readdirSync(postsDir)
|
|
359
368
|
.filter(f => fs.statSync(path.join(postsDir, f)).isDirectory())
|
|
@@ -362,61 +371,139 @@ async function processArchive(zipPath) {
|
|
|
362
371
|
for (let order = 0; order < postFolders.length; order++) {
|
|
363
372
|
const entry = postFolders[order];
|
|
364
373
|
const entryPath = path.join(postsDir, entry);
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
374
|
+
const folderTitle = entry.replace(/^\d+-/, '');
|
|
375
|
+
|
|
376
|
+
const infoPath = path.join(entryPath, 'info.json');
|
|
377
|
+
const info = fs.existsSync(infoPath)
|
|
378
|
+
? JSON.parse(fs.readFileSync(infoPath, 'utf8'))
|
|
379
|
+
: {};
|
|
380
|
+
const seriesTitle = info.title || folderTitle;
|
|
381
|
+
|
|
382
|
+
// Check if this is a multi-part series (has numbered subdirectories)
|
|
383
|
+
const subDirs = fs.readdirSync(entryPath)
|
|
384
|
+
.filter(f => fs.statSync(path.join(entryPath, f)).isDirectory())
|
|
385
|
+
.sort();
|
|
386
|
+
const mdFiles = fs.readdirSync(entryPath).filter(f => f.endsWith('.md'));
|
|
387
|
+
const isSeries = subDirs.length > 0;
|
|
388
|
+
|
|
389
|
+
if (isSeries) {
|
|
390
|
+
// Register the series itself as a parent product
|
|
391
|
+
try {
|
|
392
|
+
const description = info.description || `A ${subDirs.length}-part series`;
|
|
393
|
+
await sanoraCreateProduct(tenant, seriesTitle, 'post-series', description, 0, 0, `post,series,order:${order}`);
|
|
394
|
+
|
|
395
|
+
const covers = fs.readdirSync(entryPath).filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()));
|
|
396
|
+
if (covers.length > 0) {
|
|
397
|
+
const coverBuf = fs.readFileSync(path.join(entryPath, covers[0]));
|
|
398
|
+
await sanoraUploadImage(tenant, seriesTitle, coverBuf, covers[0]);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Optional series-level intro .md
|
|
402
|
+
if (mdFiles.length > 0) {
|
|
403
|
+
const mdBuf = fs.readFileSync(path.join(entryPath, mdFiles[0]));
|
|
404
|
+
await sanoraUploadArtifact(tenant, seriesTitle, mdBuf, mdFiles[0], 'text');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
console.log(`[shoppe] 📝 series [${order + 1}]: ${seriesTitle} (${subDirs.length} parts)`);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
console.warn(`[shoppe] ⚠️ series ${entry}: ${err.message}`);
|
|
410
|
+
}
|
|
371
411
|
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
412
|
+
// Register each part
|
|
413
|
+
for (let partIndex = 0; partIndex < subDirs.length; partIndex++) {
|
|
414
|
+
const partEntry = subDirs[partIndex];
|
|
415
|
+
const partPath = path.join(entryPath, partEntry);
|
|
416
|
+
const partFolderTitle = partEntry.replace(/^\d+-/, '');
|
|
417
|
+
|
|
418
|
+
const partInfoPath = path.join(partPath, 'info.json');
|
|
419
|
+
const partInfo = fs.existsSync(partInfoPath)
|
|
420
|
+
? JSON.parse(fs.readFileSync(partInfoPath, 'utf8'))
|
|
421
|
+
: {};
|
|
422
|
+
const partTitle = partInfo.title || partFolderTitle;
|
|
423
|
+
// Sanora product title must be unique — namespace under series
|
|
424
|
+
const productTitle = `${seriesTitle}: ${partTitle}`;
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const partMdFiles = fs.readdirSync(partPath).filter(f => f.endsWith('.md'));
|
|
428
|
+
if (partMdFiles.length === 0) {
|
|
429
|
+
console.warn(`[shoppe] ⚠️ part ${partEntry}: no .md file, skipping`);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const mdBuf = fs.readFileSync(path.join(partPath, partMdFiles[0]));
|
|
434
|
+
const firstHeading = mdBuf.toString('utf8').split('\n')[0].replace(/^#+\s*/, '');
|
|
435
|
+
const description = partInfo.description || firstHeading || partTitle;
|
|
436
|
+
|
|
437
|
+
await sanoraCreateProduct(tenant, productTitle, 'post', description, 0, 0,
|
|
438
|
+
`post,blog,series:${seriesTitle},part:${partIndex + 1},order:${order}`);
|
|
439
|
+
|
|
440
|
+
await sanoraUploadArtifact(tenant, productTitle, mdBuf, partMdFiles[0], 'text');
|
|
441
|
+
|
|
442
|
+
const partCovers = fs.readdirSync(partPath).filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()));
|
|
443
|
+
if (partCovers.length > 0) {
|
|
444
|
+
const coverBuf = fs.readFileSync(path.join(partPath, partCovers[0]));
|
|
445
|
+
await sanoraUploadImage(tenant, productTitle, coverBuf, partCovers[0]);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const partAssets = fs.readdirSync(partPath).filter(f =>
|
|
449
|
+
!f.endsWith('.md') && f !== 'info.json' && f !== partCovers[0] &&
|
|
450
|
+
IMAGE_EXTS.has(path.extname(f).toLowerCase())
|
|
451
|
+
);
|
|
452
|
+
for (const asset of partAssets) {
|
|
453
|
+
const buf = fs.readFileSync(path.join(partPath, asset));
|
|
454
|
+
await sanoraUploadArtifact(tenant, productTitle, buf, asset, 'image');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
console.log(`[shoppe] part ${partIndex + 1}: ${partTitle}`);
|
|
458
|
+
} catch (err) {
|
|
459
|
+
console.warn(`[shoppe] ⚠️ part ${partEntry}: ${err.message}`);
|
|
460
|
+
}
|
|
377
461
|
}
|
|
378
|
-
const mdBuf = fs.readFileSync(path.join(entryPath, mdFiles[0]));
|
|
379
|
-
const mdContent = mdBuf.toString('utf8');
|
|
380
462
|
|
|
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;
|
|
463
|
+
results.posts.push({ title: seriesTitle, order, parts: subDirs.length });
|
|
386
464
|
|
|
387
|
-
|
|
465
|
+
} else {
|
|
466
|
+
// Single post
|
|
467
|
+
try {
|
|
468
|
+
if (mdFiles.length === 0) {
|
|
469
|
+
console.warn(`[shoppe] ⚠️ post ${entry}: no .md file found, skipping`);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const mdBuf = fs.readFileSync(path.join(entryPath, mdFiles[0]));
|
|
473
|
+
const firstHeading = mdBuf.toString('utf8').split('\n')[0].replace(/^#+\s*/, '');
|
|
474
|
+
const title = info.title || folderTitle;
|
|
475
|
+
const description = info.description || firstHeading || title;
|
|
388
476
|
|
|
389
|
-
|
|
390
|
-
|
|
477
|
+
await sanoraCreateProduct(tenant, title, 'post', description, 0, 0, `post,blog,order:${order}`);
|
|
478
|
+
await sanoraUploadArtifact(tenant, title, mdBuf, mdFiles[0], 'text');
|
|
391
479
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
480
|
+
const covers = fs.readdirSync(entryPath).filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()));
|
|
481
|
+
if (covers.length > 0) {
|
|
482
|
+
const coverBuf = fs.readFileSync(path.join(entryPath, covers[0]));
|
|
483
|
+
await sanoraUploadImage(tenant, title, coverBuf, covers[0]);
|
|
484
|
+
}
|
|
398
485
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
486
|
+
const assets = fs.readdirSync(entryPath).filter(f =>
|
|
487
|
+
!f.endsWith('.md') && f !== 'info.json' && f !== covers[0] &&
|
|
488
|
+
IMAGE_EXTS.has(path.extname(f).toLowerCase())
|
|
489
|
+
);
|
|
490
|
+
for (const asset of assets) {
|
|
491
|
+
const buf = fs.readFileSync(path.join(entryPath, asset));
|
|
492
|
+
await sanoraUploadArtifact(tenant, title, buf, asset, 'image');
|
|
493
|
+
}
|
|
408
494
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
495
|
+
results.posts.push({ title, order });
|
|
496
|
+
console.log(`[shoppe] 📝 post [${order + 1}]: ${title}`);
|
|
497
|
+
} catch (err) {
|
|
498
|
+
console.warn(`[shoppe] ⚠️ post ${entry}: ${err.message}`);
|
|
499
|
+
}
|
|
413
500
|
}
|
|
414
501
|
}
|
|
415
502
|
}
|
|
416
503
|
|
|
417
504
|
// ---- albums/ ----
|
|
418
505
|
// Each subfolder is a photo album
|
|
419
|
-
const albumsDir = path.join(
|
|
506
|
+
const albumsDir = path.join(root, 'albums');
|
|
420
507
|
if (fs.existsSync(albumsDir)) {
|
|
421
508
|
for (const entry of fs.readdirSync(albumsDir)) {
|
|
422
509
|
const entryPath = path.join(albumsDir, entry);
|
|
@@ -442,7 +529,7 @@ async function processArchive(zipPath) {
|
|
|
442
529
|
|
|
443
530
|
// ---- products/ ----
|
|
444
531
|
// Each subfolder is a physical product with cover.jpg + info.json
|
|
445
|
-
const productsDir = path.join(
|
|
532
|
+
const productsDir = path.join(root, 'products');
|
|
446
533
|
if (fs.existsSync(productsDir)) {
|
|
447
534
|
for (const entry of fs.readdirSync(productsDir)) {
|
|
448
535
|
const entryPath = path.join(productsDir, entry);
|
|
@@ -619,7 +706,8 @@ function generateShoppeHTML(tenant, goods) {
|
|
|
619
706
|
async function startServer(params) {
|
|
620
707
|
const app = params.app;
|
|
621
708
|
|
|
622
|
-
if (!fs.existsSync(
|
|
709
|
+
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
710
|
+
if (!fs.existsSync(TMP_DIR)) fs.mkdirSync(TMP_DIR, { recursive: true });
|
|
623
711
|
console.log('🛍️ wiki-plugin-shoppe starting...');
|
|
624
712
|
|
|
625
713
|
const owner = (req, res, next) => {
|