@playtagon/cli 0.2.9 → 0.3.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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command10 } from "commander";
4
+ import { Command as Command11 } from "commander";
5
5
 
6
6
  // src/commands/login.ts
7
7
  import { Command } from "commander";
@@ -17,7 +17,7 @@ import * as crypto from "crypto";
17
17
  // src/lib/config.ts
18
18
  import Conf from "conf";
19
19
  var DEFAULT_SUPABASE_URL = process.env.PLAYTAGON_SUPABASE_URL || "https://pthbeazcwnhjljwksuae.supabase.co";
20
- var DEFAULT_SUPABASE_ANON_KEY = process.env.PLAYTAGON_SUPABASE_ANON_KEY || "sb_publishable_N6Bokn1e6ehOsKqr6quX2g_gvj7xVfy";
20
+ var DEFAULT_SUPABASE_ANON_KEY = process.env.PLAYTAGON_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InB0aGJlYXpjd25oamxqd2tzdWFlIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjcxMjIzNDEsImV4cCI6MjA4MjY5ODM0MX0.ls0hO-UigKnl9cj2NEkxjQL4TnijkIG50iImqZIThVc";
21
21
  var configStore = new Conf({
22
22
  projectName: "playtagon",
23
23
  configFileMode: 384,
@@ -244,7 +244,7 @@ async function loginWithEmail(email, password) {
244
244
  });
245
245
  }
