@onexapis/cli 1.0.0 → 1.0.2

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/dist/cli.mjs CHANGED
@@ -1,18 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import chalk4 from 'chalk';
4
2
  import path from 'path';
5
- import fs2 from 'fs';
6
- import { execSync, spawn } from 'child_process';
7
- import inquirer from 'inquirer';
8
- import ora from 'ora';
3
+ import dotenv from 'dotenv';
9
4
  import fs from 'fs-extra';
10
5
  import ejs from 'ejs';
6
+ import { execSync, spawn } from 'child_process';
7
+ import chalk4 from 'chalk';
8
+ import ora from 'ora';
9
+ import { Command } from 'commander';
10
+ import fs2 from 'fs';
11
+ import inquirer from 'inquirer';
11
12
  import archiver from 'archiver';
12
13
  import FormData from 'form-data';
13
14
  import fetch from 'node-fetch';
14
- import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
15
- import { glob } from 'glob';
15
+ import { PutObjectCommand, GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
16
+ import os from 'os';
17
+ import AdmZip from 'adm-zip';
16
18
 
17
19
  var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
18
20
  var __forAwait = (obj, it, method) => (it = obj[__knownSymbol("asyncIterator")]) ? it.call(obj) : (obj = obj[__knownSymbol("iterator")](), it = {}, method = (key, fn) => (fn = obj[key]) && (it[key] = (arg) => new Promise((yes, no, done) => (arg = fn.call(obj, arg), done = arg.done, Promise.resolve(arg.value).then((value) => yes({ value, done }), no)))), method("next"), method("return"), it);
@@ -68,34 +70,7 @@ var Logger = class {
68
70
  };
69
71
  var logger = new Logger();
70
72
 
71
- // src/utils/validators.ts
72
- function validateName(name) {
73
- return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(name);
74
- }
75
- function toKebabCase(str) {
76
- return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
77
- }
78
- function toPascalCase(str) {
79
- return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
80
- }
81
- function validateThemeName(name) {
82
- return /^[a-z][a-z0-9-]*$/.test(name);
83
- }
84
- function getValidCategories() {
85
- return [
86
- "headers",
87
- "heroes",
88
- "content",
89
- "features",
90
- "testimonials",
91
- "galleries",
92
- "cta",
93
- "footers",
94
- "ecommerce",
95
- "blog",
96
- "contact"
97
- ];
98
- }
73
+ // src/utils/file-helpers.ts
99
74
  async function renderTemplate(templatePath, data) {
100
75
  const template = await fs.readFile(templatePath, "utf-8");
101
76
  return ejs.render(template, data);
@@ -180,18 +155,19 @@ function getProjectRoot() {
180
155
  return process.cwd();
181
156
  }
182
157
  function getThemesDir() {
183
- try {
184
- return path.join(getProjectRoot(), "src/themes");
185
- } catch (e) {
186
- return path.join(getProjectRoot(), "themes");
158
+ const root = getProjectRoot();
159
+ const themesDir = path.join(root, "themes");
160
+ if (fs.existsSync(themesDir)) {
161
+ return themesDir;
187
162
  }
163
+ return path.join(root, "src/themes");
188
164
  }
189
165
  function getFeaturesDir() {
190
166
  return path.join(getProjectRoot(), "src/features");
191
167
  }
192
168
  function isOneXProject() {
193
169
  const root = getProjectRoot();
194
- return fs.existsSync(path.join(root, "src/themes")) && fs.existsSync(path.join(root, "src/lib/registry"));
170
+ return fs.existsSync(path.join(root, "themes")) || fs.existsSync(path.join(root, "src/themes"));
195
171
  }
196
172
  function ensureOneXProject() {
197
173
  if (!isOneXProject()) {
@@ -208,12 +184,12 @@ function listThemes() {
208
184
  }
209
185
  return fs.readdirSync(themesDir).filter((name) => {
210
186
  const themePath = path.join(themesDir, name);
211
- return fs.statSync(themePath).isDirectory() && fs.existsSync(path.join(themePath, "manifest.ts"));
187
+ return fs.statSync(themePath).isDirectory() && (fs.existsSync(path.join(themePath, "theme.config.ts")) || fs.existsSync(path.join(themePath, "bundle-entry.ts")) || fs.existsSync(path.join(themePath, "manifest.ts")));
212
188
  });
213
189
  }
214
190
  function themeExists(themeName) {
215
191
  const themePath = path.join(getThemesDir(), themeName);
216
- return fs.existsSync(themePath) && fs.existsSync(path.join(themePath, "manifest.ts"));
192
+ return fs.existsSync(themePath) && (fs.existsSync(path.join(themePath, "theme.config.ts")) || fs.existsSync(path.join(themePath, "bundle-entry.ts")) || fs.existsSync(path.join(themePath, "manifest.ts")));
217
193
  }
218
194
  function detectPackageManager() {
219
195
  const userAgent = process.env.npm_config_user_agent || "";
@@ -241,6 +217,35 @@ async function installDependencies(projectPath, packageManager = "npm") {
241
217
  });
242
218
  }
243
219
 
220
+ // src/utils/validators.ts
221
+ function validateName(name) {
222
+ return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(name);
223
+ }
224
+ function toKebabCase(str) {
225
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
226
+ }
227
+ function toPascalCase(str) {
228
+ return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
229
+ }
230
+ function validateThemeName(name) {
231
+ return /^[a-z][a-z0-9-]*$/.test(name);
232
+ }
233
+ function getValidCategories() {
234
+ return [
235
+ "headers",
236
+ "heroes",
237
+ "content",
238
+ "features",
239
+ "testimonials",
240
+ "galleries",
241
+ "cta",
242
+ "footers",
243
+ "ecommerce",
244
+ "blog",
245
+ "contact"
246
+ ];
247
+ }
248
+
244
249
  // src/commands/init.ts
245
250
  async function initCommand(projectName, options = {}) {
246
251
  logger.header("Create New OneX Theme Project");
@@ -407,7 +412,9 @@ Add your theme-specific blocks here.
407
412
  logger.newLine();
408
413
  logger.section("Theme structure:");
409
414
  logger.log(" src/manifest.ts - Theme manifest and exports");
410
- logger.log(" src/config.ts - Design tokens (colors, typography, etc.)");
415
+ logger.log(
416
+ " src/config.ts - Design tokens (colors, typography, etc.)"
417
+ );
411
418
  logger.log(" src/layout.ts - Header and footer configuration");
412
419
  logger.log(" src/sections/ - Custom sections for your theme");
413
420
  logger.log(" src/blocks/ - Reusable blocks");
@@ -1410,8 +1417,16 @@ async function listThemesInfo() {
1410
1417
  }
1411
1418
  logger.log("");
1412
1419
  for (const theme of themes) {
1413
- const manifestPath = path.join(getThemesDir(), theme, "manifest.ts");
1414
- const manifestContent = fs.readFileSync(manifestPath, "utf-8");
1420
+ const themeDir = path.join(getThemesDir(), theme);
1421
+ const candidates = ["theme.config.ts", "bundle-entry.ts", "manifest.ts"];
1422
+ let manifestContent = "";
1423
+ for (const candidate of candidates) {
1424
+ const candidatePath = path.join(themeDir, candidate);
1425
+ if (fs.existsSync(candidatePath)) {
1426
+ manifestContent = fs.readFileSync(candidatePath, "utf-8");
1427
+ break;
1428
+ }
1429
+ }
1415
1430
  const nameMatch = manifestContent.match(/name:\s*["'](.+)["']/);
1416
1431
  const versionMatch = manifestContent.match(/version:\s*["'](.+)["']/);
1417
1432
  const descMatch = manifestContent.match(/description:\s*["'](.+)["']/);
@@ -1953,13 +1968,11 @@ function getS3Client() {
1953
1968
  return new S3Client({
1954
1969
  endpoint: endpointUrl,
1955
1970
  region: "us-east-1",
1956
- // MinIO doesn't use real regions
1957
1971
  credentials: {
1958
1972
  accessKeyId: process.env.MINIO_ACCESS_KEY || "minioadmin",
1959
1973
  secretAccessKey: process.env.MINIO_SECRET_KEY || "minioadmin"
1960
1974
  },
1961
1975
  forcePathStyle: true
1962
- // Required for MinIO
1963
1976
  });
1964
1977
  }
1965
1978
  if (adapterMode === "local") {
@@ -2029,36 +2042,40 @@ async function readManifest() {
2029
2042
  "No manifest.ts or package.json found. Are you in a theme directory?"
2030
2043
  );
2031
2044
  }
2032
- function getContentType(filename) {
2033
- const ext = path.extname(filename).toLowerCase();
2034
- const types = {
2035
- ".js": "application/javascript",
2036
- ".mjs": "application/javascript",
2037
- ".json": "application/json",
2038
- ".css": "text/css",
2039
- ".png": "image/png",
2040
- ".jpg": "image/jpeg",
2041
- ".jpeg": "image/jpeg",
2042
- ".svg": "image/svg+xml",
2043
- ".woff2": "font/woff2",
2044
- ".woff": "font/woff",
2045
- ".ttf": "font/ttf",
2046
- ".ts": "text/plain"
2047
- };
2048
- return types[ext] || "application/octet-stream";
2045
+ async function createZipFromDir(sourceDir, outputPath, excludePatterns = []) {
2046
+ return new Promise((resolve, reject) => {
2047
+ const output = fs.createWriteStream(outputPath);
2048
+ const archive = archiver("zip", { zlib: { level: 6 } });
2049
+ output.on("close", () => resolve());
2050
+ archive.on("error", (err) => reject(err));
2051
+ archive.pipe(output);
2052
+ archive.glob("**/*", {
2053
+ cwd: sourceDir,
2054
+ dot: true,
2055
+ ignore: excludePatterns
2056
+ });
2057
+ archive.finalize();
2058
+ });
2049
2059
  }
2050
- async function uploadFile(s3Client, bucket, themeId, version, baseDir, file) {
2051
- const filePath = path.join(baseDir, file);
2052
- const content = await fs.readFile(filePath);
2053
- const s3Key = `themes/${themeId}/${version}/${file}`;
2054
- await s3Client.send(
2055
- new PutObjectCommand({
2056
- Bucket: bucket,
2057
- Key: s3Key,
2058
- Body: content,
2059
- ContentType: getContentType(file)
2060
- })
2061
- );
2060
+ async function findSourceDir(themeId, explicitDir) {
2061
+ if (explicitDir) {
2062
+ if (await fs.pathExists(explicitDir)) return explicitDir;
2063
+ return null;
2064
+ }
2065
+ const searchPaths = [
2066
+ process.cwd(),
2067
+ path.resolve(process.cwd(), `../../themes/${themeId}`),
2068
+ path.resolve(process.cwd(), `../themes/${themeId}`)
2069
+ ];
2070
+ const markers = ["theme.config.ts", "bundle-entry.ts"];
2071
+ for (const dir of searchPaths) {
2072
+ for (const marker of markers) {
2073
+ if (await fs.pathExists(path.join(dir, marker))) {
2074
+ return dir;
2075
+ }
2076
+ }
2077
+ }
2078
+ return null;
2062
2079
  }
2063
2080
  async function updateLatestPointer(s3Client, bucket, themeId, version) {
2064
2081
  const latestData = {
@@ -2076,12 +2093,18 @@ async function updateLatestPointer(s3Client, bucket, themeId, version) {
2076
2093
  }
2077
2094
  async function uploadCommand(options) {
2078
2095
  logger.header("Upload Theme to S3");
2079
- ensureOneXProject();
2080
2096
  const spinner = ora("Preparing theme upload...").start();
2081
2097
  try {
2082
- const manifest = await readManifest();
2083
- const themeId = manifest.themeId;
2084
- const version = options.version || manifest.version || "1.0.0";
2098
+ let themeId;
2099
+ let version;
2100
+ if (options.theme) {
2101
+ themeId = options.theme;
2102
+ version = options.version || "1.0.0";
2103
+ } else {
2104
+ const manifest = await readManifest();
2105
+ themeId = manifest.themeId;
2106
+ version = options.version || manifest.version || "1.0.0";
2107
+ }
2085
2108
  spinner.text = `Found theme: ${themeId}@${version}`;
2086
2109
  const bucket = options.bucket || getBucketName(options.environment);
2087
2110
  const s3Client = getS3Client();
@@ -2102,67 +2125,113 @@ async function uploadCommand(options) {
2102
2125
  process.exit(1);
2103
2126
  }
2104
2127
  spinner.succeed(`Found compiled theme at: ${compiledDir}`);
2105
- spinner.start(`Scanning files...`);
2106
- const files = await glob("**/*", {
2107
- cwd: compiledDir,
2108
- nodir: true,
2109
- dot: true
2110
- });
2111
- if (files.length === 0) {
2112
- spinner.fail(chalk4.red("No files found in compiled theme directory"));
2113
- process.exit(1);
2114
- }
2115
- spinner.succeed(`Found ${files.length} files to upload`);
2128
+ spinner.start("Creating bundle.zip...");
2129
+ const tmpDir = os.tmpdir();
2130
+ const bundleZipPath = path.join(tmpDir, `${themeId}-${version}-bundle.zip`);
2131
+ await createZipFromDir(compiledDir, bundleZipPath);
2132
+ const bundleZipBuffer = await fs.readFile(bundleZipPath);
2133
+ const bundleSizeMB = (bundleZipBuffer.length / 1024 / 1024).toFixed(2);
2134
+ spinner.succeed(`Created bundle.zip (${bundleSizeMB} MB)`);
2116
2135
  if (options.dryRun) {
2117
- spinner.info(chalk4.yellow("Dry run mode - no files will be uploaded"));
2118
- console.log(chalk4.gray("\nFiles to upload:"));
2119
- const displayFiles = files.slice(0, 10);
2120
- displayFiles.forEach((file) => console.log(chalk4.gray(` - ${file}`)));
2121
- if (files.length > 10) {
2122
- console.log(chalk4.gray(` ... and ${files.length - 10} more files
2123
- `));
2124
- }
2125
- console.log(chalk4.cyan(`
2126
- S3 bucket: ${bucket}`));
2136
+ spinner.info(chalk4.yellow("Dry run mode \u2014 no files will be uploaded"));
2137
+ console.log();
2138
+ console.log(chalk4.gray(` bundle.zip: ${bundleSizeMB} MB`));
2127
2139
  console.log(
2128
- chalk4.cyan(`S3 path: s3://${bucket}/themes/${themeId}/${version}/
2129
- `)
2140
+ chalk4.cyan(
2141
+ ` S3 path: s3://${bucket}/themes/${themeId}/${version}/bundle.zip`
2142
+ )
2130
2143
  );
2144
+ if (!options.skipSource) {
2145
+ const sourceDir = await findSourceDir(themeId, options.sourceDir);
2146
+ if (sourceDir) {
2147
+ console.log(chalk4.gray(` source dir: ${sourceDir}`));
2148
+ console.log(
2149
+ chalk4.cyan(
2150
+ ` S3 path: s3://${bucket}/themes/${themeId}/${version}/source.zip`
2151
+ )
2152
+ );
2153
+ } else {
2154
+ console.log(
2155
+ chalk4.yellow(" source dir: not found (source.zip will be skipped)")
2156
+ );
2157
+ }
2158
+ }
2159
+ console.log();
2160
+ await fs.remove(bundleZipPath);
2131
2161
  return;
2132
2162
  }
2133
- spinner.start(`Uploading to S3: ${bucket}...`);
2134
- let uploaded = 0;
2135
- const total = files.length;
2136
- const batchSize = 10;
2137
- for (let i = 0; i < files.length; i += batchSize) {
2138
- const batch = files.slice(i, i + batchSize);
2139
- await Promise.all(
2140
- batch.map(async (file) => {
2141
- await uploadFile(
2142
- s3Client,
2143
- bucket,
2144
- themeId,
2145
- version,
2146
- compiledDir,
2147
- file
2148
- );
2149
- uploaded++;
2150
- spinner.text = `Uploading... ${uploaded}/${total} files (${Math.round(uploaded / total * 100)}%)`;
2151
- })
2152
- );
2163
+ spinner.start("Uploading bundle.zip to S3...");
2164
+ const bundleS3Key = `themes/${themeId}/${version}/bundle.zip`;
2165
+ await s3Client.send(
2166
+ new PutObjectCommand({
2167
+ Bucket: bucket,
2168
+ Key: bundleS3Key,
2169
+ Body: bundleZipBuffer,
2170
+ ContentType: "application/zip"
2171
+ })
2172
+ );
2173
+ spinner.succeed(
2174
+ `Uploaded bundle.zip ${chalk4.gray(`\u2192 s3://${bucket}/${bundleS3Key}`)}`
2175
+ );
2176
+ await fs.remove(bundleZipPath);
2177
+ let sourceUploaded = false;
2178
+ if (!options.skipSource) {
2179
+ spinner.start("Looking for source directory...");
2180
+ const sourceDir = await findSourceDir(themeId, options.sourceDir);
2181
+ if (sourceDir) {
2182
+ spinner.succeed(`Found source at: ${sourceDir}`);
2183
+ spinner.start("Creating source.zip...");
2184
+ const sourceZipPath = path.join(
2185
+ tmpDir,
2186
+ `${themeId}-${version}-source.zip`
2187
+ );
2188
+ await createZipFromDir(sourceDir, sourceZipPath, [
2189
+ "node_modules/**",
2190
+ "dist/**",
2191
+ ".git/**",
2192
+ "*.zip",
2193
+ ".next/**",
2194
+ ".turbo/**"
2195
+ ]);
2196
+ const sourceZipBuffer = await fs.readFile(sourceZipPath);
2197
+ const sourceSizeMB = (sourceZipBuffer.length / 1024 / 1024).toFixed(2);
2198
+ spinner.succeed(`Created source.zip (${sourceSizeMB} MB)`);
2199
+ spinner.start("Uploading source.zip to S3...");
2200
+ const sourceS3Key = `themes/${themeId}/${version}/source.zip`;
2201
+ await s3Client.send(
2202
+ new PutObjectCommand({
2203
+ Bucket: bucket,
2204
+ Key: sourceS3Key,
2205
+ Body: sourceZipBuffer,
2206
+ ContentType: "application/zip"
2207
+ })
2208
+ );
2209
+ spinner.succeed(
2210
+ `Uploaded source.zip ${chalk4.gray(`\u2192 s3://${bucket}/${sourceS3Key}`)}`
2211
+ );
2212
+ await fs.remove(sourceZipPath);
2213
+ sourceUploaded = true;
2214
+ } else {
2215
+ spinner.warn(
2216
+ chalk4.yellow("Source directory not found \u2014 skipping source.zip")
2217
+ );
2218
+ }
2153
2219
  }
2154
- spinner.succeed(`Uploaded ${total} files to S3`);
2155
2220
  spinner.start("Updating latest.json pointer...");
2156
2221
  await updateLatestPointer(s3Client, bucket, themeId, version);
2157
2222
  spinner.succeed("Updated latest.json pointer");
2158
2223
  console.log();
2159
- logger.success(chalk4.green.bold("\u2705 Theme uploaded successfully!"));
2224
+ logger.success(chalk4.green.bold("Theme uploaded successfully!"));
2160
2225
  console.log();
2161
2226
  console.log(
2162
2227
  chalk4.cyan(" Theme: ") + chalk4.white(`${themeId}@${version}`)
2163
2228
  );
2164
2229
  console.log(chalk4.cyan(" Bucket: ") + chalk4.white(bucket));
2165
- console.log(chalk4.cyan(" Files: ") + chalk4.white(total));
2230
+ console.log(
2231
+ chalk4.cyan(" Files: ") + chalk4.white(
2232
+ `bundle.zip${sourceUploaded ? " + source.zip" : ""}`
2233
+ )
2234
+ );
2166
2235
  console.log(
2167
2236
  chalk4.cyan(" Path: ") + chalk4.gray(`s3://${bucket}/themes/${themeId}/${version}/`)
2168
2237
  );
@@ -2266,79 +2335,45 @@ async function resolveLatestVersion(s3Client, bucket, themeId) {
2266
2335
  );
2267
2336
  }
2268
2337
  }
2269
- async function downloadManifest(s3Client, bucket, themeId, version) {
2270
- try {
2271
- const response = await s3Client.send(
2272
- new GetObjectCommand({
2273
- Bucket: bucket,
2274
- Key: `themes/${themeId}/${version}/manifest.json`
2275
- })
2276
- );
2277
- const body = await streamToString(response.Body);
2278
- return JSON.parse(body);
2279
- } catch (error) {
2280
- throw new Error(
2281
- `Failed to download manifest for ${themeId}@${version}: ${error.message}
2282
- The theme may not exist in S3 bucket "${bucket}".`
2283
- );
2284
- }
2285
- }
2286
- async function downloadFile(s3Client, bucket, themeId, version, file, outputDir) {
2287
- const s3Key = `themes/${themeId}/${version}/${file}`;
2288
- const localPath = path.join(outputDir, file);
2289
- await fs.ensureDir(path.dirname(localPath));
2290
- try {
2291
- const response = await s3Client.send(
2292
- new GetObjectCommand({
2293
- Bucket: bucket,
2294
- Key: s3Key
2295
- })
2296
- );
2297
- const content = await streamToBuffer(response.Body);
2298
- await fs.writeFile(localPath, content);
2299
- } catch (error) {
2300
- throw new Error(`Failed to download ${file}: ${error.message}`);
2301
- }
2302
- }
2303
- function collectFilesToDownload(manifest) {
2304
- const files = ["manifest.json"];
2305
- if (manifest.output) {
2306
- if (manifest.output.entry) {
2307
- files.push(manifest.output.entry);
2308
- }
2309
- if (manifest.output.chunks) {
2310
- files.push(...manifest.output.chunks);
2311
- }
2312
- if (manifest.output.assets) {
2313
- files.push(...manifest.output.assets);
2338
+ async function createCompatibilityFiles(outputDir, manifest) {
2339
+ var _a;
2340
+ const entryFile = ((_a = manifest.output) == null ? void 0 : _a.entry) || "bundle-entry.js";
2341
+ if (entryFile !== "bundle-entry.js" && entryFile.startsWith("bundle-entry-")) {
2342
+ const hashedPath = path.join(outputDir, entryFile);
2343
+ const stablePath = path.join(outputDir, "bundle-entry.js");
2344
+ if (await fs.pathExists(hashedPath)) {
2345
+ await fs.copy(hashedPath, stablePath);
2346
+ const mapPath = hashedPath + ".map";
2347
+ if (await fs.pathExists(mapPath)) {
2348
+ await fs.copy(mapPath, stablePath + ".map");
2349
+ }
2314
2350
  }
2315
2351
  }
2316
- return files;
2317
- }
2318
- async function createCompatibilityFiles(outputDir) {
2319
2352
  const sectionsRegistryPath = path.join(outputDir, "sections-registry.js");
2320
2353
  const content = `// Re-export all sections from bundle-entry
2321
2354
  // This file exists to maintain compatibility with the import path
2322
2355
  export * from './bundle-entry.js';
2323
2356
  `;
2324
2357
  await fs.writeFile(sectionsRegistryPath, content, "utf-8");
2358
+ const pkgJsonPath = path.join(outputDir, "package.json");
2359
+ await fs.writeFile(pkgJsonPath, '{\n "type": "module"\n}\n', "utf-8");
2325
2360
  }
2326
2361
  function showDownloadFailureHelp(themeId, bucket) {
2327
2362
  console.log();
2328
- logger.error(chalk4.red.bold("\u274C Theme download failed"));
2363
+ logger.error(chalk4.red.bold("Theme download failed"));
2329
2364
  console.log();
2330
2365
  console.log(chalk4.yellow("Possible reasons:"));
2331
2366
  console.log(chalk4.gray(" 1. Theme not uploaded to S3 yet"));
2332
2367
  console.log(chalk4.gray(" 2. AWS credentials not configured correctly"));
2333
2368
  console.log(chalk4.gray(" 3. Bucket name or region is incorrect"));
2334
- console.log(chalk4.gray(" 4. Theme files are incomplete or corrupted"));
2369
+ console.log(chalk4.gray(" 4. bundle.zip is missing or corrupted"));
2335
2370
  console.log();
2336
2371
  console.log(chalk4.cyan.bold("To fix this:"));
2337
2372
  console.log();
2338
2373
  console.log(chalk4.white("1. Compile and upload the theme:"));
2339
2374
  console.log(chalk4.gray(` cd themes/${themeId}`));
2340
2375
  console.log(chalk4.gray(" pnpm build"));
2341
- console.log(chalk4.gray(" pnpm upload"));
2376
+ console.log(chalk4.gray(" onex upload"));
2342
2377
  console.log();
2343
2378
  console.log(chalk4.white("2. Verify AWS credentials are set:"));
2344
2379
  console.log(
@@ -2354,7 +2389,11 @@ function showDownloadFailureHelp(themeId, bucket) {
2354
2389
  );
2355
2390
  console.log();
2356
2391
  console.log(chalk4.white("4. Verify theme exists in S3:"));
2357
- console.log(chalk4.gray(` aws s3 ls s3://${bucket}/themes/${themeId}/`));
2392
+ console.log(
2393
+ chalk4.gray(
2394
+ ` aws s3 ls s3://${bucket}/themes/${themeId}/`
2395
+ )
2396
+ );
2358
2397
  console.log();
2359
2398
  }
2360
2399
  async function downloadCommand(options) {
@@ -2382,55 +2421,44 @@ async function downloadCommand(options) {
2382
2421
  spinner.succeed(
2383
2422
  `Resolved latest version: ${chalk4.cyan(resolvedVersion)}`
2384
2423
  );
2385
- spinner.start("Downloading manifest...");
2386
2424
  }
2387
- const manifest = await downloadManifest(
2388
- s3Client,
2389
- bucket,
2390
- themeId,
2391
- resolvedVersion
2425
+ spinner.start(
2426
+ `Downloading bundle.zip for ${themeId}@${resolvedVersion}...`
2427
+ );
2428
+ const s3Key = `themes/${themeId}/${resolvedVersion}/bundle.zip`;
2429
+ const response = await s3Client.send(
2430
+ new GetObjectCommand({
2431
+ Bucket: bucket,
2432
+ Key: s3Key
2433
+ })
2392
2434
  );
2393
- spinner.succeed("Downloaded manifest");
2394
- spinner.start("Preparing output directory...");
2435
+ const zipBuffer = await streamToBuffer(response.Body);
2436
+ const sizeMB = (zipBuffer.length / 1024 / 1024).toFixed(2);
2437
+ spinner.succeed(`Downloaded bundle.zip (${sizeMB} MB)`);
2438
+ spinner.start("Extracting bundle...");
2395
2439
  await fs.remove(outputDir);
2396
2440
  await fs.ensureDir(outputDir);
2397
- spinner.succeed("Output directory ready");
2398
- spinner.start("Downloading theme files...");
2399
- const files = collectFilesToDownload(manifest);
2400
- let downloaded = 0;
2401
- const total = files.length;
2402
- const batchSize = 10;
2403
- for (let i = 0; i < files.length; i += batchSize) {
2404
- const batch = files.slice(i, i + batchSize);
2405
- await Promise.all(
2406
- batch.map(async (file) => {
2407
- await downloadFile(
2408
- s3Client,
2409
- bucket,
2410
- themeId,
2411
- resolvedVersion,
2412
- file,
2413
- outputDir
2414
- );
2415
- downloaded++;
2416
- spinner.text = `Downloading... ${downloaded}/${total} files (${Math.round(downloaded / total * 100)}%)`;
2417
- })
2418
- );
2419
- }
2420
- spinner.succeed(`Downloaded ${total} files`);
2421
- await createCompatibilityFiles(outputDir);
2441
+ const zip = new AdmZip(zipBuffer);
2442
+ zip.extractAllTo(outputDir, true);
2443
+ const entries = zip.getEntries().filter((e) => !e.isDirectory);
2444
+ spinner.succeed(`Extracted ${entries.length} files to ${outputDir}`);
2445
+ const manifestPath = path.join(outputDir, "manifest.json");
2446
+ const manifest = await fs.readJson(manifestPath);
2447
+ await createCompatibilityFiles(outputDir, manifest);
2422
2448
  console.log();
2423
- logger.success(chalk4.green.bold("\u2705 Theme downloaded successfully!"));
2449
+ logger.success(chalk4.green.bold("Theme downloaded successfully!"));
2424
2450
  console.log();
2425
2451
  console.log(
2426
2452
  chalk4.cyan(" Theme: ") + chalk4.white(`${themeId}@${resolvedVersion}`)
2427
2453
  );
2428
2454
  console.log(chalk4.cyan(" Bucket: ") + chalk4.white(bucket));
2429
2455
  console.log(chalk4.cyan(" Output: ") + chalk4.white(outputDir));
2430
- console.log(chalk4.cyan(" Files: ") + chalk4.white(total));
2431
- console.log(
2432
- chalk4.cyan(" Sections:") + chalk4.white(` ${manifest.counts.sections}`)
2433
- );
2456
+ console.log(chalk4.cyan(" Files: ") + chalk4.white(entries.length));
2457
+ if (manifest.counts) {
2458
+ console.log(
2459
+ chalk4.cyan(" Sections:") + chalk4.white(` ${manifest.counts.sections}`)
2460
+ );
2461
+ }
2434
2462
  console.log();
2435
2463
  } catch (error) {
2436
2464
  spinner.fail(chalk4.red("Download failed"));
@@ -2441,8 +2469,215 @@ async function downloadCommand(options) {
2441
2469
  process.exit(1);
2442
2470
  }
2443
2471
  }
2472
+ function getS3Client3() {
2473
+ const adapterMode = (process.env.ADAPTER_MODE || "aws").trim().toLowerCase();
2474
+ if (adapterMode === "vps") {
2475
+ const endpoint = process.env.MINIO_ENDPOINT || "localhost:9000";
2476
+ const secure = process.env.MINIO_SECURE === "true";
2477
+ const endpointUrl = endpoint.startsWith("http") ? endpoint : `${secure ? "https" : "http"}://${endpoint}`;
2478
+ return new S3Client({
2479
+ endpoint: endpointUrl,
2480
+ region: "us-east-1",
2481
+ credentials: {
2482
+ accessKeyId: process.env.MINIO_ACCESS_KEY || "minioadmin",
2483
+ secretAccessKey: process.env.MINIO_SECRET_KEY || "minioadmin"
2484
+ },
2485
+ forcePathStyle: true
2486
+ });
2487
+ }
2488
+ if (adapterMode === "local") {
2489
+ return new S3Client({
2490
+ endpoint: "http://localhost:4569",
2491
+ region: "ap-southeast-1",
2492
+ credentials: {
2493
+ accessKeyId: "S3RVER",
2494
+ secretAccessKey: "S3RVER"
2495
+ },
2496
+ forcePathStyle: true
2497
+ });
2498
+ }
2499
+ return new S3Client({
2500
+ region: process.env.AWS_REGION || "ap-southeast-1"
2501
+ });
2502
+ }
2503
+ function getBucketName3(env) {
2504
+ if (process.env.BUCKET_NAME) {
2505
+ return process.env.BUCKET_NAME;
2506
+ }
2507
+ const environment = env || process.env.ENVIRONMENT || "staging";
2508
+ return environment === "production" ? "onex-themes-prod" : "onex-themes-staging";
2509
+ }
2510
+ async function streamToString2(stream) {
2511
+ const chunks = [];
2512
+ try {
2513
+ for (var iter = __forAwait(stream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
2514
+ const chunk = temp.value;
2515
+ chunks.push(Buffer.from(chunk));
2516
+ }
2517
+ } catch (temp) {
2518
+ error = [temp];
2519
+ } finally {
2520
+ try {
2521
+ more && (temp = iter.return) && await temp.call(iter);
2522
+ } finally {
2523
+ if (error)
2524
+ throw error[0];
2525
+ }
2526
+ }
2527
+ return Buffer.concat(chunks).toString("utf-8");
2528
+ }
2529
+ async function streamToBuffer2(stream) {
2530
+ const chunks = [];
2531
+ try {
2532
+ for (var iter = __forAwait(stream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
2533
+ const chunk = temp.value;
2534
+ chunks.push(Buffer.from(chunk));
2535
+ }
2536
+ } catch (temp) {
2537
+ error = [temp];
2538
+ } finally {
2539
+ try {
2540
+ more && (temp = iter.return) && await temp.call(iter);
2541
+ } finally {
2542
+ if (error)
2543
+ throw error[0];
2544
+ }
2545
+ }
2546
+ return Buffer.concat(chunks);
2547
+ }
2548
+ async function resolveLatestVersion2(s3Client, bucket, themeId) {
2549
+ try {
2550
+ const response = await s3Client.send(
2551
+ new GetObjectCommand({
2552
+ Bucket: bucket,
2553
+ Key: `themes/${themeId}/latest.json`
2554
+ })
2555
+ );
2556
+ const body = await streamToString2(response.Body);
2557
+ return JSON.parse(body).version;
2558
+ } catch (error) {
2559
+ throw new Error(
2560
+ `Failed to resolve latest version for theme "${themeId}": ${error.message}`
2561
+ );
2562
+ }
2563
+ }
2564
+ function runInstall(cwd) {
2565
+ return new Promise((resolve) => {
2566
+ const proc = spawn("pnpm", ["install"], {
2567
+ cwd,
2568
+ stdio: "inherit",
2569
+ shell: true
2570
+ });
2571
+ proc.on("close", (code) => resolve(code === 0));
2572
+ proc.on("error", () => resolve(false));
2573
+ });
2574
+ }
2575
+ async function cloneCommand(themeName, options) {
2576
+ logger.header("Clone Theme Source");
2577
+ const spinner = ora("Initializing clone...").start();
2578
+ try {
2579
+ const bucket = options.bucket || getBucketName3(options.environment);
2580
+ const outputDir = options.output || path.resolve(process.cwd(), themeName);
2581
+ const s3Client = getS3Client3();
2582
+ if (await fs.pathExists(outputDir)) {
2583
+ spinner.fail(
2584
+ chalk4.red(`Directory already exists: ${outputDir}`)
2585
+ );
2586
+ logger.info(
2587
+ chalk4.gray(
2588
+ "Use -o to specify a different output directory, or remove the existing directory."
2589
+ )
2590
+ );
2591
+ process.exit(1);
2592
+ }
2593
+ let version = options.version || "latest";
2594
+ if (version === "latest") {
2595
+ spinner.text = "Resolving latest version...";
2596
+ version = await resolveLatestVersion2(s3Client, bucket, themeName);
2597
+ spinner.succeed(`Resolved latest version: ${chalk4.cyan(version)}`);
2598
+ }
2599
+ spinner.start(
2600
+ `Downloading source.zip for ${themeName}@${version}...`
2601
+ );
2602
+ const s3Key = `themes/${themeName}/${version}/source.zip`;
2603
+ let zipBuffer;
2604
+ try {
2605
+ const response = await s3Client.send(
2606
+ new GetObjectCommand({
2607
+ Bucket: bucket,
2608
+ Key: s3Key
2609
+ })
2610
+ );
2611
+ zipBuffer = await streamToBuffer2(response.Body);
2612
+ } catch (error) {
2613
+ spinner.fail(chalk4.red(`Source not found: s3://${bucket}/${s3Key}`));
2614
+ console.log();
2615
+ console.log(
2616
+ chalk4.yellow("The theme source may not have been uploaded yet.")
2617
+ );
2618
+ console.log(
2619
+ chalk4.gray(
2620
+ `Upload source with: onex upload --theme ${themeName}`
2621
+ )
2622
+ );
2623
+ console.log();
2624
+ process.exit(1);
2625
+ }
2626
+ const sizeMB = (zipBuffer.length / 1024 / 1024).toFixed(2);
2627
+ spinner.succeed(`Downloaded source.zip (${sizeMB} MB)`);
2628
+ spinner.start(`Extracting to ${outputDir}...`);
2629
+ await fs.ensureDir(outputDir);
2630
+ const zip = new AdmZip(zipBuffer);
2631
+ zip.extractAllTo(outputDir, true);
2632
+ const entries = zip.getEntries().filter((e) => !e.isDirectory);
2633
+ spinner.succeed(`Extracted ${entries.length} files`);
2634
+ if (options.install !== false) {
2635
+ const hasPkgJson = await fs.pathExists(
2636
+ path.join(outputDir, "package.json")
2637
+ );
2638
+ if (hasPkgJson) {
2639
+ spinner.start("Installing dependencies...");
2640
+ const success = await runInstall(outputDir);
2641
+ if (success) {
2642
+ spinner.succeed("Dependencies installed");
2643
+ } else {
2644
+ spinner.warn(
2645
+ chalk4.yellow(
2646
+ "Failed to install dependencies \u2014 run 'pnpm install' manually"
2647
+ )
2648
+ );
2649
+ }
2650
+ }
2651
+ }
2652
+ console.log();
2653
+ logger.success(chalk4.green.bold("Theme cloned successfully!"));
2654
+ console.log();
2655
+ console.log(
2656
+ chalk4.cyan(" Theme: ") + chalk4.white(`${themeName}@${version}`)
2657
+ );
2658
+ console.log(chalk4.cyan(" Location: ") + chalk4.white(outputDir));
2659
+ console.log(chalk4.cyan(" Files: ") + chalk4.white(entries.length));
2660
+ console.log();
2661
+ console.log(chalk4.cyan("Next steps:"));
2662
+ console.log(
2663
+ chalk4.gray(` cd ${path.relative(process.cwd(), outputDir)}`)
2664
+ );
2665
+ if (options.install === false) {
2666
+ console.log(chalk4.gray(" pnpm install"));
2667
+ }
2668
+ console.log(chalk4.gray(" onex build"));
2669
+ console.log();
2670
+ } catch (error) {
2671
+ spinner.fail(chalk4.red(`Clone failed: ${error.message}`));
2672
+ logger.error(error.stack || error.message);
2673
+ process.exit(1);
2674
+ }
2675
+ }
2444
2676
 
2445
2677
  // src/cli.ts
2678
+ var projectRoot = getProjectRoot();
2679
+ dotenv.config({ path: path.join(projectRoot, ".env.local"), quiet: true });
2680
+ dotenv.config({ path: path.join(projectRoot, ".env"), quiet: true });
2446
2681
  var program = new Command();
2447
2682
  program.name("onex").description("CLI tool for OneX theme development").version("0.1.0");
2448
2683
  program.command("init").description("Create a new OneX theme project").argument("[project-name]", "Name of the project").option(
@@ -2474,7 +2709,7 @@ program.command("upload").description("Upload compiled theme to S3 bucket").opti
2474
2709
  "-e, --environment <env>",
2475
2710
  "Environment (staging|production)",
2476
2711
  "staging"
2477
- ).option("--dry-run", "Show what would be uploaded without uploading").action(uploadCommand);
2712
+ ).option("--dry-run", "Show what would be uploaded without uploading").option("--skip-source", "Skip uploading source.zip").option("--source-dir <dir>", "Source directory path").action(uploadCommand);
2478
2713
  program.command("download").description("Download theme from S3 bucket").option("-t, --theme-id <id>", "Theme ID to download").option(
2479
2714
  "-v, --version <version>",
2480
2715
  "Theme version (default: latest)",
@@ -2484,6 +2719,15 @@ program.command("download").description("Download theme from S3 bucket").option(
2484
2719
  "Environment (staging|production)",
2485
2720
  "staging"
2486
2721
  ).option("-o, --output <dir>", "Output directory", "./active-theme").action(downloadCommand);
2722
+ program.command("clone").description("Clone theme source code from S3").argument("<theme-name>", "Theme to clone").option(
2723
+ "-v, --version <version>",
2724
+ "Theme version (default: latest)",
2725
+ "latest"
2726
+ ).option("-o, --output <dir>", "Output directory").option("-b, --bucket <name>", "S3 bucket name").option(
2727
+ "-e, --environment <env>",
2728
+ "Environment (staging|production)",
2729
+ "staging"
2730
+ ).option("--no-install", "Skip running pnpm install after clone").action(cloneCommand);
2487
2731
  program.configureOutput({
2488
2732
  writeErr: (str) => process.stderr.write(chalk4.red(str))
2489
2733
  });