@playtagon/cli 0.1.0
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/bin/playtagon.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1599 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1599 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command10 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/login.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import * as readline from "readline";
|
|
9
|
+
import ora from "ora";
|
|
10
|
+
|
|
11
|
+
// src/lib/auth.ts
|
|
12
|
+
import { createClient } from "@supabase/supabase-js";
|
|
13
|
+
|
|
14
|
+
// src/lib/config.ts
|
|
15
|
+
import Conf from "conf";
|
|
16
|
+
var DEFAULT_SUPABASE_URL = process.env.PLAYTAGON_SUPABASE_URL || "https://yutpzwvdpktvblylglwz.supabase.co";
|
|
17
|
+
var DEFAULT_SUPABASE_ANON_KEY = process.env.PLAYTAGON_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1dHB6d3ZkcGt0dmJseWxnbHd6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzU4MjMwNjcsImV4cCI6MjA1MTM5OTA2N30.xyNgJxBLPG3W27Lv6oCctRz-DJLy0jxiHCm7W1yQ4j0";
|
|
18
|
+
var configStore = new Conf({
|
|
19
|
+
projectName: "playtagon",
|
|
20
|
+
configFileMode: 384,
|
|
21
|
+
defaults: {
|
|
22
|
+
supabaseUrl: DEFAULT_SUPABASE_URL,
|
|
23
|
+
supabaseAnonKey: DEFAULT_SUPABASE_ANON_KEY
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
var credentialsStore = new Conf({
|
|
27
|
+
projectName: "playtagon",
|
|
28
|
+
configName: "credentials",
|
|
29
|
+
configFileMode: 384
|
|
30
|
+
});
|
|
31
|
+
var config = {
|
|
32
|
+
get supabaseUrl() {
|
|
33
|
+
return configStore.get("supabaseUrl") || DEFAULT_SUPABASE_URL;
|
|
34
|
+
},
|
|
35
|
+
get supabaseAnonKey() {
|
|
36
|
+
return configStore.get("supabaseAnonKey") || DEFAULT_SUPABASE_ANON_KEY;
|
|
37
|
+
},
|
|
38
|
+
get defaultStudio() {
|
|
39
|
+
return configStore.get("defaultStudio");
|
|
40
|
+
},
|
|
41
|
+
set defaultStudio(value) {
|
|
42
|
+
if (value) {
|
|
43
|
+
configStore.set("defaultStudio", value);
|
|
44
|
+
} else {
|
|
45
|
+
configStore.delete("defaultStudio");
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
get defaultGame() {
|
|
49
|
+
return configStore.get("defaultGame");
|
|
50
|
+
},
|
|
51
|
+
set defaultGame(value) {
|
|
52
|
+
if (value) {
|
|
53
|
+
configStore.set("defaultGame", value);
|
|
54
|
+
} else {
|
|
55
|
+
configStore.delete("defaultGame");
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
// Get full config
|
|
59
|
+
getAll() {
|
|
60
|
+
return configStore.store;
|
|
61
|
+
},
|
|
62
|
+
// Get config file path
|
|
63
|
+
get path() {
|
|
64
|
+
return configStore.path;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var credentials = {
|
|
68
|
+
get accessToken() {
|
|
69
|
+
return credentialsStore.get("accessToken");
|
|
70
|
+
},
|
|
71
|
+
get refreshToken() {
|
|
72
|
+
return credentialsStore.get("refreshToken");
|
|
73
|
+
},
|
|
74
|
+
get expiresAt() {
|
|
75
|
+
return credentialsStore.get("expiresAt");
|
|
76
|
+
},
|
|
77
|
+
get userId() {
|
|
78
|
+
return credentialsStore.get("userId");
|
|
79
|
+
},
|
|
80
|
+
get email() {
|
|
81
|
+
return credentialsStore.get("email");
|
|
82
|
+
},
|
|
83
|
+
isLoggedIn() {
|
|
84
|
+
return !!credentialsStore.get("accessToken");
|
|
85
|
+
},
|
|
86
|
+
isExpired() {
|
|
87
|
+
const expiresAt = credentialsStore.get("expiresAt");
|
|
88
|
+
if (!expiresAt) return true;
|
|
89
|
+
return Date.now() > expiresAt - 5 * 60 * 1e3;
|
|
90
|
+
},
|
|
91
|
+
save(creds) {
|
|
92
|
+
if (creds.accessToken) credentialsStore.set("accessToken", creds.accessToken);
|
|
93
|
+
if (creds.refreshToken) credentialsStore.set("refreshToken", creds.refreshToken);
|
|
94
|
+
if (creds.expiresAt) credentialsStore.set("expiresAt", creds.expiresAt);
|
|
95
|
+
if (creds.userId) credentialsStore.set("userId", creds.userId);
|
|
96
|
+
if (creds.email) credentialsStore.set("email", creds.email);
|
|
97
|
+
},
|
|
98
|
+
clear() {
|
|
99
|
+
credentialsStore.clear();
|
|
100
|
+
},
|
|
101
|
+
// Get credentials file path
|
|
102
|
+
get path() {
|
|
103
|
+
return credentialsStore.path;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/utils/logger.ts
|
|
108
|
+
import chalk from "chalk";
|
|
109
|
+
var logger = {
|
|
110
|
+
info: (message) => {
|
|
111
|
+
console.log(chalk.blue("info"), message);
|
|
112
|
+
},
|
|
113
|
+
success: (message) => {
|
|
114
|
+
console.log(chalk.green("success"), message);
|
|
115
|
+
},
|
|
116
|
+
warn: (message) => {
|
|
117
|
+
console.log(chalk.yellow("warn"), message);
|
|
118
|
+
},
|
|
119
|
+
error: (message) => {
|
|
120
|
+
console.log(chalk.red("error"), message);
|
|
121
|
+
},
|
|
122
|
+
debug: (message) => {
|
|
123
|
+
if (process.env.DEBUG) {
|
|
124
|
+
console.log(chalk.gray("debug"), message);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
// Styled output for specific contexts
|
|
128
|
+
file: (filename) => chalk.cyan(filename),
|
|
129
|
+
value: (val) => chalk.yellow(val),
|
|
130
|
+
command: (cmd) => chalk.bold.white(cmd),
|
|
131
|
+
url: (url) => chalk.underline.blue(url),
|
|
132
|
+
// Section headers
|
|
133
|
+
header: (title) => {
|
|
134
|
+
console.log();
|
|
135
|
+
console.log(chalk.bold.white(title));
|
|
136
|
+
console.log(chalk.gray("\u2500".repeat(title.length)));
|
|
137
|
+
},
|
|
138
|
+
// List item
|
|
139
|
+
item: (label, value) => {
|
|
140
|
+
console.log(` ${chalk.gray(label + ":")} ${value}`);
|
|
141
|
+
},
|
|
142
|
+
// Validation result formatting
|
|
143
|
+
validationError: (message, detail) => {
|
|
144
|
+
console.log(` ${chalk.red("\u2717")} ${message}`);
|
|
145
|
+
if (detail) {
|
|
146
|
+
console.log(` ${chalk.gray(detail)}`);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
validationSuccess: (message) => {
|
|
150
|
+
console.log(` ${chalk.green("\u2713")} ${message}`);
|
|
151
|
+
},
|
|
152
|
+
validationWarn: (message) => {
|
|
153
|
+
console.log(` ${chalk.yellow("!")} ${message}`);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// src/lib/auth.ts
|
|
158
|
+
var supabaseClient = null;
|
|
159
|
+
function getSupabaseClient() {
|
|
160
|
+
if (!supabaseClient) {
|
|
161
|
+
supabaseClient = createClient(config.supabaseUrl, config.supabaseAnonKey, {
|
|
162
|
+
auth: {
|
|
163
|
+
persistSession: false,
|
|
164
|
+
autoRefreshToken: false
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return supabaseClient;
|
|
169
|
+
}
|
|
170
|
+
async function getAuthenticatedClient() {
|
|
171
|
+
const client = getSupabaseClient();
|
|
172
|
+
if (!credentials.isLoggedIn()) {
|
|
173
|
+
throw new Error("Not logged in. Run `playtagon login` first.");
|
|
174
|
+
}
|
|
175
|
+
if (credentials.isExpired() && credentials.refreshToken) {
|
|
176
|
+
logger.debug("Token expired, refreshing...");
|
|
177
|
+
await refreshSession();
|
|
178
|
+
}
|
|
179
|
+
const accessToken = credentials.accessToken;
|
|
180
|
+
if (!accessToken) {
|
|
181
|
+
throw new Error("No access token found. Run `playtagon login` first.");
|
|
182
|
+
}
|
|
183
|
+
await client.auth.setSession({
|
|
184
|
+
access_token: accessToken,
|
|
185
|
+
refresh_token: credentials.refreshToken || ""
|
|
186
|
+
});
|
|
187
|
+
return client;
|
|
188
|
+
}
|
|
189
|
+
async function refreshSession() {
|
|
190
|
+
const client = getSupabaseClient();
|
|
191
|
+
const refreshToken = credentials.refreshToken;
|
|
192
|
+
if (!refreshToken) {
|
|
193
|
+
throw new Error("No refresh token found. Run `playtagon login` first.");
|
|
194
|
+
}
|
|
195
|
+
const { data, error } = await client.auth.refreshSession({
|
|
196
|
+
refresh_token: refreshToken
|
|
197
|
+
});
|
|
198
|
+
if (error) {
|
|
199
|
+
credentials.clear();
|
|
200
|
+
throw new Error(`Failed to refresh session: ${error.message}`);
|
|
201
|
+
}
|
|
202
|
+
if (data.session) {
|
|
203
|
+
credentials.save({
|
|
204
|
+
accessToken: data.session.access_token,
|
|
205
|
+
refreshToken: data.session.refresh_token,
|
|
206
|
+
expiresAt: data.session.expires_at ? data.session.expires_at * 1e3 : Date.now() + 3600 * 1e3
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function loginWithEmail(email, password) {
|
|
211
|
+
const client = getSupabaseClient();
|
|
212
|
+
const { data, error } = await client.auth.signInWithPassword({
|
|
213
|
+
email,
|
|
214
|
+
password
|
|
215
|
+
});
|
|
216
|
+
if (error) {
|
|
217
|
+
throw new Error(`Login failed: ${error.message}`);
|
|
218
|
+
}
|
|
219
|
+
if (!data.session) {
|
|
220
|
+
throw new Error("Login failed: No session returned");
|
|
221
|
+
}
|
|
222
|
+
credentials.save({
|
|
223
|
+
accessToken: data.session.access_token,
|
|
224
|
+
refreshToken: data.session.refresh_token,
|
|
225
|
+
expiresAt: data.session.expires_at ? data.session.expires_at * 1e3 : Date.now() + 3600 * 1e3,
|
|
226
|
+
userId: data.user?.id,
|
|
227
|
+
email: data.user?.email
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function logout() {
|
|
231
|
+
credentials.clear();
|
|
232
|
+
}
|
|
233
|
+
async function getCurrentUser() {
|
|
234
|
+
try {
|
|
235
|
+
const client = await getAuthenticatedClient();
|
|
236
|
+
const {
|
|
237
|
+
data: { user },
|
|
238
|
+
error
|
|
239
|
+
} = await client.auth.getUser();
|
|
240
|
+
if (error || !user) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const { data: memberships } = await client.from("studio_members").select(`
|
|
244
|
+
studio:studios (
|
|
245
|
+
id,
|
|
246
|
+
name,
|
|
247
|
+
slug
|
|
248
|
+
)
|
|
249
|
+
`).eq("user_id", user.id);
|
|
250
|
+
const studios = memberships?.map((m) => m.studio).filter(Boolean) || [];
|
|
251
|
+
return {
|
|
252
|
+
id: user.id,
|
|
253
|
+
email: user.email || "",
|
|
254
|
+
studios
|
|
255
|
+
};
|
|
256
|
+
} catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function getAccessToken() {
|
|
261
|
+
return credentials.accessToken;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/commands/login.ts
|
|
265
|
+
var loginCommand = new Command("login").description("Authenticate with Playtagon").option("-e, --email <email>", "Login with email").option("-b, --browser", "Open browser for OAuth login").action(async (options) => {
|
|
266
|
+
if (credentials.isLoggedIn()) {
|
|
267
|
+
const user = await getCurrentUser();
|
|
268
|
+
if (user) {
|
|
269
|
+
logger.info(`Already logged in as ${logger.value(user.email)}`);
|
|
270
|
+
logger.info(`Run ${logger.command("playtagon logout")} to sign out.`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (options.browser) {
|
|
275
|
+
await browserLogin();
|
|
276
|
+
} else if (options.email) {
|
|
277
|
+
await emailLogin(options.email);
|
|
278
|
+
} else {
|
|
279
|
+
await promptEmailLogin();
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
async function promptEmailLogin() {
|
|
283
|
+
const rl = readline.createInterface({
|
|
284
|
+
input: process.stdin,
|
|
285
|
+
output: process.stdout
|
|
286
|
+
});
|
|
287
|
+
const question = (prompt) => new Promise((resolve4) => {
|
|
288
|
+
rl.question(prompt, resolve4);
|
|
289
|
+
});
|
|
290
|
+
try {
|
|
291
|
+
const email = await question("Email: ");
|
|
292
|
+
const password = await questionHidden("Password: ", rl);
|
|
293
|
+
const spinner = ora("Logging in...").start();
|
|
294
|
+
try {
|
|
295
|
+
await loginWithEmail(email, password);
|
|
296
|
+
spinner.succeed("Logged in successfully!");
|
|
297
|
+
const user = await getCurrentUser();
|
|
298
|
+
if (user) {
|
|
299
|
+
logger.header("Account");
|
|
300
|
+
logger.item("Email", user.email);
|
|
301
|
+
if (user.studios.length > 0) {
|
|
302
|
+
logger.item(
|
|
303
|
+
"Studios",
|
|
304
|
+
user.studios.map((s) => s.name).join(", ")
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} catch (error) {
|
|
309
|
+
spinner.fail("Login failed");
|
|
310
|
+
logger.error(error instanceof Error ? error.message : "Unknown error");
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
} finally {
|
|
314
|
+
rl.close();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function emailLogin(email) {
|
|
318
|
+
const rl = readline.createInterface({
|
|
319
|
+
input: process.stdin,
|
|
320
|
+
output: process.stdout
|
|
321
|
+
});
|
|
322
|
+
try {
|
|
323
|
+
const password = await questionHidden("Password: ", rl);
|
|
324
|
+
const spinner = ora("Logging in...").start();
|
|
325
|
+
try {
|
|
326
|
+
await loginWithEmail(email, password);
|
|
327
|
+
spinner.succeed("Logged in successfully!");
|
|
328
|
+
const user = await getCurrentUser();
|
|
329
|
+
if (user) {
|
|
330
|
+
logger.header("Account");
|
|
331
|
+
logger.item("Email", user.email);
|
|
332
|
+
}
|
|
333
|
+
} catch (error) {
|
|
334
|
+
spinner.fail("Login failed");
|
|
335
|
+
logger.error(error instanceof Error ? error.message : "Unknown error");
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
} finally {
|
|
339
|
+
rl.close();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async function browserLogin() {
|
|
343
|
+
const client = getSupabaseClient();
|
|
344
|
+
logger.info("Opening browser for authentication...");
|
|
345
|
+
const { data, error } = await client.auth.signInWithOtp({
|
|
346
|
+
email: "",
|
|
347
|
+
// Will prompt
|
|
348
|
+
options: {
|
|
349
|
+
shouldCreateUser: false
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
if (error) {
|
|
353
|
+
logger.error(`Failed to initiate login: ${error.message}`);
|
|
354
|
+
logger.info("");
|
|
355
|
+
logger.info("Please use email login instead:");
|
|
356
|
+
logger.info(` ${logger.command("playtagon login -e your@email.com")}`);
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
logger.info("Browser-based login is not yet implemented.");
|
|
360
|
+
logger.info("Please use email login:");
|
|
361
|
+
logger.info(` ${logger.command("playtagon login")}`);
|
|
362
|
+
}
|
|
363
|
+
function questionHidden(prompt, rl) {
|
|
364
|
+
return new Promise((resolve4) => {
|
|
365
|
+
const stdin = process.stdin;
|
|
366
|
+
const stdout = process.stdout;
|
|
367
|
+
stdout.write(prompt);
|
|
368
|
+
const wasRaw = stdin.isRaw;
|
|
369
|
+
if (stdin.isTTY) {
|
|
370
|
+
stdin.setRawMode(true);
|
|
371
|
+
}
|
|
372
|
+
let password = "";
|
|
373
|
+
const onData = (char) => {
|
|
374
|
+
const c = char.toString("utf8");
|
|
375
|
+
switch (c) {
|
|
376
|
+
case "\n":
|
|
377
|
+
case "\r":
|
|
378
|
+
case "":
|
|
379
|
+
if (stdin.isTTY) {
|
|
380
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
381
|
+
}
|
|
382
|
+
stdin.removeListener("data", onData);
|
|
383
|
+
stdout.write("\n");
|
|
384
|
+
resolve4(password);
|
|
385
|
+
break;
|
|
386
|
+
case "":
|
|
387
|
+
process.exit(1);
|
|
388
|
+
break;
|
|
389
|
+
case "\x7F":
|
|
390
|
+
password = password.slice(0, -1);
|
|
391
|
+
break;
|
|
392
|
+
default:
|
|
393
|
+
password += c;
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
stdin.on("data", onData);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/commands/logout.ts
|
|
402
|
+
import { Command as Command2 } from "commander";
|
|
403
|
+
var logoutCommand = new Command2("logout").description("Sign out of Playtagon").action(() => {
|
|
404
|
+
if (!credentials.isLoggedIn()) {
|
|
405
|
+
logger.info("Not currently logged in.");
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const email = credentials.email;
|
|
409
|
+
logout();
|
|
410
|
+
if (email) {
|
|
411
|
+
logger.success(`Logged out from ${logger.value(email)}`);
|
|
412
|
+
} else {
|
|
413
|
+
logger.success("Logged out successfully");
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// src/commands/whoami.ts
|
|
418
|
+
import { Command as Command3 } from "commander";
|
|
419
|
+
import ora2 from "ora";
|
|
420
|
+
var whoamiCommand = new Command3("whoami").description("Show current logged in user").action(async () => {
|
|
421
|
+
if (!credentials.isLoggedIn()) {
|
|
422
|
+
logger.info("Not logged in.");
|
|
423
|
+
logger.info(`Run ${logger.command("playtagon login")} to authenticate.`);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const spinner = ora2("Fetching user info...").start();
|
|
427
|
+
try {
|
|
428
|
+
const user = await getCurrentUser();
|
|
429
|
+
if (!user) {
|
|
430
|
+
spinner.fail("Session expired");
|
|
431
|
+
logger.info(`Run ${logger.command("playtagon login")} to re-authenticate.`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
spinner.stop();
|
|
435
|
+
logger.header("Current User");
|
|
436
|
+
logger.item("ID", user.id);
|
|
437
|
+
logger.item("Email", user.email);
|
|
438
|
+
if (user.studios.length > 0) {
|
|
439
|
+
console.log();
|
|
440
|
+
logger.header("Studios");
|
|
441
|
+
for (const studio of user.studios) {
|
|
442
|
+
console.log(` ${studio.name} (${logger.value(studio.slug)})`);
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
console.log();
|
|
446
|
+
logger.warn("No studios found. Join or create a studio in the platform.");
|
|
447
|
+
}
|
|
448
|
+
} catch (error) {
|
|
449
|
+
spinner.fail("Failed to fetch user info");
|
|
450
|
+
logger.error(error instanceof Error ? error.message : "Unknown error");
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// src/commands/spine/index.ts
|
|
456
|
+
import { Command as Command7 } from "commander";
|
|
457
|
+
|
|
458
|
+
// src/commands/spine/validate.ts
|
|
459
|
+
import { Command as Command4 } from "commander";
|
|
460
|
+
import * as path3 from "path";
|
|
461
|
+
import ora3 from "ora";
|
|
462
|
+
|
|
463
|
+
// src/lib/spine-validator.ts
|
|
464
|
+
import * as fs2 from "fs";
|
|
465
|
+
import * as path2 from "path";
|
|
466
|
+
|
|
467
|
+
// src/utils/files.ts
|
|
468
|
+
import * as fs from "fs";
|
|
469
|
+
import * as path from "path";
|
|
470
|
+
var SKELETON_EXTENSIONS = [".json", ".skel"];
|
|
471
|
+
var ATLAS_EXTENSIONS = [".atlas"];
|
|
472
|
+
var TEXTURE_EXTENSIONS = [".png", ".jpg", ".jpeg"];
|
|
473
|
+
function discoverSpineFiles(directory) {
|
|
474
|
+
const absolutePath = path.resolve(directory);
|
|
475
|
+
if (!fs.existsSync(absolutePath)) {
|
|
476
|
+
throw new Error(`Directory not found: ${directory}`);
|
|
477
|
+
}
|
|
478
|
+
if (!fs.statSync(absolutePath).isDirectory()) {
|
|
479
|
+
throw new Error(`Not a directory: ${directory}`);
|
|
480
|
+
}
|
|
481
|
+
const files = fs.readdirSync(absolutePath);
|
|
482
|
+
const skeletons = [];
|
|
483
|
+
const atlases = [];
|
|
484
|
+
const textures = [];
|
|
485
|
+
for (const file of files) {
|
|
486
|
+
const ext = path.extname(file).toLowerCase();
|
|
487
|
+
const fullPath = path.join(absolutePath, file);
|
|
488
|
+
if (fs.statSync(fullPath).isDirectory()) continue;
|
|
489
|
+
if (SKELETON_EXTENSIONS.includes(ext)) {
|
|
490
|
+
if (ext === ".json") {
|
|
491
|
+
if (isSpineSkeletonJson(fullPath)) {
|
|
492
|
+
skeletons.push(fullPath);
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
skeletons.push(fullPath);
|
|
496
|
+
}
|
|
497
|
+
} else if (ATLAS_EXTENSIONS.includes(ext)) {
|
|
498
|
+
atlases.push(fullPath);
|
|
499
|
+
} else if (TEXTURE_EXTENSIONS.includes(ext)) {
|
|
500
|
+
textures.push(fullPath);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return { skeletons, atlases, textures };
|
|
504
|
+
}
|
|
505
|
+
function isSpineSkeletonJson(filePath) {
|
|
506
|
+
try {
|
|
507
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
508
|
+
const json = JSON.parse(content);
|
|
509
|
+
return json.skeleton && typeof json.skeleton.spine === "string";
|
|
510
|
+
} catch {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function matchSkeletonToAtlas(skeletonPath, atlases) {
|
|
515
|
+
const skeletonName = path.basename(skeletonPath, path.extname(skeletonPath));
|
|
516
|
+
const skeletonDir = path.dirname(skeletonPath);
|
|
517
|
+
for (const atlas of atlases) {
|
|
518
|
+
const atlasName = path.basename(atlas, ".atlas");
|
|
519
|
+
if (atlasName === skeletonName) {
|
|
520
|
+
return atlas;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const sameDirectoryAtlases = atlases.filter(
|
|
524
|
+
(a) => path.dirname(a) === skeletonDir
|
|
525
|
+
);
|
|
526
|
+
if (sameDirectoryAtlases.length === 1) {
|
|
527
|
+
return sameDirectoryAtlases[0];
|
|
528
|
+
}
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
function parseAtlasTextures(atlasPath) {
|
|
532
|
+
const content = fs.readFileSync(atlasPath, "utf-8");
|
|
533
|
+
const lines = content.split("\n");
|
|
534
|
+
const textures = [];
|
|
535
|
+
const atlasDir = path.dirname(atlasPath);
|
|
536
|
+
for (const line of lines) {
|
|
537
|
+
const trimmed = line.trim();
|
|
538
|
+
if (trimmed.endsWith(".png") || trimmed.endsWith(".jpg")) {
|
|
539
|
+
const texturePath = path.join(atlasDir, trimmed);
|
|
540
|
+
if (fs.existsSync(texturePath)) {
|
|
541
|
+
textures.push(texturePath);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return textures;
|
|
546
|
+
}
|
|
547
|
+
function getFileSize(filePath) {
|
|
548
|
+
return fs.statSync(filePath).size;
|
|
549
|
+
}
|
|
550
|
+
function getTotalSize(files) {
|
|
551
|
+
return files.reduce((total, file) => total + getFileSize(file), 0);
|
|
552
|
+
}
|
|
553
|
+
function formatFileSize(bytes) {
|
|
554
|
+
if (bytes === 0) return "0 B";
|
|
555
|
+
const k = 1024;
|
|
556
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
557
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
558
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
559
|
+
}
|
|
560
|
+
function getBaseName(filePath) {
|
|
561
|
+
return path.basename(filePath);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/lib/spine-validator.ts
|
|
565
|
+
var SPINE_VALIDATION_RULES = {
|
|
566
|
+
supportedVersions: ["4.0", "4.1", "4.2", "4.3"],
|
|
567
|
+
maxTextureSize: 4096,
|
|
568
|
+
// pixels per dimension
|
|
569
|
+
maxTotalSize: 50 * 1024 * 1024,
|
|
570
|
+
// 50MB
|
|
571
|
+
maxTextureCount: 8,
|
|
572
|
+
slugPattern: /^[a-z0-9-]+$/,
|
|
573
|
+
animationNamePattern: /^[a-zA-Z0-9_-]+$/
|
|
574
|
+
};
|
|
575
|
+
function validateDirectory(directory, batchMode = false) {
|
|
576
|
+
const discovered = discoverSpineFiles(directory);
|
|
577
|
+
const issues = [];
|
|
578
|
+
if (discovered.skeletons.length === 0) {
|
|
579
|
+
issues.push({
|
|
580
|
+
type: "error",
|
|
581
|
+
message: "No Spine skeleton files found",
|
|
582
|
+
detail: "Expected .json (Spine skeleton) or .skel files"
|
|
583
|
+
});
|
|
584
|
+
return {
|
|
585
|
+
assets: [],
|
|
586
|
+
sharedAtlas: null,
|
|
587
|
+
sharedTextures: [],
|
|
588
|
+
totalSize: 0,
|
|
589
|
+
issues,
|
|
590
|
+
isValid: false,
|
|
591
|
+
isBatchMode: false
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
const isBatchMode = batchMode || discovered.skeletons.length > 1;
|
|
595
|
+
if (isBatchMode && discovered.skeletons.length > 1) {
|
|
596
|
+
return validateBatch(discovered, issues);
|
|
597
|
+
} else {
|
|
598
|
+
return validateSingle(discovered, issues);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function validateSingle(discovered, issues) {
|
|
602
|
+
const skeletonPath = discovered.skeletons[0];
|
|
603
|
+
const skeleton = parseSkeletonFile(skeletonPath);
|
|
604
|
+
if (!skeleton) {
|
|
605
|
+
issues.push({
|
|
606
|
+
type: "error",
|
|
607
|
+
message: "Failed to parse skeleton file",
|
|
608
|
+
file: path2.basename(skeletonPath)
|
|
609
|
+
});
|
|
610
|
+
return {
|
|
611
|
+
assets: [],
|
|
612
|
+
sharedAtlas: null,
|
|
613
|
+
sharedTextures: [],
|
|
614
|
+
totalSize: 0,
|
|
615
|
+
issues,
|
|
616
|
+
isValid: false,
|
|
617
|
+
isBatchMode: false
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
const versionIssues = validateSpineVersion(skeleton);
|
|
621
|
+
issues.push(...versionIssues);
|
|
622
|
+
const atlasPath = matchSkeletonToAtlas(skeletonPath, discovered.atlases);
|
|
623
|
+
if (!atlasPath) {
|
|
624
|
+
if (discovered.atlases.length === 0) {
|
|
625
|
+
issues.push({
|
|
626
|
+
type: "error",
|
|
627
|
+
message: "No atlas file found",
|
|
628
|
+
detail: "Expected .atlas file"
|
|
629
|
+
});
|
|
630
|
+
} else {
|
|
631
|
+
issues.push({
|
|
632
|
+
type: "warning",
|
|
633
|
+
message: "Could not match skeleton to atlas",
|
|
634
|
+
detail: `Found atlases: ${discovered.atlases.map((a) => path2.basename(a)).join(", ")}`
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
let texturePaths = [];
|
|
639
|
+
if (atlasPath) {
|
|
640
|
+
texturePaths = parseAtlasTextures(atlasPath);
|
|
641
|
+
const textureIssues = validateTextures(texturePaths);
|
|
642
|
+
issues.push(...textureIssues);
|
|
643
|
+
}
|
|
644
|
+
const orphanTextures = discovered.textures.filter(
|
|
645
|
+
(t) => !texturePaths.includes(t)
|
|
646
|
+
);
|
|
647
|
+
if (orphanTextures.length > 0) {
|
|
648
|
+
issues.push({
|
|
649
|
+
type: "warning",
|
|
650
|
+
message: `${orphanTextures.length} texture(s) not referenced in atlas`,
|
|
651
|
+
detail: orphanTextures.map((t) => path2.basename(t)).join(", ")
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const allFiles = [skeletonPath, atlasPath, ...texturePaths].filter(
|
|
655
|
+
Boolean
|
|
656
|
+
);
|
|
657
|
+
const totalSize = getTotalSize(allFiles);
|
|
658
|
+
if (totalSize > SPINE_VALIDATION_RULES.maxTotalSize) {
|
|
659
|
+
issues.push({
|
|
660
|
+
type: "error",
|
|
661
|
+
message: `Total size ${formatFileSize(totalSize)} exceeds ${formatFileSize(SPINE_VALIDATION_RULES.maxTotalSize)} limit`
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
const animationIssues = validateAnimationNames(skeleton.animations);
|
|
665
|
+
issues.push(...animationIssues);
|
|
666
|
+
const hasErrors = issues.some((i) => i.type === "error");
|
|
667
|
+
const asset = {
|
|
668
|
+
skeleton,
|
|
669
|
+
atlasPath,
|
|
670
|
+
texturePaths,
|
|
671
|
+
totalSize,
|
|
672
|
+
issues: issues.filter(
|
|
673
|
+
(i) => i.file === void 0 || i.file === path2.basename(skeletonPath)
|
|
674
|
+
),
|
|
675
|
+
isValid: !hasErrors
|
|
676
|
+
};
|
|
677
|
+
return {
|
|
678
|
+
assets: [asset],
|
|
679
|
+
sharedAtlas: null,
|
|
680
|
+
sharedTextures: [],
|
|
681
|
+
totalSize,
|
|
682
|
+
issues,
|
|
683
|
+
isValid: !hasErrors,
|
|
684
|
+
isBatchMode: false
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function validateBatch(discovered, issues) {
|
|
688
|
+
const assets = [];
|
|
689
|
+
let sharedAtlas = null;
|
|
690
|
+
let sharedTextures = [];
|
|
691
|
+
if (discovered.atlases.length === 1) {
|
|
692
|
+
sharedAtlas = discovered.atlases[0];
|
|
693
|
+
sharedTextures = parseAtlasTextures(sharedAtlas);
|
|
694
|
+
issues.push({
|
|
695
|
+
type: "info",
|
|
696
|
+
message: `Batch mode: ${discovered.skeletons.length} skeletons sharing 1 atlas`
|
|
697
|
+
});
|
|
698
|
+
} else if (discovered.atlases.length === 0) {
|
|
699
|
+
issues.push({
|
|
700
|
+
type: "error",
|
|
701
|
+
message: "No atlas file found for batch upload"
|
|
702
|
+
});
|
|
703
|
+
} else {
|
|
704
|
+
issues.push({
|
|
705
|
+
type: "warning",
|
|
706
|
+
message: `Multiple atlases found (${discovered.atlases.length}). Batch mode works best with shared atlas.`
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
for (const skeletonPath of discovered.skeletons) {
|
|
710
|
+
const skeleton = parseSkeletonFile(skeletonPath);
|
|
711
|
+
if (!skeleton) {
|
|
712
|
+
issues.push({
|
|
713
|
+
type: "error",
|
|
714
|
+
message: "Failed to parse skeleton file",
|
|
715
|
+
file: path2.basename(skeletonPath)
|
|
716
|
+
});
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const versionIssues = validateSpineVersion(skeleton);
|
|
720
|
+
issues.push(...versionIssues);
|
|
721
|
+
const animationIssues = validateAnimationNames(skeleton.animations);
|
|
722
|
+
issues.push(...animationIssues);
|
|
723
|
+
const atlasPath = sharedAtlas || matchSkeletonToAtlas(skeletonPath, discovered.atlases);
|
|
724
|
+
const texturePaths = atlasPath ? parseAtlasTextures(atlasPath) : [];
|
|
725
|
+
const skeletonSize = getFileSize(skeletonPath);
|
|
726
|
+
assets.push({
|
|
727
|
+
skeleton,
|
|
728
|
+
atlasPath,
|
|
729
|
+
texturePaths,
|
|
730
|
+
totalSize: skeletonSize,
|
|
731
|
+
issues: [],
|
|
732
|
+
isValid: true
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
if (sharedTextures.length > 0) {
|
|
736
|
+
const textureIssues = validateTextures(sharedTextures);
|
|
737
|
+
issues.push(...textureIssues);
|
|
738
|
+
}
|
|
739
|
+
const skeletonSizes = assets.reduce((sum, a) => sum + a.totalSize, 0);
|
|
740
|
+
const atlasSize = sharedAtlas ? getFileSize(sharedAtlas) : 0;
|
|
741
|
+
const textureSize = getTotalSize(sharedTextures);
|
|
742
|
+
const totalSize = skeletonSizes + atlasSize + textureSize;
|
|
743
|
+
if (totalSize > SPINE_VALIDATION_RULES.maxTotalSize) {
|
|
744
|
+
issues.push({
|
|
745
|
+
type: "error",
|
|
746
|
+
message: `Total batch size ${formatFileSize(totalSize)} exceeds ${formatFileSize(SPINE_VALIDATION_RULES.maxTotalSize)} limit`
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
const hasErrors = issues.some((i) => i.type === "error");
|
|
750
|
+
return {
|
|
751
|
+
assets,
|
|
752
|
+
sharedAtlas,
|
|
753
|
+
sharedTextures,
|
|
754
|
+
totalSize,
|
|
755
|
+
issues,
|
|
756
|
+
isValid: !hasErrors,
|
|
757
|
+
isBatchMode: true
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function parseSkeletonFile(filePath) {
|
|
761
|
+
try {
|
|
762
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
763
|
+
const json = JSON.parse(content);
|
|
764
|
+
if (!json.skeleton?.spine) {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
const name = path2.basename(filePath, path2.extname(filePath));
|
|
768
|
+
const spineVersion = json.skeleton.spine;
|
|
769
|
+
const animations = json.animations ? Object.keys(json.animations) : [];
|
|
770
|
+
const skins = json.skins ? Array.isArray(json.skins) ? json.skins.map((s) => s.name || "default") : Object.keys(json.skins) : ["default"];
|
|
771
|
+
const hasEvents = !!json.events && Object.keys(json.events).length > 0;
|
|
772
|
+
const boneCount = json.bones ? json.bones.length : 0;
|
|
773
|
+
const slotCount = json.slots ? json.slots.length : 0;
|
|
774
|
+
return {
|
|
775
|
+
path: filePath,
|
|
776
|
+
name,
|
|
777
|
+
spineVersion,
|
|
778
|
+
animations,
|
|
779
|
+
skins,
|
|
780
|
+
hasEvents,
|
|
781
|
+
boneCount,
|
|
782
|
+
slotCount
|
|
783
|
+
};
|
|
784
|
+
} catch {
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function validateSpineVersion(skeleton) {
|
|
789
|
+
const issues = [];
|
|
790
|
+
const version = skeleton.spineVersion;
|
|
791
|
+
const majorMinor = version.match(/^(\d+\.\d+)/)?.[1];
|
|
792
|
+
if (!majorMinor || !SPINE_VALIDATION_RULES.supportedVersions.includes(
|
|
793
|
+
majorMinor
|
|
794
|
+
)) {
|
|
795
|
+
issues.push({
|
|
796
|
+
type: "error",
|
|
797
|
+
message: `Unsupported Spine version: ${version}`,
|
|
798
|
+
file: path2.basename(skeleton.path),
|
|
799
|
+
detail: `Supported versions: ${SPINE_VALIDATION_RULES.supportedVersions.join(", ")}`
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
return issues;
|
|
803
|
+
}
|
|
804
|
+
function validateAnimationNames(animations) {
|
|
805
|
+
const issues = [];
|
|
806
|
+
for (const name of animations) {
|
|
807
|
+
if (!SPINE_VALIDATION_RULES.animationNamePattern.test(name)) {
|
|
808
|
+
issues.push({
|
|
809
|
+
type: "warning",
|
|
810
|
+
message: `Animation name "${name}" contains invalid characters`,
|
|
811
|
+
detail: "Recommended: a-z, A-Z, 0-9, _, -"
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return issues;
|
|
816
|
+
}
|
|
817
|
+
function validateTextures(texturePaths) {
|
|
818
|
+
const issues = [];
|
|
819
|
+
if (texturePaths.length > SPINE_VALIDATION_RULES.maxTextureCount) {
|
|
820
|
+
issues.push({
|
|
821
|
+
type: "error",
|
|
822
|
+
message: `Too many textures: ${texturePaths.length}`,
|
|
823
|
+
detail: `Maximum allowed: ${SPINE_VALIDATION_RULES.maxTextureCount}`
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
for (const texturePath of texturePaths) {
|
|
827
|
+
if (!fs2.existsSync(texturePath)) {
|
|
828
|
+
issues.push({
|
|
829
|
+
type: "error",
|
|
830
|
+
message: `Texture file not found: ${path2.basename(texturePath)}`
|
|
831
|
+
});
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
const size = getFileSize(texturePath);
|
|
835
|
+
if (size > 20 * 1024 * 1024) {
|
|
836
|
+
issues.push({
|
|
837
|
+
type: "warning",
|
|
838
|
+
message: `Large texture file: ${path2.basename(texturePath)} (${formatFileSize(size)})`,
|
|
839
|
+
detail: "Consider optimizing texture size"
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return issues;
|
|
844
|
+
}
|
|
845
|
+
function generateSlug(name) {
|
|
846
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
847
|
+
}
|
|
848
|
+
function validateSlug(slug) {
|
|
849
|
+
return SPINE_VALIDATION_RULES.slugPattern.test(slug);
|
|
850
|
+
}
|
|
851
|
+
var SPINE_EXPORT_PRESET = {
|
|
852
|
+
class: "export-json",
|
|
853
|
+
name: "Playtagon Platform",
|
|
854
|
+
output: "{projectDir}/exports/{skeletonName}",
|
|
855
|
+
extension: ".json",
|
|
856
|
+
format: "json",
|
|
857
|
+
nonessential: false,
|
|
858
|
+
cleanUp: true,
|
|
859
|
+
warnings: true,
|
|
860
|
+
packAtlas: true,
|
|
861
|
+
packSource: "attachments",
|
|
862
|
+
packTarget: "perskeleton",
|
|
863
|
+
packSettings: {
|
|
864
|
+
maxWidth: 4096,
|
|
865
|
+
maxHeight: 4096,
|
|
866
|
+
paddingX: 2,
|
|
867
|
+
paddingY: 2,
|
|
868
|
+
edgePadding: true,
|
|
869
|
+
duplicatePadding: true,
|
|
870
|
+
rotation: true,
|
|
871
|
+
stripWhitespaceX: true,
|
|
872
|
+
stripWhitespaceY: true,
|
|
873
|
+
pot: false,
|
|
874
|
+
filterMin: "Linear",
|
|
875
|
+
filterMag: "Linear",
|
|
876
|
+
premultiplyAlpha: true
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
// src/commands/spine/validate.ts
|
|
881
|
+
var validateCommand = new Command4("validate").description("Validate Spine files before uploading").argument("<directory>", "Directory containing Spine files").option("--batch", "Enable batch mode for multiple skeletons sharing atlas").action(async (directory, options) => {
|
|
882
|
+
const absolutePath = path3.resolve(directory);
|
|
883
|
+
const spinner = ora3(`Validating ${logger.file(directory)}...`).start();
|
|
884
|
+
try {
|
|
885
|
+
const result = validateDirectory(absolutePath, options.batch);
|
|
886
|
+
spinner.stop();
|
|
887
|
+
printValidationResult(result, directory);
|
|
888
|
+
if (!result.isValid) {
|
|
889
|
+
process.exit(1);
|
|
890
|
+
}
|
|
891
|
+
} catch (error) {
|
|
892
|
+
spinner.fail("Validation failed");
|
|
893
|
+
logger.error(error instanceof Error ? error.message : "Unknown error");
|
|
894
|
+
process.exit(1);
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
function printValidationResult(result, directory) {
|
|
898
|
+
console.log();
|
|
899
|
+
logger.header(`Validation: ${directory}`);
|
|
900
|
+
if (result.isBatchMode) {
|
|
901
|
+
console.log(` Mode: ${logger.value("Batch")} (${result.assets.length} skeletons)`);
|
|
902
|
+
if (result.sharedAtlas) {
|
|
903
|
+
console.log(` Shared Atlas: ${logger.file(path3.basename(result.sharedAtlas))}`);
|
|
904
|
+
console.log(` Shared Textures: ${result.sharedTextures.length}`);
|
|
905
|
+
}
|
|
906
|
+
} else if (result.assets.length > 0) {
|
|
907
|
+
console.log(` Mode: ${logger.value("Single")}`);
|
|
908
|
+
}
|
|
909
|
+
console.log(` Total Size: ${logger.value(formatFileSize(result.totalSize))}`);
|
|
910
|
+
console.log();
|
|
911
|
+
for (const asset of result.assets) {
|
|
912
|
+
const statusIcon = asset.isValid ? "\u2713" : "\u2717";
|
|
913
|
+
const statusColor = asset.isValid ? "green" : "red";
|
|
914
|
+
console.log(
|
|
915
|
+
` ${statusIcon} ${logger.file(asset.skeleton.name)}`
|
|
916
|
+
);
|
|
917
|
+
console.log(` Spine: ${asset.skeleton.spineVersion}`);
|
|
918
|
+
console.log(` Animations: ${asset.skeleton.animations.length}`);
|
|
919
|
+
console.log(` Skins: ${asset.skeleton.skins.length}`);
|
|
920
|
+
if (asset.atlasPath) {
|
|
921
|
+
console.log(` Atlas: ${path3.basename(asset.atlasPath)}`);
|
|
922
|
+
console.log(` Textures: ${asset.texturePaths.length}`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
console.log();
|
|
926
|
+
const errors = result.issues.filter((i) => i.type === "error");
|
|
927
|
+
const warnings = result.issues.filter((i) => i.type === "warning");
|
|
928
|
+
const infos = result.issues.filter((i) => i.type === "info");
|
|
929
|
+
for (const info of infos) {
|
|
930
|
+
logger.info(info.message);
|
|
931
|
+
}
|
|
932
|
+
if (warnings.length > 0) {
|
|
933
|
+
console.log();
|
|
934
|
+
for (const warning of warnings) {
|
|
935
|
+
logger.validationWarn(
|
|
936
|
+
warning.file ? `${warning.file}: ${warning.message}` : warning.message
|
|
937
|
+
);
|
|
938
|
+
if (warning.detail) {
|
|
939
|
+
console.log(` ${warning.detail}`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (errors.length > 0) {
|
|
944
|
+
console.log();
|
|
945
|
+
for (const error of errors) {
|
|
946
|
+
logger.validationError(
|
|
947
|
+
error.file ? `${error.file}: ${error.message}` : error.message,
|
|
948
|
+
error.detail
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
console.log();
|
|
953
|
+
if (result.isValid) {
|
|
954
|
+
logger.success("Validation passed! Ready for upload.");
|
|
955
|
+
} else {
|
|
956
|
+
logger.error(`Validation failed with ${errors.length} error(s).`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/commands/spine/upload.ts
|
|
961
|
+
import { Command as Command5 } from "commander";
|
|
962
|
+
import * as fs3 from "fs";
|
|
963
|
+
import * as path4 from "path";
|
|
964
|
+
import ora4 from "ora";
|
|
965
|
+
|
|
966
|
+
// src/lib/api.ts
|
|
967
|
+
var API_BASE = config.supabaseUrl;
|
|
968
|
+
async function uploadSpineAsset(formData) {
|
|
969
|
+
const token = getAccessToken();
|
|
970
|
+
if (!token) {
|
|
971
|
+
throw new Error("Not authenticated. Run `playtagon login` first.");
|
|
972
|
+
}
|
|
973
|
+
const response = await fetch(`${API_BASE}/functions/v1/spine-upload`, {
|
|
974
|
+
method: "POST",
|
|
975
|
+
headers: {
|
|
976
|
+
Authorization: `Bearer ${token}`
|
|
977
|
+
},
|
|
978
|
+
body: formData
|
|
979
|
+
});
|
|
980
|
+
if (!response.ok) {
|
|
981
|
+
const errorText = await response.text();
|
|
982
|
+
let errorMessage;
|
|
983
|
+
try {
|
|
984
|
+
const errorJson = JSON.parse(errorText);
|
|
985
|
+
errorMessage = errorJson.error || errorJson.message || errorText;
|
|
986
|
+
} catch {
|
|
987
|
+
errorMessage = errorText;
|
|
988
|
+
}
|
|
989
|
+
throw new Error(`Upload failed: ${errorMessage}`);
|
|
990
|
+
}
|
|
991
|
+
return response.json();
|
|
992
|
+
}
|
|
993
|
+
async function getStudios() {
|
|
994
|
+
const token = getAccessToken();
|
|
995
|
+
if (!token) {
|
|
996
|
+
throw new Error("Not authenticated. Run `playtagon login` first.");
|
|
997
|
+
}
|
|
998
|
+
const response = await fetch(
|
|
999
|
+
`${API_BASE}/rest/v1/studio_members?select=studio:studios(id,name,slug)`,
|
|
1000
|
+
{
|
|
1001
|
+
headers: {
|
|
1002
|
+
Authorization: `Bearer ${token}`,
|
|
1003
|
+
apikey: config.supabaseAnonKey
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
);
|
|
1007
|
+
if (!response.ok) {
|
|
1008
|
+
throw new Error("Failed to fetch studios");
|
|
1009
|
+
}
|
|
1010
|
+
const data = await response.json();
|
|
1011
|
+
return data.map((m) => m.studio).filter(Boolean);
|
|
1012
|
+
}
|
|
1013
|
+
async function getGames(studioId) {
|
|
1014
|
+
const token = getAccessToken();
|
|
1015
|
+
if (!token) {
|
|
1016
|
+
throw new Error("Not authenticated. Run `playtagon login` first.");
|
|
1017
|
+
}
|
|
1018
|
+
const response = await fetch(
|
|
1019
|
+
`${API_BASE}/rest/v1/games?studio_id=eq.${studioId}&select=id,name,slug`,
|
|
1020
|
+
{
|
|
1021
|
+
headers: {
|
|
1022
|
+
Authorization: `Bearer ${token}`,
|
|
1023
|
+
apikey: config.supabaseAnonKey
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
);
|
|
1027
|
+
if (!response.ok) {
|
|
1028
|
+
throw new Error("Failed to fetch games");
|
|
1029
|
+
}
|
|
1030
|
+
return response.json();
|
|
1031
|
+
}
|
|
1032
|
+
async function resolveStudio(studioIdOrSlug) {
|
|
1033
|
+
const token = getAccessToken();
|
|
1034
|
+
if (!token) {
|
|
1035
|
+
throw new Error("Not authenticated. Run `playtagon login` first.");
|
|
1036
|
+
}
|
|
1037
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
1038
|
+
studioIdOrSlug
|
|
1039
|
+
);
|
|
1040
|
+
const query = isUuid ? `id=eq.${studioIdOrSlug}` : `slug=eq.${studioIdOrSlug}`;
|
|
1041
|
+
const response = await fetch(
|
|
1042
|
+
`${API_BASE}/rest/v1/studios?${query}&select=id,name,slug`,
|
|
1043
|
+
{
|
|
1044
|
+
headers: {
|
|
1045
|
+
Authorization: `Bearer ${token}`,
|
|
1046
|
+
apikey: config.supabaseAnonKey
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
);
|
|
1050
|
+
if (!response.ok) {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
const data = await response.json();
|
|
1054
|
+
return data[0] || null;
|
|
1055
|
+
}
|
|
1056
|
+
async function resolveGame(studioId, gameIdOrSlug) {
|
|
1057
|
+
const token = getAccessToken();
|
|
1058
|
+
if (!token) {
|
|
1059
|
+
throw new Error("Not authenticated. Run `playtagon login` first.");
|
|
1060
|
+
}
|
|
1061
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
1062
|
+
gameIdOrSlug
|
|
1063
|
+
);
|
|
1064
|
+
const idQuery = isUuid ? `id=eq.${gameIdOrSlug}` : `slug=eq.${gameIdOrSlug}`;
|
|
1065
|
+
const response = await fetch(
|
|
1066
|
+
`${API_BASE}/rest/v1/games?studio_id=eq.${studioId}&${idQuery}&select=id,name,slug`,
|
|
1067
|
+
{
|
|
1068
|
+
headers: {
|
|
1069
|
+
Authorization: `Bearer ${token}`,
|
|
1070
|
+
apikey: config.supabaseAnonKey
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
);
|
|
1074
|
+
if (!response.ok) {
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
const data = await response.json();
|
|
1078
|
+
return data[0] || null;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/commands/spine/upload.ts
|
|
1082
|
+
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) => {
|
|
1083
|
+
if (!credentials.isLoggedIn()) {
|
|
1084
|
+
logger.error("Not logged in.");
|
|
1085
|
+
logger.info(`Run ${logger.command("playtagon login")} first.`);
|
|
1086
|
+
process.exit(1);
|
|
1087
|
+
}
|
|
1088
|
+
const studioOption = options.studio || config.defaultStudio;
|
|
1089
|
+
const gameOption = options.game || config.defaultGame;
|
|
1090
|
+
if (!studioOption) {
|
|
1091
|
+
logger.error("Studio is required.");
|
|
1092
|
+
logger.info(`Either provide ${logger.command("--studio <slug>")} or set a default:`);
|
|
1093
|
+
logger.info(` ${logger.command("playtagon config --studio <slug>")}`);
|
|
1094
|
+
logger.info(` ${logger.command("playtagon setup spine-integration --studio <slug>")}`);
|
|
1095
|
+
process.exit(1);
|
|
1096
|
+
}
|
|
1097
|
+
const absolutePath = path4.resolve(directory);
|
|
1098
|
+
const spinner = ora4("Validating files...").start();
|
|
1099
|
+
let validation;
|
|
1100
|
+
try {
|
|
1101
|
+
validation = validateDirectory(absolutePath, options.batch);
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
spinner.fail("Validation failed");
|
|
1104
|
+
logger.error(error instanceof Error ? error.message : "Unknown error");
|
|
1105
|
+
process.exit(1);
|
|
1106
|
+
}
|
|
1107
|
+
if (!validation.isValid) {
|
|
1108
|
+
spinner.fail("Validation failed");
|
|
1109
|
+
printValidationErrors(validation);
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
}
|
|
1112
|
+
spinner.succeed(`Validated ${validation.assets.length} asset(s)`);
|
|
1113
|
+
spinner.start("Resolving studio...");
|
|
1114
|
+
const studio = await resolveStudio(studioOption);
|
|
1115
|
+
if (!studio) {
|
|
1116
|
+
spinner.fail("Studio not found");
|
|
1117
|
+
logger.error(`Studio "${studioOption}" not found or you don't have access.`);
|
|
1118
|
+
process.exit(1);
|
|
1119
|
+
}
|
|
1120
|
+
spinner.succeed(`Studio: ${studio.name}`);
|
|
1121
|
+
let game = null;
|
|
1122
|
+
if (gameOption) {
|
|
1123
|
+
spinner.start("Resolving game...");
|
|
1124
|
+
game = await resolveGame(studio.id, gameOption);
|
|
1125
|
+
if (!game) {
|
|
1126
|
+
spinner.fail("Game not found");
|
|
1127
|
+
logger.error(`Game "${gameOption}" not found in studio "${studio.name}".`);
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
}
|
|
1130
|
+
spinner.succeed(`Game: ${game.name}`);
|
|
1131
|
+
}
|
|
1132
|
+
const assetName = options.name || path4.basename(absolutePath);
|
|
1133
|
+
let slug = options.slug || generateSlug(assetName);
|
|
1134
|
+
if (!validateSlug(slug)) {
|
|
1135
|
+
logger.error(`Invalid slug: "${slug}". Use lowercase letters, numbers, and hyphens only.`);
|
|
1136
|
+
process.exit(1);
|
|
1137
|
+
}
|
|
1138
|
+
console.log();
|
|
1139
|
+
logger.header("Upload Summary");
|
|
1140
|
+
logger.item("Directory", directory);
|
|
1141
|
+
logger.item("Studio", studio.name);
|
|
1142
|
+
if (game) logger.item("Game", game.name);
|
|
1143
|
+
logger.item("Name", assetName);
|
|
1144
|
+
logger.item("Slug", slug);
|
|
1145
|
+
logger.item("Size", formatFileSize(validation.totalSize));
|
|
1146
|
+
if (validation.isBatchMode) {
|
|
1147
|
+
logger.item("Mode", "Batch");
|
|
1148
|
+
logger.item("Skeletons", String(validation.assets.length));
|
|
1149
|
+
}
|
|
1150
|
+
if (options.dryRun) {
|
|
1151
|
+
console.log();
|
|
1152
|
+
logger.success("Dry run complete. No files were uploaded.");
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
console.log();
|
|
1156
|
+
spinner.start("Uploading...");
|
|
1157
|
+
try {
|
|
1158
|
+
const formData = await buildFormData(validation, {
|
|
1159
|
+
studioId: studio.id,
|
|
1160
|
+
gameId: game?.id,
|
|
1161
|
+
name: assetName,
|
|
1162
|
+
slug,
|
|
1163
|
+
description: options.description,
|
|
1164
|
+
tags: options.tags?.split(",").map((t) => t.trim()),
|
|
1165
|
+
batchMode: validation.isBatchMode
|
|
1166
|
+
});
|
|
1167
|
+
const result = await uploadSpineAsset(formData);
|
|
1168
|
+
if (result.success) {
|
|
1169
|
+
spinner.succeed("Upload complete!");
|
|
1170
|
+
console.log();
|
|
1171
|
+
if (result.assets && result.assets.length > 1) {
|
|
1172
|
+
logger.header("Uploaded Assets");
|
|
1173
|
+
for (const asset of result.assets) {
|
|
1174
|
+
console.log(` ${logger.value(asset.name)} (${asset.slug})`);
|
|
1175
|
+
console.log(` ID: ${asset.id}`);
|
|
1176
|
+
console.log(` Status: ${asset.status}`);
|
|
1177
|
+
}
|
|
1178
|
+
} else if (result.asset) {
|
|
1179
|
+
logger.header("Uploaded Asset");
|
|
1180
|
+
logger.item("ID", result.asset.id);
|
|
1181
|
+
logger.item("Name", result.asset.name);
|
|
1182
|
+
logger.item("Slug", result.asset.slug);
|
|
1183
|
+
logger.item("Status", result.asset.status);
|
|
1184
|
+
}
|
|
1185
|
+
} else {
|
|
1186
|
+
spinner.fail("Upload failed");
|
|
1187
|
+
logger.error(result.error || "Unknown error");
|
|
1188
|
+
process.exit(1);
|
|
1189
|
+
}
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
spinner.fail("Upload failed");
|
|
1192
|
+
logger.error(error instanceof Error ? error.message : "Unknown error");
|
|
1193
|
+
process.exit(1);
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
async function buildFormData(validation, options) {
|
|
1197
|
+
const formData = new FormData();
|
|
1198
|
+
formData.append("studioId", options.studioId);
|
|
1199
|
+
if (options.gameId) formData.append("gameId", options.gameId);
|
|
1200
|
+
formData.append("name", options.name);
|
|
1201
|
+
formData.append("slug", options.slug);
|
|
1202
|
+
if (options.description) formData.append("description", options.description);
|
|
1203
|
+
if (options.tags) formData.append("tags", JSON.stringify(options.tags));
|
|
1204
|
+
if (options.batchMode) formData.append("batchMode", "true");
|
|
1205
|
+
const filesAdded = /* @__PURE__ */ new Set();
|
|
1206
|
+
for (const asset of validation.assets) {
|
|
1207
|
+
const filePath = asset.skeleton.path;
|
|
1208
|
+
if (!filesAdded.has(filePath)) {
|
|
1209
|
+
const buffer = fs3.readFileSync(filePath);
|
|
1210
|
+
const blob = new Blob([buffer]);
|
|
1211
|
+
formData.append("files", blob, getBaseName(filePath));
|
|
1212
|
+
filesAdded.add(filePath);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (validation.sharedAtlas && !filesAdded.has(validation.sharedAtlas)) {
|
|
1216
|
+
const buffer = fs3.readFileSync(validation.sharedAtlas);
|
|
1217
|
+
const blob = new Blob([buffer]);
|
|
1218
|
+
formData.append("files", blob, getBaseName(validation.sharedAtlas));
|
|
1219
|
+
filesAdded.add(validation.sharedAtlas);
|
|
1220
|
+
}
|
|
1221
|
+
for (const texturePath of validation.sharedTextures) {
|
|
1222
|
+
if (!filesAdded.has(texturePath)) {
|
|
1223
|
+
const buffer = fs3.readFileSync(texturePath);
|
|
1224
|
+
const blob = new Blob([buffer]);
|
|
1225
|
+
formData.append("files", blob, getBaseName(texturePath));
|
|
1226
|
+
filesAdded.add(texturePath);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
for (const asset of validation.assets) {
|
|
1230
|
+
if (asset.atlasPath && !filesAdded.has(asset.atlasPath)) {
|
|
1231
|
+
const buffer = fs3.readFileSync(asset.atlasPath);
|
|
1232
|
+
const blob = new Blob([buffer]);
|
|
1233
|
+
formData.append("files", blob, getBaseName(asset.atlasPath));
|
|
1234
|
+
filesAdded.add(asset.atlasPath);
|
|
1235
|
+
}
|
|
1236
|
+
for (const texturePath of asset.texturePaths) {
|
|
1237
|
+
if (!filesAdded.has(texturePath)) {
|
|
1238
|
+
const buffer = fs3.readFileSync(texturePath);
|
|
1239
|
+
const blob = new Blob([buffer]);
|
|
1240
|
+
formData.append("files", blob, getBaseName(texturePath));
|
|
1241
|
+
filesAdded.add(texturePath);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return formData;
|
|
1246
|
+
}
|
|
1247
|
+
function printValidationErrors(validation) {
|
|
1248
|
+
const errors = validation.issues.filter((i) => i.type === "error");
|
|
1249
|
+
console.log();
|
|
1250
|
+
for (const error of errors) {
|
|
1251
|
+
logger.validationError(
|
|
1252
|
+
error.file ? `${error.file}: ${error.message}` : error.message,
|
|
1253
|
+
error.detail
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
console.log();
|
|
1257
|
+
logger.info(`Run ${logger.command("playtagon spine validate <dir>")} for full report.`);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// src/commands/spine/preset.ts
|
|
1261
|
+
import { Command as Command6 } from "commander";
|
|
1262
|
+
var presetCommand = new Command6("preset").description("Output Spine export preset JSON for consistent exports").option("--pretty", "Pretty print JSON (default: true)", true).option("--compact", "Output compact JSON").action((options) => {
|
|
1263
|
+
const pretty = !options.compact;
|
|
1264
|
+
if (pretty) {
|
|
1265
|
+
console.error(logger.info("Spine Export Preset for Playtagon Platform"));
|
|
1266
|
+
console.error();
|
|
1267
|
+
console.error("Usage:");
|
|
1268
|
+
console.error(" playtagon spine preset > playtagon-export.json");
|
|
1269
|
+
console.error(" Import this file into Spine Editor as an export preset.");
|
|
1270
|
+
console.error();
|
|
1271
|
+
}
|
|
1272
|
+
const json = pretty ? JSON.stringify(SPINE_EXPORT_PRESET, null, 2) : JSON.stringify(SPINE_EXPORT_PRESET);
|
|
1273
|
+
console.log(json);
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
// src/commands/spine/index.ts
|
|
1277
|
+
var spineCommand = new Command7("spine").description("Manage Spine animation assets").addCommand(validateCommand).addCommand(uploadCommand).addCommand(presetCommand);
|
|
1278
|
+
|
|
1279
|
+
// src/commands/setup.ts
|
|
1280
|
+
import { Command as Command8 } from "commander";
|
|
1281
|
+
import * as fs4 from "fs";
|
|
1282
|
+
import * as path5 from "path";
|
|
1283
|
+
import * as os from "os";
|
|
1284
|
+
import ora5 from "ora";
|
|
1285
|
+
var PLAYTAGON_DIR = path5.join(os.homedir(), ".playtagon");
|
|
1286
|
+
var setupCommand = new Command8("setup").description("Set up integrations").addCommand(spineIntegrationCommand());
|
|
1287
|
+
function spineIntegrationCommand() {
|
|
1288
|
+
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) => {
|
|
1289
|
+
if (!credentials.isLoggedIn()) {
|
|
1290
|
+
logger.error("Not logged in.");
|
|
1291
|
+
logger.info(`Run ${logger.command("playtagon login")} first.`);
|
|
1292
|
+
process.exit(1);
|
|
1293
|
+
}
|
|
1294
|
+
const spinner = ora5("Checking authentication...").start();
|
|
1295
|
+
const user = await getCurrentUser();
|
|
1296
|
+
if (!user) {
|
|
1297
|
+
spinner.fail("Session expired");
|
|
1298
|
+
logger.info(`Run ${logger.command("playtagon login")} to re-authenticate.`);
|
|
1299
|
+
process.exit(1);
|
|
1300
|
+
}
|
|
1301
|
+
spinner.succeed(`Logged in as ${user.email}`);
|
|
1302
|
+
let studioSlug = options.studio;
|
|
1303
|
+
if (!studioSlug) {
|
|
1304
|
+
spinner.start("Fetching studios...");
|
|
1305
|
+
const studios = await getStudios();
|
|
1306
|
+
spinner.stop();
|
|
1307
|
+
if (studios.length === 0) {
|
|
1308
|
+
logger.error("No studios found. Join or create a studio first.");
|
|
1309
|
+
process.exit(1);
|
|
1310
|
+
}
|
|
1311
|
+
if (studios.length === 1) {
|
|
1312
|
+
studioSlug = studios[0].slug;
|
|
1313
|
+
logger.info(`Using studio: ${logger.value(studios[0].name)}`);
|
|
1314
|
+
} else {
|
|
1315
|
+
logger.header("Available Studios");
|
|
1316
|
+
studios.forEach((s, i) => {
|
|
1317
|
+
console.log(` ${i + 1}. ${s.name} (${logger.value(s.slug)})`);
|
|
1318
|
+
});
|
|
1319
|
+
console.log();
|
|
1320
|
+
logger.info(`Specify studio with ${logger.command("--studio <slug>")}`);
|
|
1321
|
+
logger.info(`Example: ${logger.command(`playtagon setup spine-integration --studio ${studios[0].slug}`)}`);
|
|
1322
|
+
process.exit(1);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
spinner.start("Creating setup files...");
|
|
1326
|
+
if (!fs4.existsSync(PLAYTAGON_DIR)) {
|
|
1327
|
+
fs4.mkdirSync(PLAYTAGON_DIR, { recursive: true, mode: 448 });
|
|
1328
|
+
}
|
|
1329
|
+
const scriptPaths = generatePostExportScripts(studioSlug, options.game, options.force);
|
|
1330
|
+
const presetPath = generateExportPreset(scriptPaths.sh, options.force);
|
|
1331
|
+
spinner.succeed("Setup complete!");
|
|
1332
|
+
console.log();
|
|
1333
|
+
logger.header("Setup Complete");
|
|
1334
|
+
console.log();
|
|
1335
|
+
logger.info("Files created:");
|
|
1336
|
+
console.log(` ${logger.file(scriptPaths.sh)}`);
|
|
1337
|
+
if (process.platform === "win32") {
|
|
1338
|
+
console.log(` ${logger.file(scriptPaths.bat)}`);
|
|
1339
|
+
}
|
|
1340
|
+
console.log(` ${logger.file(presetPath)}`);
|
|
1341
|
+
console.log();
|
|
1342
|
+
logger.header("Next Steps");
|
|
1343
|
+
console.log();
|
|
1344
|
+
console.log(" 1. Open Spine Editor");
|
|
1345
|
+
console.log(" 2. Go to File \u2192 Export...");
|
|
1346
|
+
console.log(" 3. Click the gear icon (\u2699) next to the preset dropdown");
|
|
1347
|
+
console.log(' 4. Click "Import" and select:');
|
|
1348
|
+
console.log(` ${logger.file(presetPath)}`);
|
|
1349
|
+
console.log(' 5. The preset "Upload to Playtagon" will be available');
|
|
1350
|
+
console.log();
|
|
1351
|
+
console.log(" Now when you export with this preset, files automatically");
|
|
1352
|
+
console.log(" upload to Playtagon Platform!");
|
|
1353
|
+
console.log();
|
|
1354
|
+
let gameSlug = options.game;
|
|
1355
|
+
if (gameSlug) {
|
|
1356
|
+
spinner.start("Verifying game...");
|
|
1357
|
+
const studios = await getStudios();
|
|
1358
|
+
const studio = studios.find((s) => s.slug === studioSlug);
|
|
1359
|
+
if (studio) {
|
|
1360
|
+
const games = await getGames(studio.id);
|
|
1361
|
+
const game = games.find(
|
|
1362
|
+
(g) => g.slug === gameSlug || g.id === gameSlug || g.name === gameSlug
|
|
1363
|
+
);
|
|
1364
|
+
if (!game) {
|
|
1365
|
+
spinner.fail("Game not found");
|
|
1366
|
+
logger.error(`Game "${gameSlug}" not found in studio "${studioSlug}".`);
|
|
1367
|
+
if (games.length > 0) {
|
|
1368
|
+
console.log();
|
|
1369
|
+
logger.info("Available games:");
|
|
1370
|
+
games.forEach((g) => {
|
|
1371
|
+
console.log(` ${logger.value(g.slug)} - ${g.name}`);
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
process.exit(1);
|
|
1375
|
+
}
|
|
1376
|
+
gameSlug = game.slug;
|
|
1377
|
+
spinner.succeed(`Game: ${game.name}`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
config.defaultStudio = studioSlug;
|
|
1381
|
+
if (gameSlug) {
|
|
1382
|
+
config.defaultGame = gameSlug;
|
|
1383
|
+
}
|
|
1384
|
+
if (gameSlug) {
|
|
1385
|
+
logger.info(`Uploads will go to: ${logger.value(studioSlug)} / ${logger.value(gameSlug)}`);
|
|
1386
|
+
} else {
|
|
1387
|
+
logger.info(`Uploads will go to studio: ${logger.value(studioSlug)}`);
|
|
1388
|
+
logger.info(`Add ${logger.command("--game <slug>")} to also set default game.`);
|
|
1389
|
+
}
|
|
1390
|
+
console.log();
|
|
1391
|
+
logger.info("Defaults saved. You can change them later with:");
|
|
1392
|
+
logger.info(` ${logger.command("playtagon config --studio <slug> --game <slug>")}`);
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
function generatePostExportScripts(studioSlug, gameSlug, force = false) {
|
|
1396
|
+
const shPath = path5.join(PLAYTAGON_DIR, "upload.sh");
|
|
1397
|
+
const batPath = path5.join(PLAYTAGON_DIR, "upload.bat");
|
|
1398
|
+
let uploadCmd = `playtagon spine upload "$1" --studio ${studioSlug}`;
|
|
1399
|
+
if (gameSlug) {
|
|
1400
|
+
uploadCmd += ` --game ${gameSlug}`;
|
|
1401
|
+
}
|
|
1402
|
+
const shScript = `#!/bin/bash
|
|
1403
|
+
# Playtagon Spine Upload Script
|
|
1404
|
+
# Auto-generated by: playtagon setup spine-integration
|
|
1405
|
+
#
|
|
1406
|
+
# This script is called by Spine Editor after export.
|
|
1407
|
+
# It uploads the exported files to Playtagon Platform.
|
|
1408
|
+
|
|
1409
|
+
set -e
|
|
1410
|
+
|
|
1411
|
+
EXPORT_DIR="$1"
|
|
1412
|
+
|
|
1413
|
+
if [ -z "$EXPORT_DIR" ]; then
|
|
1414
|
+
echo "Error: No export directory provided"
|
|
1415
|
+
exit 1
|
|
1416
|
+
fi
|
|
1417
|
+
|
|
1418
|
+
echo "Uploading to Playtagon..."
|
|
1419
|
+
${uploadCmd}
|
|
1420
|
+
|
|
1421
|
+
# macOS notification (optional)
|
|
1422
|
+
if command -v osascript &> /dev/null; then
|
|
1423
|
+
osascript -e 'display notification "Spine export uploaded successfully" with title "Playtagon"' 2>/dev/null || true
|
|
1424
|
+
fi
|
|
1425
|
+
|
|
1426
|
+
# Linux notification (optional)
|
|
1427
|
+
if command -v notify-send &> /dev/null; then
|
|
1428
|
+
notify-send "Playtagon" "Spine export uploaded successfully" 2>/dev/null || true
|
|
1429
|
+
fi
|
|
1430
|
+
|
|
1431
|
+
echo "Upload complete!"
|
|
1432
|
+
`;
|
|
1433
|
+
let uploadCmdWin = `playtagon spine upload "%~1" --studio ${studioSlug}`;
|
|
1434
|
+
if (gameSlug) {
|
|
1435
|
+
uploadCmdWin += ` --game ${gameSlug}`;
|
|
1436
|
+
}
|
|
1437
|
+
const batScript = `@echo off
|
|
1438
|
+
REM Playtagon Spine Upload Script
|
|
1439
|
+
REM Auto-generated by: playtagon setup spine-integration
|
|
1440
|
+
REM
|
|
1441
|
+
REM This script is called by Spine Editor after export.
|
|
1442
|
+
REM It uploads the exported files to Playtagon Platform.
|
|
1443
|
+
|
|
1444
|
+
set EXPORT_DIR=%~1
|
|
1445
|
+
|
|
1446
|
+
if "%EXPORT_DIR%"=="" (
|
|
1447
|
+
echo Error: No export directory provided
|
|
1448
|
+
exit /b 1
|
|
1449
|
+
)
|
|
1450
|
+
|
|
1451
|
+
echo Uploading to Playtagon...
|
|
1452
|
+
${uploadCmdWin}
|
|
1453
|
+
|
|
1454
|
+
REM Windows notification (PowerShell)
|
|
1455
|
+
powershell -Command "& {Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('Spine export uploaded successfully', 'Playtagon', 'OK', 'Information')}" 2>nul
|
|
1456
|
+
|
|
1457
|
+
echo Upload complete!
|
|
1458
|
+
`;
|
|
1459
|
+
if (!fs4.existsSync(shPath) || force) {
|
|
1460
|
+
fs4.writeFileSync(shPath, shScript, { mode: 493 });
|
|
1461
|
+
} else {
|
|
1462
|
+
logger.warn(`${shPath} already exists. Use --force to overwrite.`);
|
|
1463
|
+
}
|
|
1464
|
+
if (!fs4.existsSync(batPath) || force) {
|
|
1465
|
+
fs4.writeFileSync(batPath, batScript);
|
|
1466
|
+
}
|
|
1467
|
+
return { sh: shPath, bat: batPath };
|
|
1468
|
+
}
|
|
1469
|
+
function generateExportPreset(scriptPath, force = false) {
|
|
1470
|
+
const presetPath = path5.join(PLAYTAGON_DIR, "playtagon-spine-preset.export.json");
|
|
1471
|
+
const preset = {
|
|
1472
|
+
...SPINE_EXPORT_PRESET,
|
|
1473
|
+
name: "Upload to Playtagon",
|
|
1474
|
+
postScript: process.platform === "win32" ? `"${scriptPath.replace(".sh", ".bat")}" "{output}"` : `"${scriptPath}" "{output}"`
|
|
1475
|
+
};
|
|
1476
|
+
if (!fs4.existsSync(presetPath) || force) {
|
|
1477
|
+
fs4.writeFileSync(presetPath, JSON.stringify(preset, null, 2));
|
|
1478
|
+
} else {
|
|
1479
|
+
logger.warn(`${presetPath} already exists. Use --force to overwrite.`);
|
|
1480
|
+
}
|
|
1481
|
+
return presetPath;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// src/commands/config.ts
|
|
1485
|
+
import { Command as Command9 } from "commander";
|
|
1486
|
+
import ora6 from "ora";
|
|
1487
|
+
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) => {
|
|
1488
|
+
if (options.clear) {
|
|
1489
|
+
config.defaultStudio = void 0;
|
|
1490
|
+
config.defaultGame = void 0;
|
|
1491
|
+
logger.success("Default settings cleared.");
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
if (options.studio) {
|
|
1495
|
+
if (!credentials.isLoggedIn()) {
|
|
1496
|
+
logger.error("Not logged in.");
|
|
1497
|
+
logger.info(`Run ${logger.command("playtagon login")} first.`);
|
|
1498
|
+
process.exit(1);
|
|
1499
|
+
}
|
|
1500
|
+
const spinner = ora6("Verifying studio...").start();
|
|
1501
|
+
const studios = await getStudios();
|
|
1502
|
+
const studio = studios.find(
|
|
1503
|
+
(s) => s.slug === options.studio || s.id === options.studio || s.name === options.studio
|
|
1504
|
+
);
|
|
1505
|
+
if (!studio) {
|
|
1506
|
+
spinner.fail("Studio not found");
|
|
1507
|
+
logger.error(`Studio "${options.studio}" not found or you don't have access.`);
|
|
1508
|
+
console.log();
|
|
1509
|
+
logger.info("Available studios:");
|
|
1510
|
+
studios.forEach((s) => {
|
|
1511
|
+
console.log(` ${logger.value(s.slug)} - ${s.name}`);
|
|
1512
|
+
});
|
|
1513
|
+
process.exit(1);
|
|
1514
|
+
}
|
|
1515
|
+
spinner.succeed(`Default studio set: ${studio.name} (${studio.slug})`);
|
|
1516
|
+
config.defaultStudio = studio.slug;
|
|
1517
|
+
}
|
|
1518
|
+
if (options.game) {
|
|
1519
|
+
if (!credentials.isLoggedIn()) {
|
|
1520
|
+
logger.error("Not logged in.");
|
|
1521
|
+
logger.info(`Run ${logger.command("playtagon login")} first.`);
|
|
1522
|
+
process.exit(1);
|
|
1523
|
+
}
|
|
1524
|
+
const studioSlug = options.studio || config.defaultStudio;
|
|
1525
|
+
if (!studioSlug) {
|
|
1526
|
+
logger.error("Studio is required to set default game.");
|
|
1527
|
+
logger.info(`Either provide ${logger.command("--studio <slug>")} or set a default studio first.`);
|
|
1528
|
+
process.exit(1);
|
|
1529
|
+
}
|
|
1530
|
+
const spinner = ora6("Verifying game...").start();
|
|
1531
|
+
const studios = await getStudios();
|
|
1532
|
+
const studio = studios.find(
|
|
1533
|
+
(s) => s.slug === studioSlug || s.id === studioSlug || s.name === studioSlug
|
|
1534
|
+
);
|
|
1535
|
+
if (!studio) {
|
|
1536
|
+
spinner.fail("Studio not found");
|
|
1537
|
+
process.exit(1);
|
|
1538
|
+
}
|
|
1539
|
+
const games = await getGames(studio.id);
|
|
1540
|
+
const game = games.find(
|
|
1541
|
+
(g) => g.slug === options.game || g.id === options.game || g.name === options.game
|
|
1542
|
+
);
|
|
1543
|
+
if (!game) {
|
|
1544
|
+
spinner.fail("Game not found");
|
|
1545
|
+
logger.error(`Game "${options.game}" not found in studio "${studio.name}".`);
|
|
1546
|
+
console.log();
|
|
1547
|
+
if (games.length > 0) {
|
|
1548
|
+
logger.info("Available games:");
|
|
1549
|
+
games.forEach((g) => {
|
|
1550
|
+
console.log(` ${logger.value(g.slug)} - ${g.name}`);
|
|
1551
|
+
});
|
|
1552
|
+
} else {
|
|
1553
|
+
logger.info("No games in this studio yet.");
|
|
1554
|
+
}
|
|
1555
|
+
process.exit(1);
|
|
1556
|
+
}
|
|
1557
|
+
spinner.succeed(`Default game set: ${game.name} (${game.slug})`);
|
|
1558
|
+
config.defaultGame = game.slug;
|
|
1559
|
+
}
|
|
1560
|
+
if (!options.studio && !options.game && !options.clear) {
|
|
1561
|
+
logger.header("Current Configuration");
|
|
1562
|
+
console.log();
|
|
1563
|
+
logger.item("Config file", config.path);
|
|
1564
|
+
console.log();
|
|
1565
|
+
const currentStudio = config.defaultStudio;
|
|
1566
|
+
const currentGame = config.defaultGame;
|
|
1567
|
+
if (currentStudio || currentGame) {
|
|
1568
|
+
logger.header("Default Upload Target");
|
|
1569
|
+
if (currentStudio) {
|
|
1570
|
+
logger.item("Studio", currentStudio);
|
|
1571
|
+
}
|
|
1572
|
+
if (currentGame) {
|
|
1573
|
+
logger.item("Game", currentGame);
|
|
1574
|
+
}
|
|
1575
|
+
console.log();
|
|
1576
|
+
logger.info("These defaults are used when --studio/--game are not provided.");
|
|
1577
|
+
console.log();
|
|
1578
|
+
logger.info(`To change: ${logger.command("playtagon config --studio <slug> --game <slug>")}`);
|
|
1579
|
+
logger.info(`To clear: ${logger.command("playtagon config --clear")}`);
|
|
1580
|
+
} else {
|
|
1581
|
+
logger.info("No default studio or game set.");
|
|
1582
|
+
console.log();
|
|
1583
|
+
logger.info(`Set defaults: ${logger.command("playtagon config --studio <slug> --game <slug>")}`);
|
|
1584
|
+
logger.info(`Or run setup: ${logger.command("playtagon setup spine-integration")}`);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
// src/index.ts
|
|
1590
|
+
var program = new Command10();
|
|
1591
|
+
program.name("playtagon").description("Playtagon CLI - Upload and manage game assets").version("0.1.0");
|
|
1592
|
+
program.addCommand(loginCommand);
|
|
1593
|
+
program.addCommand(logoutCommand);
|
|
1594
|
+
program.addCommand(whoamiCommand);
|
|
1595
|
+
program.addCommand(spineCommand);
|
|
1596
|
+
program.addCommand(setupCommand);
|
|
1597
|
+
program.addCommand(configCommand);
|
|
1598
|
+
program.parse();
|
|
1599
|
+
//# sourceMappingURL=index.js.map
|