246
246
  async function loginWithBrowser() {
247
- return new Promise((resolve4) => {
247
+ return new Promise((resolve5) => {
248
248
  const port = 19419;
249
249
  const redirectUri = `http://localhost:${port}/callback`;
250
250
  const codeVerifier = generateCodeVerifier();
@@ -274,7 +274,7 @@ async function loginWithBrowser() {
274
274
  res.writeHead(200, { "Content-Type": "text/html" });
275
275
  res.end(getErrorHtml(errorDescription || error));
276
276
  cleanup();
277
- resolve4({ success: false, error: errorDescription || error });
277
+ resolve5({ success: false, error: errorDescription || error });
278
278
  return;
279
279
  }
280
280
  const accessToken = url.searchParams.get("access_token");
@@ -288,7 +288,7 @@ async function loginWithBrowser() {
288
288
  res.writeHead(200, { "Content-Type": "text/html" });
289
289
  res.end(getSuccessHtml(""));
290
290
  cleanup();
291
- resolve4({ success: true });
291
+ resolve5({ success: true });
292
292
  return;
293
293
  }
294
294
  const code = url.searchParams.get("code");
@@ -300,7 +300,7 @@ async function loginWithBrowser() {
300
300
  res.writeHead(200, { "Content-Type": "text/html" });
301
301
  res.end(getErrorHtml(exchangeError?.message || "Failed to exchange code"));
302
302
  cleanup();
303
- resolve4({ success: false, error: exchangeError?.message || "Failed to exchange code" });
303
+ resolve5({ success: false, error: exchangeError?.message || "Failed to exchange code" });
304
304
  return;
305
305
  }
306
306
  credentials.save({
@@ -313,19 +313,19 @@ async function loginWithBrowser() {
313
313
  res.writeHead(200, { "Content-Type": "text/html" });
314
314
  res.end(getSuccessHtml(data.user?.email || ""));
315
315
  cleanup();
316
- resolve4({ success: true });
316
+ resolve5({ success: true });
317
317
  } catch (err) {
318
318
  res.writeHead(200, { "Content-Type": "text/html" });
319
319
  res.end(getErrorHtml(err instanceof Error ? err.message : "Unknown error"));
320
320
  cleanup();
321
- resolve4({ success: false, error: err instanceof Error ? err.message : "Unknown error" });
321
+ resolve5({ success: false, error: err instanceof Error ? err.message : "Unknown error" });
322
322
  }
323
323
  return;
324
324
  }
325
325
  res.writeHead(200, { "Content-Type": "text/html" });
326
326
  res.end(getErrorHtml("No authorization token received"));
327
327
  cleanup();
328
- resolve4({ success: false, error: "No authorization token received" });
328
+ resolve5({ success: false, error: "No authorization token received" });
329
329
  } else {
330
330
  res.writeHead(404);
331
331
  res.end("Not found");
@@ -337,15 +337,15 @@ async function loginWithBrowser() {
337
337
  server.on("error", (err) => {
338
338
  if (err.code === "EADDRINUSE") {
339
339
  cleanup();
340
- resolve4({ success: false, error: `Port ${port} is already in use. Please close any applications using it.` });
340
+ resolve5({ success: false, error: `Port ${port} is already in use. Please close any applications using it.` });
341
341
  } else {
342
342
  cleanup();
343
- resolve4({ success: false, error: err.message });
343
+ resolve5({ success: false, error: err.message });
344
344
  }
345
345
  });
346
346
  timeoutId = setTimeout(() => {
347
347
  cleanup();
348
- resolve4({ success: false, error: "Authentication timed out. Please try again." });
348
+ resolve5({ success: false, error: "Authentication timed out. Please try again." });
349
349
  }, 5 * 60 * 1e3);
350
350
  });
351
351
  }
@@ -497,8 +497,8 @@ async function promptEmailLogin() {
497
497
  input: process.stdin,
498
498
  output: process.stdout
499
499
  });
500
- const question = (prompt) => new Promise((resolve4) => {
501
- rl.question(prompt, resolve4);
500
+ const question = (prompt) => new Promise((resolve5) => {
501
+ rl.question(prompt, resolve5);
502
502
  });
503
503
  try {
504
504
  const email = await question("Email: ");
@@ -590,7 +590,7 @@ async function browserLogin() {
590
590
  }
591
591
  }
592
592
  function questionHidden(prompt, rl) {
593
- return new Promise((resolve4) => {
593
+ return new Promise((resolve5) => {
594
594
  const stdin = process.stdin;
595
595
  const stdout = process.stdout;
596
596
  stdout.write(prompt);
@@ -610,7 +610,7 @@ function questionHidden(prompt, rl) {
610
610
  }
611
611
  stdin.removeListener("data", onData);
612
612
  stdout.write("\n");
613
- resolve4(password);
613
+ resolve5(password);
614
614
  break;
615
615
  case "":
616
616
  process.exit(1);
@@ -682,7 +682,7 @@ var whoamiCommand = new Command3("whoami").description("Show current logged in u
682
682
  });
683
683
 
684
684
  // src/commands/spine/index.ts
685
- import { Command as Command7 } from "commander";
685
+ import { Command as Command8 } from "commander";
686
686
 
687
687
  // src/commands/spine/validate.ts
688
688
  import { Command as Command4 } from "commander";
@@ -698,14 +698,28 @@ import * as fs from "fs";
698
698
  import * as path from "path";
699
699
  var SKELETON_EXTENSIONS = [".json", ".skel"];
700
700
  var ATLAS_EXTENSIONS = [".atlas"];
701
- var TEXTURE_EXTENSIONS = [".png", ".jpg", ".jpeg"];
701
+ var TEXTURE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp"];
702
702
  function discoverSpineFiles(directory) {
703
703
  const absolutePath = path.resolve(directory);
704
704
  if (!fs.existsSync(absolutePath)) {
705
- throw new Error(`Directory not found: ${directory}`);
705
+ throw new Error(`Directory not found: ${absolutePath}
706
+
707
+ Make sure you're pointing to a folder containing exported Spine files (.json, .atlas, .png), not a .spine project file.`);
706
708
  }
707
709
  if (!fs.statSync(absolutePath).isDirectory()) {
708
- throw new Error(`Not a directory: ${directory}`);
710
+ const ext = path.extname(absolutePath).toLowerCase();
711
+ if (ext === ".spine") {
712
+ throw new Error(`Cannot upload .spine project files directly.
713
+
714
+ You need to export from Spine Editor first:
715
+ 1. Open ${path.basename(absolutePath)} in Spine Editor
716
+ 2. Go to File \u2192 Export
717
+ 3. Export as JSON with atlas
718
+ 4. Upload the exported folder`);
719
+ }
720
+ throw new Error(`Not a directory: ${absolutePath}
721
+
722
+ The upload command expects a folder containing exported Spine files (.json, .atlas, .png).`);
709
723
  }
710
724
  const files = fs.readdirSync(absolutePath);
711
725
  const skeletons = [];
@@ -764,8 +778,8 @@ function parseAtlasTextures(atlasPath) {
764
778
  const atlasDir = path.dirname(atlasPath);
765
779
  for (const line of lines) {
766
780
  const trimmed = line.trim();
767
- if (trimmed.endsWith(".png") || trimmed.endsWith(".jpg")) {
768
- const texturePath = path.join(atlasDir, trimmed);
781
+ if (trimmed.endsWith(".png") || trimmed.endsWith(".jpg") || trimmed.endsWith(".jpeg") || trimmed.endsWith(".webp")) {
782
+ const texturePath = path.isAbsolute(trimmed) ? trimmed : path.join(atlasDir, trimmed);
769
783
  if (fs.existsSync(texturePath)) {
770
784
  textures.push(texturePath);
771
785
  }
@@ -807,8 +821,8 @@ function validateDirectory(directory, batchMode = false) {
807
821
  if (discovered.skeletons.length === 0) {
808
822
  issues.push({
809
823
  type: "error",
810
- message: "No Spine skeleton files found",
811
- detail: "Expected .json (Spine skeleton) or .skel files"
824
+ message: "No Spine skeleton files found in this directory",
825
+ detail: "Expected exported files: skeleton.json + skeleton.atlas + texture.png. Export from Spine Editor first (File \u2192 Export \u2192 JSON)."
812
826
  });
813
827
  return {
814
828
  assets: [],
@@ -1080,30 +1094,11 @@ function validateSlug(slug) {
1080
1094
  var SPINE_EXPORT_PRESET = {
1081
1095
  class: "export-json",
1082
1096
  name: "Playtagon Platform",
1083
- output: "{projectDir}/exports/{skeletonName}",
1084
- extension: ".json",
1085
- format: "json",
1097
+ // output is intentionally omitted - user must set it in Spine UI
1098
+ // This prevents path issues and allows flexibility
1086
1099
  nonessential: false,
1087
1100
  cleanUp: true,
1088
- warnings: true,
1089
- packAtlas: true,
1090
- packSource: "attachments",
1091
- packTarget: "perskeleton",
1092
- packSettings: {
1093
- maxWidth: 4096,
1094
- maxHeight: 4096,
1095
- paddingX: 2,
1096
- paddingY: 2,
1097
- edgePadding: true,
1098
- duplicatePadding: true,
1099
- rotation: true,
1100
- stripWhitespaceX: true,
1101
- stripWhitespaceY: true,
1102
- pot: false,
1103
- filterMin: "Linear",
1104
- filterMag: "Linear",
1105
- premultiplyAlpha: true
1106
- }
1101
+ pack: true
1107
1102
  };
1108
1103
 
1109
1104
  // src/commands/spine/validate.ts
@@ -1202,7 +1197,8 @@ async function uploadSpineAsset(formData) {
1202
1197
  const response = await fetch(`${API_BASE}/functions/v1/spine-upload`, {
1203
1198
  method: "POST",
1204
1199
  headers: {
1205
- Authorization: `Bearer ${token}`
1200
+ Authorization: `Bearer ${token}`,
1201
+ apikey: config.supabaseAnonKey
1206
1202
  },
1207
1203
  body: formData
1208
1204
  });
@@ -1306,6 +1302,37 @@ async function resolveGame(studioId, gameIdOrSlug) {
1306
1302
  const data = await response.json();
1307
1303
  return data[0] || null;
1308
1304
  }
1305
+ async function getSyncManifest(studioId, gameId, since) {
1306
+ const token = getAccessToken();
1307
+ if (!token) {
1308
+ throw new Error("Not authenticated. Run `playtagon login` first.");
1309
+ }
1310
+ const params = new URLSearchParams();
1311
+ params.set("studioId", studioId);
1312
+ if (gameId) params.set("gameId", gameId);
1313
+ if (since) params.set("since", since);
1314
+ const response = await fetch(
1315
+ `${API_BASE}/functions/v1/spine-assets/sync?${params}`,
1316
+ {
1317
+ headers: {
1318
+ Authorization: `Bearer ${token}`,
1319
+ apikey: config.supabaseAnonKey
1320
+ }
1321
+ }
1322
+ );
1323
+ if (!response.ok) {
1324
+ const errorText = await response.text();
1325
+ let errorMessage;
1326
+ try {
1327
+ const errorJson = JSON.parse(errorText);
1328
+ errorMessage = errorJson.error || errorJson.message || errorText;
1329
+ } catch {
1330
+ errorMessage = errorText;
1331
+ }
1332
+ throw new Error(`Failed to fetch sync manifest: ${errorMessage}`);
1333
+ }
1334
+ return response.json();
1335
+ }
1309
1336
 
1310
1337
  // src/commands/spine/upload.ts
1311
1338
  var uploadCommand = new Command5("upload").description("Upload Spine files to Playtagon").argument("<directory>", "Directory containing Spine files").option("-s, --studio <studio>", "Studio ID or slug (uses default if set)").option("-g, --game <game>", "Game ID or slug (uses default if set)").option("-n, --name <name>", "Asset name (defaults to directory name)").option("--slug <slug>", "Custom slug for the asset").option("--batch", "Enable batch mode for multiple skeletons sharing atlas").option("--dry-run", "Validate only, do not upload").option("--description <text>", "Asset description").option("--tags <tags>", "Comma-separated tags").action(async (directory, options) => {
@@ -1502,26 +1529,387 @@ var presetCommand = new Command6("preset").description("Output Spine export pres
1502
1529
  console.log(json);
1503
1530
  });
1504
1531
 
1532
+ // src/commands/spine/sync.ts
1533
+ import { Command as Command7 } from "commander";
1534
+ import * as fs4 from "fs";
1535
+ import * as path5 from "path";
1536
+ import ora5 from "ora";
1537
+
1538
+ // src/lib/codegen.ts
1539
+ function pascalCase(str) {
1540
+ return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
1541
+ }
1542
+ function escapeString(str) {
1543
+ return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"');
1544
+ }
1545
+ function generateAssetTypes(asset) {
1546
+ const name = pascalCase(asset.slug);
1547
+ const ext = asset.skeleton_format === "binary" ? ".skel" : ".json";
1548
+ const animationNames = asset.animations.map((a) => a.name);
1549
+ const skinNames = asset.skins.map((s) => s.name);
1550
+ const eventNames = asset.events.map((e) => e.name);
1551
+ return `// Auto-generated by @playtagon/cli
1552
+ // Do not edit manually - changes will be overwritten on next sync
1553
+
1554
+ /**
1555
+ * ${asset.name}
1556
+ * @version ${asset.version}
1557
+ */
1558
+
1559
+ export const ${name}Animations = {
1560
+ ${animationNames.map((n) => ` '${escapeString(n)}': '${escapeString(n)}',`).join("\n")}
1561
+ } as const;
1562
+
1563
+ export type ${name}Animation = typeof ${name}Animations[keyof typeof ${name}Animations];
1564
+
1565
+ export const ${name}Skins = {
1566
+ ${skinNames.map((n) => ` '${escapeString(n)}': '${escapeString(n)}',`).join("\n")}
1567
+ } as const;
1568
+
1569
+ export type ${name}Skin = typeof ${name}Skins[keyof typeof ${name}Skins];
1570
+
1571
+ ${eventNames.length > 0 ? `export const ${name}Events = {
1572
+ ${eventNames.map((n) => ` '${escapeString(n)}': '${escapeString(n)}',`).join("\n")}
1573
+ } as const;
1574
+
1575
+ export type ${name}Event = typeof ${name}Events[keyof typeof ${name}Events];
1576
+ ` : `export const ${name}Events = {} as const;
1577
+
1578
+ export type ${name}Event = never;
1579
+ `}
1580
+ export interface ${name}AnimationInfo {
1581
+ name: ${name}Animation;
1582
+ duration: number;
1583
+ }
1584
+
1585
+ export const ${name}AnimationDurations: Record<${name}Animation, number> = {
1586
+ ${asset.animations.map((a) => ` '${escapeString(a.name)}': ${a.duration},`).join("\n")}
1587
+ };
1588
+
1589
+ export const ${name} = {
1590
+ id: '${asset.slug}',
1591
+ name: '${escapeString(asset.name)}',
1592
+ version: ${asset.version},
1593
+ skeleton: './${asset.slug}${ext}',
1594
+ atlas: './${asset.slug}.atlas',
1595
+ animations: ${name}Animations,
1596
+ skins: ${name}Skins,
1597
+ events: ${name}Events,
1598
+ animationDurations: ${name}AnimationDurations,
1599
+ defaultAnimation: '${escapeString(animationNames[0] || "idle")}' as ${name}Animation,
1600
+ defaultSkin: '${escapeString(skinNames[0] || "default")}' as ${name}Skin,
1601
+ } as const;
1602
+
1603
+ export type ${name}Config = typeof ${name};
1604
+
1605
+ export default ${name};
1606
+ `;
1607
+ }
1608
+ function generateAssetStory(asset) {
1609
+ const name = pascalCase(asset.slug);
1610
+ const animationStories = asset.animations.map((anim) => {
1611
+ return `<Story name="${escapeString(anim.name)}">
1612
+ {#snippet template(args)}
1613
+ <StoryPixiApp {assets}>
1614
+ <SpineAnimation {...args} asset={${name}} animation="${escapeString(anim.name)}" />
1615
+ </StoryPixiApp>
1616
+ {/snippet}
1617
+ </Story>`;
1618
+ }).join("\n\n");
1619
+ return `<!-- Auto-generated by @playtagon/cli -->
1620
+ <!-- Do not edit manually - changes will be overwritten on next sync -->
1621
+
1622
+ <script lang="ts" module>
1623
+ import { defineMeta } from '@storybook/addon-svelte-csf';
1624
+ import { SpineAnimation, createSpineAssets } from 'pixi-svelte';
1625
+ import { StoryPixiApp } from 'components-storybook';
1626
+ import ${name}, { ${name}Animations, ${name}Skins } from './index';
1627
+
1628
+ const { Story } = defineMeta({
1629
+ title: 'Spine/${escapeString(asset.name)}',
1630
+ args: {
1631
+ width: 400,
1632
+ height: 400,
1633
+ x: 200,
1634
+ y: 200,
1635
+ loop: true,
1636
+ speed: 1,
1637
+ },
1638
+ argTypes: {
1639
+ animation: {
1640
+ control: 'select',
1641
+ options: Object.values(${name}Animations),
1642
+ description: 'Animation to play',
1643
+ },
1644
+ skin: {
1645
+ control: 'select',
1646
+ options: Object.values(${name}Skins),
1647
+ description: 'Skin to apply',
1648
+ },
1649
+ loop: {
1650
+ control: 'boolean',
1651
+ description: 'Whether to loop the animation',
1652
+ },
1653
+ speed: {
1654
+ control: { type: 'range', min: 0.1, max: 3, step: 0.1 },
1655
+ description: 'Playback speed multiplier',
1656
+ },
1657
+ },
1658
+ });
1659
+
1660
+ // Create assets for Storybook - adjust baseUrl to match your asset serving path
1661
+ const assets = createSpineAssets([${name}], '.');
1662
+ </script>
1663
+
1664
+ <!-- Default story with all controls -->
1665
+ <Story name="Default">
1666
+ {#snippet template(args)}
1667
+ <StoryPixiApp {assets}>
1668
+ <SpineAnimation
1669
+ {...args}
1670
+ asset={${name}}
1671
+ animation={args.animation ?? ${name}.defaultAnimation}
1672
+ skin={args.skin ?? ${name}.defaultSkin}
1673
+ />
1674
+ </StoryPixiApp>
1675
+ {/snippet}
1676
+ </Story>
1677
+
1678
+ <!-- Individual animation stories -->
1679
+ ${animationStories}
1680
+ `;
1681
+ }
1682
+ function generateManifest(slugs) {
1683
+ const imports = slugs.map((slug) => {
1684
+ const name = pascalCase(slug);
1685
+ return `import ${name} from './${slug}';`;
1686
+ }).join("\n");
1687
+ const exports = slugs.map((slug) => {
1688
+ const name = pascalCase(slug);
1689
+ return ` ${name},`;
1690
+ }).join("\n");
1691
+ const reexports = slugs.map((slug) => {
1692
+ const name = pascalCase(slug);
1693
+ return `export * from './${slug}';`;
1694
+ }).join("\n");
1695
+ return `// Auto-generated by @playtagon/cli
1696
+ // Do not edit manually - changes will be overwritten on next sync
1697
+
1698
+ ${imports}
1699
+
1700
+ export const SpineAssets = {
1701
+ ${exports}
1702
+ } as const;
1703
+
1704
+ export type SpineAssetName = keyof typeof SpineAssets;
1705
+
1706
+ // Re-export all individual assets
1707
+ ${reexports}
1708
+
1709
+ export default SpineAssets;
1710
+ `;
1711
+ }
1712
+
1713
+ // src/commands/spine/sync.ts
1714
+ var syncCommand = new Command7("sync").description("Sync approved Spine assets to local project").option("-s, --studio <studio>", "Studio ID or slug (uses default if set)").option("-g, --game <game>", "Game ID or slug (uses default if set)").option("-o, --output <dir>", "Output directory", "./src/assets/spine").option("--types", "Generate TypeScript types (default: true)", true).option("--no-types", "Skip TypeScript type generation").option("--stories", "Generate Storybook stories (default: true)", true).option("--no-stories", "Skip Storybook story generation").option("--assets <slugs>", "Comma-separated asset slugs to sync (default: all)").option("--force", "Overwrite local changes").option("--dry-run", "Show what would be synced without making changes").action(async (options) => {
1715
+ if (!credentials.isLoggedIn()) {
1716
+ logger.error("Not logged in.");
1717
+ logger.info(`Run ${logger.command("playtagon login")} first.`);
1718
+ process.exit(1);
1719
+ }
1720
+ const studioOption = options.studio || config.defaultStudio;
1721
+ const gameOption = options.game || config.defaultGame;
1722
+ if (!studioOption) {
1723
+ logger.error("Studio is required.");
1724
+ logger.info(`Either provide ${logger.command("--studio <slug>")} or set a default:`);
1725
+ logger.info(` ${logger.command("playtagon config --studio <slug>")}`);
1726
+ process.exit(1);
1727
+ }
1728
+ const outputDir = path5.resolve(options.output);
1729
+ const manifestPath = path5.join(outputDir, ".sync-manifest.json");
1730
+ const spinner = ora5("Resolving studio...").start();
1731
+ const studio = await resolveStudio(studioOption);
1732
+ if (!studio) {
1733
+ spinner.fail("Studio not found");
1734
+ logger.error(`Studio "${studioOption}" not found or you don't have access.`);
1735
+ process.exit(1);
1736
+ }
1737
+ spinner.succeed(`Studio: ${studio.name}`);
1738
+ let game = null;
1739
+ if (gameOption) {
1740
+ spinner.start("Resolving game...");
1741
+ game = await resolveGame(studio.id, gameOption);
1742
+ if (!game) {
1743
+ spinner.fail("Game not found");
1744
+ logger.error(`Game "${gameOption}" not found in studio "${studio.name}".`);
1745
+ process.exit(1);
1746
+ }
1747
+ spinner.succeed(`Game: ${game.name}`);
1748
+ }
1749
+ let existingManifest = null;
1750
+ let since;
1751
+ if (fs4.existsSync(manifestPath) && !options.force) {
1752
+ try {
1753
+ existingManifest = JSON.parse(fs4.readFileSync(manifestPath, "utf-8"));
1754
+ if (existingManifest && existingManifest.studioId === studio.id) {
1755
+ since = existingManifest.syncedAt;
1756
+ logger.info(`Incremental sync from ${new Date(since).toLocaleString()}`);
1757
+ }
1758
+ } catch {
1759
+ }
1760
+ }
1761
+ spinner.start("Fetching asset manifest...");
1762
+ let manifest;
1763
+ try {
1764
+ manifest = await getSyncManifest(studio.id, game?.id, options.force ? void 0 : since);
1765
+ } catch (error) {
1766
+ spinner.fail("Failed to fetch manifest");
1767
+ logger.error(error instanceof Error ? error.message : "Unknown error");
1768
+ process.exit(1);
1769
+ }
1770
+ const assets = manifest.assets;
1771
+ let assetsToSync = assets;
1772
+ if (options.assets) {
1773
+ const requestedSlugs = options.assets.split(",").map((s) => s.trim());
1774
+ assetsToSync = assets.filter((a) => requestedSlugs.includes(a.slug));
1775
+ const found = assetsToSync.map((a) => a.slug);
1776
+ const notFound = requestedSlugs.filter((s) => !found.includes(s));
1777
+ if (notFound.length > 0) {
1778
+ logger.warn(`Assets not found (or not approved): ${notFound.join(", ")}`);
1779
+ }
1780
+ }
1781
+ if (assetsToSync.length === 0) {
1782
+ spinner.succeed("No assets to sync");
1783
+ logger.info("All assets are up to date or no approved assets found.");
1784
+ return;
1785
+ }
1786
+ spinner.succeed(`Found ${assetsToSync.length} asset(s) to sync`);
1787
+ const totalSize = assetsToSync.reduce((sum, a) => sum + a.total_size_bytes, 0);
1788
+ console.log();
1789
+ logger.header("Sync Summary");
1790
+ logger.item("Studio", studio.name);
1791
+ if (game) logger.item("Game", game.name);
1792
+ logger.item("Output", outputDir);
1793
+ logger.item("Assets", String(assetsToSync.length));
1794
+ logger.item("Total Size", formatFileSize(totalSize));
1795
+ logger.item("TypeScript Types", options.types ? "Yes" : "No");
1796
+ logger.item("Storybook Stories", options.stories ? "Yes" : "No");
1797
+ console.log();
1798
+ logger.header("Assets");
1799
+ for (const asset of assetsToSync) {
1800
+ console.log(` ${logger.value(asset.name)} (${asset.slug}) v${asset.version}`);
1801
+ }
1802
+ if (options.dryRun) {
1803
+ console.log();
1804
+ logger.success("Dry run complete. No files were written.");
1805
+ return;
1806
+ }
1807
+ if (!fs4.existsSync(outputDir)) {
1808
+ fs4.mkdirSync(outputDir, { recursive: true });
1809
+ }
1810
+ console.log();
1811
+ const syncedAssets = {};
1812
+ for (const asset of assetsToSync) {
1813
+ spinner.start(`Syncing ${asset.name}...`);
1814
+ try {
1815
+ const assetDir = path5.join(outputDir, asset.slug);
1816
+ if (!fs4.existsSync(assetDir)) {
1817
+ fs4.mkdirSync(assetDir, { recursive: true });
1818
+ }
1819
+ const skeletonExt = asset.skeleton_format === "binary" ? ".skel" : ".json";
1820
+ const skeletonPath = path5.join(assetDir, `${asset.slug}${skeletonExt}`);
1821
+ await downloadFile(asset.skeleton_url, skeletonPath);
1822
+ const atlasPath = path5.join(assetDir, `${asset.slug}.atlas`);
1823
+ await downloadFile(asset.atlas_url, atlasPath);
1824
+ for (const textureUrl of asset.texture_urls) {
1825
+ const textureName = path5.basename(new URL(textureUrl).pathname);
1826
+ const texturePath = path5.join(assetDir, textureName);
1827
+ await downloadFile(textureUrl, texturePath);
1828
+ }
1829
+ if (options.types) {
1830
+ const typesContent = generateAssetTypes(asset);
1831
+ const typesPath = path5.join(assetDir, "index.ts");
1832
+ fs4.writeFileSync(typesPath, typesContent);
1833
+ }
1834
+ if (options.stories) {
1835
+ const storyContent = generateAssetStory(asset);
1836
+ const storyPath = path5.join(assetDir, `${pascalCase2(asset.slug)}.stories.svelte`);
1837
+ fs4.writeFileSync(storyPath, storyContent);
1838
+ }
1839
+ syncedAssets[asset.slug] = {
1840
+ id: asset.id,
1841
+ version: asset.version,
1842
+ checksum: generateChecksum(asset),
1843
+ updatedAt: asset.updated_at
1844
+ };
1845
+ spinner.succeed(`Synced ${asset.name}`);
1846
+ } catch (error) {
1847
+ spinner.fail(`Failed to sync ${asset.name}`);
1848
+ logger.error(error instanceof Error ? error.message : "Unknown error");
1849
+ }
1850
+ }
1851
+ const finalAssets = {
1852
+ ...existingManifest?.assets || {},
1853
+ ...syncedAssets
1854
+ };
1855
+ if (options.types) {
1856
+ spinner.start("Generating manifest...");
1857
+ const manifestContent = generateManifest(Object.keys(finalAssets));
1858
+ fs4.writeFileSync(path5.join(outputDir, "manifest.ts"), manifestContent);
1859
+ spinner.succeed("Generated manifest.ts");
1860
+ }
1861
+ const newManifest = {
1862
+ studioId: studio.id,
1863
+ gameId: game?.id,
1864
+ syncedAt: manifest.syncedAt,
1865
+ assets: finalAssets
1866
+ };
1867
+ fs4.writeFileSync(manifestPath, JSON.stringify(newManifest, null, 2));
1868
+ console.log();
1869
+ logger.success(`Synced ${Object.keys(syncedAssets).length} asset(s) to ${outputDir}`);
1870
+ });
1871
+ async function downloadFile(url, destPath) {
1872
+ const response = await fetch(url);
1873
+ if (!response.ok) {
1874
+ throw new Error(`Failed to download: ${response.status} ${response.statusText}`);
1875
+ }
1876
+ const buffer = await response.arrayBuffer();
1877
+ fs4.writeFileSync(destPath, Buffer.from(buffer));
1878
+ }
1879
+ function generateChecksum(asset) {
1880
+ const data = `${asset.id}:${asset.version}:${asset.updated_at}`;
1881
+ let hash = 0;
1882
+ for (let i = 0; i < data.length; i++) {
1883
+ const char = data.charCodeAt(i);
1884
+ hash = (hash << 5) - hash + char;
1885
+ hash = hash & hash;
1886
+ }
1887
+ return Math.abs(hash).toString(16);
1888
+ }
1889
+ function pascalCase2(str) {
1890
+ return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
1891
+ }
1892
+
1505
1893
  // src/commands/spine/index.ts
1506
- var spineCommand = new Command7("spine").description("Manage Spine animation assets").addCommand(validateCommand).addCommand(uploadCommand).addCommand(presetCommand);
1894
+ var spineCommand = new Command8("spine").description("Manage Spine animation assets").addCommand(validateCommand).addCommand(uploadCommand).addCommand(presetCommand).addCommand(syncCommand);
1507
1895
 
1508
1896
  // src/commands/setup.ts
1509
- import { Command as Command8 } from "commander";
1510
- import * as fs4 from "fs";
1511
- import * as path5 from "path";
1897
+ import { Command as Command9 } from "commander";
1898
+ import * as fs5 from "fs";
1899
+ import * as path6 from "path";
1512
1900
  import * as os from "os";
1513
1901
  import { execFile } from "child_process";
1514
- import ora5 from "ora";
1515
- var PLAYTAGON_DIR = path5.join(os.homedir(), ".playtagon");
1516
- var setupCommand = new Command8("setup").description("Set up integrations").addCommand(spineIntegrationCommand());
1902
+ import ora6 from "ora";
1903
+ var PLAYTAGON_DIR = path6.join(os.homedir(), ".playtagon");
1904
+ var setupCommand = new Command9("setup").description("Set up integrations").addCommand(spineIntegrationCommand());
1517
1905
  function spineIntegrationCommand() {
1518
- return new Command8("spine-integration").description("Set up automatic Spine Editor to Platform upload").option("-s, --studio <studio>", "Default studio for uploads").option("-g, --game <game>", "Default game for uploads").option("--force", "Overwrite existing setup files").action(async (options) => {
1906
+ return new Command9("spine-integration").description("Set up automatic Spine Editor to Platform upload").option("-s, --studio <studio>", "Default studio for uploads").option("-g, --game <game>", "Default game for uploads").option("--force", "Overwrite existing setup files").action(async (options) => {
1519
1907
  if (!credentials.isLoggedIn()) {
1520
1908
  logger.error("Not logged in.");
1521
1909
  logger.info(`Run ${logger.command("playtagon login")} first.`);
1522
1910
  process.exit(1);
1523
1911
  }
1524
- const spinner = ora5("Checking authentication...").start();
1912
+ const spinner = ora6("Checking authentication...").start();
1525
1913
  const user = await getCurrentUser();
1526
1914
  if (!user) {
1527
1915
  spinner.fail("Session expired");
@@ -1553,8 +1941,8 @@ function spineIntegrationCommand() {
1553
1941
  }
1554
1942
  }
1555
1943
  spinner.start("Creating setup files...");
1556
- if (!fs4.existsSync(PLAYTAGON_DIR)) {
1557
- fs4.mkdirSync(PLAYTAGON_DIR, { recursive: true, mode: 448 });
1944
+ if (!fs5.existsSync(PLAYTAGON_DIR)) {
1945
+ fs5.mkdirSync(PLAYTAGON_DIR, { recursive: true, mode: 448 });
1558
1946
  }
1559
1947
  const scriptPaths = generatePostExportScripts(studioSlug, options.game, options.force);
1560
1948
  const presetPath = generateExportPreset(scriptPaths.sh, options.force);
@@ -1623,8 +2011,8 @@ function spineIntegrationCommand() {
1623
2011
  });
1624
2012
  }
1625
2013
  function generatePostExportScripts(studioSlug, gameSlug, force = false) {
1626
- const shPath = path5.join(PLAYTAGON_DIR, "upload.sh");
1627
- const batPath = path5.join(PLAYTAGON_DIR, "upload.bat");
2014
+ const shPath = path6.join(PLAYTAGON_DIR, "upload.sh");
2015
+ const batPath = path6.join(PLAYTAGON_DIR, "upload.bat");
1628
2016
  let uploadCmd = `playtagon spine upload "$1" --studio ${studioSlug}`;
1629
2017
  if (gameSlug) {
1630
2018
  uploadCmd += ` --game ${gameSlug}`;
@@ -1686,25 +2074,25 @@ powershell -Command "& {Add-Type -AssemblyName System.Windows.Forms; [System.Win
1686
2074
 
1687
2075
  echo Upload complete!
1688
2076
  `;
1689
- if (!fs4.existsSync(shPath) || force) {
1690
- fs4.writeFileSync(shPath, shScript, { mode: 493 });
2077
+ if (!fs5.existsSync(shPath) || force) {
2078
+ fs5.writeFileSync(shPath, shScript, { mode: 493 });
1691
2079
  } else {
1692
2080
  logger.warn(`${shPath} already exists. Use --force to overwrite.`);
1693
2081
  }
1694
- if (!fs4.existsSync(batPath) || force) {
1695
- fs4.writeFileSync(batPath, batScript);
2082
+ if (!fs5.existsSync(batPath) || force) {
2083
+ fs5.writeFileSync(batPath, batScript);
1696
2084
  }
1697
2085
  return { sh: shPath, bat: batPath };
1698
2086
  }
1699
2087
  function generateExportPreset(scriptPath, force = false) {
1700
- const presetPath = path5.join(PLAYTAGON_DIR, "playtagon-spine-preset.export.json");
2088
+ const presetPath = path6.join(PLAYTAGON_DIR, "playtagon-spine-preset.export.json");
1701
2089
  const preset = {
1702
2090
  ...SPINE_EXPORT_PRESET,
1703
2091
  name: "Upload to Playtagon",
1704
2092
  postScript: process.platform === "win32" ? `"${scriptPath.replace(".sh", ".bat")}" "{output}"` : `"${scriptPath}" "{output}"`
1705
2093
  };
1706
- if (!fs4.existsSync(presetPath) || force) {
1707
- fs4.writeFileSync(presetPath, JSON.stringify(preset, null, 2));
2094
+ if (!fs5.existsSync(presetPath) || force) {
2095
+ fs5.writeFileSync(presetPath, JSON.stringify(preset, null, 2));
1708
2096
  } else {
1709
2097
  logger.warn(`${presetPath} already exists. Use --force to overwrite.`);
1710
2098
  }
@@ -1732,9 +2120,9 @@ function openFolder(folderPath) {
1732
2120
  }
1733
2121
 
1734
2122
  // src/commands/config.ts
1735
- import { Command as Command9 } from "commander";
1736
- import ora6 from "ora";
1737
- var configCommand = new Command9("config").description("View or set CLI configuration").option("-s, --studio <studio>", "Set default studio").option("-g, --game <game>", "Set default game").option("--clear", "Clear all default settings").action(async (options) => {
2123
+ import { Command as Command10 } from "commander";
2124
+ import ora7 from "ora";
2125
+ var configCommand = new Command10("config").description("View or set CLI configuration").option("-s, --studio <studio>", "Set default studio").option("-g, --game <game>", "Set default game").option("--clear", "Clear all default settings").action(async (options) => {
1738
2126
  if (options.clear) {
1739
2127
  config.defaultStudio = void 0;
1740
2128
  config.defaultGame = void 0;
@@ -1747,7 +2135,7 @@ var configCommand = new Command9("config").description("View or set CLI configur
1747
2135
  logger.info(`Run ${logger.command("playtagon login")} first.`);
1748
2136
  process.exit(1);
1749
2137
  }
1750
- const spinner = ora6("Verifying studio...").start();
2138
+ const spinner = ora7("Verifying studio...").start();
1751
2139
  const studios = await getStudios();
1752
2140
  const studio = studios.find(
1753
2141
  (s) => s.slug === options.studio || s.id === options.studio || s.name === options.studio
@@ -1777,7 +2165,7 @@ var configCommand = new Command9("config").description("View or set CLI configur
1777
2165
  logger.info(`Either provide ${logger.command("--studio <slug>")} or set a default studio first.`);
1778
2166
  process.exit(1);
1779
2167
  }
1780
- const spinner = ora6("Verifying game...").start();
2168
+ const spinner = ora7("Verifying game...").start();
1781
2169
  const studios = await getStudios();
1782
2170
  const studio = studios.find(
1783
2171
  (s) => s.slug === studioSlug || s.id === studioSlug || s.name === studioSlug
@@ -1837,8 +2225,8 @@ var configCommand = new Command9("config").description("View or set CLI configur
1837
2225
  });
1838
2226
 
1839
2227
  // src/index.ts
1840
- var program = new Command10();
1841
- program.name("playtagon").description("Playtagon CLI - Upload and manage game assets").version("0.2.9");
2228
+ var program = new Command11();
2229
+ program.name("playtagon").description("Playtagon CLI - Upload and manage game assets").version("0.3.0");
1842
2230
  program.addCommand(loginCommand);
1843
2231
  program.addCommand(logoutCommand);
1844
2232
  program.addCommand(whoamiCommand);