@deneb-ui/cli 2.0.0
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/README.md +197 -0
- package/bin/index.js +784 -0
- package/package.json +34 -0
- package/src/tools/fivora-template-validator.cjs +37 -0
- package/src/tools/local-template-lab.cjs +136 -0
- package/src/tools/template-preview-focus-bridge.cjs +4 -0
package/bin/index.js
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const command = args[0];
|
|
9
|
+
|
|
10
|
+
const toolsDir = path.join(__dirname, '..', 'src', 'tools');
|
|
11
|
+
|
|
12
|
+
const FORBIDDEN_DIRS = new Set([
|
|
13
|
+
'.git',
|
|
14
|
+
'.next',
|
|
15
|
+
'.turbo',
|
|
16
|
+
'.cache',
|
|
17
|
+
'.npm',
|
|
18
|
+
'.pnpm-store',
|
|
19
|
+
'__macosx',
|
|
20
|
+
'node_modules',
|
|
21
|
+
'out',
|
|
22
|
+
'dist',
|
|
23
|
+
'build',
|
|
24
|
+
'coverage',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function isForbiddenFile(filename) {
|
|
28
|
+
const lower = filename.toLowerCase();
|
|
29
|
+
return (
|
|
30
|
+
lower.startsWith('.env') ||
|
|
31
|
+
lower.endsWith('.zip') ||
|
|
32
|
+
lower.endsWith('.log') ||
|
|
33
|
+
lower.endsWith('.tsbuildinfo') ||
|
|
34
|
+
lower === '.ds_store' ||
|
|
35
|
+
lower === 'thumbs.db'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function packageCleanZip(sourceDir, outputPath) {
|
|
40
|
+
let AdmZip;
|
|
41
|
+
try {
|
|
42
|
+
AdmZip = require('adm-zip');
|
|
43
|
+
} catch {
|
|
44
|
+
try {
|
|
45
|
+
const { createRequire } = require('node:module');
|
|
46
|
+
const projectRequire = createRequire(path.resolve(process.cwd(), 'package.json'));
|
|
47
|
+
AdmZip = projectRequire('adm-zip');
|
|
48
|
+
} catch {
|
|
49
|
+
console.error('Error: adm-zip is required to package templates. Run "npm install -D adm-zip" or install @fivora/cli with its dependencies.');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const zip = new AdmZip();
|
|
55
|
+
let fileCount = 0;
|
|
56
|
+
|
|
57
|
+
function addFolder(dir, base) {
|
|
58
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
if (!FORBIDDEN_DIRS.has(entry.name.toLowerCase())) {
|
|
62
|
+
addFolder(path.join(dir, entry.name), path.join(base, entry.name));
|
|
63
|
+
}
|
|
64
|
+
} else if (entry.isFile()) {
|
|
65
|
+
if (!isForbiddenFile(entry.name)) {
|
|
66
|
+
const fullPath = path.join(dir, entry.name);
|
|
67
|
+
const zipPath = path.join(base, entry.name).replace(/\\/g, '/');
|
|
68
|
+
const targetDirInZip = path.dirname(zipPath) === '.' ? '' : path.dirname(zipPath);
|
|
69
|
+
zip.addLocalFile(fullPath, targetDirInZip);
|
|
70
|
+
fileCount++;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
addFolder(sourceDir, '');
|
|
77
|
+
zip.writeZip(outputPath);
|
|
78
|
+
console.log(`\n✓ Successfully packaged clean template ZIP: ${outputPath}`);
|
|
79
|
+
console.log(` Packaged ${fileCount} clean source files.`);
|
|
80
|
+
console.log(` Automatically excluded: node_modules, .next, .git, .env, cache, and build files.`);
|
|
81
|
+
console.log(` Ready for upload at Developer Portal > Upload Template!\n`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function copyCleanFolder(src, dest) {
|
|
85
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
86
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
const srcPath = path.join(src, entry.name);
|
|
89
|
+
const destPath = path.join(dest, entry.name);
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
if (!FORBIDDEN_DIRS.has(entry.name.toLowerCase())) {
|
|
92
|
+
copyCleanFolder(srcPath, destPath);
|
|
93
|
+
}
|
|
94
|
+
} else if (entry.isFile()) {
|
|
95
|
+
if (!isForbiddenFile(entry.name)) {
|
|
96
|
+
fs.copyFileSync(srcPath, destPath);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createTemplate(targetName) {
|
|
103
|
+
const projectName = targetName || 'fivora-template';
|
|
104
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
105
|
+
const monorepoTemplateDir = path.resolve(__dirname, '..', '..', '..', 'templates', 'nextjs');
|
|
106
|
+
|
|
107
|
+
if (fs.existsSync(monorepoTemplateDir)) {
|
|
108
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
109
|
+
console.error(`\nError: Directory "${projectName}" already exists and is not empty.\n`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(`\n🚀 Initializing new Fivora Template in ${targetDir}...\n`);
|
|
114
|
+
copyCleanFolder(monorepoTemplateDir, targetDir);
|
|
115
|
+
|
|
116
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
117
|
+
if (fs.existsSync(pkgPath)) {
|
|
118
|
+
try {
|
|
119
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
120
|
+
pkg.name = path.basename(targetDir).toLowerCase().replace(/[^a-z0-9_-]/g, '-');
|
|
121
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
122
|
+
} catch {
|
|
123
|
+
// ignore
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log(`✓ Created Fivora template in ${targetDir}`);
|
|
128
|
+
console.log(`\nNext steps:`);
|
|
129
|
+
console.log(` cd ${projectName}`);
|
|
130
|
+
console.log(` npm install`);
|
|
131
|
+
console.log(` npm run dev # Start local development with hot-reload`);
|
|
132
|
+
console.log(` npm run lab # Test in Visual Editing Lab`);
|
|
133
|
+
console.log(` npm run validate # Run Fivora preflight checks`);
|
|
134
|
+
console.log(` npm run zip # Create clean upload-ready ZIP\n`);
|
|
135
|
+
} else {
|
|
136
|
+
console.log(`\n🚀 Scaffolding new Fivora Template via @fivora/create-template...\n`);
|
|
137
|
+
const res = spawnSync('npx', ['--yes', '@fivora/create-template', projectName], {
|
|
138
|
+
stdio: 'inherit',
|
|
139
|
+
shell: process.platform === 'win32',
|
|
140
|
+
});
|
|
141
|
+
process.exit(res.status ?? 0);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function detectPages(projectDir) {
|
|
146
|
+
const pages = [
|
|
147
|
+
{ id: 'home', label: 'Home', route: '/', required: true },
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const candidateDirs = [
|
|
151
|
+
path.join(projectDir, 'src', 'app'),
|
|
152
|
+
path.join(projectDir, 'app'),
|
|
153
|
+
path.join(projectDir, 'pages'),
|
|
154
|
+
path.join(projectDir, 'src', 'pages'),
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const foundRoutes = new Set(['/']);
|
|
158
|
+
|
|
159
|
+
for (const cDir of candidateDirs) {
|
|
160
|
+
if (fs.existsSync(cDir) && fs.statSync(cDir).isDirectory()) {
|
|
161
|
+
try {
|
|
162
|
+
const entries = fs.readdirSync(cDir, { withFileTypes: true });
|
|
163
|
+
for (const entry of entries) {
|
|
164
|
+
if (entry.isDirectory()) {
|
|
165
|
+
const routeName = entry.name;
|
|
166
|
+
if (routeName.startsWith('(') || routeName.startsWith('_') || routeName === 'api') {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!foundRoutes.has('/' + routeName)) {
|
|
170
|
+
foundRoutes.add('/' + routeName);
|
|
171
|
+
const label = routeName
|
|
172
|
+
.replace(/[-_]/g, ' ')
|
|
173
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
174
|
+
const isContact = routeName.toLowerCase().includes('contact');
|
|
175
|
+
pages.push({
|
|
176
|
+
id: routeName.replace(/[^a-z0-9_-]/gi, '_').toLowerCase(),
|
|
177
|
+
label: label,
|
|
178
|
+
route: `/${routeName}`,
|
|
179
|
+
...(isContact ? { required: true } : {}),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
// ignore scanning errors
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const hasContact = pages.some((p) => p.id === 'contact' || p.route === '/contact');
|
|
191
|
+
if (!hasContact) {
|
|
192
|
+
pages.push({ id: 'contact', label: 'Contact', route: '/contact', required: true });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return pages;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getDefaultManifest(projectName, pages) {
|
|
199
|
+
return {
|
|
200
|
+
framework: 'nextjs-static-export',
|
|
201
|
+
version: 2,
|
|
202
|
+
visualEditing: {
|
|
203
|
+
contractVersion: 1,
|
|
204
|
+
mode: 'strict',
|
|
205
|
+
controlOnlyPaths: [],
|
|
206
|
+
},
|
|
207
|
+
siteDataFile: 'src/data/site-data.json',
|
|
208
|
+
outputDirectory: 'out',
|
|
209
|
+
installCommand: 'npm install',
|
|
210
|
+
buildCommand: 'npm run build',
|
|
211
|
+
basePathEnvVar: 'NEXT_PUBLIC_SITE_BASE_PATH',
|
|
212
|
+
pages: pages,
|
|
213
|
+
editorSchema: {
|
|
214
|
+
version: 1,
|
|
215
|
+
sections: [
|
|
216
|
+
{
|
|
217
|
+
id: 'common',
|
|
218
|
+
path: 'common',
|
|
219
|
+
type: 'object',
|
|
220
|
+
label: 'Shared Website Content',
|
|
221
|
+
fields: [
|
|
222
|
+
{
|
|
223
|
+
key: 'websiteTitle',
|
|
224
|
+
type: 'text',
|
|
225
|
+
label: 'Website Title',
|
|
226
|
+
required: true,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
key: 'shortDescription',
|
|
230
|
+
type: 'textarea',
|
|
231
|
+
label: 'Short Description',
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
key: 'logoUrl',
|
|
235
|
+
type: 'image',
|
|
236
|
+
label: 'Website Logo',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
key: 'headerCtaLabel',
|
|
240
|
+
type: 'text',
|
|
241
|
+
label: 'Header Button Label',
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: 'home',
|
|
247
|
+
path: 'home',
|
|
248
|
+
type: 'object',
|
|
249
|
+
label: 'Home Page Content',
|
|
250
|
+
fields: [
|
|
251
|
+
{
|
|
252
|
+
key: 'heroTitle',
|
|
253
|
+
type: 'text',
|
|
254
|
+
label: 'Hero Title',
|
|
255
|
+
required: true,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
key: 'heroSummary',
|
|
259
|
+
type: 'textarea',
|
|
260
|
+
label: 'Hero Summary',
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
key: 'primaryCtaLabel',
|
|
264
|
+
type: 'text',
|
|
265
|
+
label: 'Primary CTA Label',
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
key: 'secondaryCtaLabel',
|
|
269
|
+
type: 'text',
|
|
270
|
+
label: 'Secondary CTA Label',
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function getDefaultSiteData(projectName, pages) {
|
|
280
|
+
const navLabels = {};
|
|
281
|
+
for (const page of pages) {
|
|
282
|
+
navLabels[page.id] = page.label;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
project: {
|
|
287
|
+
id: `${projectName}-project`,
|
|
288
|
+
title: projectName,
|
|
289
|
+
status: 'APPROVED',
|
|
290
|
+
},
|
|
291
|
+
merchant: {
|
|
292
|
+
businessName: projectName,
|
|
293
|
+
description: 'A modern commerce storefront built for the Fivora.',
|
|
294
|
+
},
|
|
295
|
+
template: {
|
|
296
|
+
id: `${projectName}-template`,
|
|
297
|
+
name: projectName,
|
|
298
|
+
engine: 'NEXT_STATIC_EXPORT',
|
|
299
|
+
structure: {
|
|
300
|
+
pages: pages.map((p) => p.id),
|
|
301
|
+
theme: {
|
|
302
|
+
primaryColor: '#016a7e',
|
|
303
|
+
secondaryColor: '#0a1931',
|
|
304
|
+
accentColor: '#00adb5',
|
|
305
|
+
backgroundColor: '#ffffff',
|
|
306
|
+
textColor: '#0f172a',
|
|
307
|
+
headingFont: 'Inter',
|
|
308
|
+
bodyFont: 'Inter',
|
|
309
|
+
baseSize: '16px',
|
|
310
|
+
heroMinHeight: '70vh',
|
|
311
|
+
sectionPadding: '4rem',
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
requirements: {
|
|
316
|
+
requiredPages: pages.filter((p) => p.required).map((p) => p.id),
|
|
317
|
+
requiredFeatures: [],
|
|
318
|
+
},
|
|
319
|
+
content: {
|
|
320
|
+
common: {
|
|
321
|
+
websiteTitle: projectName,
|
|
322
|
+
shortDescription: 'A high-converting online storefront built on the Fivora.',
|
|
323
|
+
logoUrl: '/fivora-logo.png',
|
|
324
|
+
headerCtaLabel: 'Contact Us',
|
|
325
|
+
navLabels: navLabels,
|
|
326
|
+
footerHeading: 'Powered by Fivora',
|
|
327
|
+
copyright: `${projectName}. All rights reserved.`,
|
|
328
|
+
},
|
|
329
|
+
home: {
|
|
330
|
+
heroTitle: `Welcome to ${projectName}`,
|
|
331
|
+
heroSummary: 'Discover our premium collection with fast delivery and great support.',
|
|
332
|
+
primaryCtaLabel: 'Shop Now',
|
|
333
|
+
secondaryCtaLabel: 'Learn More',
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function initProject(targetInput) {
|
|
340
|
+
const targetDir = path.resolve(process.cwd(), targetInput || '.');
|
|
341
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
342
|
+
|
|
343
|
+
console.log(`\n\x1b[36m╔═══════════════════════════════════════════════════════╗\x1b[0m`);
|
|
344
|
+
console.log(`\x1b[36m║ \x1b[1mFIVORA TEMPLATE INITIALIZER\x1b[0m\x1b[36m ║\x1b[0m`);
|
|
345
|
+
console.log(`\x1b[36m║ \x1b[90mConfigure existing Next.js app for Fivora\x1b[0m\x1b[36m ║\x1b[0m`);
|
|
346
|
+
console.log(`\x1b[36m╚═══════════════════════════════════════════════════════╝\x1b[0m\n`);
|
|
347
|
+
|
|
348
|
+
if (!fs.existsSync(pkgPath)) {
|
|
349
|
+
console.error(`\x1b[31mError:\x1b[0m No package.json found in "${targetDir}".`);
|
|
350
|
+
console.error(`\nPlease run 'fivora init' inside your Next.js project root, or scaffold a new project with:`);
|
|
351
|
+
console.error(` \x1b[36mnpx @fivora/create-template <app-name>\x1b[0m\n`);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let pkg;
|
|
356
|
+
try {
|
|
357
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.error(`\x1b[31mError:\x1b[0m Failed to parse package.json: ${err.message}\n`);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const projectName = pkg.name || path.basename(targetDir);
|
|
364
|
+
console.log(`Configuring project: \x1b[1m${projectName}\x1b[0m in ${targetDir}...\n`);
|
|
365
|
+
|
|
366
|
+
// Detect Next.js
|
|
367
|
+
const hasNext = Boolean(
|
|
368
|
+
(pkg.dependencies && pkg.dependencies.next) ||
|
|
369
|
+
(pkg.devDependencies && pkg.devDependencies.next)
|
|
370
|
+
);
|
|
371
|
+
if (!hasNext) {
|
|
372
|
+
console.log(`\x1b[33m⚠ Warning:\x1b[0m Next.js was not detected in dependencies. Fivora templates require Next.js with static export.\n`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 1. Scan / Detect Pages
|
|
376
|
+
const detectedPages = detectPages(targetDir);
|
|
377
|
+
|
|
378
|
+
// 2. Generate fivora-template.json (or keep fivora-template.json if present)
|
|
379
|
+
const fivoraManifestPath = path.join(targetDir, 'fivora-template.json');
|
|
380
|
+
const legacyManifestPath = path.join(targetDir, 'fivora-template.json');
|
|
381
|
+
const manifestPath = fs.existsSync(legacyManifestPath) && !fs.existsSync(fivoraManifestPath)
|
|
382
|
+
? legacyManifestPath
|
|
383
|
+
: fivoraManifestPath;
|
|
384
|
+
|
|
385
|
+
if (!fs.existsSync(manifestPath)) {
|
|
386
|
+
const manifest = getDefaultManifest(projectName, detectedPages);
|
|
387
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
388
|
+
console.log(`\x1b[32m✔ Created\x1b[0m fivora-template.json (version 2, strict visual editing contract)`);
|
|
389
|
+
} else {
|
|
390
|
+
console.log(`\x1b[90m⏩ Kept existing\x1b[0m ${path.basename(manifestPath)}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 3. Generate siteDataFile
|
|
394
|
+
let manifestObj;
|
|
395
|
+
try {
|
|
396
|
+
manifestObj = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
397
|
+
} catch {
|
|
398
|
+
manifestObj = {};
|
|
399
|
+
}
|
|
400
|
+
const relSiteData = manifestObj.siteDataFile || 'src/data/site-data.json';
|
|
401
|
+
const siteDataPath = path.join(targetDir, relSiteData);
|
|
402
|
+
|
|
403
|
+
if (!fs.existsSync(siteDataPath)) {
|
|
404
|
+
fs.mkdirSync(path.dirname(siteDataPath), { recursive: true });
|
|
405
|
+
const siteData = getDefaultSiteData(projectName, detectedPages);
|
|
406
|
+
fs.writeFileSync(siteDataPath, JSON.stringify(siteData, null, 2) + '\n');
|
|
407
|
+
console.log(`\x1b[32m✔ Created\x1b[0m ${relSiteData} (merchant & editable site data)`);
|
|
408
|
+
} else {
|
|
409
|
+
console.log(`\x1b[90m⏩ Kept existing\x1b[0m ${relSiteData}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// 4. Update package.json scripts
|
|
413
|
+
pkg.scripts = pkg.scripts || {};
|
|
414
|
+
const scriptsToAdd = {
|
|
415
|
+
'lab': 'fivora lab .',
|
|
416
|
+
'validate': 'fivora validate .',
|
|
417
|
+
'zip': 'fivora zip .',
|
|
418
|
+
'package:template': 'fivora package .',
|
|
419
|
+
'update:fivora': 'fivora update',
|
|
420
|
+
};
|
|
421
|
+
let addedCount = 0;
|
|
422
|
+
for (const [key, val] of Object.entries(scriptsToAdd)) {
|
|
423
|
+
if (!pkg.scripts[key]) {
|
|
424
|
+
pkg.scripts[key] = val;
|
|
425
|
+
addedCount++;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
429
|
+
if (addedCount > 0) {
|
|
430
|
+
console.log(`\x1b[32m✔ Injected\x1b[0m Fivora scripts into package.json (lab, validate, zip, package:template, update:fivora)`);
|
|
431
|
+
} else {
|
|
432
|
+
console.log(`\x1b[90m⏩ Fivora scripts already present\x1b[0m in package.json`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// 5. Check next.config for static export
|
|
436
|
+
const nextConfigFiles = ['next.config.ts', 'next.config.mjs', 'next.config.js'];
|
|
437
|
+
let foundConfig = null;
|
|
438
|
+
let hasExport = false;
|
|
439
|
+
for (const cfg of nextConfigFiles) {
|
|
440
|
+
const cfgPath = path.join(targetDir, cfg);
|
|
441
|
+
if (fs.existsSync(cfgPath)) {
|
|
442
|
+
foundConfig = cfg;
|
|
443
|
+
const content = fs.readFileSync(cfgPath, 'utf8');
|
|
444
|
+
if (/output\s*:\s*['"]export['"]/.test(content)) {
|
|
445
|
+
hasExport = true;
|
|
446
|
+
}
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (foundConfig && !hasExport) {
|
|
452
|
+
console.log(`\n\x1b[33mℹ Note for ${foundConfig}:\x1b[0m Remember to configure static export:`);
|
|
453
|
+
console.log(` \x1b[90mconst nextConfig = { output: 'export' };\x1b[0m`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 6. Install Fivora packages if missing
|
|
457
|
+
const skipInstall = process.argv.includes('--skip-install');
|
|
458
|
+
const hasEditable = Boolean(
|
|
459
|
+
(pkg.dependencies && pkg.dependencies['@fivora/editable-components']) ||
|
|
460
|
+
(pkg.devDependencies && pkg.devDependencies['@fivora/editable-components'])
|
|
461
|
+
);
|
|
462
|
+
const hasCli = Boolean(
|
|
463
|
+
(pkg.devDependencies && pkg.devDependencies['@fivora/cli']) ||
|
|
464
|
+
(pkg.dependencies && pkg.dependencies['@fivora/cli'])
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
if (!skipInstall && (!hasEditable || !hasCli)) {
|
|
468
|
+
const toInstall = [];
|
|
469
|
+
if (!hasEditable) toInstall.push('@fivora/editable-components');
|
|
470
|
+
if (!hasCli) toInstall.push('@fivora/cli');
|
|
471
|
+
|
|
472
|
+
console.log(`\n📦 Installing ${toInstall.join(' and ')}...`);
|
|
473
|
+
const installRes = spawnSync('npm', ['install', ...toInstall], {
|
|
474
|
+
cwd: targetDir,
|
|
475
|
+
stdio: 'inherit',
|
|
476
|
+
shell: process.platform === 'win32',
|
|
477
|
+
});
|
|
478
|
+
if (installRes.status === 0) {
|
|
479
|
+
console.log(`\x1b[32m✔ Packages installed successfully!\x1b[0m`);
|
|
480
|
+
} else {
|
|
481
|
+
console.log(`\x1b[33mNote: npm install exited with code ${installRes.status}. You can run 'npm i ${toInstall.join(' ')}' manually.\x1b[0m`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
console.log(`\n\x1b[32m✔ Template initialization complete!\x1b[0m`);
|
|
486
|
+
console.log(`\nYou can now run:`);
|
|
487
|
+
console.log(` \x1b[36mnpm run lab\x1b[0m \x1b[90m# Launch Local Visual Editing Lab\x1b[0m`);
|
|
488
|
+
console.log(` \x1b[36mnpm run validate\x1b[0m \x1b[90m# Check compliance with Fivora contract\x1b[0m`);
|
|
489
|
+
console.log(` \x1b[36mnpm run zip\x1b[0m \x1b[90m# Package clean ZIP for 1-click upload\x1b[0m\n`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
function updateDependencies(cmdArgs = []) {
|
|
494
|
+
let targetDir = '.';
|
|
495
|
+
let isLocal = false;
|
|
496
|
+
let localPath = '/mnt/GAMES/GitHub/fivora_package';
|
|
497
|
+
|
|
498
|
+
for (let i = 0; i < cmdArgs.length; i++) {
|
|
499
|
+
const arg = cmdArgs[i];
|
|
500
|
+
if (arg === '--local' || arg === '-l') {
|
|
501
|
+
isLocal = true;
|
|
502
|
+
if (cmdArgs[i + 1] && !cmdArgs[i + 1].startsWith('-')) {
|
|
503
|
+
localPath = cmdArgs[++i];
|
|
504
|
+
}
|
|
505
|
+
} else if (arg.startsWith('--local=')) {
|
|
506
|
+
isLocal = true;
|
|
507
|
+
localPath = arg.split('=')[1];
|
|
508
|
+
} else if (!arg.startsWith('-')) {
|
|
509
|
+
targetDir = arg;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
targetDir = path.resolve(targetDir);
|
|
514
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
515
|
+
|
|
516
|
+
if (!fs.existsSync(pkgPath)) {
|
|
517
|
+
console.error('\x1b[31mError:\x1b[0m No package.json found in ' + targetDir);
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
let pkg;
|
|
522
|
+
try {
|
|
523
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
524
|
+
} catch (err) {
|
|
525
|
+
console.error('\x1b[31mError:\x1b[0m Failed to parse package.json: ' + err.message);
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
console.log('\n\x1b[36m╔═══════════════════════════════════════════════════════╗\x1b[0m');
|
|
530
|
+
console.log('\x1b[36m║ \x1b[1mFIVORA PACKAGE UPDATER\x1b[0m\x1b[36m ║\x1b[0m');
|
|
531
|
+
console.log('\x1b[36m╚═══════════════════════════════════════════════════════╝\x1b[0m\n');
|
|
532
|
+
|
|
533
|
+
const currentEditable = String(pkg.dependencies?.['@fivora/editable-components'] || pkg.devDependencies?.['@fivora/editable-components'] || '');
|
|
534
|
+
const currentCli = String(pkg.dependencies?.['@fivora/cli'] || pkg.devDependencies?.['@fivora/cli'] || '');
|
|
535
|
+
if (!isLocal && (currentEditable.startsWith('file:') || currentCli.startsWith('file:'))) {
|
|
536
|
+
isLocal = true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (isLocal) {
|
|
540
|
+
const editableDir = path.join(localPath, 'packages', 'editable-components');
|
|
541
|
+
const cliDir = path.join(localPath, 'cli', 'fivora-cli');
|
|
542
|
+
|
|
543
|
+
if (fs.existsSync(editableDir) && fs.existsSync(cliDir)) {
|
|
544
|
+
console.log('🔄 Updating from local Fivora workspace:');
|
|
545
|
+
console.log(' - ' + editableDir);
|
|
546
|
+
console.log(' - ' + cliDir + '\n');
|
|
547
|
+
|
|
548
|
+
const installRes = spawnSync('npm', ['install', editableDir, cliDir], {
|
|
549
|
+
cwd: targetDir,
|
|
550
|
+
stdio: 'inherit',
|
|
551
|
+
shell: process.platform === 'win32',
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
if (installRes.status === 0) {
|
|
555
|
+
console.log('\n\x1b[32m✔ Local Fivora packages re-linked & updated successfully!\x1b[0m\n');
|
|
556
|
+
} else {
|
|
557
|
+
console.log('\n\x1b[31m✖ Failed to link local packages (exit code: ' + installRes.status + ')\x1b[0m\n');
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
console.log('📦 Updating @fivora/editable-components and @fivora/cli to latest from npm...');
|
|
564
|
+
const packagesToInstall = ['@fivora/editable-components@latest', '@fivora/cli@latest'];
|
|
565
|
+
|
|
566
|
+
const installRes = spawnSync('npm', ['install', ...packagesToInstall], {
|
|
567
|
+
cwd: targetDir,
|
|
568
|
+
stdio: 'inherit',
|
|
569
|
+
shell: process.platform === 'win32',
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
if (installRes.status === 0) {
|
|
573
|
+
console.log('\n\x1b[32m✔ Fivora packages updated to latest versions successfully!\x1b[0m\n');
|
|
574
|
+
} else {
|
|
575
|
+
console.log('\n\x1b[31m✖ npm install failed with exit code ' + installRes.status + '\x1b[0m\n');
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function addComponent(componentName, targetDirInput) {
|
|
580
|
+
const targetDir = path.resolve(targetDirInput || '.');
|
|
581
|
+
const uiDir = path.join(targetDir, 'src', 'components', 'ui');
|
|
582
|
+
fs.mkdirSync(uiDir, { recursive: true });
|
|
583
|
+
|
|
584
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
585
|
+
let importPkg = '@deneb-ui/ui';
|
|
586
|
+
if (fs.existsSync(pkgPath)) {
|
|
587
|
+
try {
|
|
588
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
589
|
+
if (pkg.dependencies?.['@deneb-ui/ui'] || pkg.devDependencies?.['@deneb-ui/ui']) {
|
|
590
|
+
importPkg = '@deneb-ui/ui';
|
|
591
|
+
} else if (pkg.dependencies?.['@deneb/ui'] || pkg.devDependencies?.['@deneb/ui']) {
|
|
592
|
+
importPkg = '@deneb/ui';
|
|
593
|
+
} else if (pkg.dependencies?.['@fivora/editable-components'] || pkg.devDependencies?.['@fivora/editable-components']) {
|
|
594
|
+
importPkg = '@fivora/editable-components';
|
|
595
|
+
}
|
|
596
|
+
} catch {
|
|
597
|
+
// ignore
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const REGISTRY = {
|
|
602
|
+
'button': {
|
|
603
|
+
file: 'Button.tsx',
|
|
604
|
+
component: 'Button',
|
|
605
|
+
code: `'use client';\n\nimport { Button, type EditableButtonProps } from '${importPkg}';\n\nexport { Button, type EditableButtonProps };\n`,
|
|
606
|
+
},
|
|
607
|
+
'dialog': {
|
|
608
|
+
file: 'Dialog.tsx',
|
|
609
|
+
component: 'Dialog',
|
|
610
|
+
code: `'use client';\n\nimport { Dialog, DialogHeader, DialogTitle, DialogDescription, DialogFooter, type DialogProps } from '${importPkg}';\n\nexport { Dialog, DialogHeader, DialogTitle, DialogDescription, DialogFooter, type DialogProps };\n`,
|
|
611
|
+
},
|
|
612
|
+
'card': {
|
|
613
|
+
file: 'Card.tsx',
|
|
614
|
+
component: 'Card',
|
|
615
|
+
code: `'use client';\n\nimport { Card, type EditableCardProps } from '${importPkg}';\n\nexport { Card, type EditableCardProps };\n`,
|
|
616
|
+
},
|
|
617
|
+
'product-card': {
|
|
618
|
+
file: 'ProductCard.tsx',
|
|
619
|
+
component: 'EditableProductCard',
|
|
620
|
+
code: `'use client';\n\nimport { EditableProductCard, type EditableProductCardProps } from '${importPkg}';\n\nexport function ProductCard(props: EditableProductCardProps) {\n return <EditableProductCard {...props} />;\n}\n`,
|
|
621
|
+
},
|
|
622
|
+
'pricing-card': {
|
|
623
|
+
file: 'PricingCard.tsx',
|
|
624
|
+
component: 'EditablePricingCard',
|
|
625
|
+
code: `'use client';\n\nimport { EditablePricingCard, type EditablePricingCardProps } from '${importPkg}';\n\nexport function PricingCard(props: EditablePricingCardProps) {\n return <EditablePricingCard {...props} />;\n}\n`,
|
|
626
|
+
},
|
|
627
|
+
'testimonial-card': {
|
|
628
|
+
file: 'TestimonialCard.tsx',
|
|
629
|
+
component: 'EditableTestimonialCard',
|
|
630
|
+
code: `'use client';\n\nimport { EditableTestimonialCard, type EditableTestimonialCardProps } from '${importPkg}';\n\nexport function TestimonialCard(props: EditableTestimonialCardProps) {\n return <EditableTestimonialCard {...props} />;\n}\n`,
|
|
631
|
+
},
|
|
632
|
+
'contact-form': {
|
|
633
|
+
file: 'ContactForm.tsx',
|
|
634
|
+
component: 'EditableContactForm',
|
|
635
|
+
code: `'use client';\n\nimport { EditableContactForm, type EditableContactFormProps } from '${importPkg}';\n\nexport function ContactForm(props: EditableContactFormProps) {\n return <EditableContactForm {...props} />;\n}\n`,
|
|
636
|
+
},
|
|
637
|
+
'faq': {
|
|
638
|
+
file: 'FAQAccordion.tsx',
|
|
639
|
+
component: 'EditableFAQAccordion',
|
|
640
|
+
code: `'use client';\n\nimport { EditableFAQAccordion, EditableFAQItem, type EditableFAQAccordionProps } from '${importPkg}';\n\nexport function FAQAccordion(props: EditableFAQAccordionProps) {\n return <EditableFAQAccordion {...props} />;\n}\n\nexport { EditableFAQItem };\n`,
|
|
641
|
+
},
|
|
642
|
+
'navbar': {
|
|
643
|
+
file: 'Navbar.tsx',
|
|
644
|
+
component: 'EditableNavbar',
|
|
645
|
+
code: `'use client';\n\nimport { EditableNavbar, type EditableNavbarProps } from '${importPkg}';\n\nexport function Navbar(props: EditableNavbarProps) {\n return <EditableNavbar {...props} />;\n}\n`,
|
|
646
|
+
},
|
|
647
|
+
'footer': {
|
|
648
|
+
file: 'Footer.tsx',
|
|
649
|
+
component: 'EditableFooter',
|
|
650
|
+
code: `'use client';\n\nimport { EditableFooter, type EditableFooterProps } from '${importPkg}';\n\nexport function Footer(props: EditableFooterProps) {\n return <EditableFooter {...props} />;\n}\n`,
|
|
651
|
+
},
|
|
652
|
+
'hero': {
|
|
653
|
+
file: 'Hero.tsx',
|
|
654
|
+
component: 'EditableHeroCentered',
|
|
655
|
+
code: `'use client';\n\nimport { EditableHeroCentered, EditableHeroSplit, type EditableHeroCenteredProps, type EditableHeroSplitProps } from '${importPkg}';\n\nexport function HeroCentered(props: EditableHeroCenteredProps) {\n return <EditableHeroCentered {...props} />;\n}\n\nexport function HeroSplit(props: EditableHeroSplitProps) {\n return <EditableHeroSplit {...props} />;\n}\n`,
|
|
656
|
+
},
|
|
657
|
+
'whatsapp-button': {
|
|
658
|
+
file: 'WhatsAppButton.tsx',
|
|
659
|
+
component: 'WhatsAppButton',
|
|
660
|
+
code: `'use client';\n\nimport { WhatsAppButton, type WhatsAppButtonProps } from '${importPkg}';\n\nexport { WhatsAppButton, type WhatsAppButtonProps };\n`,
|
|
661
|
+
},
|
|
662
|
+
'phone-button': {
|
|
663
|
+
file: 'PhoneButton.tsx',
|
|
664
|
+
component: 'PhoneButton',
|
|
665
|
+
code: `'use client';\n\nimport { PhoneButton, type PhoneButtonProps } from '${importPkg}';\n\nexport { PhoneButton, type PhoneButtonProps };\n`,
|
|
666
|
+
},
|
|
667
|
+
'email-button': {
|
|
668
|
+
file: 'EmailButton.tsx',
|
|
669
|
+
component: 'EmailButton',
|
|
670
|
+
code: `'use client';\n\nimport { EmailButton, type EmailButtonProps } from '${importPkg}';\n\nexport { EmailButton, type EmailButtonProps };\n`,
|
|
671
|
+
},
|
|
672
|
+
'contact-actions': {
|
|
673
|
+
file: 'ContactActions.tsx',
|
|
674
|
+
component: 'ContactActions',
|
|
675
|
+
code: `'use client';\n\nimport { ContactActions, type ContactActionsProps } from '${importPkg}';\n\nexport { ContactActions, type ContactActionsProps };\n`,
|
|
676
|
+
},
|
|
677
|
+
'location-card': {
|
|
678
|
+
file: 'LocationCard.tsx',
|
|
679
|
+
component: 'LocationCard',
|
|
680
|
+
code: `'use client';\n\nimport { LocationCard, type LocationCardProps } from '${importPkg}';\n\nexport { LocationCard, type LocationCardProps };\n`,
|
|
681
|
+
},
|
|
682
|
+
'location-link': {
|
|
683
|
+
file: 'LocationLink.tsx',
|
|
684
|
+
component: 'LocationLink',
|
|
685
|
+
code: `'use client';\n\nimport { LocationLink, type LocationLinkProps } from '${importPkg}';\n\nexport { LocationLink, type LocationLinkProps };\n`,
|
|
686
|
+
},
|
|
687
|
+
'map-embed': {
|
|
688
|
+
file: 'MapEmbed.tsx',
|
|
689
|
+
component: 'MapEmbed',
|
|
690
|
+
code: `'use client';\n\nimport { MapEmbed, type MapEmbedProps } from '${importPkg}';\n\nexport { MapEmbed, type MapEmbedProps };\n`,
|
|
691
|
+
},
|
|
692
|
+
'social-links': {
|
|
693
|
+
file: 'SocialLinks.tsx',
|
|
694
|
+
component: 'SocialLinks',
|
|
695
|
+
code: `'use client';\n\nimport { SocialLinks, type SocialLinksProps } from '${importPkg}';\n\nexport { SocialLinks, type SocialLinksProps };\n`,
|
|
696
|
+
},
|
|
697
|
+
'social-button': {
|
|
698
|
+
file: 'SocialButton.tsx',
|
|
699
|
+
component: 'SocialButton',
|
|
700
|
+
code: `'use client';\n\nimport { SocialButton, type SocialButtonProps } from '${importPkg}';\n\nexport { SocialButton, type SocialButtonProps };\n`,
|
|
701
|
+
},
|
|
702
|
+
'business-hours': {
|
|
703
|
+
file: 'BusinessHours.tsx',
|
|
704
|
+
component: 'BusinessHours',
|
|
705
|
+
code: `'use client';\n\nimport { BusinessHours, type BusinessHoursProps } from '${importPkg}';\n\nexport { BusinessHours, type BusinessHoursProps };\n`,
|
|
706
|
+
},
|
|
707
|
+
'deneb-action': {
|
|
708
|
+
file: 'DenebAction.tsx',
|
|
709
|
+
component: 'DenebAction',
|
|
710
|
+
code: `'use client';\n\nimport { DenebAction, type DenebActionProps } from '${importPkg}';\n\nexport { DenebAction, type DenebActionProps };\n`,
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
if (!componentName || componentName === 'list') {
|
|
715
|
+
console.log('\n\x1b[36m╔═══════════════════════════════════════════════════════╗\x1b[0m');
|
|
716
|
+
console.log('\x1b[36m║ \x1b[1mDENEB UI COMPONENT REGISTRY\x1b[0m\x1b[36m ║\x1b[0m');
|
|
717
|
+
console.log('\x1b[36m╚═══════════════════════════════════════════════════════╝\x1b[0m\n');
|
|
718
|
+
console.log('Available components to add (run "deneb add <name>"):');
|
|
719
|
+
for (const [key, val] of Object.entries(REGISTRY)) {
|
|
720
|
+
console.log(` - \x1b[32m${key.padEnd(18)}\x1b[0m -> src/components/ui/${val.file}`);
|
|
721
|
+
}
|
|
722
|
+
console.log(` - \x1b[32m${'all'.padEnd(18)}\x1b[0m -> Install all components into src/components/ui/\n`);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const keysToAdd = componentName === 'all' ? Object.keys(REGISTRY) : [componentName.toLowerCase()];
|
|
727
|
+
|
|
728
|
+
console.log(`\n\x1b[36m📦 Adding DENEB UI components to:\x1b[0m ${uiDir}\n`);
|
|
729
|
+
|
|
730
|
+
for (const key of keysToAdd) {
|
|
731
|
+
const item = REGISTRY[key];
|
|
732
|
+
if (!item) {
|
|
733
|
+
console.error(`\x1b[31m✖ Unknown component:\x1b[0m "${key}". Run "deneb add list" to view available components.`);
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
const filePath = path.join(uiDir, item.file);
|
|
737
|
+
fs.writeFileSync(filePath, item.code);
|
|
738
|
+
console.log(` \x1b[32m✔ Added\x1b[0m src/components/ui/${item.file}`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
console.log('\n\x1b[32m✔ Component(s) added successfully!\x1b[0m');
|
|
742
|
+
console.log('Import them in your pages:\n \x1b[90mimport { ... } from "@/components/ui/...";\x1b[0m\n');
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (command === 'init') {
|
|
746
|
+
initProject(args[1]);
|
|
747
|
+
} else if (command === 'create') {
|
|
748
|
+
createTemplate(args[1]);
|
|
749
|
+
} else if (command === 'add') {
|
|
750
|
+
addComponent(args[1], args[2]);
|
|
751
|
+
} else if (command === 'lab') {
|
|
752
|
+
const script = path.join(toolsDir, 'local-template-lab.cjs');
|
|
753
|
+
const res = spawnSync(process.execPath, [script, ...args.slice(1)], { stdio: 'inherit' });
|
|
754
|
+
process.exit(res.status ?? 0);
|
|
755
|
+
} else if (command === 'validate') {
|
|
756
|
+
const script = path.join(toolsDir, 'fivora-template-validator.cjs');
|
|
757
|
+
const res = spawnSync(process.execPath, [script, 'validate', ...args.slice(1)], { stdio: 'inherit' });
|
|
758
|
+
process.exit(res.status ?? 0);
|
|
759
|
+
} else if (command === 'pack' || command === 'zip') {
|
|
760
|
+
const targetDir = path.resolve(args[1] || '.');
|
|
761
|
+
const outputZip = path.resolve(targetDir, 'fivora-template.zip');
|
|
762
|
+
packageCleanZip(targetDir, outputZip);
|
|
763
|
+
} else if (command === 'update' || command === 'upgrade') {
|
|
764
|
+
updateDependencies(args.slice(1));
|
|
765
|
+
} else if (command === 'package') {
|
|
766
|
+
const script = path.join(toolsDir, 'fivora-template-validator.cjs');
|
|
767
|
+
const res = spawnSync(process.execPath, [script, 'package', ...args.slice(1)], { stdio: 'inherit' });
|
|
768
|
+
process.exit(res.status ?? 0);
|
|
769
|
+
} else {
|
|
770
|
+
console.log(`Usage: deneb <command> [options] (or fivora <command> [options])
|
|
771
|
+
DENEB UI Framework — Created by Chamika Gayashan (CEE G)
|
|
772
|
+
|
|
773
|
+
Commands:
|
|
774
|
+
add Add a DENEB UI component to src/components/ui/ (e.g. deneb add product-card)
|
|
775
|
+
init Configure an existing Next.js project with missing Fivora files & scripts
|
|
776
|
+
create Scaffold a new storefront template (e.g. deneb create my-store)
|
|
777
|
+
lab Start the local visual editing lab
|
|
778
|
+
validate Validate template visual editing contracts and empty states
|
|
779
|
+
update Update DENEB & Fivora packages to latest
|
|
780
|
+
pack Quick-package a clean, upload-ready ZIP without junk files (node_modules, .next, .git, .env)
|
|
781
|
+
zip Alias for pack
|
|
782
|
+
package Run full preflight verification and generate upload ZIP`);
|
|
783
|
+
process.exit(1);
|
|
784
|
+
}
|