@playtagon/cli 0.2.8 → 0.3.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/README.md +61 -0
- package/dist/index.js +484 -76
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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 || "
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
+
resolve5({ success: false, error: `Port ${port} is already in use. Please close any applications using it.` });
|
|
341
341
|
} else {
|
|
342
342
|
cleanup();
|
|
343
|
-
|
|
343
|
+
resolve5({ success: false, error: err.message });
|
|
344
344
|
}
|
|
345
345
|
});
|
|
346
346
|
timeoutId = setTimeout(() => {
|
|
347
347
|
cleanup();
|
|
348
|
-
|
|
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((
|
|
501
|
-
rl.question(prompt,
|
|
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((
|
|
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
|
-
|
|
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
|
|
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: ${
|
|
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
|
-
|
|
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
|
|
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,10 @@ function validateSlug(slug) {
|
|
|
1080
1094
|
var SPINE_EXPORT_PRESET = {
|
|
1081
1095
|
class: "export-json",
|
|
1082
1096
|
name: "Playtagon Platform",
|
|
1083
|
-
output: "{projectDir}/exports
|
|
1084
|
-
extension: ".json",
|
|
1085
|
-
format: "json",
|
|
1097
|
+
output: "{projectDir}/exports",
|
|
1086
1098
|
nonessential: false,
|
|
1087
1099
|
cleanUp: true,
|
|
1088
|
-
|
|
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
|
-
}
|
|
1100
|
+
pack: true
|
|
1107
1101
|
};
|
|
1108
1102
|
|
|
1109
1103
|
// src/commands/spine/validate.ts
|
|
@@ -1202,7 +1196,8 @@ async function uploadSpineAsset(formData) {
|
|
|
1202
1196
|
const response = await fetch(`${API_BASE}/functions/v1/spine-upload`, {
|
|
1203
1197
|
method: "POST",
|
|
1204
1198
|
headers: {
|
|
1205
|
-
Authorization: `Bearer ${token}
|
|
1199
|
+
Authorization: `Bearer ${token}`,
|
|
1200
|
+
apikey: config.supabaseAnonKey
|
|
1206
1201
|
},
|
|
1207
1202
|
body: formData
|
|
1208
1203
|
});
|
|
@@ -1306,6 +1301,37 @@ async function resolveGame(studioId, gameIdOrSlug) {
|
|
|
1306
1301
|
const data = await response.json();
|
|
1307
1302
|
return data[0] || null;
|
|
1308
1303
|
}
|
|
1304
|
+
async function getSyncManifest(studioId, gameId, since) {
|
|
1305
|
+
const token = getAccessToken();
|
|
1306
|
+
if (!token) {
|
|
1307
|
+
throw new Error("Not authenticated. Run `playtagon login` first.");
|
|
1308
|
+
}
|
|
1309
|
+
const params = new URLSearchParams();
|
|
1310
|
+
params.set("studioId", studioId);
|
|
1311
|
+
if (gameId) params.set("gameId", gameId);
|
|
1312
|
+
if (since) params.set("since", since);
|
|
1313
|
+
const response = await fetch(
|
|
1314
|
+
`${API_BASE}/functions/v1/spine-assets/sync?${params}`,
|
|
1315
|
+
{
|
|
1316
|
+
headers: {
|
|
1317
|
+
Authorization: `Bearer ${token}`,
|
|
1318
|
+
apikey: config.supabaseAnonKey
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
);
|
|
1322
|
+
if (!response.ok) {
|
|
1323
|
+
const errorText = await response.text();
|
|
1324
|
+
let errorMessage;
|
|
1325
|
+
try {
|
|
1326
|
+
const errorJson = JSON.parse(errorText);
|
|
1327
|
+
errorMessage = errorJson.error || errorJson.message || errorText;
|
|
1328
|
+
} catch {
|
|
1329
|
+
errorMessage = errorText;
|
|
1330
|
+
}
|
|
1331
|
+
throw new Error(`Failed to fetch sync manifest: ${errorMessage}`);
|
|
1332
|
+
}
|
|
1333
|
+
return response.json();
|
|
1334
|
+
}
|
|
1309
1335
|
|
|
1310
1336
|
// src/commands/spine/upload.ts
|
|
1311
1337
|
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,25 +1528,387 @@ var presetCommand = new Command6("preset").description("Output Spine export pres
|
|
|
1502
1528
|
console.log(json);
|
|
1503
1529
|
});
|
|
1504
1530
|
|
|
1531
|
+
// src/commands/spine/sync.ts
|
|
1532
|
+
import { Command as Command7 } from "commander";
|
|
1533
|
+
import * as fs4 from "fs";
|
|
1534
|
+
import * as path5 from "path";
|
|
1535
|
+
import ora5 from "ora";
|
|
1536
|
+
|
|
1537
|
+
// src/lib/codegen.ts
|
|
1538
|
+
function pascalCase(str) {
|
|
1539
|
+
return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
1540
|
+
}
|
|
1541
|
+
function escapeString(str) {
|
|
1542
|
+
return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"');
|
|
1543
|
+
}
|
|
1544
|
+
function generateAssetTypes(asset) {
|
|
1545
|
+
const name = pascalCase(asset.slug);
|
|
1546
|
+
const ext = asset.skeleton_format === "binary" ? ".skel" : ".json";
|
|
1547
|
+
const animationNames = asset.animations.map((a) => a.name);
|
|
1548
|
+
const skinNames = asset.skins.map((s) => s.name);
|
|
1549
|
+
const eventNames = asset.events.map((e) => e.name);
|
|
1550
|
+
return `// Auto-generated by @playtagon/cli
|
|
1551
|
+
// Do not edit manually - changes will be overwritten on next sync
|
|
1552
|
+
|
|
1553
|
+
/**
|
|
1554
|
+
* ${asset.name}
|
|
1555
|
+
* @version ${asset.version}
|
|
1556
|
+
*/
|
|
1557
|
+
|
|
1558
|
+
export const ${name}Animations = {
|
|
1559
|
+
${animationNames.map((n) => ` '${escapeString(n)}': '${escapeString(n)}',`).join("\n")}
|
|
1560
|
+
} as const;
|
|
1561
|
+
|
|
1562
|
+
export type ${name}Animation = typeof ${name}Animations[keyof typeof ${name}Animations];
|
|
1563
|
+
|
|
1564
|
+
export const ${name}Skins = {
|
|
1565
|
+
${skinNames.map((n) => ` '${escapeString(n)}': '${escapeString(n)}',`).join("\n")}
|
|
1566
|
+
} as const;
|
|
1567
|
+
|
|
1568
|
+
export type ${name}Skin = typeof ${name}Skins[keyof typeof ${name}Skins];
|
|
1569
|
+
|
|
1570
|
+
${eventNames.length > 0 ? `export const ${name}Events = {
|
|
1571
|
+
${eventNames.map((n) => ` '${escapeString(n)}': '${escapeString(n)}',`).join("\n")}
|
|
1572
|
+
} as const;
|
|
1573
|
+
|
|
1574
|
+
export type ${name}Event = typeof ${name}Events[keyof typeof ${name}Events];
|
|
1575
|
+
` : `export const ${name}Events = {} as const;
|
|
1576
|
+
|
|
1577
|
+
export type ${name}Event = never;
|
|
1578
|
+
`}
|
|
1579
|
+
export interface ${name}AnimationInfo {
|
|
1580
|
+
name: ${name}Animation;
|
|
1581
|
+
duration: number;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
export const ${name}AnimationDurations: Record<${name}Animation, number> = {
|
|
1585
|
+
${asset.animations.map((a) => ` '${escapeString(a.name)}': ${a.duration},`).join("\n")}
|
|
1586
|
+
};
|
|
1587
|
+
|
|
1588
|
+
export const ${name} = {
|
|
1589
|
+
id: '${asset.slug}',
|
|
1590
|
+
name: '${escapeString(asset.name)}',
|
|
1591
|
+
version: ${asset.version},
|
|
1592
|
+
skeleton: './${asset.slug}${ext}',
|
|
1593
|
+
atlas: './${asset.slug}.atlas',
|
|
1594
|
+
animations: ${name}Animations,
|
|
1595
|
+
skins: ${name}Skins,
|
|
1596
|
+
events: ${name}Events,
|
|
1597
|
+
animationDurations: ${name}AnimationDurations,
|
|
1598
|
+
defaultAnimation: '${escapeString(animationNames[0] || "idle")}' as ${name}Animation,
|
|
1599
|
+
defaultSkin: '${escapeString(skinNames[0] || "default")}' as ${name}Skin,
|
|
1600
|
+
} as const;
|
|
1601
|
+
|
|
1602
|
+
export type ${name}Config = typeof ${name};
|
|
1603
|
+
|
|
1604
|
+
export default ${name};
|
|
1605
|
+
`;
|
|
1606
|
+
}
|
|
1607
|
+
function generateAssetStory(asset) {
|
|
1608
|
+
const name = pascalCase(asset.slug);
|
|
1609
|
+
const animationStories = asset.animations.map((anim) => {
|
|
1610
|
+
return `<Story name="${escapeString(anim.name)}">
|
|
1611
|
+
{#snippet template(args)}
|
|
1612
|
+
<StoryPixiApp {assets}>
|
|
1613
|
+
<SpineAnimation {...args} asset={${name}} animation="${escapeString(anim.name)}" />
|
|
1614
|
+
</StoryPixiApp>
|
|
1615
|
+
{/snippet}
|
|
1616
|
+
</Story>`;
|
|
1617
|
+
}).join("\n\n");
|
|
1618
|
+
return `<!-- Auto-generated by @playtagon/cli -->
|
|
1619
|
+
<!-- Do not edit manually - changes will be overwritten on next sync -->
|
|
1620
|
+
|
|
1621
|
+
<script lang="ts" module>
|
|
1622
|
+
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
1623
|
+
import { SpineAnimation, createSpineAssets } from 'pixi-svelte';
|
|
1624
|
+
import { StoryPixiApp } from 'components-storybook';
|
|
1625
|
+
import ${name}, { ${name}Animations, ${name}Skins } from './index';
|
|
1626
|
+
|
|
1627
|
+
const { Story } = defineMeta({
|
|
1628
|
+
title: 'Spine/${escapeString(asset.name)}',
|
|
1629
|
+
args: {
|
|
1630
|
+
width: 400,
|
|
1631
|
+
height: 400,
|
|
1632
|
+
x: 200,
|
|
1633
|
+
y: 200,
|
|
1634
|
+
loop: true,
|
|
1635
|
+
speed: 1,
|
|
1636
|
+
},
|
|
1637
|
+
argTypes: {
|
|
1638
|
+
animation: {
|
|
1639
|
+
control: 'select',
|
|
1640
|
+
options: Object.values(${name}Animations),
|
|
1641
|
+
description: 'Animation to play',
|
|
1642
|
+
},
|
|
1643
|
+
skin: {
|
|
1644
|
+
control: 'select',
|
|
1645
|
+
options: Object.values(${name}Skins),
|
|
1646
|
+
description: 'Skin to apply',
|
|
1647
|
+
},
|
|
1648
|
+
loop: {
|
|
1649
|
+
control: 'boolean',
|
|
1650
|
+
description: 'Whether to loop the animation',
|
|
1651
|
+
},
|
|
1652
|
+
speed: {
|
|
1653
|
+
control: { type: 'range', min: 0.1, max: 3, step: 0.1 },
|
|
1654
|
+
description: 'Playback speed multiplier',
|
|
1655
|
+
},
|
|
1656
|
+
},
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
// Create assets for Storybook - adjust baseUrl to match your asset serving path
|
|
1660
|
+
const assets = createSpineAssets([${name}], '.');
|
|
1661
|
+
</script>
|
|
1662
|
+
|
|
1663
|
+
<!-- Default story with all controls -->
|
|
1664
|
+
<Story name="Default">
|
|
1665
|
+
{#snippet template(args)}
|
|
1666
|
+
<StoryPixiApp {assets}>
|
|
1667
|
+
<SpineAnimation
|
|
1668
|
+
{...args}
|
|
1669
|
+
asset={${name}}
|
|
1670
|
+
animation={args.animation ?? ${name}.defaultAnimation}
|
|
1671
|
+
skin={args.skin ?? ${name}.defaultSkin}
|
|
1672
|
+
/>
|
|
1673
|
+
</StoryPixiApp>
|
|
1674
|
+
{/snippet}
|
|
1675
|
+
</Story>
|
|
1676
|
+
|
|
1677
|
+
<!-- Individual animation stories -->
|
|
1678
|
+
${animationStories}
|
|
1679
|
+
`;
|
|
1680
|
+
}
|
|
1681
|
+
function generateManifest(slugs) {
|
|
1682
|
+
const imports = slugs.map((slug) => {
|
|
1683
|
+
const name = pascalCase(slug);
|
|
1684
|
+
return `import ${name} from './${slug}';`;
|
|
1685
|
+
}).join("\n");
|
|
1686
|
+
const exports = slugs.map((slug) => {
|
|
1687
|
+
const name = pascalCase(slug);
|
|
1688
|
+
return ` ${name},`;
|
|
1689
|
+
}).join("\n");
|
|
1690
|
+
const reexports = slugs.map((slug) => {
|
|
1691
|
+
const name = pascalCase(slug);
|
|
1692
|
+
return `export * from './${slug}';`;
|
|
1693
|
+
}).join("\n");
|
|
1694
|
+
return `// Auto-generated by @playtagon/cli
|
|
1695
|
+
// Do not edit manually - changes will be overwritten on next sync
|
|
1696
|
+
|
|
1697
|
+
${imports}
|
|
1698
|
+
|
|
1699
|
+
export const SpineAssets = {
|
|
1700
|
+
${exports}
|
|
1701
|
+
} as const;
|
|
1702
|
+
|
|
1703
|
+
export type SpineAssetName = keyof typeof SpineAssets;
|
|
1704
|
+
|
|
1705
|
+
// Re-export all individual assets
|
|
1706
|
+
${reexports}
|
|
1707
|
+
|
|
1708
|
+
export default SpineAssets;
|
|
1709
|
+
`;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// src/commands/spine/sync.ts
|
|
1713
|
+
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) => {
|
|
1714
|
+
if (!credentials.isLoggedIn()) {
|
|
1715
|
+
logger.error("Not logged in.");
|
|
1716
|
+
logger.info(`Run ${logger.command("playtagon login")} first.`);
|
|
1717
|
+
process.exit(1);
|
|
1718
|
+
}
|
|
1719
|
+
const studioOption = options.studio || config.defaultStudio;
|
|
1720
|
+
const gameOption = options.game || config.defaultGame;
|
|
1721
|
+
if (!studioOption) {
|
|
1722
|
+
logger.error("Studio is required.");
|
|
1723
|
+
logger.info(`Either provide ${logger.command("--studio <slug>")} or set a default:`);
|
|
1724
|
+
logger.info(` ${logger.command("playtagon config --studio <slug>")}`);
|
|
1725
|
+
process.exit(1);
|
|
1726
|
+
}
|
|
1727
|
+
const outputDir = path5.resolve(options.output);
|
|
1728
|
+
const manifestPath = path5.join(outputDir, ".sync-manifest.json");
|
|
1729
|
+
const spinner = ora5("Resolving studio...").start();
|
|
1730
|
+
const studio = await resolveStudio(studioOption);
|
|
1731
|
+
if (!studio) {
|
|
1732
|
+
spinner.fail("Studio not found");
|
|
1733
|
+
logger.error(`Studio "${studioOption}" not found or you don't have access.`);
|
|
1734
|
+
process.exit(1);
|
|
1735
|
+
}
|
|
1736
|
+
spinner.succeed(`Studio: ${studio.name}`);
|
|
1737
|
+
let game = null;
|
|
1738
|
+
if (gameOption) {
|
|
1739
|
+
spinner.start("Resolving game...");
|
|
1740
|
+
game = await resolveGame(studio.id, gameOption);
|
|
1741
|
+
if (!game) {
|
|
1742
|
+
spinner.fail("Game not found");
|
|
1743
|
+
logger.error(`Game "${gameOption}" not found in studio "${studio.name}".`);
|
|
1744
|
+
process.exit(1);
|
|
1745
|
+
}
|
|
1746
|
+
spinner.succeed(`Game: ${game.name}`);
|
|
1747
|
+
}
|
|
1748
|
+
let existingManifest = null;
|
|
1749
|
+
let since;
|
|
1750
|
+
if (fs4.existsSync(manifestPath) && !options.force) {
|
|
1751
|
+
try {
|
|
1752
|
+
existingManifest = JSON.parse(fs4.readFileSync(manifestPath, "utf-8"));
|
|
1753
|
+
if (existingManifest && existingManifest.studioId === studio.id) {
|
|
1754
|
+
since = existingManifest.syncedAt;
|
|
1755
|
+
logger.info(`Incremental sync from ${new Date(since).toLocaleString()}`);
|
|
1756
|
+
}
|
|
1757
|
+
} catch {
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
spinner.start("Fetching asset manifest...");
|
|
1761
|
+
let manifest;
|
|
1762
|
+
try {
|
|
1763
|
+
manifest = await getSyncManifest(studio.id, game?.id, options.force ? void 0 : since);
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
spinner.fail("Failed to fetch manifest");
|
|
1766
|
+
logger.error(error instanceof Error ? error.message : "Unknown error");
|
|
1767
|
+
process.exit(1);
|
|
1768
|
+
}
|
|
1769
|
+
const assets = manifest.assets;
|
|
1770
|
+
let assetsToSync = assets;
|
|
1771
|
+
if (options.assets) {
|
|
1772
|
+
const requestedSlugs = options.assets.split(",").map((s) => s.trim());
|
|
1773
|
+
assetsToSync = assets.filter((a) => requestedSlugs.includes(a.slug));
|
|
1774
|
+
const found = assetsToSync.map((a) => a.slug);
|
|
1775
|
+
const notFound = requestedSlugs.filter((s) => !found.includes(s));
|
|
1776
|
+
if (notFound.length > 0) {
|
|
1777
|
+
logger.warn(`Assets not found (or not approved): ${notFound.join(", ")}`);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
if (assetsToSync.length === 0) {
|
|
1781
|
+
spinner.succeed("No assets to sync");
|
|
1782
|
+
logger.info("All assets are up to date or no approved assets found.");
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
spinner.succeed(`Found ${assetsToSync.length} asset(s) to sync`);
|
|
1786
|
+
const totalSize = assetsToSync.reduce((sum, a) => sum + a.total_size_bytes, 0);
|
|
1787
|
+
console.log();
|
|
1788
|
+
logger.header("Sync Summary");
|
|
1789
|
+
logger.item("Studio", studio.name);
|
|
1790
|
+
if (game) logger.item("Game", game.name);
|
|
1791
|
+
logger.item("Output", outputDir);
|
|
1792
|
+
logger.item("Assets", String(assetsToSync.length));
|
|
1793
|
+
logger.item("Total Size", formatFileSize(totalSize));
|
|
1794
|
+
logger.item("TypeScript Types", options.types ? "Yes" : "No");
|
|
1795
|
+
logger.item("Storybook Stories", options.stories ? "Yes" : "No");
|
|
1796
|
+
console.log();
|
|
1797
|
+
logger.header("Assets");
|
|
1798
|
+
for (const asset of assetsToSync) {
|
|
1799
|
+
console.log(` ${logger.value(asset.name)} (${asset.slug}) v${asset.version}`);
|
|
1800
|
+
}
|
|
1801
|
+
if (options.dryRun) {
|
|
1802
|
+
console.log();
|
|
1803
|
+
logger.success("Dry run complete. No files were written.");
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
if (!fs4.existsSync(outputDir)) {
|
|
1807
|
+
fs4.mkdirSync(outputDir, { recursive: true });
|
|
1808
|
+
}
|
|
1809
|
+
console.log();
|
|
1810
|
+
const syncedAssets = {};
|
|
1811
|
+
for (const asset of assetsToSync) {
|
|
1812
|
+
spinner.start(`Syncing ${asset.name}...`);
|
|
1813
|
+
try {
|
|
1814
|
+
const assetDir = path5.join(outputDir, asset.slug);
|
|
1815
|
+
if (!fs4.existsSync(assetDir)) {
|
|
1816
|
+
fs4.mkdirSync(assetDir, { recursive: true });
|
|
1817
|
+
}
|
|
1818
|
+
const skeletonExt = asset.skeleton_format === "binary" ? ".skel" : ".json";
|
|
1819
|
+
const skeletonPath = path5.join(assetDir, `${asset.slug}${skeletonExt}`);
|
|
1820
|
+
await downloadFile(asset.skeleton_url, skeletonPath);
|
|
1821
|
+
const atlasPath = path5.join(assetDir, `${asset.slug}.atlas`);
|
|
1822
|
+
await downloadFile(asset.atlas_url, atlasPath);
|
|
1823
|
+
for (const textureUrl of asset.texture_urls) {
|
|
1824
|
+
const textureName = path5.basename(new URL(textureUrl).pathname);
|
|
1825
|
+
const texturePath = path5.join(assetDir, textureName);
|
|
1826
|
+
await downloadFile(textureUrl, texturePath);
|
|
1827
|
+
}
|
|
1828
|
+
if (options.types) {
|
|
1829
|
+
const typesContent = generateAssetTypes(asset);
|
|
1830
|
+
const typesPath = path5.join(assetDir, "index.ts");
|
|
1831
|
+
fs4.writeFileSync(typesPath, typesContent);
|
|
1832
|
+
}
|
|
1833
|
+
if (options.stories) {
|
|
1834
|
+
const storyContent = generateAssetStory(asset);
|
|
1835
|
+
const storyPath = path5.join(assetDir, `${pascalCase2(asset.slug)}.stories.svelte`);
|
|
1836
|
+
fs4.writeFileSync(storyPath, storyContent);
|
|
1837
|
+
}
|
|
1838
|
+
syncedAssets[asset.slug] = {
|
|
1839
|
+
id: asset.id,
|
|
1840
|
+
version: asset.version,
|
|
1841
|
+
checksum: generateChecksum(asset),
|
|
1842
|
+
updatedAt: asset.updated_at
|
|
1843
|
+
};
|
|
1844
|
+
spinner.succeed(`Synced ${asset.name}`);
|
|
1845
|
+
} catch (error) {
|
|
1846
|
+
spinner.fail(`Failed to sync ${asset.name}`);
|
|
1847
|
+
logger.error(error instanceof Error ? error.message : "Unknown error");
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
const finalAssets = {
|
|
1851
|
+
...existingManifest?.assets || {},
|
|
1852
|
+
...syncedAssets
|
|
1853
|
+
};
|
|
1854
|
+
if (options.types) {
|
|
1855
|
+
spinner.start("Generating manifest...");
|
|
1856
|
+
const manifestContent = generateManifest(Object.keys(finalAssets));
|
|
1857
|
+
fs4.writeFileSync(path5.join(outputDir, "manifest.ts"), manifestContent);
|
|
1858
|
+
spinner.succeed("Generated manifest.ts");
|
|
1859
|
+
}
|
|
1860
|
+
const newManifest = {
|
|
1861
|
+
studioId: studio.id,
|
|
1862
|
+
gameId: game?.id,
|
|
1863
|
+
syncedAt: manifest.syncedAt,
|
|
1864
|
+
assets: finalAssets
|
|
1865
|
+
};
|
|
1866
|
+
fs4.writeFileSync(manifestPath, JSON.stringify(newManifest, null, 2));
|
|
1867
|
+
console.log();
|
|
1868
|
+
logger.success(`Synced ${Object.keys(syncedAssets).length} asset(s) to ${outputDir}`);
|
|
1869
|
+
});
|
|
1870
|
+
async function downloadFile(url, destPath) {
|
|
1871
|
+
const response = await fetch(url);
|
|
1872
|
+
if (!response.ok) {
|
|
1873
|
+
throw new Error(`Failed to download: ${response.status} ${response.statusText}`);
|
|
1874
|
+
}
|
|
1875
|
+
const buffer = await response.arrayBuffer();
|
|
1876
|
+
fs4.writeFileSync(destPath, Buffer.from(buffer));
|
|
1877
|
+
}
|
|
1878
|
+
function generateChecksum(asset) {
|
|
1879
|
+
const data = `${asset.id}:${asset.version}:${asset.updated_at}`;
|
|
1880
|
+
let hash = 0;
|
|
1881
|
+
for (let i = 0; i < data.length; i++) {
|
|
1882
|
+
const char = data.charCodeAt(i);
|
|
1883
|
+
hash = (hash << 5) - hash + char;
|
|
1884
|
+
hash = hash & hash;
|
|
1885
|
+
}
|
|
1886
|
+
return Math.abs(hash).toString(16);
|
|
1887
|
+
}
|
|
1888
|
+
function pascalCase2(str) {
|
|
1889
|
+
return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1505
1892
|
// src/commands/spine/index.ts
|
|
1506
|
-
var spineCommand = new
|
|
1893
|
+
var spineCommand = new Command8("spine").description("Manage Spine animation assets").addCommand(validateCommand).addCommand(uploadCommand).addCommand(presetCommand).addCommand(syncCommand);
|
|
1507
1894
|
|
|
1508
1895
|
// src/commands/setup.ts
|
|
1509
|
-
import { Command as
|
|
1510
|
-
import * as
|
|
1511
|
-
import * as
|
|
1896
|
+
import { Command as Command9 } from "commander";
|
|
1897
|
+
import * as fs5 from "fs";
|
|
1898
|
+
import * as path6 from "path";
|
|
1512
1899
|
import * as os from "os";
|
|
1513
|
-
import
|
|
1514
|
-
|
|
1515
|
-
var
|
|
1900
|
+
import { execFile } from "child_process";
|
|
1901
|
+
import ora6 from "ora";
|
|
1902
|
+
var PLAYTAGON_DIR = path6.join(os.homedir(), ".playtagon");
|
|
1903
|
+
var setupCommand = new Command9("setup").description("Set up integrations").addCommand(spineIntegrationCommand());
|
|
1516
1904
|
function spineIntegrationCommand() {
|
|
1517
|
-
return new
|
|
1905
|
+
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) => {
|
|
1518
1906
|
if (!credentials.isLoggedIn()) {
|
|
1519
1907
|
logger.error("Not logged in.");
|
|
1520
1908
|
logger.info(`Run ${logger.command("playtagon login")} first.`);
|
|
1521
1909
|
process.exit(1);
|
|
1522
1910
|
}
|
|
1523
|
-
const spinner =
|
|
1911
|
+
const spinner = ora6("Checking authentication...").start();
|
|
1524
1912
|
const user = await getCurrentUser();
|
|
1525
1913
|
if (!user) {
|
|
1526
1914
|
spinner.fail("Session expired");
|
|
@@ -1552,8 +1940,8 @@ function spineIntegrationCommand() {
|
|
|
1552
1940
|
}
|
|
1553
1941
|
}
|
|
1554
1942
|
spinner.start("Creating setup files...");
|
|
1555
|
-
if (!
|
|
1556
|
-
|
|
1943
|
+
if (!fs5.existsSync(PLAYTAGON_DIR)) {
|
|
1944
|
+
fs5.mkdirSync(PLAYTAGON_DIR, { recursive: true, mode: 448 });
|
|
1557
1945
|
}
|
|
1558
1946
|
const scriptPaths = generatePostExportScripts(studioSlug, options.game, options.force);
|
|
1559
1947
|
const presetPath = generateExportPreset(scriptPaths.sh, options.force);
|
|
@@ -1573,13 +1961,13 @@ function spineIntegrationCommand() {
|
|
|
1573
1961
|
console.log(" 1. Open Spine Editor");
|
|
1574
1962
|
console.log(" 2. Go to File \u2192 Export...");
|
|
1575
1963
|
console.log(" 3. Click the gear icon (\u2699) next to the preset dropdown");
|
|
1576
|
-
console.log(' 4. Click "Import" and select
|
|
1577
|
-
console.log(` ${logger.file(presetPath)}`);
|
|
1964
|
+
console.log(' 4. Click "Import" and select the preset file from the folder that just opened');
|
|
1578
1965
|
console.log(' 5. The preset "Upload to Playtagon" will be available');
|
|
1579
1966
|
console.log();
|
|
1580
1967
|
console.log(" Now when you export with this preset, files automatically");
|
|
1581
1968
|
console.log(" upload to Playtagon Platform!");
|
|
1582
1969
|
console.log();
|
|
1970
|
+
openFolder(PLAYTAGON_DIR);
|
|
1583
1971
|
let gameSlug = options.game;
|
|
1584
1972
|
if (gameSlug) {
|
|
1585
1973
|
spinner.start("Verifying game...");
|
|
@@ -1622,8 +2010,8 @@ function spineIntegrationCommand() {
|
|
|
1622
2010
|
});
|
|
1623
2011
|
}
|
|
1624
2012
|
function generatePostExportScripts(studioSlug, gameSlug, force = false) {
|
|
1625
|
-
const shPath =
|
|
1626
|
-
const batPath =
|
|
2013
|
+
const shPath = path6.join(PLAYTAGON_DIR, "upload.sh");
|
|
2014
|
+
const batPath = path6.join(PLAYTAGON_DIR, "upload.bat");
|
|
1627
2015
|
let uploadCmd = `playtagon spine upload "$1" --studio ${studioSlug}`;
|
|
1628
2016
|
if (gameSlug) {
|
|
1629
2017
|
uploadCmd += ` --game ${gameSlug}`;
|
|
@@ -1685,35 +2073,55 @@ powershell -Command "& {Add-Type -AssemblyName System.Windows.Forms; [System.Win
|
|
|
1685
2073
|
|
|
1686
2074
|
echo Upload complete!
|
|
1687
2075
|
`;
|
|
1688
|
-
if (!
|
|
1689
|
-
|
|
2076
|
+
if (!fs5.existsSync(shPath) || force) {
|
|
2077
|
+
fs5.writeFileSync(shPath, shScript, { mode: 493 });
|
|
1690
2078
|
} else {
|
|
1691
2079
|
logger.warn(`${shPath} already exists. Use --force to overwrite.`);
|
|
1692
2080
|
}
|
|
1693
|
-
if (!
|
|
1694
|
-
|
|
2081
|
+
if (!fs5.existsSync(batPath) || force) {
|
|
2082
|
+
fs5.writeFileSync(batPath, batScript);
|
|
1695
2083
|
}
|
|
1696
2084
|
return { sh: shPath, bat: batPath };
|
|
1697
2085
|
}
|
|
1698
2086
|
function generateExportPreset(scriptPath, force = false) {
|
|
1699
|
-
const presetPath =
|
|
2087
|
+
const presetPath = path6.join(PLAYTAGON_DIR, "playtagon-spine-preset.export.json");
|
|
1700
2088
|
const preset = {
|
|
1701
2089
|
...SPINE_EXPORT_PRESET,
|
|
1702
2090
|
name: "Upload to Playtagon",
|
|
1703
2091
|
postScript: process.platform === "win32" ? `"${scriptPath.replace(".sh", ".bat")}" "{output}"` : `"${scriptPath}" "{output}"`
|
|
1704
2092
|
};
|
|
1705
|
-
if (!
|
|
1706
|
-
|
|
2093
|
+
if (!fs5.existsSync(presetPath) || force) {
|
|
2094
|
+
fs5.writeFileSync(presetPath, JSON.stringify(preset, null, 2));
|
|
1707
2095
|
} else {
|
|
1708
2096
|
logger.warn(`${presetPath} already exists. Use --force to overwrite.`);
|
|
1709
2097
|
}
|
|
1710
2098
|
return presetPath;
|
|
1711
2099
|
}
|
|
2100
|
+
function openFolder(folderPath) {
|
|
2101
|
+
const platform = process.platform;
|
|
2102
|
+
let cmd;
|
|
2103
|
+
let args;
|
|
2104
|
+
if (platform === "darwin") {
|
|
2105
|
+
cmd = "open";
|
|
2106
|
+
args = [folderPath];
|
|
2107
|
+
} else if (platform === "win32") {
|
|
2108
|
+
cmd = "explorer";
|
|
2109
|
+
args = [folderPath];
|
|
2110
|
+
} else {
|
|
2111
|
+
cmd = "xdg-open";
|
|
2112
|
+
args = [folderPath];
|
|
2113
|
+
}
|
|
2114
|
+
execFile(cmd, args, (error) => {
|
|
2115
|
+
if (error) {
|
|
2116
|
+
logger.debug(`Could not open folder: ${error.message}`);
|
|
2117
|
+
}
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
1712
2120
|
|
|
1713
2121
|
// src/commands/config.ts
|
|
1714
|
-
import { Command as
|
|
1715
|
-
import
|
|
1716
|
-
var configCommand = new
|
|
2122
|
+
import { Command as Command10 } from "commander";
|
|
2123
|
+
import ora7 from "ora";
|
|
2124
|
+
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) => {
|
|
1717
2125
|
if (options.clear) {
|
|
1718
2126
|
config.defaultStudio = void 0;
|
|
1719
2127
|
config.defaultGame = void 0;
|
|
@@ -1726,7 +2134,7 @@ var configCommand = new Command9("config").description("View or set CLI configur
|
|
|
1726
2134
|
logger.info(`Run ${logger.command("playtagon login")} first.`);
|
|
1727
2135
|
process.exit(1);
|
|
1728
2136
|
}
|
|
1729
|
-
const spinner =
|
|
2137
|
+
const spinner = ora7("Verifying studio...").start();
|
|
1730
2138
|
const studios = await getStudios();
|
|
1731
2139
|
const studio = studios.find(
|
|
1732
2140
|
(s) => s.slug === options.studio || s.id === options.studio || s.name === options.studio
|
|
@@ -1756,7 +2164,7 @@ var configCommand = new Command9("config").description("View or set CLI configur
|
|
|
1756
2164
|
logger.info(`Either provide ${logger.command("--studio <slug>")} or set a default studio first.`);
|
|
1757
2165
|
process.exit(1);
|
|
1758
2166
|
}
|
|
1759
|
-
const spinner =
|
|
2167
|
+
const spinner = ora7("Verifying game...").start();
|
|
1760
2168
|
const studios = await getStudios();
|
|
1761
2169
|
const studio = studios.find(
|
|
1762
2170
|
(s) => s.slug === studioSlug || s.id === studioSlug || s.name === studioSlug
|
|
@@ -1816,8 +2224,8 @@ var configCommand = new Command9("config").description("View or set CLI configur
|
|
|
1816
2224
|
});
|
|
1817
2225
|
|
|
1818
2226
|
// src/index.ts
|
|
1819
|
-
var program = new
|
|
1820
|
-
program.name("playtagon").description("Playtagon CLI - Upload and manage game assets").version("0.
|
|
2227
|
+
var program = new Command11();
|
|
2228
|
+
program.name("playtagon").description("Playtagon CLI - Upload and manage game assets").version("0.3.0");
|
|
1821
2229
|
program.addCommand(loginCommand);
|
|
1822
2230
|
program.addCommand(logoutCommand);
|
|
1823
2231
|
program.addCommand(whoamiCommand);
|