@onexapis/cli 1.1.16 → 1.1.18

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.
Files changed (63) hide show
  1. package/README.md +117 -51
  2. package/bin/onexthm.js +4 -0
  3. package/dist/cli.js +750 -328
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cli.mjs +747 -325
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/index.js +162 -299
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +162 -299
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/preview/preview-app.tsx +175 -53
  12. package/package.json +14 -12
  13. package/templates/default/.env.example +1 -1
  14. package/templates/default/.mcp.json +8 -0
  15. package/templates/default/CLAUDE.md +941 -0
  16. package/templates/default/bundle-entry.ts +18 -0
  17. package/templates/default/index.ts +26 -0
  18. package/templates/default/package.json +37 -0
  19. package/templates/default/pages/about.ts +66 -0
  20. package/templates/default/pages/home.ts +93 -0
  21. package/templates/default/pages/showcase.ts +146 -0
  22. package/templates/default/sections/about/about-default.tsx +237 -0
  23. package/templates/default/sections/about/about.schema.ts +259 -0
  24. package/templates/default/sections/about/index.ts +15 -0
  25. package/templates/default/sections/cta/cta-default.tsx +180 -0
  26. package/templates/default/sections/cta/cta.schema.ts +210 -0
  27. package/templates/default/sections/cta/index.ts +11 -0
  28. package/templates/default/sections/features/features-default.tsx +154 -0
  29. package/templates/default/sections/features/features.schema.ts +330 -0
  30. package/templates/default/sections/features/index.ts +11 -0
  31. package/templates/default/sections/gallery/gallery-default.tsx +134 -0
  32. package/templates/default/sections/gallery/gallery.schema.ts +397 -0
  33. package/templates/default/sections/gallery/index.ts +11 -0
  34. package/templates/default/sections/hero/hero-default.tsx +212 -0
  35. package/templates/default/sections/hero/hero.schema.ts +273 -0
  36. package/templates/default/sections/hero/index.ts +15 -0
  37. package/templates/default/sections/stats/index.ts +11 -0
  38. package/templates/default/sections/stats/stats-default.tsx +103 -0
  39. package/templates/default/sections/stats/stats.schema.ts +266 -0
  40. package/templates/default/sections/testimonials/index.ts +11 -0
  41. package/templates/default/sections/testimonials/testimonials-default.tsx +130 -0
  42. package/templates/default/sections/testimonials/testimonials.schema.ts +371 -0
  43. package/templates/default/sections-registry.ts +32 -0
  44. package/templates/default/theme.config.ts +107 -0
  45. package/templates/default/theme.layout.ts +21 -0
  46. package/templates/default/tsconfig.json +16 -7
  47. package/templates/default/README.md.ejs +0 -129
  48. package/templates/default/esbuild.config.js +0 -81
  49. package/templates/default/package.json.ejs +0 -31
  50. package/templates/default/src/config.ts.ejs +0 -98
  51. package/templates/default/src/index.ts.ejs +0 -11
  52. package/templates/default/src/layout.ts +0 -23
  53. package/templates/default/src/manifest.ts.ejs +0 -47
  54. package/templates/default/src/pages/home.ts.ejs +0 -37
  55. package/templates/default/src/sections/footer/footer-default.tsx +0 -28
  56. package/templates/default/src/sections/footer/footer.schema.ts +0 -45
  57. package/templates/default/src/sections/footer/index.ts +0 -2
  58. package/templates/default/src/sections/header/header-default.tsx +0 -61
  59. package/templates/default/src/sections/header/header.schema.ts +0 -46
  60. package/templates/default/src/sections/header/index.ts +0 -2
  61. package/templates/default/src/sections/hero/hero-default.tsx +0 -52
  62. package/templates/default/src/sections/hero/hero.schema.ts +0 -52
  63. package/templates/default/src/sections/hero/index.ts +0 -2
package/dist/cli.mjs CHANGED
@@ -4,20 +4,20 @@ import ora from 'ora';
4
4
  import * as esbuild from 'esbuild';
5
5
  import path8 from 'path';
6
6
  import fs7 from 'fs/promises';
7
- import crypto from 'crypto';
7
+ import crypto2 from 'crypto';
8
8
  import { glob } from 'glob';
9
- import os from 'os';
9
+ import { createRequire } from 'module';
10
+ import os3 from 'os';
10
11
  import dotenv from 'dotenv';
11
12
  import fs from 'fs-extra';
12
13
  import ejs from 'ejs';
13
14
  import { execSync, spawn } from 'child_process';
14
15
  import { Command } from 'commander';
15
- import { createRequire } from 'module';
16
16
  import fs2 from 'fs';
17
17
  import inquirer from 'inquirer';
18
18
  import archiver from 'archiver';
19
19
  import FormData from 'form-data';
20
- import fetch from 'node-fetch';
20
+ import fetch2 from 'node-fetch';
21
21
  import { PutObjectCommand, GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
22
22
  import AdmZip from 'adm-zip';
23
23
  import chokidar from 'chokidar';
@@ -96,7 +96,7 @@ __export(compile_theme_exports, {
96
96
  compilePreviewRuntime: () => compilePreviewRuntime,
97
97
  compileStandaloneTheme: () => compileStandaloneTheme,
98
98
  compileStandaloneThemeDev: () => compileStandaloneThemeDev,
99
- generateManifest: () => generateManifest2
99
+ generateManifest: () => generateManifest
100
100
  });
101
101
  async function resolveNodeModulesFile(startDir, relativePath) {
102
102
  let dir = startDir;
@@ -243,6 +243,12 @@ function createThemeDepsStubPlugin(themePath) {
243
243
  if (!result.errors.length) return result;
244
244
  } catch {
245
245
  }
246
+ try {
247
+ const req = createRequire(import.meta.url || __filename);
248
+ const resolved = req.resolve(args.path);
249
+ if (resolved) return { path: resolved, namespace: "file" };
250
+ } catch {
251
+ }
246
252
  return { path: args.path, namespace };
247
253
  });
248
254
  };
