@onexapis/cli 1.0.0 → 1.0.1

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
@@ -11,8 +11,9 @@ import ejs from 'ejs';
11
11
  import archiver from 'archiver';
12
12
  import FormData from 'form-data';
13
13
  import fetch from 'node-fetch';
14
- import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
15
- import { glob } from 'glob';
14
+ import { PutObjectCommand, GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
15
+ import os from 'os';
16
+ import AdmZip from 'adm-zip';
16
17
 
17
18
  var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
18
19
  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);
@@ -180,18 +181,19 @@ function getProjectRoot() {
180
181
  return process.cwd();
181
182
  }
182
183
  function getThemesDir() {
183
- try {
184
- return path.join(getProjectRoot(), "src/themes");
185
- } catch (e) {
186
- return path.join(getProjectRoot(), "themes");
184
+ const root = getProjectRoot();
185
+ const themesDir = path.join(root, "themes");
186
+ if (fs.existsSync(themesDir)) {
187
+ return themesDir;
187
188
  }
189
+ return path.join(root, "src/themes");
188
190
  }
189
191
  function getFeaturesDir() {
190
192
  return path.join(getProjectRoot(), "src/features");
191
193
  }
192
194
  function isOneXProject() {
193
195
  const root = getProjectRoot();
194
- return fs.existsSync(path.join(root, "src/themes")) && fs.existsSync(path.join(root, "src/lib/registry"));
196
+ return fs.existsSync(path.join(root, "themes")) || fs.existsSync(path.join(root, "src/themes"));
195
197
  }
196
198
  function ensureOneXProject() {
197
199
  if (!isOneXProject()) {
@@ -208,12 +210,12 @@ function listThemes() {
208
210
  }
209
211
  return fs.readdirSync(themesDir).filter((name) => {
210
212
  const themePath = path.join(themesDir, name);
211
- return fs.statSync(themePath).isDirectory() && fs.existsSync(path.join(themePath, "manifest.ts"));
213
+ 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
214
  });
213
215
  }
214
216
  function themeExists(themeName) {
215
217
  const themePath = path.join(getThemesDir(), themeName);
216
- return fs.existsSync(themePath) && fs.existsSync(path.join(themePath, "manifest.ts"));
218
+ 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
219
  }
218
220
  function detectPackageManager() {
219
221
  const userAgent = process.env.npm_config_user_agent || "";
@@ -407,7 +409,9 @@ Add your theme-specific blocks here.
407
409
  logger.newLine();
408
410
  logger.section("Theme structure:");
409
411
  logger.log(" src/manifest.ts - Theme manifest and exports");
410
- logger.log(" src/config.ts - Design tokens (colors, typography, etc.)");
412
+ logger.log(
413
+ " src/config.ts - Design tokens (colors, typography, etc.)"
414
+ );
411
415
  logger.log(" src/layout.ts - Header and footer configuration");
412
416
  logger.log(" src/sections/ - Custom sections for your theme");
413
417
  logger.log(" src/blocks/ - Reusable blocks");
@@ -1410,8 +1414,16 @@ async function listThemesInfo() {
1410
1414
  }
1411
1415
  logger.log("");
1412
1416
  for (const theme of themes) {
1413
- const manifestPath = path.join(getThemesDir(), theme, "manifest.ts");
1414
- const manifestContent = fs.readFileSync(manifestPath, "utf-8");
1417
+ const themeDir = path.join(getThemesDir(), theme);
1418
+ const candidates = ["theme.config.ts", "bundle-entry.ts", "manifest.ts"];
1419
+ let manifestContent = "";
1420
+ for (const candidate of candidates) {
1421
+ const candidatePath = path.join(themeDir, candidate);
1422
+ if (fs.existsSync(candidatePath)) {
1423
+ manifestContent = fs.readFileSync(candidatePath, "utf-8");
1424
+ break;
1425
+ }
1426
+ }
1415
1427
  const nameMatch = manifestContent.match(/name:\s*["'](.+)["']/);
1416
1428
  const versionMatch = manifestContent.match(/version:\s*["'](.+)["']/);
1417
1429
  const descMatch = manifestContent.match(/description:\s*["'](.+)["']/);
@@ -1953,13 +1965,11 @@ function getS3Client() {
1953
1965
  return new S3Client({
1954
1966
  endpoint: endpointUrl,
1955
1967
  region: "us-east-1",
1956
- // MinIO doesn't use real regions
1957
1968
  credentials: {
1958
1969
  accessKeyId: process.env.MINIO_ACCESS_KEY || "minioadmin",
1959
1970
  secretAccessKey: process.env.MINIO_SECRET_KEY || "minioadmin"
1960
1971
  },
1961
1972
  forcePathStyle: true
1962
- // Required for MinIO
1963
1973
  });
1964
1974
  }
1965
1975
  if (adapterMode === "local") {
@@ -2029,36 +2039,40 @@ async function readManifest() {
2029
2039
  "No manifest.ts or package.json found. Are you in a theme directory?"
2030
2040
  );
2031
2041
  }
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";
2042
+ async function createZipFromDir(sourceDir, outputPath, excludePatterns = []) {
2043
+ return new Promise((resolve, reject) => {
2044
+ const output = fs.createWriteStream(outputPath);
2045
+ const archive = archiver("zip", { zlib: { level: 6 } });
2046
+ output.on("close", () => resolve());
2047
+ archive.on("error", (err) => reject(err));
2048
+ archive.pipe(output);
2049
+ archive.glob("**/*", {
2050
+ cwd: sourceDir,
2051
+ dot: true,
2052
+ ignore: excludePatterns
2053
+ });
2054
+ archive.finalize();
2055
+ });
2049
2056
  }
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
- );
2057
+ async function findSourceDir(themeId, explicitDir) {
2058
+ if (explicitDir) {
2059
+ if (await fs.pathExists(explicitDir)) return explicitDir;
2060
+ return null;
2061
+ }
2062
+ const searchPaths = [
2063
+ process.cwd(),
2064
+ path.resolve(process.cwd(), `../../themes/${themeId}`),
2065
+ path.resolve(process.cwd(), `../themes/${themeId}`)
2066
+ ];
2067
+ const markers = ["theme.config.ts", "bundle-entry.ts"];
2068
+ for (const dir of searchPaths) {
2069
+ for (const marker of markers) {
2070
+ if (await fs.pathExists(path.join(dir, marker))) {
2071
+ return dir;
2072
+ }
2073
+ }
2074
+ }
2075
+ return null;
2062
2076
  }
2063
2077
  async function updateLatestPointer(s3Client, bucket, themeId, version) {
2064
2078
  const latestData = {
@@ -2076,12 +2090,18 @@ async function updateLatestPointer(s3Client, bucket, themeId, version) {
2076
2090
  }
2077
2091
  async function uploadCommand(options) {
2078
2092
  logger.header("Upload Theme to S3");
2079
- ensureOneXProject();
2080
2093
  const spinner = ora("Preparing theme upload...").start();
2081
2094
  try {
2082
- const manifest = await readManifest();
2083
- const themeId = manifest.themeId;
2084
- const version = options.version || manifest.version || "1.0.0";
2095
+ let themeId;
2096
+ let version;
2097
+ if (options.theme) {
2098
+ themeId = options.theme;
2099
+ version = options.version || "1.0.0";
2100
+ } else {
2101
+ const manifest = await readManifest();
2102
+ themeId = manifest.themeId;
2103
+ version = options.version || manifest.version || "1.0.0";
2104
+ }
2085
2105
  spinner.text = `Found theme: ${themeId}@${version}`;
2086
2106
  const bucket = options.bucket || getBucketName(options.environment);
2087
2107
  const s3Client = getS3Client();
@@ -2102,67 +2122,113 @@ async function uploadCommand(options) {
2102
2122
  process.exit(1);
2103
2123
  }
2104
2124
  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`);
2125
+ spinner.start("Creating bundle.zip...");
2126
+ const tmpDir = os.tmpdir();
2127
+ const bundleZipPath = path.join(tmpDir, `${themeId}-${version}-bundle.zip`);
2128
+ await createZipFromDir(compiledDir, bundleZipPath);
2129
+ const bundleZipBuffer = await fs.readFile(bundleZipPath);
2130
+ const bundleSizeMB = (bundleZipBuffer.length / 1024 / 1024).toFixed(2);
2131
+ spinner.succeed(`Created bundle.zip (${bundleSizeMB} MB)`);
2116
2132
  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}`));
2133
+ spinner.info(chalk4.yellow("Dry run mode \u2014 no files will be uploaded"));
2134
+ console.log();
2135
+ console.log(chalk4.gray(` bundle.zip: ${bundleSizeMB} MB`));
2127
2136
  console.log(
2128
- chalk4.cyan(`S3 path: s3://${bucket}/themes/${themeId}/${version}/
2129
- `)
2137
+ chalk4.cyan(
2138
+ ` S3 path: s3://${bucket}/themes/${themeId}/${version}/bundle.zip`
2139
+ )
2130
2140
  );
2141
+ if (!options.skipSource) {
2142
+ const sourceDir = await findSourceDir(themeId, options.sourceDir);
2143
+ if (sourceDir) {
2144
+ console.log(chalk4.gray(` source dir: ${sourceDir}`));
2145
+ console.log(
2146
+ chalk4.cyan(
2147
+ ` S3 path: s3://${bucket}/themes/${themeId}/${version}/source.zip`
2148
+ )
2149
+ );
2150
+ } else {
2151
+ console.log(
2152
+ chalk4.yellow(" source dir: not found (source.zip will be skipped)")
2153
+ );
2154
+ }
2155
+ }
2156
+ console.log();
2157
+ await fs.remove(bundleZipPath);
2131
2158
  return;
2132
2159
  }
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
- );
2160
+ spinner.start("Uploading bundle.zip to S3...");
2161
+ const bundleS3Key = `themes/${themeId}/${version}/bundle.zip`;
2162
+ await s3Client.send(
2163
+ new PutObjectCommand({
2164
+ Bucket: bucket,
2165
+ Key: bundleS3Key,
2166
+ Body: bundleZipBuffer,
2167
+ ContentType: "application/zip"
2168
+ })
2169
+ );
2170
+ spinner.succeed(
2171
+ `Uploaded bundle.zip ${chalk4.gray(`\u2192 s3://${bucket}/${bundleS3Key}`)}`
2172
+ );
2173
+ await fs.remove(bundleZipPath);
2174
+ let sourceUploaded = false;
2175
+ if (!options.skipSource) {
2176
+ spinner.start("Looking for source directory...");
2177
+ const sourceDir = await findSourceDir(themeId, options.sourceDir);
2178
+ if (sourceDir) {
2179
+ spinner.succeed(`Found source at: ${sourceDir}`);
2180
+ spinner.start("Creating source.zip...");
2181
+ const sourceZipPath = path.join(
2182
+ tmpDir,
2183
+ `${themeId}-${version}-source.zip`
2184
+ );
2185
+ await createZipFromDir(sourceDir, sourceZipPath, [
2186
+ "node_modules/**",
2187
+ "dist/**",
2188
+ ".git/**",
2189
+ "*.zip",
2190
+ ".next/**",
2191
+ ".turbo/**"
2192
+ ]);
2193
+ const sourceZipBuffer = await fs.readFile(sourceZipPath);
2194
+ const sourceSizeMB = (sourceZipBuffer.length / 1024 / 1024).toFixed(2);
2195
+ spinner.succeed(`Created source.zip (${sourceSizeMB} MB)`);
2196
+ spinner.start("Uploading source.zip to S3...");
2197
+ const sourceS3Key = `themes/${themeId}/${version}/source.zip`;
2198
+ await s3Client.send(
2199
+ new PutObjectCommand({
2200
+ Bucket: bucket,
2201
+ Key: sourceS3Key,
2202
+ Body: sourceZipBuffer,
2203
+ ContentType: "application/zip"
2204
+ })
2205
+ );
2206
+ spinner.succeed(
2207
+ `Uploaded source.zip ${chalk4.gray(`\u2192 s3://${bucket}/${sourceS3Key}`)}`
2208
+ );
2209
+ await fs.remove(sourceZipPath);
2210
+ sourceUploaded = true;
2211
+ } else {
2212
+ spinner.warn(
2213
+ chalk4.yellow("Source directory not found \u2014 skipping source.zip")
2214
+ );
2215
+ }
2153
2216
  }
2154
- spinner.succeed(`Uploaded ${total} files to S3`);
2155
2217
  spinner.start("Updating latest.json pointer...");
2156
2218
  await updateLatestPointer(s3Client, bucket, themeId, version);
2157
2219
  spinner.succeed("Updated latest.json pointer");
2158
2220
  console.log();
2159
- logger.success(chalk4.green.bold("\u2705 Theme uploaded successfully!"));
2221
+ logger.success(chalk4.green.bold("Theme uploaded successfully!"));
2160
2222
  console.log();
2161
2223
  console.log(
2162
2224
  chalk4.cyan(" Theme: ") + chalk4.white(`${themeId}@${version}`)
2163
2225
  );
2164
2226
  console.log(chalk4.cyan(" Bucket: ") + chalk4.white(bucket));
2165
- console.log(chalk4.cyan(" Files: ") + chalk4.white(total));
2227
+ console.log(
2228
+ chalk4.cyan(" Files: ") + chalk4.white(
2229
+ `bundle.zip${sourceUploaded ? " + source.zip" : ""}`
2230
+ )
2231
+ );
2166
2232
  console.log(
2167
2233
  chalk4.cyan(" Path: ") + chalk4.gray(`s3://${bucket}/themes/${themeId}/${version}/`)
2168
2234
  );
@@ -2266,79 +2332,45 @@ async function resolveLatestVersion(s3Client, bucket, themeId) {
2266
2332
  );
2267
2333
  }
2268
2334
  }
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);
2335
+ async function createCompatibilityFiles(outputDir, manifest) {
2336
+ var _a;
2337
+ const entryFile = ((_a = manifest.output) == null ? void 0 : _a.entry) || "bundle-entry.js";
2338
+ if (entryFile !== "bundle-entry.js" && entryFile.startsWith("bundle-entry-")) {
2339
+ const hashedPath = path.join(outputDir, entryFile);
2340
+ const stablePath = path.join(outputDir, "bundle-entry.js");
2341
+ if (await fs.pathExists(hashedPath)) {
2342
+ await fs.copy(hashedPath, stablePath);
2343
+ const mapPath = hashedPath + ".map";
2344
+ if (await fs.pathExists(mapPath)) {
2345
+ await fs.copy(mapPath, stablePath + ".map");
2346
+ }
2314
2347
  }
2315
2348
  }
2316
- return files;
2317
- }
2318
- async function createCompatibilityFiles(outputDir) {
2319
2349
  const sectionsRegistryPath = path.join(outputDir, "sections-registry.js");
2320
2350
  const content = `// Re-export all sections from bundle-entry
2321
2351
  // This file exists to maintain compatibility with the import path
2322
2352
  export * from './bundle-entry.js';
2323
2353
  `;
2324
2354
  await fs.writeFile(sectionsRegistryPath, content, "utf-8");
2355
+ const pkgJsonPath = path.join(outputDir, "package.json");
2356
+ await fs.writeFile(pkgJsonPath, '{\n "type": "module"\n}\n', "utf-8");
2325
2357
  }
2326
2358
  function showDownloadFailureHelp(themeId, bucket) {
2327
2359
  console.log();
2328
- logger.error(chalk4.red.bold("\u274C Theme download failed"));
2360
+ logger.error(chalk4.red.bold("Theme download failed"));
2329
2361
  console.log();
2330
2362
  console.log(chalk4.yellow("Possible reasons:"));
2331
2363
  console.log(chalk4.gray(" 1. Theme not uploaded to S3 yet"));
2332
2364
  console.log(chalk4.gray(" 2. AWS credentials not configured correctly"));
2333
2365
  console.log(chalk4.gray(" 3. Bucket name or region is incorrect"));
2334
- console.log(chalk4.gray(" 4. Theme files are incomplete or corrupted"));
2366
+ console.log(chalk4.gray(" 4. bundle.zip is missing or corrupted"));
2335
2367
  console.log();
2336
2368
  console.log(chalk4.cyan.bold("To fix this:"));
2337
2369
  console.log();
2338
2370
  console.log(chalk4.white("1. Compile and upload the theme:"));
2339
2371
  console.log(chalk4.gray(` cd themes/${themeId}`));
2340
2372
  console.log(chalk4.gray(" pnpm build"));
2341
- console.log(chalk4.gray(" pnpm upload"));
2373
+ console.log(chalk4.gray(" onex upload"));
2342
2374
  console.log();
2343
2375
  console.log(chalk4.white("2. Verify AWS credentials are set:"));
2344
2376
  console.log(
@@ -2354,7 +2386,11 @@ function showDownloadFailureHelp(themeId, bucket) {
2354
2386
  );
2355
2387
  console.log();
2356
2388
  console.log(chalk4.white("4. Verify theme exists in S3:"));
2357
- console.log(chalk4.gray(` aws s3 ls s3://${bucket}/themes/${themeId}/`));
2389
+ console.log(
2390
+ chalk4.gray(
2391
+ ` aws s3 ls s3://${bucket}/themes/${themeId}/`
2392
+ )
2393
+ );
2358
2394
  console.log();
2359
2395
  }
2360
2396
  async function downloadCommand(options) {
@@ -2382,55 +2418,44 @@ async function downloadCommand(options) {
2382
2418
  spinner.succeed(
2383
2419
  `Resolved latest version: ${chalk4.cyan(resolvedVersion)}`
2384
2420
  );
2385
- spinner.start("Downloading manifest...");
2386
2421
  }
2387
- const manifest = await downloadManifest(
2388
- s3Client,
2389
- bucket,
2390
- themeId,
2391
- resolvedVersion
2422
+ spinner.start(
2423
+ `Downloading bundle.zip for ${themeId}@${resolvedVersion}...`
2424
+ );
2425
+ const s3Key = `themes/${themeId}/${resolvedVersion}/bundle.zip`;
2426
+ const response = await s3Client.send(
2427
+ new GetObjectCommand({
2428
+ Bucket: bucket,
2429
+ Key: s3Key
2430
+ })
2392
2431
  );
2393
- spinner.succeed("Downloaded manifest");
2394
- spinner.start("Preparing output directory...");
2432
+ const zipBuffer = await streamToBuffer(response.Body);
2433
+ const sizeMB = (zipBuffer.length / 1024 / 1024).toFixed(2);
2434
+ spinner.succeed(`Downloaded bundle.zip (${sizeMB} MB)`);
2435
+ spinner.start("Extracting bundle...");
2395
2436
  await fs.remove(outputDir);
2396
2437
  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);
2438
+ const zip = new AdmZip(zipBuffer);
2439
+ zip.extractAllTo(outputDir, true);
2440
+ const entries = zip.getEntries().filter((e) => !e.isDirectory);
2441
+ spinner.succeed(`Extracted ${entries.length} files to ${outputDir}`);
2442
+ const manifestPath = path.join(outputDir, "manifest.json");
2443
+ const manifest = await fs.readJson(manifestPath);
2444
+ await createCompatibilityFiles(outputDir, manifest);
2422
2445
  console.log();
2423
- logger.success(chalk4.green.bold("\u2705 Theme downloaded successfully!"));
2446
+ logger.success(chalk4.green.bold("Theme downloaded successfully!"));
2424
2447
  console.log();
2425
2448
  console.log(
2426
2449
  chalk4.cyan(" Theme: ") + chalk4.white(`${themeId}@${resolvedVersion}`)
2427
2450
  );
2428
2451
  console.log(chalk4.cyan(" Bucket: ") + chalk4.white(bucket));
2429
2452
  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
- );
2453
+ console.log(chalk4.cyan(" Files: ") + chalk4.white(entries.length));
2454
+ if (manifest.counts) {
2455
+ console.log(
2456
+ chalk4.cyan(" Sections:") + chalk4.white(` ${manifest.counts.sections}`)
2457
+ );
2458
+ }
2434
2459
  console.log();
2435
2460
  } catch (error) {
2436
2461
  spinner.fail(chalk4.red("Download failed"));
@@ -2441,6 +2466,210 @@ async function downloadCommand(options) {
2441
2466
  process.exit(1);
2442
2467
  }
2443
2468
  }
2469
+ function getS3Client3() {
2470
+ const adapterMode = (process.env.ADAPTER_MODE || "aws").trim().toLowerCase();
2471
+ if (adapterMode === "vps") {
2472
+ const endpoint = process.env.MINIO_ENDPOINT || "localhost:9000";
2473
+ const secure = process.env.MINIO_SECURE === "true";
2474
+ const endpointUrl = endpoint.startsWith("http") ? endpoint : `${secure ? "https" : "http"}://${endpoint}`;
2475
+ return new S3Client({
2476
+ endpoint: endpointUrl,
2477
+ region: "us-east-1",
2478
+ credentials: {
2479
+ accessKeyId: process.env.MINIO_ACCESS_KEY || "minioadmin",
2480
+ secretAccessKey: process.env.MINIO_SECRET_KEY || "minioadmin"
2481
+ },
2482
+ forcePathStyle: true
2483
+ });
2484
+ }
2485
+ if (adapterMode === "local") {
2486
+ return new S3Client({
2487
+ endpoint: "http://localhost:4569",
2488
+ region: "ap-southeast-1",
2489
+ credentials: {
2490
+ accessKeyId: "S3RVER",
2491
+ secretAccessKey: "S3RVER"
2492
+ },
2493
+ forcePathStyle: true
2494
+ });
2495
+ }
2496
+ return new S3Client({
2497
+ region: process.env.AWS_REGION || "ap-southeast-1"
2498
+ });
2499
+ }
2500
+ function getBucketName3(env) {
2501
+ if (process.env.BUCKET_NAME) {
2502
+ return process.env.BUCKET_NAME;
2503
+ }
2504
+ const environment = env || process.env.ENVIRONMENT || "staging";
2505
+ return environment === "production" ? "onex-themes-prod" : "onex-themes-staging";
2506
+ }
2507
+ async function streamToString2(stream) {
2508
+ const chunks = [];
2509
+ try {
2510
+ for (var iter = __forAwait(stream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
2511
+ const chunk = temp.value;
2512
+ chunks.push(Buffer.from(chunk));
2513
+ }
2514
+ } catch (temp) {
2515
+ error = [temp];
2516
+ } finally {
2517
+ try {
2518
+ more && (temp = iter.return) && await temp.call(iter);
2519
+ } finally {
2520
+ if (error)
2521
+ throw error[0];
2522
+ }
2523
+ }
2524
+ return Buffer.concat(chunks).toString("utf-8");
2525
+ }
2526
+ async function streamToBuffer2(stream) {
2527
+ const chunks = [];
2528
+ try {
2529
+ for (var iter = __forAwait(stream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
2530
+ const chunk = temp.value;
2531
+ chunks.push(Buffer.from(chunk));
2532
+ }
2533
+ } catch (temp) {
2534
+ error = [temp];
2535
+ } finally {
2536
+ try {
2537
+ more && (temp = iter.return) && await temp.call(iter);
2538
+ } finally {
2539
+ if (error)
2540
+ throw error[0];
2541
+ }
2542
+ }
2543
+ return Buffer.concat(chunks);
2544
+ }
2545
+ async function resolveLatestVersion2(s3Client, bucket, themeId) {
2546
+ try {
2547
+ const response = await s3Client.send(
2548
+ new GetObjectCommand({
2549
+ Bucket: bucket,
2550
+ Key: `themes/${themeId}/latest.json`
2551
+ })
2552
+ );
2553
+ const body = await streamToString2(response.Body);
2554
+ return JSON.parse(body).version;
2555
+ } catch (error) {
2556
+ throw new Error(
2557
+ `Failed to resolve latest version for theme "${themeId}": ${error.message}`
2558
+ );
2559
+ }
2560
+ }
2561
+ function runInstall(cwd) {
2562
+ return new Promise((resolve) => {
2563
+ const proc = spawn("pnpm", ["install"], {
2564
+ cwd,
2565
+ stdio: "inherit",
2566
+ shell: true
2567
+ });
2568
+ proc.on("close", (code) => resolve(code === 0));
2569
+ proc.on("error", () => resolve(false));
2570
+ });
2571
+ }
2572
+ async function cloneCommand(themeName, options) {
2573
+ logger.header("Clone Theme Source");
2574
+ const spinner = ora("Initializing clone...").start();
2575
+ try {
2576
+ const bucket = options.bucket || getBucketName3(options.environment);
2577
+ const outputDir = options.output || path.resolve(process.cwd(), themeName);
2578
+ const s3Client = getS3Client3();
2579
+ if (await fs.pathExists(outputDir)) {
2580
+ spinner.fail(
2581
+ chalk4.red(`Directory already exists: ${outputDir}`)
2582
+ );
2583
+ logger.info(
2584
+ chalk4.gray(
2585
+ "Use -o to specify a different output directory, or remove the existing directory."
2586
+ )
2587
+ );
2588
+ process.exit(1);
2589
+ }
2590
+ let version = options.version || "latest";
2591
+ if (version === "latest") {
2592
+ spinner.text = "Resolving latest version...";
2593
+ version = await resolveLatestVersion2(s3Client, bucket, themeName);
2594
+ spinner.succeed(`Resolved latest version: ${chalk4.cyan(version)}`);
2595
+ }
2596
+ spinner.start(
2597
+ `Downloading source.zip for ${themeName}@${version}...`
2598
+ );
2599
+ const s3Key = `themes/${themeName}/${version}/source.zip`;
2600
+ let zipBuffer;
2601
+ try {
2602
+ const response = await s3Client.send(
2603
+ new GetObjectCommand({
2604
+ Bucket: bucket,
2605
+ Key: s3Key
2606
+ })
2607
+ );
2608
+ zipBuffer = await streamToBuffer2(response.Body);
2609
+ } catch (error) {
2610
+ spinner.fail(chalk4.red(`Source not found: s3://${bucket}/${s3Key}`));
2611
+ console.log();
2612
+ console.log(
2613
+ chalk4.yellow("The theme source may not have been uploaded yet.")
2614
+ );
2615
+ console.log(
2616
+ chalk4.gray(
2617
+ `Upload source with: onex upload --theme ${themeName}`
2618
+ )
2619
+ );
2620
+ console.log();
2621
+ process.exit(1);
2622
+ }
2623
+ const sizeMB = (zipBuffer.length / 1024 / 1024).toFixed(2);
2624
+ spinner.succeed(`Downloaded source.zip (${sizeMB} MB)`);
2625
+ spinner.start(`Extracting to ${outputDir}...`);
2626
+ await fs.ensureDir(outputDir);
2627
+ const zip = new AdmZip(zipBuffer);
2628
+ zip.extractAllTo(outputDir, true);
2629
+ const entries = zip.getEntries().filter((e) => !e.isDirectory);
2630
+ spinner.succeed(`Extracted ${entries.length} files`);
2631
+ if (options.install !== false) {
2632
+ const hasPkgJson = await fs.pathExists(
2633
+ path.join(outputDir, "package.json")
2634
+ );
2635
+ if (hasPkgJson) {
2636
+ spinner.start("Installing dependencies...");
2637
+ const success = await runInstall(outputDir);
2638
+ if (success) {
2639
+ spinner.succeed("Dependencies installed");
2640
+ } else {
2641
+ spinner.warn(
2642
+ chalk4.yellow(
2643
+ "Failed to install dependencies \u2014 run 'pnpm install' manually"
2644
+ )
2645
+ );
2646
+ }
2647
+ }
2648
+ }
2649
+ console.log();
2650
+ logger.success(chalk4.green.bold("Theme cloned successfully!"));
2651
+ console.log();
2652
+ console.log(
2653
+ chalk4.cyan(" Theme: ") + chalk4.white(`${themeName}@${version}`)
2654
+ );
2655
+ console.log(chalk4.cyan(" Location: ") + chalk4.white(outputDir));
2656
+ console.log(chalk4.cyan(" Files: ") + chalk4.white(entries.length));
2657
+ console.log();
2658
+ console.log(chalk4.cyan("Next steps:"));
2659
+ console.log(
2660
+ chalk4.gray(` cd ${path.relative(process.cwd(), outputDir)}`)
2661
+ );
2662
+ if (options.install === false) {
2663
+ console.log(chalk4.gray(" pnpm install"));
2664
+ }
2665
+ console.log(chalk4.gray(" onex build"));
2666
+ console.log();
2667
+ } catch (error) {
2668
+ spinner.fail(chalk4.red(`Clone failed: ${error.message}`));
2669
+ logger.error(error.stack || error.message);
2670
+ process.exit(1);
2671
+ }
2672
+ }
2444
2673
 
2445
2674
  // src/cli.ts
2446
2675
  var program = new Command();
@@ -2474,7 +2703,7 @@ program.command("upload").description("Upload compiled theme to S3 bucket").opti
2474
2703
  "-e, --environment <env>",
2475
2704
  "Environment (staging|production)",
2476
2705
  "staging"
2477
- ).option("--dry-run", "Show what would be uploaded without uploading").action(uploadCommand);
2706
+ ).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
2707
  program.command("download").description("Download theme from S3 bucket").option("-t, --theme-id <id>", "Theme ID to download").option(
2479
2708
  "-v, --version <version>",
2480
2709
  "Theme version (default: latest)",
@@ -2484,6 +2713,15 @@ program.command("download").description("Download theme from S3 bucket").option(
2484
2713
  "Environment (staging|production)",
2485
2714
  "staging"
2486
2715
  ).option("-o, --output <dir>", "Output directory", "./active-theme").action(downloadCommand);
2716
+ program.command("clone").description("Clone theme source code from S3").argument("<theme-name>", "Theme to clone").option(
2717
+ "-v, --version <version>",
2718
+ "Theme version (default: latest)",
2719
+ "latest"
2720
+ ).option("-o, --output <dir>", "Output directory").option("-b, --bucket <name>", "S3 bucket name").option(
2721
+ "-e, --environment <env>",
2722
+ "Environment (staging|production)",
2723
+ "staging"
2724
+ ).option("--no-install", "Skip running pnpm install after clone").action(cloneCommand);
2487
2725
  program.configureOutput({
2488
2726
  writeErr: (str) => process.stderr.write(chalk4.red(str))
2489
2727
  });