@onexapis/cli 1.1.64 → 1.1.65

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
@@ -1764,21 +1764,43 @@ function getValidCategories() {
1764
1764
  ];
1765
1765
  }
1766
1766
  var AUTH_DIR = path9.join(os.homedir(), ".onexthm");
1767
- var AUTH_FILE = path9.join(AUTH_DIR, "auth.json");
1768
- function getApiUrl() {
1769
- return process.env.ONEXTHM_API_URL || "https://platform-dev.onexeos.com";
1767
+ var ENV_URLS = {
1768
+ dev: "https://platform-dev.onexeos.com",
1769
+ prod: "https://platform-staging.onexeos.com"
1770
+ };
1771
+ function getAuthFile(env = "dev") {
1772
+ const newFile = path9.join(AUTH_DIR, `auth-${env}.json`);
1773
+ if (env === "dev") {
1774
+ const legacyFile = path9.join(AUTH_DIR, "auth.json");
1775
+ if (fs.existsSync(legacyFile) && !fs.existsSync(newFile)) {
1776
+ try {
1777
+ fs.moveSync(legacyFile, newFile);
1778
+ } catch {
1779
+ try {
1780
+ fs.copySync(legacyFile, newFile);
1781
+ fs.removeSync(legacyFile);
1782
+ } catch {
1783
+ }
1784
+ }
1785
+ }
1786
+ }
1787
+ return newFile;
1788
+ }
1789
+ function getApiUrl(env = "dev") {
1790
+ return process.env.ONEXTHM_API_URL || ENV_URLS[env];
1770
1791
  }
1771
- async function saveAuthTokens(tokens) {
1792
+ async function saveAuthTokens(tokens, env = "dev") {
1772
1793
  await fs.ensureDir(AUTH_DIR);
1773
1794
  const key = getMachineKey();
1774
1795
  const data = JSON.stringify(tokens);
1775
1796
  const encrypted = encrypt(data, key);
1776
- await fs.writeFile(AUTH_FILE, encrypted, "utf-8");
1797
+ await fs.writeFile(getAuthFile(env), encrypted, "utf-8");
1777
1798
  }
1778
- function loadAuthTokens() {
1799
+ function loadAuthTokens(env = "dev") {
1779
1800
  try {
1780
- if (!fs.existsSync(AUTH_FILE)) return null;
1781
- const encrypted = fs.readFileSync(AUTH_FILE, "utf-8");
1801
+ const file = getAuthFile(env);
1802
+ if (!fs.existsSync(file)) return null;
1803
+ const encrypted = fs.readFileSync(file, "utf-8");
1782
1804
  const key = getMachineKey();
1783
1805
  const data = decrypt(encrypted, key);
1784
1806
  return JSON.parse(data);
@@ -1786,34 +1808,34 @@ function loadAuthTokens() {
1786
1808
  return null;
1787
1809
  }
1788
1810
  }
1789
- async function clearAuthTokens() {
1811
+ async function clearAuthTokens(env = "dev") {
1790
1812
  try {
1791
- await fs.remove(AUTH_FILE);
1813
+ await fs.remove(getAuthFile(env));
1792
1814
  } catch {
1793
1815
  }
1794
1816
  }
1795
1817
  function isTokenExpired(tokens) {
1796
1818
  return Date.now() / 1e3 > tokens.expiresAt - 300;
1797
1819
  }
1798
- async function getValidTokens() {
1799
- const tokens = loadAuthTokens();
1820
+ async function getValidTokens(env = "dev") {
1821
+ const tokens = loadAuthTokens(env);
1800
1822
  if (!tokens) return null;
1801
1823
  if (!isTokenExpired(tokens)) return tokens;
1802
1824
  try {
1803
- const apiUrl = getApiUrl();
1825
+ const apiUrl = getApiUrl(env);
1804
1826
  const response = await fetch(`${apiUrl}/auth/refresh`, {
1805
1827
  method: "POST",
1806
1828
  headers: { "Content-Type": "application/json" },
1807
1829
  body: JSON.stringify({ refresh_token: tokens.refreshToken })
1808
1830
  });
1809
1831
  if (!response.ok) {
1810
- await clearAuthTokens();
1832
+ await clearAuthTokens(env);
1811
1833
  return null;
1812
1834
  }
1813
1835
  const data = await response.json();
1814
1836
  const body = data.statusCode ? data.body : data;
1815
1837
  if (!body.IdToken) {
1816
- await clearAuthTokens();
1838
+ await clearAuthTokens(env);
1817
1839
  return null;
1818
1840
  }
1819
1841
  const refreshed = {
@@ -1822,17 +1844,19 @@ async function getValidTokens() {
1822
1844
  idToken: body.IdToken,
1823
1845
  expiresAt: Math.floor(Date.now() / 1e3) + (body.ExpiresIn || 3600)
1824
1846
  };
1825
- await saveAuthTokens(refreshed);
1847
+ await saveAuthTokens(refreshed, env);
1826
1848
  return refreshed;
1827
1849
  } catch {
1828
- await clearAuthTokens();
1850
+ await clearAuthTokens(env);
1829
1851
  return null;
1830
1852
  }
1831
1853
  }
1832
- async function authenticatedFetch(url, init) {
1833
- const tokens = await getValidTokens();
1854
+ async function authenticatedFetch(url, init, env = "dev") {
1855
+ const tokens = await getValidTokens(env);
1834
1856
  if (!tokens) {
1835
- throw new Error("Not logged in. Run: onexthm login");
1857
+ throw new Error(
1858
+ `Not logged in to ${env} environment. Run: onexthm login --env ${env}`
1859
+ );
1836
1860
  }
1837
1861
  const headers = new Headers(init?.headers);
1838
1862
  headers.set("Authorization", `Bearer ${tokens.idToken}`);
@@ -1916,7 +1940,7 @@ async function initCommand(projectName, options = {}) {
1916
1940
  }
1917
1941
  if (!options.yes) {
1918
1942
  try {
1919
- const apiUrl = getApiUrl();
1943
+ const apiUrl = getApiUrl(options.env ?? "dev");
1920
1944
  const controller = new AbortController();
1921
1945
  const timeout = setTimeout(() => controller.abort(), 3e3);
1922
1946
  const response = await fetch(
@@ -3865,9 +3889,7 @@ function showDownloadFailureHelp(themeId, apiUrl) {
3865
3889
  console.log();
3866
3890
  console.log(chalk4.white("2. Check API URL configuration:"));
3867
3891
  console.log(chalk4.gray(` Current API URL: ${apiUrl}`));
3868
- console.log(
3869
- chalk4.gray(" Override with NEXT_PUBLIC_API_URL or ONEXTHM_API_URL")
3870
- );
3892
+ console.log(chalk4.gray(" Override with ONEXTHM_API_URL env var if needed"));
3871
3893
  console.log();
3872
3894
  console.log(chalk4.white("3. Pin a specific version (CI/production):"));
3873
3895
  console.log(
@@ -3877,15 +3899,17 @@ function showDownloadFailureHelp(themeId, apiUrl) {
3877
3899
  }
3878
3900
  async function downloadCommand(options) {
3879
3901
  logger.header("Download Theme");
3902
+ const env = options.env ?? "dev";
3903
+ const apiUrl = getApiUrl(env);
3904
+ logger.info(`Environment: ${env} (${apiUrl})`);
3880
3905
  const spinner = ora("Initializing download...").start();
3881
- if (options.bucket || options.environment) {
3906
+ if (options.bucket) {
3882
3907
  spinner.stop();
3883
3908
  logger.warning(
3884
- "--bucket and --environment are deprecated and ignored. Themes are now served via HTTP from the website-api Lambda."
3909
+ "--bucket is deprecated and ignored. Themes are now served via HTTP from the website-api Lambda."
3885
3910
  );
3886
3911
  spinner.start();
3887
3912
  }
3888
- const apiUrl = getApiUrl();
3889
3913
  try {
3890
3914
  const themeId = options.themeId || process.env.NEXT_PUBLIC_THEME_ID || process.env.THEME_ID;
3891
3915
  const requestedVersion = options.version || process.env.THEME_VERSION || "latest";
@@ -3942,6 +3966,7 @@ async function downloadCommand(options) {
3942
3966
  console.log(
3943
3967
  chalk4.cyan(" Theme: ") + chalk4.white(`${themeId}@${resolvedVersion}`)
3944
3968
  );
3969
+ console.log(chalk4.cyan(" Env: ") + chalk4.white(env));
3945
3970
  console.log(chalk4.cyan(" Source: ") + chalk4.white(apiUrl));
3946
3971
  console.log(chalk4.cyan(" Output: ") + chalk4.white(outputDir));
3947
3972
  console.log(chalk4.cyan(" Files: ") + chalk4.white(entries.length));
@@ -3984,11 +4009,9 @@ async function resolveLatestVersion2(apiUrl, themeId) {
3984
4009
  }
3985
4010
  return latest;
3986
4011
  }
3987
- async function fetchSourceZip(apiUrl, themeId, version2) {
4012
+ async function fetchSourceZip(apiUrl, themeId, version2, env) {
3988
4013
  const url = `${apiUrl}/website-api/themes/${encodeURIComponent(themeId)}/source?version=${encodeURIComponent(version2)}`;
3989
- const response = await authenticatedFetch(url, {
3990
- method: "GET"
3991
- });
4014
+ const response = await authenticatedFetch(url, { method: "GET" }, env);
3992
4015
  if (!response.ok) {
3993
4016
  if (response.status === 404) {
3994
4017
  throw new Error(
@@ -3997,7 +4020,7 @@ async function fetchSourceZip(apiUrl, themeId, version2) {
3997
4020
  }
3998
4021
  if (response.status === 401 || response.status === 403) {
3999
4022
  throw new Error(
4000
- `Not authorized to download source for "${themeId}". Run \`onexthm login\` first.`
4023
+ `Not authorized to download source for "${themeId}". Run \`onexthm login --env ${env}\` first.`
4001
4024
  );
4002
4025
  }
4003
4026
  throw new Error(
@@ -4116,14 +4139,19 @@ async function renameTheme(themeDir, oldName, newName) {
4116
4139
  }
4117
4140
  async function cloneCommand(themeName, options) {
4118
4141
  logger.header("Clone Theme Source");
4119
- if (options.bucket || options.environment) {
4142
+ const env = options.env ?? "dev";
4143
+ const apiUrl = getApiUrl(env);
4144
+ logger.info(`Environment: ${env} (${apiUrl})`);
4145
+ if (options.bucket) {
4120
4146
  logger.warning(
4121
- "--bucket and --environment are deprecated and ignored. Source is now fetched via HTTP from the website-api Lambda."
4147
+ "--bucket is deprecated and ignored. Source is now fetched via HTTP from the website-api Lambda."
4122
4148
  );
4123
4149
  }
4124
- const tokens = await getValidTokens();
4150
+ const tokens = await getValidTokens(env);
4125
4151
  if (!tokens) {
4126
- logger.error("Not logged in. Run: onexthm login");
4152
+ logger.error(
4153
+ `Not logged in to ${env} environment. Run: onexthm login --env ${env}`
4154
+ );
4127
4155
  process.exit(1);
4128
4156
  }
4129
4157
  let newName = options.name;
@@ -4132,7 +4160,6 @@ async function cloneCommand(themeName, options) {
4132
4160
  }
4133
4161
  const spinner = ora("Initializing clone...").start();
4134
4162
  try {
4135
- const apiUrl = getApiUrl();
4136
4163
  const outputDir = options.output || path9.resolve(process.cwd(), newName);
4137
4164
  if (await fs.pathExists(outputDir)) {
4138
4165
  spinner.fail(chalk4.red(`Directory already exists: ${outputDir}`));
@@ -4152,7 +4179,7 @@ async function cloneCommand(themeName, options) {
4152
4179
  spinner.start(`Downloading source.zip for ${themeName}@${version2}...`);
4153
4180
  let zipBuffer;
4154
4181
  try {
4155
- zipBuffer = await fetchSourceZip(apiUrl, themeName, version2);
4182
+ zipBuffer = await fetchSourceZip(apiUrl, themeName, version2, env);
4156
4183
  } catch (error) {
4157
4184
  spinner.fail(chalk4.red(error.message));
4158
4185
  console.log();
@@ -4186,7 +4213,7 @@ async function cloneCommand(themeName, options) {
4186
4213
  [
4187
4214
  "# API Configuration (enables real data in preview)",
4188
4215
  "# Get your Company ID from the OneX dashboard",
4189
- "NEXT_PUBLIC_API_URL=https://platform-dev.onexeos.com",
4216
+ `NEXT_PUBLIC_API_URL=${apiUrl}`,
4190
4217
  "NEXT_PUBLIC_COMPANY_ID=",
4191
4218
  ""
4192
4219
  ].join("\n")
@@ -4239,6 +4266,7 @@ async function cloneCommand(themeName, options) {
4239
4266
  console.log(
4240
4267
  chalk4.cyan(" Source: ") + chalk4.gray(`${themeName}@${version2}`)
4241
4268
  );
4269
+ console.log(chalk4.cyan(" Env: ") + chalk4.white(env));
4242
4270
  console.log(chalk4.cyan(" Theme: ") + chalk4.white(newName));
4243
4271
  console.log(chalk4.cyan(" Location: ") + chalk4.white(outputDir));
4244
4272
  console.log(chalk4.cyan(" Files: ") + chalk4.white(entries.length));
@@ -4515,9 +4543,13 @@ async function configCommand() {
4515
4543
 
4516
4544
  // src/commands/login.ts
4517
4545
  init_logger();
4518
- async function loginCommand() {
4546
+ async function loginCommand(options = {}) {
4547
+ const env = options.env ?? "dev";
4548
+ const apiUrl = getApiUrl(env);
4519
4549
  logger.header("OneX Theme Developer Login");
4520
- const existing = loadAuthTokens();
4550
+ logger.info(`Environment: ${env} (${apiUrl})`);
4551
+ logger.newLine();
4552
+ const existing = loadAuthTokens(env);
4521
4553
  if (existing) {
4522
4554
  logger.info(`Already logged in as: ${existing.user.email}`);
4523
4555
  const { relogin } = await inquirer.prompt([
@@ -4546,7 +4578,6 @@ async function loginCommand() {
4546
4578
  ]);
4547
4579
  logger.startSpinner("Logging in...");
4548
4580
  try {
4549
- const apiUrl = getApiUrl();
4550
4581
  const response = await fetch(`${apiUrl}/auth/login`, {
4551
4582
  method: "POST",
4552
4583
  headers: { "Content-Type": "application/json" },
@@ -4581,15 +4612,18 @@ async function loginCommand() {
4581
4612
  userId: claims.sub
4582
4613
  }
4583
4614
  };
4584
- await saveAuthTokens(tokens);
4615
+ await saveAuthTokens(tokens, env);
4585
4616
  logger.stopSpinner(true, "Logged in!");
4586
4617
  logger.newLine();
4587
- logger.info(` Email: ${tokens.user.email}`);
4588
- if (tokens.user.name) logger.info(` Name: ${tokens.user.name}`);
4618
+ logger.info(` Environment: ${env}`);
4619
+ logger.info(` Email: ${tokens.user.email}`);
4620
+ if (tokens.user.name) logger.info(` Name: ${tokens.user.name}`);
4589
4621
  if (tokens.user.companyId)
4590
- logger.info(` Company: ${tokens.user.companyId}`);
4622
+ logger.info(` Company: ${tokens.user.companyId}`);
4591
4623
  logger.newLine();
4592
- logger.success("Token stored securely in ~/.onexthm/auth.json (encrypted)");
4624
+ logger.success(
4625
+ `Token stored securely in ~/.onexthm/auth-${env}.json (encrypted)`
4626
+ );
4593
4627
  } catch (error) {
4594
4628
  logger.stopSpinner(false, "Login failed");
4595
4629
  logger.error(error instanceof Error ? error.message : "Connection failed");
@@ -4599,32 +4633,37 @@ async function loginCommand() {
4599
4633
 
4600
4634
  // src/commands/logout.ts
4601
4635
  init_logger();
4602
- async function logoutCommand() {
4603
- const tokens = loadAuthTokens();
4636
+ async function logoutCommand(options = {}) {
4637
+ const env = options.env ?? "dev";
4638
+ const tokens = loadAuthTokens(env);
4604
4639
  if (!tokens) {
4605
- logger.info("Not logged in.");
4640
+ logger.info(`Not logged in to ${env} environment.`);
4606
4641
  return;
4607
4642
  }
4608
- await clearAuthTokens();
4609
- logger.success(`Logged out (was: ${tokens.user.email})`);
4643
+ await clearAuthTokens(env);
4644
+ logger.success(`Logged out of ${env} (was: ${tokens.user.email})`);
4610
4645
  }
4611
4646
 
4612
4647
  // src/commands/whoami.ts
4613
4648
  init_logger();
4614
- async function whoamiCommand() {
4615
- const tokens = loadAuthTokens();
4649
+ async function whoamiCommand(options = {}) {
4650
+ const env = options.env ?? "dev";
4651
+ const tokens = loadAuthTokens(env);
4616
4652
  if (!tokens) {
4617
- logger.error("Not logged in. Run: onexthm login");
4653
+ logger.error(
4654
+ `Not logged in to ${env} environment. Run: onexthm login --env ${env}`
4655
+ );
4618
4656
  process.exit(1);
4619
4657
  }
4620
4658
  const expired = isTokenExpired(tokens);
4621
4659
  logger.header("OneX Theme Developer");
4622
- logger.info(` Email: ${tokens.user.email}`);
4623
- if (tokens.user.name) logger.info(` Name: ${tokens.user.name}`);
4660
+ logger.info(` Environment: ${env} (${getApiUrl(env)})`);
4661
+ logger.info(` Email: ${tokens.user.email}`);
4662
+ if (tokens.user.name) logger.info(` Name: ${tokens.user.name}`);
4624
4663
  if (tokens.user.companyId)
4625
- logger.info(` Company: ${tokens.user.companyId}`);
4664
+ logger.info(` Company: ${tokens.user.companyId}`);
4626
4665
  logger.info(
4627
- ` Status: ${expired ? "\u26A0 Token expired (will auto-refresh)" : "\u2713 Active"}`
4666
+ ` Status: ${expired ? "\u26A0 Token expired (will auto-refresh)" : "\u2713 Active"}`
4628
4667
  );
4629
4668
  }
4630
4669
 
@@ -4719,10 +4758,15 @@ function buildAssetMap(entries) {
4719
4758
 
4720
4759
  // src/commands/publish.ts
4721
4760
  async function publishCommand(options) {
4761
+ const env = options.env ?? "dev";
4722
4762
  logger.header("OneX Theme Publish");
4723
- const tokens = await getValidTokens();
4763
+ logger.info(`Environment: ${env} (${getApiUrl(env)})`);
4764
+ logger.newLine();
4765
+ const tokens = await getValidTokens(env);
4724
4766
  if (!tokens) {
4725
- logger.error("Not logged in. Run: onexthm login");
4767
+ logger.error(
4768
+ `Not logged in to ${env} environment. Run: onexthm login --env ${env}`
4769
+ );
4726
4770
  process.exit(1);
4727
4771
  }
4728
4772
  logger.info(`Logged in as: ${tokens.user.email}`);
@@ -4773,7 +4817,7 @@ async function publishCommand(options) {
4773
4817
  logger.info(`Theme: ${themeId}`);
4774
4818
  logger.info(`Version: ${version2}`);
4775
4819
  logger.newLine();
4776
- const apiUrl = getApiUrl();
4820
+ const apiUrl = getApiUrl(env);
4777
4821
  logger.startSpinner("Registering theme...");
4778
4822
  try {
4779
4823
  const regResponse = await authenticatedFetch(
@@ -4790,7 +4834,8 @@ async function publishCommand(options) {
4790
4834
  tags: pkg.keywords || [],
4791
4835
  thumbnail_url: pkg.onex?.thumbnail || ""
4792
4836
  })
4793
- }
4837
+ },
4838
+ env
4794
4839
  );
4795
4840
  const regData = await regResponse.json();
4796
4841
  const regBody = regData.statusCode ? regData.body : regData;
@@ -4812,7 +4857,8 @@ async function publishCommand(options) {
4812
4857
  try {
4813
4858
  const checkResponse = await authenticatedFetch(
4814
4859
  `${apiUrl}/website-api/themes/${encodeURIComponent(themeId)}/versions/${encodeURIComponent(version2)}/exists`,
4815
- { method: "GET" }
4860
+ { method: "GET" },
4861
+ env
4816
4862
  );
4817
4863
  const checkData = await checkResponse.json();
4818
4864
  const checkBody = checkData.statusCode ? checkData.body : checkData;
@@ -4877,7 +4923,7 @@ Or use the --bump flag:
4877
4923
  logger.startSpinner(`Uploading ${videoAssets.length} video(s)...`);
4878
4924
  try {
4879
4925
  for (const video of videoAssets) {
4880
- const url = await uploadVideoMultipart(apiUrl, themeId, video);
4926
+ const url = await uploadVideoMultipart(apiUrl, themeId, video, env);
4881
4927
  videoUrls[video.originalPath] = url;
4882
4928
  }
4883
4929
  logger.stopSpinner(true, `Uploaded ${videoAssets.length} video(s)`);
@@ -4919,7 +4965,8 @@ Or use the --bump flag:
4919
4965
  content_type: a.contentType
4920
4966
  }))
4921
4967
  })
4922
- }
4968
+ },
4969
+ env
4923
4970
  );
4924
4971
  const pubData = await pubResponse.json();
4925
4972
  const pubBody = pubData.statusCode ? pubData.body : pubData;
@@ -4945,6 +4992,12 @@ Or use the --bump flag:
4945
4992
  }
4946
4993
  if (assetUploads.length > 0) {
4947
4994
  logger.startSpinner(`Uploading ${assetUploads.length} asset(s) to S3...`);
4995
+ if (assetUploads[0]) {
4996
+ logger.log(
4997
+ ` [debug] sample presigned PUT URL: ${assetUploads[0].upload_url}`
4998
+ );
4999
+ logger.log(` [debug] sample s3_key: ${assetUploads[0].s3_key}`);
5000
+ }
4948
5001
  const CONCURRENCY = 8;
4949
5002
  const byHashedPath = new Map(regularAssets.map((a) => [a.hashedPath, a]));
4950
5003
  const queue = [...assetUploads];
@@ -4971,6 +5024,13 @@ Or use the --bump flag:
4971
5024
  body: buf
4972
5025
  });
4973
5026
  if (!res.ok) {
5027
+ if (failed === 0) {
5028
+ const errBody = await res.text().catch(() => "(unreadable)");
5029
+ logger.log(` [debug] PUT ${item.upload_url}`);
5030
+ logger.log(
5031
+ ` [debug] response ${res.status} ${res.statusText}: ${errBody.slice(0, 500)}`
5032
+ );
5033
+ }
4974
5034
  throw new Error(`HTTP ${res.status}`);
4975
5035
  }
4976
5036
  uploaded++;
@@ -5055,7 +5115,8 @@ Or use the --bump flag:
5055
5115
  try {
5056
5116
  const confirmResponse = await authenticatedFetch(
5057
5117
  `${apiUrl}/website-api/themes/${encodeURIComponent(themeId)}/versions/${encodeURIComponent(version2)}/confirm`,
5058
- { method: "POST" }
5118
+ { method: "POST" },
5119
+ env
5059
5120
  );
5060
5121
  const confirmData = await confirmResponse.json();
5061
5122
  const confirmBody = confirmData.statusCode ? confirmData.body : confirmData;
@@ -5101,9 +5162,9 @@ Or use the --bump flag:
5101
5162
  }
5102
5163
  logger.newLine();
5103
5164
  logger.success(`\u2713 Theme "${themeId}" v${version2} published!`);
5104
- await uploadThumbnail(apiUrl, themeId, themePath, distDir);
5165
+ await uploadThumbnail(apiUrl, themeId, themePath, distDir, env);
5105
5166
  }
5106
- async function uploadThumbnail(apiUrl, themeId, themePath, distDir) {
5167
+ async function uploadThumbnail(apiUrl, themeId, themePath, distDir, env = "dev") {
5107
5168
  const THUMBNAIL_CANDIDATES = [
5108
5169
  { file: "thumbnail.png", mime: "image/png" },
5109
5170
  { file: "thumbnail.jpg", mime: "image/jpeg" },
@@ -5138,9 +5199,19 @@ async function uploadThumbnail(apiUrl, themeId, themePath, distDir) {
5138
5199
  }
5139
5200
  }
5140
5201
  logger.startSpinner("Uploading thumbnail...");
5202
+ const imageUploadUrl = `${apiUrl}/media/images/upload`;
5203
+ const imageRequestBody = {
5204
+ prefix: `themes/${themeId}`,
5205
+ image: imageBase64 ? `${imageBase64.slice(0, 60)}... [${Math.round(imageBase64.length * 3 / 4 / 1024)} KB]` : null,
5206
+ name: "thumbnail.png"
5207
+ };
5208
+ logger.log(` \u2192 POST ${imageUploadUrl}`);
5209
+ logger.log(
5210
+ ` \u2192 body: ${JSON.stringify({ ...imageRequestBody, image: imageBase64 ? `<base64 ${mimeType} truncated>` : null })}`
5211
+ );
5141
5212
  try {
5142
5213
  const uploadRes = await authenticatedFetch(
5143
- `${apiUrl}/media/images/upload`,
5214
+ imageUploadUrl,
5144
5215
  {
5145
5216
  method: "POST",
5146
5217
  body: JSON.stringify({
@@ -5148,30 +5219,56 @@ async function uploadThumbnail(apiUrl, themeId, themePath, distDir) {
5148
5219
  image: imageBase64,
5149
5220
  name: "thumbnail.png"
5150
5221
  })
5151
- }
5222
+ },
5223
+ env
5152
5224
  );
5153
- const uploadData = await uploadRes.json();
5225
+ logger.log(` \u2190 HTTP ${uploadRes.status} ${uploadRes.statusText}`);
5226
+ const uploadRawText = await uploadRes.text();
5227
+ logger.log(` \u2190 raw response: ${uploadRawText.slice(0, 500)}`);
5228
+ let uploadData;
5229
+ try {
5230
+ uploadData = JSON.parse(uploadRawText);
5231
+ } catch {
5232
+ throw new Error(
5233
+ `Image upload returned non-JSON (HTTP ${uploadRes.status}): ${uploadRawText.slice(0, 200)}`
5234
+ );
5235
+ }
5154
5236
  const uploadBody = uploadData.statusCode ? uploadData.body : uploadData;
5155
5237
  if (!uploadRes.ok || !uploadBody.url) {
5156
- throw new Error(uploadBody.error || "Upload failed");
5238
+ throw new Error(
5239
+ `Image upload failed \u2014 HTTP ${uploadRes.status}: ${uploadBody.error || uploadBody.message || JSON.stringify(uploadBody).slice(0, 300)}`
5240
+ );
5157
5241
  }
5242
+ const patchUrl = `${apiUrl}/website-api/themes/${encodeURIComponent(themeId)}`;
5243
+ logger.log(` \u2192 PATCH ${patchUrl}`);
5158
5244
  const patchRes = await authenticatedFetch(
5159
- `${apiUrl}/website-api/themes/${encodeURIComponent(themeId)}`,
5245
+ patchUrl,
5160
5246
  {
5161
5247
  method: "PATCH",
5162
5248
  body: JSON.stringify({ thumbnail_url: uploadBody.url })
5163
- }
5249
+ },
5250
+ env
5164
5251
  );
5252
+ logger.log(` \u2190 HTTP ${patchRes.status} ${patchRes.statusText}`);
5165
5253
  if (!patchRes.ok) {
5166
- const patchData = await patchRes.json();
5167
- const patchBody = patchData.statusCode ? patchData.body : patchData;
5168
- throw new Error(patchBody.error || "Failed to set thumbnail");
5254
+ const patchRawText = await patchRes.text();
5255
+ logger.log(` \u2190 raw response: ${patchRawText.slice(0, 500)}`);
5256
+ let patchBody = {};
5257
+ try {
5258
+ patchBody = JSON.parse(patchRawText);
5259
+ } catch {
5260
+ }
5261
+ patchBody = patchBody.statusCode ? patchBody.body : patchBody;
5262
+ throw new Error(
5263
+ `Thumbnail patch failed \u2014 HTTP ${patchRes.status}: ${patchBody.error || patchBody.message || patchRawText.slice(0, 200)}`
5264
+ );
5169
5265
  }
5170
5266
  logger.stopSpinner(true, "Thumbnail set");
5171
5267
  } catch (err) {
5172
- logger.stopSpinner(false, "Thumbnail upload skipped");
5268
+ logger.stopSpinner(false, "Thumbnail upload failed");
5269
+ logger.error(err instanceof Error ? err.message : String(err));
5173
5270
  logger.info(
5174
- `Theme published successfully. Thumbnail can be updated later.`
5271
+ "Theme published successfully. Thumbnail can be updated later."
5175
5272
  );
5176
5273
  }
5177
5274
  }
@@ -5235,25 +5332,40 @@ async function findFreePort(start) {
5235
5332
  srv.on("error", () => resolve(findFreePort(start + 1)));
5236
5333
  });
5237
5334
  }
5238
- async function uploadVideoMultipart(apiUrl, themeId, video) {
5335
+ async function uploadVideoMultipart(apiUrl, themeId, video, env = "dev") {
5239
5336
  const fileName = path9.basename(video.originalPath);
5337
+ const videoInitUrl = `${apiUrl}/media/videos/multipart/init`;
5338
+ const videoInitBody = {
5339
+ file_name: fileName,
5340
+ content_type: video.contentType,
5341
+ file_size: video.size,
5342
+ prefix: `themes/${themeId}/assets`
5343
+ };
5344
+ logger.log(` \u2192 POST ${videoInitUrl}`);
5345
+ logger.log(` \u2192 body: ${JSON.stringify(videoInitBody)}`);
5240
5346
  const initRes = await authenticatedFetch(
5241
- `${apiUrl}/media/videos/multipart/init`,
5347
+ videoInitUrl,
5242
5348
  {
5243
5349
  method: "POST",
5244
- body: JSON.stringify({
5245
- file_name: fileName,
5246
- content_type: video.contentType,
5247
- file_size: video.size,
5248
- prefix: `themes/${themeId}/assets`
5249
- })
5250
- }
5350
+ body: JSON.stringify(videoInitBody)
5351
+ },
5352
+ env
5251
5353
  );
5252
- const initData = await initRes.json();
5354
+ logger.log(` \u2190 HTTP ${initRes.status} ${initRes.statusText}`);
5355
+ const initRawText = await initRes.text();
5356
+ logger.log(` \u2190 raw response: ${initRawText.slice(0, 500)}`);
5357
+ let initData;
5358
+ try {
5359
+ initData = JSON.parse(initRawText);
5360
+ } catch {
5361
+ throw new Error(
5362
+ `Video init returned non-JSON (HTTP ${initRes.status}): ${initRawText.slice(0, 200)}`
5363
+ );
5364
+ }
5253
5365
  const initBody = initData.statusCode ? initData.body : initData;
5254
5366
  if (!initRes.ok || !initBody.upload_id) {
5255
5367
  throw new Error(
5256
- `Init multipart failed for ${fileName}: ${initBody.error || initRes.status}`
5368
+ `Init multipart failed for ${fileName} \u2014 HTTP ${initRes.status}: ${initBody.error || initBody.message || JSON.stringify(initBody).slice(0, 300)}`
5257
5369
  );
5258
5370
  }
5259
5371
  const { upload_id, file_key, chunk_size, chunk_urls } = initBody;
@@ -5288,25 +5400,47 @@ async function uploadVideoMultipart(apiUrl, themeId, video) {
5288
5400
  )
5289
5401
  );
5290
5402
  parts.sort((a, b) => a.part_number - b.part_number);
5403
+ const videoCompleteUrl = `${apiUrl}/media/videos/multipart/complete`;
5404
+ logger.log(` \u2192 POST ${videoCompleteUrl}`);
5405
+ logger.log(
5406
+ ` \u2192 body: ${JSON.stringify({ upload_id, file_key, parts: `[${parts.length} parts]` })}`
5407
+ );
5291
5408
  const completeRes = await authenticatedFetch(
5292
- `${apiUrl}/media/videos/multipart/complete`,
5409
+ videoCompleteUrl,
5293
5410
  {
5294
5411
  method: "POST",
5295
5412
  body: JSON.stringify({ upload_id, file_key, parts })
5296
- }
5413
+ },
5414
+ env
5297
5415
  );
5298
- const completeData = await completeRes.json();
5416
+ logger.log(` \u2190 HTTP ${completeRes.status} ${completeRes.statusText}`);
5417
+ const completeRawText = await completeRes.text();
5418
+ logger.log(` \u2190 raw response: ${completeRawText.slice(0, 500)}`);
5419
+ let completeData;
5420
+ try {
5421
+ completeData = JSON.parse(completeRawText);
5422
+ } catch {
5423
+ throw new Error(
5424
+ `Video complete returned non-JSON (HTTP ${completeRes.status}): ${completeRawText.slice(0, 200)}`
5425
+ );
5426
+ }
5299
5427
  const completeBody = completeData.statusCode ? completeData.body : completeData;
5300
5428
  if (!completeRes.ok || !completeBody.url) {
5301
5429
  try {
5302
- await authenticatedFetch(`${apiUrl}/media/videos/multipart/abort`, {
5303
- method: "POST",
5304
- body: JSON.stringify({ upload_id, file_key })
5305
- });
5430
+ const abortUrl = `${apiUrl}/media/videos/multipart/abort`;
5431
+ logger.log(` \u2192 POST ${abortUrl} (cleanup)`);
5432
+ await authenticatedFetch(
5433
+ abortUrl,
5434
+ {
5435
+ method: "POST",
5436
+ body: JSON.stringify({ upload_id, file_key })
5437
+ },
5438
+ env
5439
+ );
5306
5440
  } catch {
5307
5441
  }
5308
5442
  throw new Error(
5309
- `Complete multipart failed for ${fileName}: ${completeBody.error || completeRes.status}`
5443
+ `Complete multipart failed for ${fileName} \u2014 HTTP ${completeRes.status}: ${completeBody.error || completeBody.message || JSON.stringify(completeBody).slice(0, 300)}`
5310
5444
  );
5311
5445
  }
5312
5446
  return completeBody.url;
@@ -5569,7 +5703,11 @@ program.command("init").description("Create a new OneX theme project").argument(
5569
5703
  "-t, --template <template>",
5570
5704
  "Template to use (default, minimal)",
5571
5705
  "default"
5572
- ).option("--no-install", "Skip installing dependencies").option("--git", "Initialize git repository").option("-y, --yes", "Skip prompts and use defaults").action(initCommand);
5706
+ ).option("--no-install", "Skip installing dependencies").option("--git", "Initialize git repository").option("-y, --yes", "Skip prompts and use defaults").option(
5707
+ "--env <env>",
5708
+ "Target environment: dev or prod (default: dev)",
5709
+ "dev"
5710
+ ).action(initCommand);
5573
5711
  program.command("create:section").alias("cs").description("Create a new section").argument("<name>", "Name of the section (e.g., hero, features)").option("-t, --theme <theme>", "Theme to create section in").option(
5574
5712
  "-c, --category <category>",
5575
5713
  "Section category (headers, content, footers)"
@@ -5596,16 +5734,36 @@ program.command("download").description("Download a published theme via the webs
5596
5734
  "-v, --version <version>",
5597
5735
  "Theme version (default: latest)",
5598
5736
  "latest"
5599
- ).option("-b, --bucket <name>", "[deprecated] ignored").option("-e, --environment <env>", "[deprecated] ignored").option("-o, --output <dir>", "Output directory", "./active-theme").action(downloadCommand);
5737
+ ).option(
5738
+ "--env <env>",
5739
+ "Target environment: dev or prod (default: dev)",
5740
+ "dev"
5741
+ ).option("-b, --bucket <name>", "[deprecated] ignored").option("-o, --output <dir>", "Output directory", "./active-theme").action(downloadCommand);
5600
5742
  program.command("clone").description("Clone theme source code via the website-api").argument("<theme-name>", "Theme to clone").option(
5601
5743
  "-v, --version <version>",
5602
5744
  "Theme version (default: latest)",
5603
5745
  "latest"
5604
- ).option("-n, --name <name>", "New theme name (skips interactive prompt)").option("-o, --output <dir>", "Output directory").option("-b, --bucket <name>", "[deprecated] ignored").option("-e, --environment <env>", "[deprecated] ignored").option("--no-install", "Skip running pnpm install after clone").action(cloneCommand);
5746
+ ).option("-n, --name <name>", "New theme name (skips interactive prompt)").option("-o, --output <dir>", "Output directory").option(
5747
+ "--env <env>",
5748
+ "Target environment: dev or prod (default: dev)",
5749
+ "dev"
5750
+ ).option("-b, --bucket <name>", "[deprecated] ignored").option("--no-install", "Skip running pnpm install after clone").action(cloneCommand);
5605
5751
  program.command("config").description("Configure OneX CLI credentials (AWS, API keys)").action(configCommand);
5606
- program.command("login").description("Login to OneX platform").action(loginCommand);
5607
- program.command("logout").description("Logout from OneX platform").action(logoutCommand);
5608
- program.command("whoami").description("Show current logged-in developer").action(whoamiCommand);
5752
+ program.command("login").description("Login to OneX platform").option(
5753
+ "--env <env>",
5754
+ "Target environment: dev or prod (default: dev)",
5755
+ "dev"
5756
+ ).action(loginCommand);
5757
+ program.command("logout").description("Logout from OneX platform").option(
5758
+ "--env <env>",
5759
+ "Target environment: dev or prod (default: dev)",
5760
+ "dev"
5761
+ ).action(logoutCommand);
5762
+ program.command("whoami").description("Show current logged-in developer").option(
5763
+ "--env <env>",
5764
+ "Target environment: dev or prod (default: dev)",
5765
+ "dev"
5766
+ ).action(whoamiCommand);
5609
5767
  var mcpCmd = program.command("mcp").description("Manage MCP server registration and AI-context files");
5610
5768
  mcpCmd.command("setup").description(
5611
5769
  "Install .mcp.json + CLAUDE.md + AGENTS.md + .cursorrules into the current theme"
@@ -5617,6 +5775,10 @@ mcpCmd.command("doctor").description("Diagnose MCP setup in the current theme di
5617
5775
  program.command("publish").description("Build, scan, and publish theme to marketplace (requires login)").option("-t, --theme <path>", "Theme directory path").option(
5618
5776
  "--bump <type>",
5619
5777
  "Auto-bump version before publish (patch|minor|major)"
5778
+ ).option(
5779
+ "--env <env>",
5780
+ "Target environment: dev or prod (default: dev)",
5781
+ "dev"
5620
5782
  ).action(publishCommand);
5621
5783
  program.configureOutput({
5622
5784
  writeErr: (str) => process.stderr.write(chalk4.red(str))