@@ -251,11 +257,19 @@ function createThemeDepsStubPlugin(themePath) {
251
257
  const stubs = {
252
258
  "next/image": `
253
259
  import React from 'react';
254
- const Image = (props) => {
255
- const { src, alt, width, height, fill, priority, ...rest } = props;
260
+ const Image = React.forwardRef((props, ref) => {
261
+ const { src, alt, width, height, fill, priority, sizes, quality, placeholder, blurDataURL, onLoad, onError, style, className, ...rest } = props;
256
262
  const imgSrc = typeof src === 'object' ? src.src : src;
257
- return React.createElement('img', { src: imgSrc, alt, width: fill ? undefined : width, height: fill ? undefined : height, loading: priority ? 'eager' : 'lazy', ...rest });
258
- };
263
+ const fillStyle = fill ? { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: style?.objectFit || 'cover', display: 'block' } : {};
264
+ const mergedStyle = { ...fillStyle, ...style };
265
+ return React.createElement('img', {
266
+ ref, src: imgSrc, alt,
267
+ width: fill ? undefined : width, height: fill ? undefined : height,
268
+ loading: priority ? 'eager' : 'lazy',
269
+ style: Object.keys(mergedStyle).length > 0 ? mergedStyle : undefined,
270
+ className, onLoad, onError, ...rest,
271
+ });
272
+ });
259
273
  export default Image;
260
274
  `,
261
275
  "next/link": `
@@ -289,7 +303,10 @@ export function headers() { return new Headers(); }
289
303
  if (!lucideThemeScanned) {
290
304
  lucideThemeScanned = true;
291
305
  try {
292
- const scanned = await scanImportsFromPackage(themePath, "lucide-react");
306
+ const scanned = await scanImportsFromPackage(
307
+ themePath,
308
+ "lucide-react"
309
+ );
293
310
  for (const names of Object.values(scanned)) {
294
311
  for (const name of names) lucideImports.add(name);
295
312
  }
@@ -352,10 +369,13 @@ export function useFormContext() { return useForm(); }
352
369
  loader: "js"
353
370
  }));
354
371
  tryResolveOrStub(/^@hookform\/resolvers/, "hookform-resolvers-stub");
355
- build2.onLoad({ filter: /.*/, namespace: "hookform-resolvers-stub" }, () => ({
356
- contents: `export function zodResolver() { return () => ({ values: {}, errors: {} }); }`,
357
- loader: "js"
358
- }));
372
+ build2.onLoad(
373
+ { filter: /.*/, namespace: "hookform-resolvers-stub" },
374
+ () => ({
375
+ contents: `export function zodResolver() { return () => ({ values: {}, errors: {} }); }`,
376
+ loader: "js"
377
+ })
378
+ );
359
379
  tryResolveOrStub(/^next-intl$/, "next-intl-stub");
360
380
  build2.onLoad({ filter: /.*/, namespace: "next-intl-stub" }, () => ({
361
381
  contents: `
@@ -464,7 +484,7 @@ async function contentHashEntry(outputDir) {
464
484
  logger.warning("No entry file found in output, skipping content hash");
465
485
  return;
466
486
  }
467
- const hash2 = crypto.createHash("sha256").update(entryContent).digest("hex").slice(0, 8);
487
+ const hash2 = crypto2.createHash("sha256").update(entryContent).digest("hex").slice(0, 8);
468
488
  const hashedName2 = `bundle-entry-${hash2}.js`;
469
489
  const indexMapPath = path8.join(outputDir, "index.js.map");
470
490
  const hashedMapName2 = `bundle-entry-${hash2}.js.map`;
@@ -482,7 +502,7 @@ async function contentHashEntry(outputDir) {
482
502
  logger.info(`Entry hashed: ${hashedName2}`);
483
503
  return;
484
504
  }
485
- const hash = crypto.createHash("sha256").update(entryContent).digest("hex").slice(0, 8);
505
+ const hash = crypto2.createHash("sha256").update(entryContent).digest("hex").slice(0, 8);
486
506
  const hashedName = `bundle-entry-${hash}.js`;
487
507
  const hashedMapName = `bundle-entry-${hash}.js.map`;
488
508
  entryContent = entryContent.replace(
@@ -520,7 +540,7 @@ async function extractDataRequirements(themePath) {
520
540
  }
521
541
  return requirements;
522
542
  }
523
- async function generateManifest2(themeName, themePath, outputDir) {
543
+ async function generateManifest(themeName, themePath, outputDir) {
524
544
  let version2 = "1.0.0";
525
545
  let themeId = themeName;
526
546
  try {
@@ -613,7 +633,11 @@ async function compileStandaloneTheme(themePath, themeName) {
613
633
  banner: {
614
634
  js: '"use client";'
615
635
  },
616
- plugins: [reactGlobalPlugin, createCoreGlobalPlugin(themePath), createThemeDepsStubPlugin(themePath)],
636
+ plugins: [
637
+ reactGlobalPlugin,
638
+ createCoreGlobalPlugin(themePath),
639
+ createThemeDepsStubPlugin(themePath)
640
+ ],
617
641
  external: [],
618
642
  alias: {
619
643
  events: "events/",
@@ -651,7 +675,7 @@ async function compileStandaloneTheme(themePath, themeName) {
651
675
  } catch {
652
676
  }
653
677
  await contentHashEntry(outputDir);
654
- await generateManifest2(themeName, themePath, outputDir);
678
+ await generateManifest(themeName, themePath, outputDir);
655
679
  await generateThemeData(themePath, outputDir, themeName);
656
680
  if (result.metafile) {
657
681
  const outputs = result.metafile.outputs;
@@ -695,7 +719,11 @@ async function compileStandaloneThemeDev(themePath, themeName) {
695
719
  banner: {
696
720
  js: '"use client";'
697
721
  },
698
- plugins: [reactGlobalPlugin, createCoreGlobalPlugin(themePath), createThemeDepsStubPlugin(themePath)],
722
+ plugins: [
723
+ reactGlobalPlugin,
724
+ createCoreGlobalPlugin(themePath),
725
+ createThemeDepsStubPlugin(themePath)
726
+ ],
699
727
  external: [],
700
728
  alias: {
701
729
  events: "events/",
@@ -728,7 +756,7 @@ async function compileStandaloneThemeDev(themePath, themeName) {
728
756
  };
729
757
  const context2 = await esbuild.context(buildOptions);
730
758
  await context2.rebuild();
731
- await generateManifest2(themeName, themePath, outputDir);
759
+ await generateManifest(themeName, themePath, outputDir);
732
760
  await generateThemeData(themePath, outputDir, themeName);
733
761
  return { context: context2, outputDir };
734
762
  }
@@ -830,7 +858,16 @@ ${locations.join("\n")}`
830
858
  path8.join(themePath, "node_modules", "@onexapis", "core", "src"),
831
859
  path8.join(themePath, "..", "..", "packages", "core", "src"),
832
860
  // monorepo sibling
833
- path8.join(__dirname, "..", "..", "..", "..", "packages", "core", "src")
861
+ path8.join(
862
+ __dirname,
863
+ "..",
864
+ "..",
865
+ "..",
866
+ "..",
867
+ "packages",
868
+ "core",
869
+ "src"
870
+ )
834
871
  // from CLI src
835
872
  ];
836
873
  let coreSourceDir = null;
@@ -844,7 +881,10 @@ ${locations.join("\n")}`
844
881
  }
845
882
  if (coreSourceDir) {
846
883
  try {
847
- const scanned = await scanImportsFromPackage(coreSourceDir, "lucide-react");
884
+ const scanned = await scanImportsFromPackage(
885
+ coreSourceDir,
886
+ "lucide-react"
887
+ );
848
888
  for (const names of Object.values(scanned)) {
849
889
  for (const name of names) lucideIconNames.add(name);
850
890
  }
@@ -865,7 +905,10 @@ ${locations.join("\n")}`
865
905
  const mjsFiles = await glob("*.mjs", { cwd: candidate });
866
906
  const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/g;
867
907
  for (const file of mjsFiles) {
868
- const content = await fs7.readFile(path8.join(candidate, file), "utf-8");
908
+ const content = await fs7.readFile(
909
+ path8.join(candidate, file),
910
+ "utf-8"
911
+ );
869
912
  for (const match of content.matchAll(importRegex)) {
870
913
  for (const name of match[1].split(",")) {
871
914
  const original = name.trim().split(/\s+as\s+/)[0].trim();
@@ -881,7 +924,10 @@ ${locations.join("\n")}`
881
924
  }
882
925
  }
883
926
  try {
884
- const scanned = await scanImportsFromPackage(themePath, "lucide-react");
927
+ const scanned = await scanImportsFromPackage(
928
+ themePath,
929
+ "lucide-react"
930
+ );
885
931
  for (const names of Object.values(scanned)) {
886
932
  for (const name of names) lucideIconNames.add(name);
887
933
  }
@@ -911,14 +957,39 @@ export default new Proxy({}, { get: (_, name) => name === '__esModule' ? true :
911
957
  if (!result.errors.length) return result;
912
958
  } catch {
913
959
  }
960
+ try {
961
+ const req = createRequire(import.meta.url || __filename);
962
+ const cjsPath = req.resolve("framer-motion");
963
+ const pkgDir = cjsPath.replace(/[/\\]dist[/\\].*$/, "");
964
+ const esmEntry = path8.join(pkgDir, "dist", "es", "index.mjs");
965
+ const { existsSync } = await import('fs');
966
+ if (existsSync(esmEntry)) {
967
+ return { path: esmEntry, namespace: "file" };
968
+ }
969
+ return { path: cjsPath, namespace: "file" };
970
+ } catch {
971
+ }
914
972
  return { path: args.path, namespace: "motion-stub" };
915
973
  });
916
974
  build2.onLoad({ filter: /.*/, namespace: "motion-stub" }, () => ({
917
975
  contents: `
918
- const stub = (props) => props.children || null;
919
- const handler = { get: (_, name) => name === '__esModule' ? true : stub };
976
+ import React from 'react';
977
+ const MotionComponent = React.forwardRef((props, ref) => {
978
+ const { initial, animate, exit, variants, transition, whileInView, whileHover, whileTap, viewport, onAnimationComplete, layout, layoutId, style, className, ...rest } = props;
979
+ return React.createElement(rest.as || 'div', { ref, style, className, ...rest }, rest.children);
980
+ });
981
+ const handler = { get: (_, name) => {
982
+ if (name === '__esModule') return true;
983
+ if (name === 'create') return () => new Proxy({}, handler);
984
+ return MotionComponent;
985
+ }};
920
986
  export const motion = new Proxy({}, handler);
921
- export const AnimatePresence = stub;
987
+ export const AnimatePresence = (props) => props.children || null;
988
+ export const useInView = () => true;
989
+ export const useAnimation = () => ({ start: () => {}, stop: () => {}, set: () => {} });
990
+ export const useMotionValue = (v) => ({ get: () => v, set: () => {}, onChange: () => () => {} });
991
+ export const useTransform = (v) => v;
992
+ export const useScroll = () => ({ scrollY: { get: () => 0, onChange: () => () => {} }, scrollYProgress: { get: () => 0, onChange: () => () => {} } });
922
993
  export default { motion, AnimatePresence };
923
994
  `.trim(),
924
995
  loader: "jsx"
@@ -941,12 +1012,26 @@ export default { motion, AnimatePresence };
941
1012
  build2.onLoad({ filter: /.*/, namespace: "next-stub" }, (args) => {
942
1013
  const stubs = {
943
1014
  "next/image": `
944
- const Image = (props) => {
945
- const { src, alt, width, height, fill, priority, ...rest } = props;
946
- const imgSrc = typeof src === 'object' ? src.src : src;
947
- return React.createElement('img', { src: imgSrc, alt, width: fill ? undefined : width, height: fill ? undefined : height, loading: priority ? 'eager' : 'lazy', ...rest });
948
- };
949
1015
  import React from 'react';
1016
+ const Image = React.forwardRef((props, ref) => {
1017
+ const { src, alt, width, height, fill, priority, sizes, quality, placeholder, blurDataURL, onLoad, onError, style, className, ...rest } = props;
1018
+ const imgSrc = typeof src === 'object' ? src.src : src;
1019
+ const fillStyle = fill ? { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: style?.objectFit || 'cover', display: 'block' } : {};
1020
+ const mergedStyle = { ...fillStyle, ...style };
1021
+ return React.createElement('img', {
1022
+ ref,
1023
+ src: imgSrc,
1024
+ alt,
1025
+ width: fill ? undefined : width,
1026
+ height: fill ? undefined : height,
1027
+ loading: priority ? 'eager' : 'lazy',
1028
+ style: Object.keys(mergedStyle).length > 0 ? mergedStyle : undefined,
1029
+ className,
1030
+ onLoad,
1031
+ onError,
1032
+ ...rest,
1033
+ });
1034
+ });
950
1035
  export default Image;
951
1036
  `,
952
1037
  "next/link": `
@@ -1407,45 +1492,20 @@ async function initCommand(projectName, options = {}) {
1407
1492
  try {
1408
1493
  fs2.mkdirSync(projectPath, { recursive: true });
1409
1494
  await copyTemplate(template, projectPath, data);
1410
- const srcPath = path8.join(projectPath, "src");
1411
- fs2.mkdirSync(srcPath, { recursive: true });
1412
- const manifestContent = generateManifest(data);
1413
- await writeFile(path8.join(srcPath, "manifest.ts"), manifestContent);
1414
- const configContent = generateThemeConfig(data);
1415
- await writeFile(path8.join(srcPath, "config.ts"), configContent);
1416
- const layoutContent = generateThemeLayout(data);
1417
- await writeFile(path8.join(srcPath, "layout.ts"), layoutContent);
1418
- const indexContent = generateThemeIndex(data);
1419
- await writeFile(path8.join(srcPath, "index.ts"), indexContent);
1420
- const sectionsPath = path8.join(srcPath, "sections");
1421
- fs2.mkdirSync(sectionsPath, { recursive: true });
1422
- await writeFile(
1423
- path8.join(sectionsPath, "README.md"),
1424
- `# ${displayName} Sections
1425
-
1426
- Add your theme-specific sections here.
1427
- `
1495
+ await renameThemeInFiles(
1496
+ projectPath,
1497
+ name,
1498
+ displayName,
1499
+ description,
1500
+ author
1428
1501
  );
1429
- const blocksPath = path8.join(srcPath, "blocks");
1430
- fs2.mkdirSync(blocksPath, { recursive: true });
1431
- await writeFile(
1432
- path8.join(blocksPath, "README.md"),
1433
- `# ${displayName} Blocks
1434
-
1435
- Add your theme-specific blocks here.
1436
- `
1437
- );
1438
- const pagesPath = path8.join(srcPath, "pages");
1439
- fs2.mkdirSync(pagesPath, { recursive: true });
1440
- const homePageContent = generateHomePage(data);
1441
- await writeFile(path8.join(pagesPath, "home.ts"), homePageContent);
1442
1502
  logger.stopSpinner(true, "Project structure created!");
1443
1503
  if (options.git) {
1444
1504
  logger.startSpinner("Initializing git repository...");
1445
1505
  try {
1446
1506
  execSync("git init", { cwd: projectPath, stdio: "ignore" });
1447
1507
  execSync("git add .", { cwd: projectPath, stdio: "ignore" });
1448
- execSync('git commit -m "Initial commit from onex init"', {
1508
+ execSync('git commit -m "Initial commit from onexthm init"', {
1449
1509
  cwd: projectPath,
1450
1510
  stdio: "ignore"
1451
1511
  });
@@ -1482,14 +1542,14 @@ Add your theme-specific blocks here.
1482
1542
  logger.log(` npm run dev # Start development mode`);
1483
1543
  logger.newLine();
1484
1544
  logger.section("Theme structure:");
1485
- logger.log(" src/manifest.ts - Theme manifest and exports");
1545
+ logger.log(" bundle-entry.ts - Theme manifest and exports");
1486
1546
  logger.log(
1487
- " src/config.ts - Design tokens (colors, typography, etc.)"
1547
+ " theme.config.ts - Design tokens (colors, typography, etc.)"
1488
1548
  );
1489
- logger.log(" src/layout.ts - Header and footer configuration");
1490
- logger.log(" src/sections/ - Custom sections for your theme");
1491
- logger.log(" src/blocks/ - Reusable blocks");
1492
- logger.log(" src/pages/ - Page configurations");
1549
+ logger.log(" theme.layout.ts - Header and footer configuration");
1550
+ logger.log(" sections/ - Custom sections for your theme");
1551
+ logger.log(" pages/ - Page configurations");
1552
+ logger.log(" CLAUDE.md - AI assistant context");
1493
1553
  logger.newLine();
1494
1554
  logger.success(`Happy theming! \u{1F3A8}`);
1495
1555
  } catch (error) {
@@ -1503,231 +1563,33 @@ Add your theme-specific blocks here.
1503
1563
  process.exit(1);
1504
1564
  }
1505
1565
  }
1506
- function generateManifest(data) {
1507
- return `import type { ThemeExport } from "@onexapis/core";
1508
-
1509
- /**
1510
- * ${data.displayName} Theme Manifest
1511
- * ${data.description}
1512
- */
1513
- export const manifest: ThemeExport = {
1514
- id: "${data.themeName}",
1515
- name: "${data.displayName}",
1516
- description: "${data.description}",
1517
- version: "1.0.0",
1518
- author: "${data.author}",
1519
-
1520
- // Theme configuration
1521
- config: () => import("./config").then((m) => m.themeConfig),
1522
-
1523
- // Theme layout (header/footer sections)
1524
- layout: () => import("./layout").then((m) => m.themeLayout),
1525
-
1526
- // Available sections in this theme
1527
- sections: {
1528
- // Example: hero: () => import("./sections/hero").then((m) => m.heroSchema),
1529
- },
1530
-
1531
- // Available blocks in this theme
1532
- blocks: {
1533
- // Example: productCard: () => import("./blocks/product-card").then((m) => m.productCardDefinition),
1534
- },
1535
-
1536
- // Default pages
1537
- pages: {
1538
- home: () => import("./pages/home").then((m) => m.homePageConfig),
1539
- },
1540
-
1541
- // Supported page types
1542
- supportedPageTypes: ["home", "about", "contact", "custom"],
1543
-
1544
- // Preview image (optional)
1545
- preview: undefined,
1546
-
1547
- // Tags for categorization (optional)
1548
- tags: ["custom"],
1549
- };
1550
-
1551
- export default manifest;
1552
- `;
1553
- }
1554
- function generateThemeConfig(data) {
1555
- return `import type { ThemeConfig } from "@onexapis/core";
1556
-
1557
- /**
1558
- * ${data.displayName} Theme Configuration
1559
- * Design tokens: colors, typography, spacing, etc.
1560
- */
1561
- export const themeConfig: ThemeConfig = {
1562
- // Color palette
1563
- colors: {
1564
- primary: {
1565
- 50: "#eff6ff",
1566
- 100: "#dbeafe",
1567
- 200: "#bfdbfe",
1568
- 300: "#93c5fd",
1569
- 400: "#60a5fa",
1570
- 500: "#3b82f6",
1571
- 600: "#2563eb",
1572
- 700: "#1d4ed8",
1573
- 800: "#1e40af",
1574
- 900: "#1e3a8a",
1575
- },
1576
- secondary: {
1577
- 50: "#f8fafc",
1578
- 100: "#f1f5f9",
1579
- 200: "#e2e8f0",
1580
- 300: "#cbd5e1",
1581
- 400: "#94a3b8",
1582
- 500: "#64748b",
1583
- 600: "#475569",
1584
- 700: "#334155",
1585
- 800: "#1e293b",
1586
- 900: "#0f172a",
1587
- },
1588
- accent: {
1589
- 50: "#fdf4ff",
1590
- 100: "#fae8ff",
1591
- 200: "#f5d0fe",
1592
- 300: "#f0abfc",
1593
- 400: "#e879f9",
1594
- 500: "#d946ef",
1595
- 600: "#c026d3",
1596
- 700: "#a21caf",
1597
- 800: "#86198f",
1598
- 900: "#701a75",
1599
- },
1600
- },
1601
-
1602
- // Typography
1603
- typography: {
1604
- fontFamily: {
1605
- sans: ["Inter", "system-ui", "sans-serif"],
1606
- serif: ["Georgia", "serif"],
1607
- mono: ["Monaco", "monospace"],
1608
- },
1609
- fontSize: {
1610
- xs: "0.75rem",
1611
- sm: "0.875rem",
1612
- base: "1rem",
1613
- lg: "1.125rem",
1614
- xl: "1.25rem",
1615
- "2xl": "1.5rem",
1616
- "3xl": "1.875rem",
1617
- "4xl": "2.25rem",
1618
- "5xl": "3rem",
1619
- },
1620
- },
1621
-
1622
- // Spacing
1623
- spacing: {
1624
- xs: "0.5rem",
1625
- sm: "1rem",
1626
- md: "1.5rem",
1627
- lg: "2rem",
1628
- xl: "3rem",
1629
- "2xl": "4rem",
1630
- "3xl": "6rem",
1631
- "4xl": "8rem",
1632
- },
1633
-
1634
- // Border radius
1635
- borderRadius: {
1636
- none: "0",
1637
- sm: "0.125rem",
1638
- md: "0.375rem",
1639
- lg: "0.5rem",
1640
- xl: "0.75rem",
1641
- full: "9999px",
1642
- },
1643
-
1644
- // Breakpoints
1645
- breakpoints: {
1646
- sm: "640px",
1647
- md: "768px",
1648
- lg: "1024px",
1649
- xl: "1280px",
1650
- "2xl": "1536px",
1651
- },
1652
- };
1653
- `;
1654
- }
1655
- function generateThemeLayout(data) {
1656
- return `import type { ThemeLayoutConfig } from "@onexapis/core";
1657
-
1658
- /**
1659
- * ${data.themeName} Theme Layout
1660
- * Define header and footer sections
1661
- */
1662
- export const themeLayout: ThemeLayoutConfig = {
1663
- // Header section configuration
1664
- header: undefined,
1665
- // Example:
1666
- // header: {
1667
- // type: "header",
1668
- // template: "default",
1669
- // enabled: true,
1670
- // settings: {},
1671
- // },
1672
-
1673
- // Footer section configuration
1674
- footer: undefined,
1675
- // Example:
1676
- // footer: {
1677
- // type: "footer",
1678
- // template: "default",
1679
- // enabled: true,
1680
- // settings: {},
1681
- // },
1682
- };
1683
- `;
1684
- }
1685
- function generateThemeIndex(data) {
1686
- return `/**
1687
- * ${data.themeNamePascal} Theme
1688
- */
1689
-
1690
- export { manifest as ${data.themeNamePascal}Manifest } from "./manifest";
1691
- export { themeConfig as ${data.themeNamePascal}Config } from "./config";
1692
- export { themeLayout as ${data.themeNamePascal}Layout } from "./layout";
1693
- `;
1694
- }
1695
- function generateHomePage(data) {
1696
- return `import type { PageConfig } from "@onexapis/core";
1697
-
1698
- /**
1699
- * Home Page Configuration
1700
- */
1701
- export const homePageConfig: PageConfig = {
1702
- type: "home",
1703
- title: "${data.displayName}",
1704
- description: "Welcome to ${data.displayName}",
1705
-
1706
- // SEO metadata
1707
- seo: {
1708
- title: "${data.displayName} - Home",
1709
- description: "Welcome to ${data.displayName}",
1710
- keywords: [],
1711
- ogImage: undefined,
1712
- },
1713
-
1714
- // Page sections
1715
- sections: [
1716
- // Add your sections here
1717
- // Example:
1718
- // {
1719
- // id: "hero-1",
1720
- // type: "hero",
1721
- // template: "default",
1722
- // order: 0,
1723
- // enabled: true,
1724
- // settings: {},
1725
- // components: [],
1726
- // blocks: [],
1727
- // },
1728
- ],
1729
- };
1730
- `;
1566
+ async function renameThemeInFiles(projectPath, themeName, displayName, description, author) {
1567
+ const configPath = path8.join(projectPath, "theme.config.ts");
1568
+ if (fs2.existsSync(configPath)) {
1569
+ let content = fs2.readFileSync(configPath, "utf-8");
1570
+ content = content.replace(
1571
+ /name: "My Simple Theme"/,
1572
+ `name: "${displayName}"`
1573
+ );
1574
+ content = content.replace(
1575
+ /description: ".*?"/,
1576
+ `description: "${description}"`
1577
+ );
1578
+ fs2.writeFileSync(configPath, content, "utf-8");
1579
+ }
1580
+ const pkgPath = path8.join(projectPath, "package.json");
1581
+ if (fs2.existsSync(pkgPath)) {
1582
+ let content = fs2.readFileSync(pkgPath, "utf-8");
1583
+ content = content.replace(
1584
+ /@onex-themes\/my-simple/g,
1585
+ `@onex-themes/${themeName}`
1586
+ );
1587
+ content = content.replace(
1588
+ /"description": ".*?"/,
1589
+ `"description": "${description}"`
1590
+ );
1591
+ fs2.writeFileSync(pkgPath, content, "utf-8");
1592
+ }
1731
1593
  }
1732
1594
 
1733
1595
  // src/commands/create-section.ts
@@ -2795,7 +2657,7 @@ async function buildCommand(options) {
2795
2657
  logger.stopSpinner(true, "Lint passed");
2796
2658
  const pkgJson = fs.readJsonSync(packageJsonPath);
2797
2659
  const buildScript = pkgJson.scripts?.build || "";
2798
- const isRecursive = buildScript.includes("onex build") || buildScript.includes("onex-cli build");
2660
+ const isRecursive = buildScript.includes("onexthm build") || buildScript.includes("onex-cli build");
2799
2661
  logger.startSpinner(
2800
2662
  options.watch ? "Building (watch mode)..." : "Building..."
2801
2663
  );
@@ -2941,7 +2803,7 @@ async function packageCommand(options) {
2941
2803
  logger.newLine();
2942
2804
  logger.section("Next steps:");
2943
2805
  logger.log(
2944
- ` onex deploy --package ${path8.relative(process.cwd(), outputPath)}`
2806
+ ` onexthm deploy --package ${path8.relative(process.cwd(), outputPath)}`
2945
2807
  );
2946
2808
  } catch (error) {
2947
2809
  logger.stopSpinner(false, "Failed to create package");
@@ -3003,7 +2865,7 @@ async function deployCommand(options) {
3003
2865
  } else if (options.theme) {
3004
2866
  const distDir = path8.join(process.cwd(), "dist");
3005
2867
  if (!fs.existsSync(distDir)) {
3006
- logger.error("No dist/ directory found. Run 'onex package' first.");
2868
+ logger.error("No dist/ directory found. Run 'onexthm package' first.");
3007
2869
  process.exit(1);
3008
2870
  }
3009
2871
  const files = fs.readdirSync(distDir);
@@ -3012,7 +2874,7 @@ async function deployCommand(options) {
3012
2874
  );
3013
2875
  if (packageFiles.length === 0) {
3014
2876
  logger.error(`No package found for theme "${options.theme}".`);
3015
- logger.info("Run: onex package --theme " + options.theme);
2877
+ logger.info("Run: onexthm package --theme " + options.theme);
3016
2878
  process.exit(1);
3017
2879
  }
3018
2880
  packageFiles.sort().reverse();
@@ -3020,8 +2882,8 @@ async function deployCommand(options) {
3020
2882
  } else {
3021
2883
  logger.error("Either --package or --theme must be specified.");
3022
2884
  logger.info("Examples:");
3023
- logger.log(" onex deploy --package dist/tinan-1.0.0.zip");
3024
- logger.log(" onex deploy --theme tinan");
2885
+ logger.log(" onexthm deploy --package dist/tinan-1.0.0.zip");
2886
+ logger.log(" onexthm deploy --theme tinan");
3025
2887
  process.exit(1);
3026
2888
  }
3027
2889
  if (!fs.existsSync(packagePath)) {
@@ -3037,7 +2899,7 @@ async function deployCommand(options) {
3037
2899
  logger.log(`Path: ${path8.relative(process.cwd(), packagePath)}`);
3038
2900
  logger.newLine();
3039
2901
  const apiUrl = options.apiUrl || process.env.ONEX_API_URL || "http://localhost:3001";
3040
- const uploadEndpoint = `${apiUrl}/api/themes/upload`;
2902
+ const uploadEndpoint = `${apiUrl}/website-api/themes/upload`;
3041
2903
  logger.section("Uploading to API Server");
3042
2904
  logger.info(`Endpoint: ${uploadEndpoint}`);
3043
2905
  logger.newLine();
@@ -3054,7 +2916,7 @@ async function deployCommand(options) {
3054
2916
  if (options.environment) {
3055
2917
  formData.append("environment", options.environment);
3056
2918
  }
3057
- const response = await fetch(uploadEndpoint, {
2919
+ const response = await fetch2(uploadEndpoint, {
3058
2920
  method: "POST",
3059
2921
  body: formData,
3060
2922
  headers: formData.getHeaders()
@@ -3135,7 +2997,7 @@ function getBucketName(env) {
3135
2997
  return process.env.BUCKET_NAME;
3136
2998
  }
3137
2999
  const environment = env || process.env.ENVIRONMENT || "staging";
3138
- return environment === "production" ? "onex-themes-prod" : "onex-themes-staging";
3000
+ return environment === "production" ? "theme-s3-bucket" : "theme-s3-bucket";
3139
3001
  }
3140
3002
  async function findCompiledThemeDir(themeId, version2) {
3141
3003
  const searchPaths = [path8.resolve(process.cwd(), "dist")];
@@ -3242,7 +3104,7 @@ async function uploadCommand(options) {
3242
3104
  if (!compiledDir) {
3243
3105
  spinner.fail(
3244
3106
  chalk4.red(
3245
- `Compiled theme not found for ${themeId}@${version2}. Run 'onex build' first.`
3107
+ `Compiled theme not found for ${themeId}@${version2}. Run 'onexthm build' first.`
3246
3108
  )
3247
3109
  );
3248
3110
  logger.info(chalk4.gray(`Expected location:
@@ -3251,7 +3113,7 @@ async function uploadCommand(options) {
3251
3113
  }
3252
3114
  spinner.succeed(`Found compiled theme at: ${compiledDir}`);
3253
3115
  spinner.start("Creating bundle.zip...");
3254
- const tmpDir = os.tmpdir();
3116
+ const tmpDir = os3.tmpdir();
3255
3117
  const bundleZipPath = path8.join(tmpDir, `${themeId}-${version2}-bundle.zip`);
3256
3118
  await createZipFromDir(compiledDir, bundleZipPath);
3257
3119
  const bundleZipBuffer = await fs.readFile(bundleZipPath);
@@ -3404,7 +3266,7 @@ function getBucketName2(env) {
3404
3266
  return process.env.BUCKET_NAME;
3405
3267
  }
3406
3268
  const environment = env || process.env.ENVIRONMENT || "staging";
3407
- return environment === "production" ? "onex-themes-prod" : "onex-themes-staging";
3269
+ return environment === "production" ? "theme-s3-bucket" : "theme-s3-bucket";
3408
3270
  }
3409
3271
  async function streamToString(stream) {
3410
3272
  const chunks = [];
@@ -3474,7 +3336,7 @@ function showDownloadFailureHelp(themeId, bucket) {
3474
3336
  console.log(chalk4.white("1. Compile and upload the theme:"));
3475
3337
  console.log(chalk4.gray(` cd themes/${themeId}`));
3476
3338
  console.log(chalk4.gray(" pnpm build"));
3477
- console.log(chalk4.gray(" onex upload"));
3339
+ console.log(chalk4.gray(" onexthm upload"));
3478
3340
  console.log();
3479
3341
  console.log(chalk4.white("2. Verify AWS credentials are set:"));
3480
3342
  console.log(
@@ -3605,7 +3467,7 @@ function getBucketName3(env) {
3605
3467
  return process.env.BUCKET_NAME;
3606
3468
  }
3607
3469
  const environment = env || process.env.ENVIRONMENT || "staging";
3608
- return environment === "production" ? "onex-themes-prod" : "onex-themes-staging";
3470
+ return environment === "production" ? "theme-s3-bucket" : "theme-s3-bucket";
3609
3471
  }
3610
3472
  async function streamToString2(stream) {
3611
3473
  const chunks = [];
@@ -3649,8 +3511,8 @@ function runInstall(cwd) {
3649
3511
  });
3650
3512
  }
3651
3513
  async function promptThemeName(originalName) {
3652
- const { default: inquirer6 } = await import('inquirer');
3653
- const { themeName } = await inquirer6.prompt([
3514
+ const { default: inquirer7 } = await import('inquirer');
3515
+ const { themeName } = await inquirer7.prompt([
3654
3516
  {
3655
3517
  type: "input",
3656
3518
  name: "themeName",
@@ -3776,7 +3638,7 @@ async function cloneCommand(themeName, options) {
3776
3638
  chalk4.yellow("The theme source may not have been uploaded yet.")
3777
3639
  );
3778
3640
  console.log(
3779
- chalk4.gray(`Upload source with: onex upload --theme ${themeName}`)
3641
+ chalk4.gray(`Upload source with: onexthm upload --theme ${themeName}`)
3780
3642
  );
3781
3643
  console.log();
3782
3644
  process.exit(1);
@@ -3803,7 +3665,7 @@ async function cloneCommand(themeName, options) {
3803
3665
  [
3804
3666
  "# API Configuration (enables real data in preview)",
3805
3667
  "# Get your Company ID from the OneX dashboard",
3806
- "NEXT_PUBLIC_API_URL=https://api-dev.onexeos.com",
3668
+ "NEXT_PUBLIC_API_URL=https://platform-dev.onexeos.com",
3807
3669
  "NEXT_PUBLIC_COMPANY_ID=",
3808
3670
  ""
3809
3671
  ].join("\n")
@@ -3845,7 +3707,7 @@ async function cloneCommand(themeName, options) {
3845
3707
  if (options.install === false) {
3846
3708
  console.log(chalk4.gray(" pnpm install"));
3847
3709
  }
3848
- console.log(chalk4.gray(" onex build"));
3710
+ console.log(chalk4.gray(" onexthm build"));
3849
3711
  console.log();
3850
3712
  } catch (error) {
3851
3713
  spinner.fail(chalk4.red(`Clone failed: ${error.message}`));
@@ -3873,6 +3735,7 @@ var MIME_TYPES = {
3873
3735
  };
3874
3736
  function createDevServer(options) {
3875
3737
  const clients = /* @__PURE__ */ new Set();
3738
+ const themeDataPath = path8.join(options.distDir, "theme-data.json");
3876
3739
  const server = http.createServer((req, res) => {
3877
3740
  res.setHeader("Access-Control-Allow-Origin", "*");
3878
3741
  res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
@@ -3886,7 +3749,9 @@ function createDevServer(options) {
3886
3749
  const pathname = url.pathname;
3887
3750
  if (pathname === "/" || pathname === "/index.html") {
3888
3751
  res.writeHead(200, { "Content-Type": "text/html" });
3889
- res.end(generatePreviewHTML(options.themeName));
3752
+ res.end(
3753
+ generatePreviewHTML(options.themeName, options.port, themeDataPath)
3754
+ );
3890
3755
  return;
3891
3756
  }
3892
3757
  if (pathname === "/preview-runtime.js") {
@@ -3905,6 +3770,37 @@ function createDevServer(options) {
3905
3770
  serveFile(res, assetPath);
3906
3771
  return;
3907
3772
  }
3773
+ if (pathname.startsWith("/themes/")) {
3774
+ const match = pathname.match(/^\/themes\/[^/]+\/assets\/(.+)/);
3775
+ if (match) {
3776
+ const assetPath = path8.join(options.themePath, "assets", match[1]);
3777
+ if (!assetPath.startsWith(path8.join(options.themePath, "assets"))) {
3778
+ res.writeHead(403);
3779
+ res.end("Forbidden");
3780
+ return;
3781
+ }
3782
+ serveFile(res, assetPath);
3783
+ return;
3784
+ }
3785
+ }
3786
+ if (pathname.startsWith("/assets/")) {
3787
+ const subpath = pathname.replace(/^\/assets\//, "");
3788
+ const segments = subpath.split("/");
3789
+ let assetPath;
3790
+ if (segments[0] === options.themeName || segments[0] === options.themeName.replace(/^my-/, "")) {
3791
+ assetPath = path8.join(
3792
+ options.themePath,
3793
+ "assets",
3794
+ segments.slice(1).join("/")
3795
+ );
3796
+ } else {
3797
+ assetPath = path8.join(options.themePath, "assets", subpath);
3798
+ }
3799
+ if (assetPath.startsWith(path8.join(options.themePath, "assets")) && fs2.existsSync(assetPath)) {
3800
+ serveFile(res, assetPath);
3801
+ return;
3802
+ }
3803
+ }
3908
3804
  const filePath = path8.join(options.distDir, pathname);
3909
3805
  if (!filePath.startsWith(options.distDir)) {
3910
3806
  res.writeHead(403);
@@ -3915,7 +3811,9 @@ function createDevServer(options) {
3915
3811
  serveFile(res, filePath);
3916
3812
  } else {
3917
3813
  res.writeHead(200, { "Content-Type": "text/html" });
3918
- res.end(generatePreviewHTML(options.themeName));
3814
+ res.end(
3815
+ generatePreviewHTML(options.themeName, options.port, themeDataPath)
3816
+ );
3919
3817
  }
3920
3818
  });
3921
3819
  const wss = new WebSocketServer({ server });
@@ -3956,17 +3854,82 @@ function serveFile(res, filePath) {
3956
3854
  res.end("Internal Server Error");
3957
3855
  }
3958
3856
  }
3959
- function generatePreviewHTML(themeName, port) {
3857
+ function generatePreviewHTML(themeName, port, themeDataPath) {
3858
+ let fontLinks = "";
3859
+ let fontVarsCSS = "";
3860
+ if (themeDataPath) {
3861
+ try {
3862
+ const themeData = JSON.parse(fs2.readFileSync(themeDataPath, "utf-8"));
3863
+ const typography = (themeData?.themeConfig || themeData?.theme?.config)?.typography?.fontFamily;
3864
+ if (typography) {
3865
+ const fontFamilies = /* @__PURE__ */ new Set();
3866
+ for (const value of Object.values(typography)) {
3867
+ const primary = value.split(",")[0].trim();
3868
+ if (primary && ![
3869
+ "serif",
3870
+ "sans-serif",
3871
+ "monospace",
3872
+ "system-ui",
3873
+ "Georgia",
3874
+ "Inter",
3875
+ "Consolas"
3876
+ ].includes(primary)) {
3877
+ fontFamilies.add(primary);
3878
+ }
3879
+ }
3880
+ if (fontFamilies.size > 0) {
3881
+ const families = Array.from(fontFamilies).map(
3882
+ (f) => `family=${f.replace(/\s+/g, "+")}:wght@300;400;500;600;700;800;900`
3883
+ ).join("&");
3884
+ fontLinks = `<link rel="preconnect" href="https://fonts.googleapis.com">
3885
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
3886
+ <link href="https://fonts.googleapis.com/css2?${families}&display=swap" rel="stylesheet">`;
3887
+ }
3888
+ const heading = typography.heading || typography.body || "system-ui";
3889
+ const body = typography.body || "system-ui";
3890
+ fontVarsCSS = `
3891
+ :root {
3892
+ --font-heading: ${heading};
3893
+ --font-body: ${body};
3894
+ }
3895
+ body { font-family: var(--font-body); }
3896
+ h1, h2, h3, h4, h5, h6 { font-family: var(--font-heading); }`;
3897
+ }
3898
+ } catch {
3899
+ }
3900
+ }
3960
3901
  return `<!DOCTYPE html>
3961
3902
  <html lang="en">
3962
3903
  <head>
3963
3904
  <meta charset="UTF-8">
3964
3905
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
3965
3906
  <title>OneX Dev \u2014 ${themeName}</title>
3907
+ ${fontLinks}
3966
3908
  <!-- Tailwind CSS Play CDN \u2014 JIT compilation in browser for dev preview -->
3967
3909
  <script src="https://cdn.tailwindcss.com"></script>
3910
+ <script>
3911
+ tailwind.config = {
3912
+ theme: {
3913
+ extend: {
3914
+ aspectRatio: {
3915
+ '2/1': '2 / 1',
3916
+ '3/2': '3 / 2',
3917
+ '4/3': '4 / 3',
3918
+ '3/4': '3 / 4',
3919
+ '5/4': '5 / 4',
3920
+ '16/9': '16 / 9',
3921
+ '21/9': '21 / 9',
3922
+ },
3923
+ fontFamily: {
3924
+ playfair: ['Playfair Display', 'Georgia', 'serif'],
3925
+ roboto: ['Roboto', 'system-ui', 'sans-serif'],
3926
+ },
3927
+ },
3928
+ },
3929
+ }
3930
+ </script>
3968
3931
  <style>
3969
- #onex-preview-root { margin-top: 0; }
3932
+ #onex-preview-root { margin-top: 0; }${fontVarsCSS}
3970
3933
  </style>
3971
3934
  </head>
3972
3935
  <body>
@@ -4050,7 +4013,7 @@ async function devCommand(options) {
4050
4013
  logger.info(`File changed: ${filePath}`);
4051
4014
  try {
4052
4015
  await context2.rebuild();
4053
- await generateManifest2(themeName, themePath, outputDir);
4016
+ await generateManifest(themeName, themePath, outputDir);
4054
4017
  server.broadcast({ type: "reload", timestamp: Date.now() });
4055
4018
  logger.success("Rebuilt successfully");
4056
4019
  } catch (error) {
@@ -4085,7 +4048,7 @@ async function devCommand(options) {
4085
4048
 
4086
4049
  // src/commands/config.ts
4087
4050
  init_logger();
4088
- var CONFIG_DIR = path8.join(os.homedir(), ".onex");
4051
+ var CONFIG_DIR = path8.join(os3.homedir(), ".onexthm");
4089
4052
  var CONFIG_FILE = path8.join(CONFIG_DIR, ".env");
4090
4053
  var CONFIG_ENTRIES = [
4091
4054
  {
@@ -4115,7 +4078,7 @@ var CONFIG_ENTRIES = [
4115
4078
  key: "NEXT_PUBLIC_API_URL",
4116
4079
  label: "API URL",
4117
4080
  required: false,
4118
- defaultValue: "https://api-dev.onexeos.com"
4081
+ defaultValue: "https://platform-dev.onexeos.com"
4119
4082
  },
4120
4083
  {
4121
4084
  key: "NEXT_PUBLIC_COMPANY_ID",
@@ -4139,7 +4102,7 @@ function parseEnvFile(content) {
4139
4102
  function serializeEnv(values) {
4140
4103
  const lines = [
4141
4104
  "# OneX CLI Configuration",
4142
- "# Generated by: onex config",
4105
+ "# Generated by: onexthm config",
4143
4106
  ""
4144
4107
  ];
4145
4108
  lines.push("# AWS / S3 Configuration");
@@ -4226,6 +4189,461 @@ async function configCommand() {
4226
4189
  );
4227
4190
  }
4228
4191
 
4192
+ // src/commands/login.ts
4193
+ init_logger();
4194
+ var AUTH_DIR = path8.join(os3.homedir(), ".onexthm");
4195
+ var AUTH_FILE = path8.join(AUTH_DIR, "auth.json");
4196
+ function getApiUrl() {
4197
+ return process.env.NEXT_PUBLIC_API_URL || process.env.ONEXTHM_API_URL || "https://platform-dev.onexeos.com";
4198
+ }
4199
+ async function saveAuthTokens(tokens) {
4200
+ await fs.ensureDir(AUTH_DIR);
4201
+ const key = getMachineKey();
4202
+ const data = JSON.stringify(tokens);
4203
+ const encrypted = encrypt(data, key);
4204
+ await fs.writeFile(AUTH_FILE, encrypted, "utf-8");
4205
+ }
4206
+ function loadAuthTokens() {
4207
+ try {
4208
+ if (!fs.existsSync(AUTH_FILE)) return null;
4209
+ const encrypted = fs.readFileSync(AUTH_FILE, "utf-8");
4210
+ const key = getMachineKey();
4211
+ const data = decrypt(encrypted, key);
4212
+ return JSON.parse(data);
4213
+ } catch {
4214
+ return null;
4215
+ }
4216
+ }
4217
+ async function clearAuthTokens() {
4218
+ try {
4219
+ await fs.remove(AUTH_FILE);
4220
+ } catch {
4221
+ }
4222
+ }
4223
+ function isTokenExpired(tokens) {
4224
+ return Date.now() / 1e3 > tokens.expiresAt - 60;
4225
+ }
4226
+ async function getValidTokens() {
4227
+ const tokens = loadAuthTokens();
4228
+ if (!tokens) return null;
4229
+ if (!isTokenExpired(tokens)) return tokens;
4230
+ try {
4231
+ const apiUrl = getApiUrl();
4232
+ const response = await fetch(`${apiUrl}/auth/refresh`, {
4233
+ method: "POST",
4234
+ headers: { "Content-Type": "application/json" },
4235
+ body: JSON.stringify({ refresh_token: tokens.refreshToken })
4236
+ });
4237
+ if (!response.ok) {
4238
+ await clearAuthTokens();
4239
+ return null;
4240
+ }
4241
+ const data = await response.json();
4242
+ const body = data.statusCode ? data.body : data;
4243
+ const refreshed = {
4244
+ ...tokens,
4245
+ accessToken: body.AccessToken || tokens.accessToken,
4246
+ idToken: body.IdToken || tokens.idToken,
4247
+ expiresAt: Math.floor(Date.now() / 1e3) + (body.ExpiresIn || 3600)
4248
+ };
4249
+ await saveAuthTokens(refreshed);
4250
+ return refreshed;
4251
+ } catch {
4252
+ await clearAuthTokens();
4253
+ return null;
4254
+ }
4255
+ }
4256
+ async function authenticatedFetch(url, init) {
4257
+ const tokens = await getValidTokens();
4258
+ if (!tokens) {
4259
+ throw new Error("Not logged in. Run: onexthm login");
4260
+ }
4261
+ const headers = new Headers(init?.headers);
4262
+ headers.set("Authorization", `Bearer ${tokens.idToken}`);
4263
+ headers.set("Content-Type", "application/json");
4264
+ return fetch(url, { ...init, headers });
4265
+ }
4266
+ function getMachineKey() {
4267
+ let seed;
4268
+ if (process.platform === "darwin") {
4269
+ seed = `onexthm:${os3.hostname()}:${os3.userInfo().username}`;
4270
+ } else if (process.platform === "linux") {
4271
+ try {
4272
+ seed = `onexthm:${fs.readFileSync("/etc/machine-id", "utf-8").trim()}`;
4273
+ } catch {
4274
+ seed = `onexthm:${os3.hostname()}:${os3.userInfo().username}`;
4275
+ }
4276
+ } else {
4277
+ seed = `onexthm:${os3.hostname()}:${os3.userInfo().username}`;
4278
+ }
4279
+ return crypto2.createHash("sha256").update(seed).digest();
4280
+ }
4281
+ function encrypt(text, key) {
4282
+ const iv = crypto2.randomBytes(16);
4283
+ const cipher = crypto2.createCipheriv("aes-256-gcm", key, iv);
4284
+ let encrypted = cipher.update(text, "utf-8", "hex");
4285
+ encrypted += cipher.final("hex");
4286
+ const tag = cipher.getAuthTag();
4287
+ return `${iv.toString("hex")}:${tag.toString("hex")}:${encrypted}`;
4288
+ }
4289
+ function decrypt(text, key) {
4290
+ const [ivHex, tagHex, encrypted] = text.split(":");
4291
+ const iv = Buffer.from(ivHex, "hex");
4292
+ const tag = Buffer.from(tagHex, "hex");
4293
+ const decipher = crypto2.createDecipheriv("aes-256-gcm", key, iv);
4294
+ decipher.setAuthTag(tag);
4295
+ let decrypted = decipher.update(encrypted, "hex", "utf-8");
4296
+ decrypted += decipher.final("utf-8");
4297
+ return decrypted;
4298
+ }
4299
+ function parseJwtClaims(idToken) {
4300
+ try {
4301
+ const payload = idToken.split(".")[1];
4302
+ const decoded = Buffer.from(payload, "base64url").toString("utf-8");
4303
+ return JSON.parse(decoded);
4304
+ } catch {
4305
+ return {};
4306
+ }
4307
+ }
4308
+
4309
+ // src/commands/login.ts
4310
+ async function loginCommand() {
4311
+ logger.header("OneX Theme Developer Login");
4312
+ const existing = loadAuthTokens();
4313
+ if (existing) {
4314
+ logger.info(`Already logged in as: ${existing.user.email}`);
4315
+ const { relogin } = await inquirer.prompt([
4316
+ {
4317
+ type: "confirm",
4318
+ name: "relogin",
4319
+ message: "Re-login with different account?",
4320
+ default: false
4321
+ }
4322
+ ]);
4323
+ if (!relogin) return;
4324
+ }
4325
+ const { email, password } = await inquirer.prompt([
4326
+ {
4327
+ type: "input",
4328
+ name: "email",
4329
+ message: "Email:",
4330
+ validate: (input) => input.includes("@") ? true : "Enter a valid email"
4331
+ },
4332
+ {
4333
+ type: "password",
4334
+ name: "password",
4335
+ message: "Password:",
4336
+ validate: (input) => input.length >= 6 ? true : "Password too short"
4337
+ }
4338
+ ]);
4339
+ logger.startSpinner("Logging in...");
4340
+ try {
4341
+ const apiUrl = getApiUrl();
4342
+ const response = await fetch(`${apiUrl}/auth/login`, {
4343
+ method: "POST",
4344
+ headers: { "Content-Type": "application/json" },
4345
+ body: JSON.stringify({ username: email, password })
4346
+ });
4347
+ const raw = await response.json();
4348
+ const data = raw.statusCode ? raw.body : raw;
4349
+ if (!response.ok || data.error) {
4350
+ logger.stopSpinner(false, "Login failed");
4351
+ logger.error(data.message || data.error || "Invalid credentials");
4352
+ process.exit(1);
4353
+ }
4354
+ const idToken = data.IdToken;
4355
+ const accessToken = data.AccessToken;
4356
+ const refreshToken = data.RefreshToken;
4357
+ const expiresIn = data.ExpiresIn || 3600;
4358
+ if (!idToken) {
4359
+ logger.stopSpinner(false, "Login failed");
4360
+ logger.error("No token received from server");
4361
+ process.exit(1);
4362
+ }
4363
+ const claims = parseJwtClaims(idToken);
4364
+ const tokens = {
4365
+ accessToken,
4366
+ idToken,
4367
+ refreshToken,
4368
+ expiresAt: Math.floor(Date.now() / 1e3) + expiresIn,
4369
+ user: {
4370
+ email: claims.email || email,
4371
+ name: claims.name,
4372
+ companyId: claims["custom:company_id"],
4373
+ userId: claims.sub
4374
+ }
4375
+ };
4376
+ await saveAuthTokens(tokens);
4377
+ logger.stopSpinner(true, "Logged in!");
4378
+ logger.newLine();
4379
+ logger.info(` Email: ${tokens.user.email}`);
4380
+ if (tokens.user.name) logger.info(` Name: ${tokens.user.name}`);
4381
+ if (tokens.user.companyId)
4382
+ logger.info(` Company: ${tokens.user.companyId}`);
4383
+ logger.newLine();
4384
+ logger.success("Token stored securely in ~/.onexthm/auth.json (encrypted)");
4385
+ } catch (error) {
4386
+ logger.stopSpinner(false, "Login failed");
4387
+ logger.error(error instanceof Error ? error.message : "Connection failed");
4388
+ process.exit(1);
4389
+ }
4390
+ }
4391
+
4392
+ // src/commands/logout.ts
4393
+ init_logger();
4394
+ async function logoutCommand() {
4395
+ const tokens = loadAuthTokens();
4396
+ if (!tokens) {
4397
+ logger.info("Not logged in.");
4398
+ return;
4399
+ }
4400
+ await clearAuthTokens();
4401
+ logger.success(`Logged out (was: ${tokens.user.email})`);
4402
+ }
4403
+
4404
+ // src/commands/whoami.ts
4405
+ init_logger();
4406
+ async function whoamiCommand() {
4407
+ const tokens = loadAuthTokens();
4408
+ if (!tokens) {
4409
+ logger.error("Not logged in. Run: onexthm login");
4410
+ process.exit(1);
4411
+ }
4412
+ const expired = isTokenExpired(tokens);
4413
+ logger.header("OneX Theme Developer");
4414
+ logger.info(` Email: ${tokens.user.email}`);
4415
+ if (tokens.user.name) logger.info(` Name: ${tokens.user.name}`);
4416
+ if (tokens.user.companyId)
4417
+ logger.info(` Company: ${tokens.user.companyId}`);
4418
+ logger.info(
4419
+ ` Status: ${expired ? "\u26A0 Token expired (will auto-refresh)" : "\u2713 Active"}`
4420
+ );
4421
+ }
4422
+
4423
+ // src/commands/publish.ts
4424
+ init_logger();
4425
+ async function publishCommand(options) {
4426
+ logger.header("OneX Theme Publish");
4427
+ const tokens = await getValidTokens();
4428
+ if (!tokens) {
4429
+ logger.error("Not logged in. Run: onexthm login");
4430
+ process.exit(1);
4431
+ }
4432
+ logger.info(`Logged in as: ${tokens.user.email}`);
4433
+ let themePath;
4434
+ if (options.theme) {
4435
+ themePath = path8.resolve(options.theme);
4436
+ } else {
4437
+ const isThemeDir = [
4438
+ "theme.config.ts",
4439
+ "bundle-entry.ts",
4440
+ "manifest.ts"
4441
+ ].some((f) => fs.existsSync(path8.join(process.cwd(), f)));
4442
+ if (isThemeDir) {
4443
+ themePath = process.cwd();
4444
+ } else {
4445
+ logger.error(
4446
+ "Not in a theme directory. Run from theme root or use --theme flag."
4447
+ );
4448
+ process.exit(1);
4449
+ }
4450
+ }
4451
+ const pkgPath = path8.join(themePath, "package.json");
4452
+ if (!fs.existsSync(pkgPath)) {
4453
+ logger.error("No package.json found in theme directory");
4454
+ process.exit(1);
4455
+ }
4456
+ const pkg = fs.readJsonSync(pkgPath);
4457
+ const themeId = pkg.name?.replace("@onex-themes/", "") || path8.basename(themePath);
4458
+ const version2 = pkg.version || "1.0.0";
4459
+ logger.newLine();
4460
+ logger.info(`Theme: ${themeId}`);
4461
+ logger.info(`Version: ${version2}`);
4462
+ logger.newLine();
4463
+ const apiUrl = getApiUrl();
4464
+ logger.startSpinner("Registering theme...");
4465
+ try {
4466
+ const regResponse = await authenticatedFetch(
4467
+ `${apiUrl}/website-api/themes/register`,
4468
+ {
4469
+ method: "POST",
4470
+ body: JSON.stringify({
4471
+ themeId,
4472
+ name: pkg.displayName || themeId,
4473
+ description: pkg.description || "",
4474
+ email: tokens.user.email
4475
+ })
4476
+ }
4477
+ );
4478
+ const regData = await regResponse.json();
4479
+ const regBody = regData.statusCode ? regData.body : regData;
4480
+ if (!regResponse.ok && regBody.error && !regBody.error.includes("already registered")) {
4481
+ logger.stopSpinner(false, "Registration failed");
4482
+ logger.error(regBody.error);
4483
+ process.exit(1);
4484
+ }
4485
+ logger.stopSpinner(true, regBody.message || "Theme registered");
4486
+ } catch (error) {
4487
+ logger.stopSpinner(false, "Registration failed");
4488
+ logger.error(error instanceof Error ? error.message : "Connection failed");
4489
+ process.exit(1);
4490
+ }
4491
+ logger.startSpinner("Building theme...");
4492
+ try {
4493
+ const { execSync: execSync3 } = await import('child_process');
4494
+ execSync3(
4495
+ "npx tsup bundle-entry.ts --format esm --target es2020 --outDir dist",
4496
+ {
4497
+ cwd: themePath,
4498
+ stdio: "ignore"
4499
+ }
4500
+ );
4501
+ logger.stopSpinner(true, "Theme compiled");
4502
+ } catch {
4503
+ logger.stopSpinner(false, "Build failed");
4504
+ logger.error("Run 'onexthm build' to see build errors");
4505
+ process.exit(1);
4506
+ }
4507
+ logger.startSpinner("Getting upload URL...");
4508
+ let bundleUploadUrl;
4509
+ let sourceUploadUrl;
4510
+ try {
4511
+ const pubResponse = await authenticatedFetch(
4512
+ `${apiUrl}/website-api/themes/${encodeURIComponent(themeId)}/versions`,
4513
+ {
4514
+ method: "POST",
4515
+ body: JSON.stringify({ version: version2 })
4516
+ }
4517
+ );
4518
+ const pubData = await pubResponse.json();
4519
+ const pubBody = pubData.statusCode ? pubData.body : pubData;
4520
+ if (!pubResponse.ok || !pubBody.bundleUploadUrl) {
4521
+ logger.stopSpinner(false, "Failed to get upload URL");
4522
+ logger.error(pubBody.error || "Server error");
4523
+ process.exit(1);
4524
+ }
4525
+ bundleUploadUrl = pubBody.bundleUploadUrl;
4526
+ sourceUploadUrl = pubBody.sourceUploadUrl;
4527
+ logger.stopSpinner(true, "Upload URL obtained");
4528
+ } catch (error) {
4529
+ logger.stopSpinner(false, "Failed");
4530
+ logger.error(error instanceof Error ? error.message : "Connection failed");
4531
+ process.exit(1);
4532
+ }
4533
+ logger.startSpinner("Uploading bundle...");
4534
+ try {
4535
+ const archiver3 = await import('archiver');
4536
+ const { createWriteStream } = await import('fs');
4537
+ const distDir = path8.join(themePath, "dist");
4538
+ if (!fs.existsSync(distDir)) {
4539
+ logger.stopSpinner(false, "No dist/ directory");
4540
+ logger.error("Build the theme first: onexthm build");
4541
+ process.exit(1);
4542
+ }
4543
+ const bundleZipPath = path8.join(themePath, "dist", "bundle.zip");
4544
+ await createZip(distDir, bundleZipPath, ["bundle.zip"]);
4545
+ const bundleBuffer = fs.readFileSync(bundleZipPath);
4546
+ const bundleRes = await fetch(bundleUploadUrl, {
4547
+ method: "PUT",
4548
+ headers: { "Content-Type": "application/zip" },
4549
+ body: bundleBuffer
4550
+ });
4551
+ if (!bundleRes.ok) {
4552
+ throw new Error(`Upload failed: ${bundleRes.status}`);
4553
+ }
4554
+ const sizeMB = (bundleBuffer.length / 1024 / 1024).toFixed(2);
4555
+ logger.stopSpinner(true, `Bundle uploaded (${sizeMB} MB)`);
4556
+ } catch (error) {
4557
+ logger.stopSpinner(false, "Upload failed");
4558
+ logger.error(error instanceof Error ? error.message : "Upload error");
4559
+ process.exit(1);
4560
+ }
4561
+ logger.startSpinner("Uploading source...");
4562
+ try {
4563
+ const sourceZipPath = path8.join(themePath, "dist", "source.zip");
4564
+ await createZip(themePath, sourceZipPath, [
4565
+ "node_modules",
4566
+ "dist",
4567
+ ".git",
4568
+ ".env",
4569
+ ".env.local"
4570
+ ]);
4571
+ const sourceBuffer = fs.readFileSync(sourceZipPath);
4572
+ const sourceRes = await fetch(sourceUploadUrl, {
4573
+ method: "PUT",
4574
+ headers: { "Content-Type": "application/zip" },
4575
+ body: sourceBuffer
4576
+ });
4577
+ if (!sourceRes.ok) {
4578
+ throw new Error(`Source upload failed: ${sourceRes.status}`);
4579
+ }
4580
+ const sizeMB = (sourceBuffer.length / 1024 / 1024).toFixed(2);
4581
+ logger.stopSpinner(true, `Source uploaded (${sizeMB} MB)`);
4582
+ } catch (error) {
4583
+ logger.stopSpinner(false, "Source upload failed");
4584
+ logger.info("Source upload skipped (bundle was uploaded successfully)");
4585
+ }
4586
+ logger.startSpinner("Scanning and publishing...");
4587
+ try {
4588
+ const confirmResponse = await authenticatedFetch(
4589
+ `${apiUrl}/website-api/themes/${encodeURIComponent(themeId)}/versions/${encodeURIComponent(version2)}/confirm`,
4590
+ { method: "POST" }
4591
+ );
4592
+ const confirmData = await confirmResponse.json();
4593
+ const confirmBody = confirmData.statusCode ? confirmData.body : confirmData;
4594
+ if (!confirmResponse.ok || !confirmBody.success) {
4595
+ logger.stopSpinner(false, "Publishing failed");
4596
+ if (confirmBody.violations) {
4597
+ logger.error("Theme rejected \u2014 security violations found:");
4598
+ for (const v of confirmBody.violations) {
4599
+ logger.log(` \u274C ${v.file}: ${v.violation}`);
4600
+ }
4601
+ } else {
4602
+ logger.error(confirmBody.error || "Unknown error");
4603
+ }
4604
+ if (confirmBody.warnings?.length) {
4605
+ logger.newLine();
4606
+ logger.info("Warnings:");
4607
+ for (const w of confirmBody.warnings) {
4608
+ logger.log(` \u26A0\uFE0F ${w.file}: ${w.warning}`);
4609
+ }
4610
+ }
4611
+ process.exit(1);
4612
+ }
4613
+ logger.stopSpinner(true, confirmBody.message || "Published!");
4614
+ if (confirmBody.warnings?.length) {
4615
+ logger.newLine();
4616
+ logger.info("Warnings (non-blocking):");
4617
+ for (const w of confirmBody.warnings) {
4618
+ logger.log(` \u26A0\uFE0F ${w.file}: ${w.warning}`);
4619
+ }
4620
+ }
4621
+ } catch (error) {
4622
+ logger.stopSpinner(false, "Publishing failed");
4623
+ logger.error(error instanceof Error ? error.message : "Connection failed");
4624
+ process.exit(1);
4625
+ }
4626
+ logger.newLine();
4627
+ logger.success(`\u2713 Theme "${themeId}" v${version2} published!`);
4628
+ }
4629
+ async function createZip(sourceDir, outputPath, exclude) {
4630
+ const archiver3 = (await import('archiver')).default;
4631
+ const { createWriteStream } = await import('fs');
4632
+ return new Promise((resolve, reject) => {
4633
+ const output = createWriteStream(outputPath);
4634
+ const archive = archiver3("zip", { zlib: { level: 9 } });
4635
+ output.on("close", resolve);
4636
+ archive.on("error", reject);
4637
+ archive.pipe(output);
4638
+ archive.glob("**/*", {
4639
+ cwd: sourceDir,
4640
+ ignore: exclude.map((e) => `${e}/**`),
4641
+ dot: false
4642
+ });
4643
+ archive.finalize();
4644
+ });
4645
+ }
4646
+
4229
4647
  // src/cli.ts
4230
4648
  try {
4231
4649
  const projectRoot = getProjectRoot();
@@ -4237,13 +4655,13 @@ try {
4237
4655
  } catch {
4238
4656
  }
4239
4657
  dotenv.config({
4240
- path: path8.join(os.homedir(), ".onex", ".env"),
4658
+ path: path8.join(os3.homedir(), ".onexthm", ".env"),
4241
4659
  quiet: true
4242
4660
  });
4243
4661
  var require2 = createRequire(import.meta.url);
4244
4662
  var { version } = require2("../package.json");
4245
4663
  var program = new Command();
4246
- program.name("onex").description("CLI tool for OneX theme development").version(version);
4664
+ program.name("onexthm").description("CLI tool for OneX theme development").version(version);
4247
4665
  program.command("init").description("Create a new OneX theme project").argument("[project-name]", "Name of the project").option(
4248
4666
  "-t, --template <template>",
4249
4667
  "Template to use (default, minimal)",
@@ -4294,6 +4712,10 @@ program.command("clone").description("Clone theme source code from S3").argument
4294
4712
  "staging"
4295
4713
  ).option("--no-install", "Skip running pnpm install after clone").action(cloneCommand);
4296
4714
  program.command("config").description("Configure OneX CLI credentials (AWS, API keys)").action(configCommand);
4715
+ program.command("login").description("Login to OneX platform").action(loginCommand);
4716
+ program.command("logout").description("Logout from OneX platform").action(logoutCommand);
4717
+ program.command("whoami").description("Show current logged-in developer").action(whoamiCommand);
4718
+ program.command("publish").description("Build, scan, and publish theme to marketplace (requires login)").option("-t, --theme <path>", "Theme directory path").action(publishCommand);
4297
4719
  program.configureOutput({
4298
4720
  writeErr: (str) => process.stderr.write(chalk4.red(str))
4299
4721
  });