castle-cli-reboot 1.0.3 → 1.0.4
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/package.json +3 -2
- package/tool/castle-terminal.js +14 -2
- package/tool/ua.js +157 -0
- package/gd.js +0 -135
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "castle-cli-reboot",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ct": "./tool/castle-terminal.js"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
+
"axios": "^1.20.0",
|
|
9
10
|
"castle-cli": "^2.0.6",
|
|
11
|
+
"form-data": "^4.0.0",
|
|
10
12
|
"http-server": "^14.1.1"
|
|
11
13
|
}
|
|
12
14
|
}
|
|
13
|
-
|
package/tool/castle-terminal.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import readline from 'readline';
|
|
3
3
|
import { getDeck } from './gd.js';
|
|
4
4
|
import { saveDeck } from './sd.js';
|
|
5
|
+
import { uploadAudio } from './ua.js';
|
|
5
6
|
|
|
6
7
|
const rl = readline.createInterface({
|
|
7
8
|
input: process.stdin,
|
|
@@ -11,10 +12,11 @@ const rl = readline.createInterface({
|
|
|
11
12
|
|
|
12
13
|
console.clear();
|
|
13
14
|
console.log("======================================================");
|
|
14
|
-
console.log("
|
|
15
|
+
console.log(" CASTLE COMMAND LINE INTERFACE REBOOT ");
|
|
15
16
|
console.log("Available Commands: ");
|
|
16
17
|
console.log(" gd <id> [folder] - Extracts deck using its ID ");
|
|
17
18
|
console.log(" sd [folder] - Saves the local deck ");
|
|
19
|
+
console.log(" ua <file> - Uploads audio asset to Castle ");
|
|
18
20
|
console.log(" exit - Exits Castle custom terminal ");
|
|
19
21
|
console.log("======================================================");
|
|
20
22
|
|
|
@@ -28,11 +30,13 @@ rl.on('line', async (line) => {
|
|
|
28
30
|
return;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
// Handle space-separated command aliases
|
|
33
|
+
// Handle space-separated command aliases
|
|
32
34
|
if (/^get\s+deck\b/i.test(input)) {
|
|
33
35
|
input = input.replace(/^get\s+deck\b/i, 'gd');
|
|
34
36
|
} else if (/^save\s+deck\b/i.test(input)) {
|
|
35
37
|
input = input.replace(/^save\s+deck\b/i, 'sd');
|
|
38
|
+
} else if (/^upload\s+audio\b/i.test(input)) {
|
|
39
|
+
input = input.replace(/^upload\s+audio\b/i, 'ua');
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
const parts = input.split(/\s+/);
|
|
@@ -56,6 +60,14 @@ rl.on('line', async (line) => {
|
|
|
56
60
|
await saveDeck(customPath);
|
|
57
61
|
break;
|
|
58
62
|
}
|
|
63
|
+
case 'ua':
|
|
64
|
+
case 'upload-audio':
|
|
65
|
+
case 'uploadaudio':
|
|
66
|
+
case 'upload': {
|
|
67
|
+
const filePath = parts[1];
|
|
68
|
+
await uploadAudio(filePath);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
59
71
|
case 'exit':
|
|
60
72
|
case 'quit':
|
|
61
73
|
console.log("Exiting Terminal...");
|
package/tool/ua.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import FormData from 'form-data';
|
|
7
|
+
|
|
8
|
+
function getToken() {
|
|
9
|
+
try {
|
|
10
|
+
const configPath = path.join(os.homedir(), '.castle', 'config.json');
|
|
11
|
+
if (fs.existsSync(configPath)) {
|
|
12
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8')).token;
|
|
13
|
+
}
|
|
14
|
+
} catch (e) {}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getAudioDuration(filePath) {
|
|
19
|
+
try {
|
|
20
|
+
const cmd = `ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${filePath}"`;
|
|
21
|
+
const durationStr = execSync(cmd).toString().trim();
|
|
22
|
+
return parseFloat(durationStr);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error("Error checking audio duration with ffprobe. Ensure ffmpeg/ffprobe is installed.");
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function extractUrlId(url) {
|
|
30
|
+
if (!url) return '';
|
|
31
|
+
const filename = url.split('/').pop() || '';
|
|
32
|
+
return filename.replace(/\.[^/.]+$/, '');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function uploadSingleFile(filePath) {
|
|
36
|
+
const filename = path.basename(filePath);
|
|
37
|
+
const token = getToken();
|
|
38
|
+
|
|
39
|
+
const form = new FormData();
|
|
40
|
+
const operations = {
|
|
41
|
+
query: `
|
|
42
|
+
mutation uploadAudioFile($file: Upload!) {
|
|
43
|
+
uploadAudioFile(file: $file) {
|
|
44
|
+
fileId
|
|
45
|
+
url
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`,
|
|
49
|
+
variables: { file: null }
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
form.append('operations', JSON.stringify(operations));
|
|
53
|
+
form.append('map', JSON.stringify({ "0": ["variables.file"] }));
|
|
54
|
+
form.append('0', fs.createReadStream(filePath), filename);
|
|
55
|
+
|
|
56
|
+
const headers = {
|
|
57
|
+
...form.getHeaders(),
|
|
58
|
+
'X-OS': 'cli',
|
|
59
|
+
'X-CLI-API-Version': '4',
|
|
60
|
+
'Accept': 'application/json',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (token) {
|
|
64
|
+
headers['X-Auth-Token'] = token;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const response = await axios.post('https://api.castle.xyz/graphql', form, { headers });
|
|
68
|
+
|
|
69
|
+
if (response.data.errors) {
|
|
70
|
+
throw new Error(response.data.errors[0].message);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = response.data?.data?.uploadAudioFile;
|
|
74
|
+
if (result && result.url) {
|
|
75
|
+
result.urlId = extractUrlId(result.url);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function uploadAudio(filePath) {
|
|
82
|
+
if (!filePath) {
|
|
83
|
+
console.log("Usage: ua <filePath>");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const fullPath = path.resolve(filePath);
|
|
88
|
+
if (!fs.existsSync(fullPath)) {
|
|
89
|
+
console.log(`Error: File not found at ${fullPath}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const duration = getAudioDuration(fullPath);
|
|
94
|
+
const ext = path.extname(fullPath);
|
|
95
|
+
const baseName = path.basename(fullPath, ext);
|
|
96
|
+
const dir = path.dirname(fullPath);
|
|
97
|
+
|
|
98
|
+
console.log(`Audio Duration: ${duration.toFixed(2)}s`);
|
|
99
|
+
|
|
100
|
+
const results = [];
|
|
101
|
+
|
|
102
|
+
if (duration <= 30) {
|
|
103
|
+
console.log(`Uploading ${path.basename(fullPath)}...`);
|
|
104
|
+
try {
|
|
105
|
+
const res = await uploadSingleFile(fullPath);
|
|
106
|
+
console.log(`\nSuccess! Single Audio Uploaded:`);
|
|
107
|
+
console.log(`File ID: ${res.fileId}`);
|
|
108
|
+
console.log(`URL ID: ${res.urlId}`);
|
|
109
|
+
console.log(`URL: ${res.url}\n`);
|
|
110
|
+
return [res];
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error("Upload error:", err.message);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const chunkDuration = 30;
|
|
118
|
+
const totalChunks = Math.ceil(duration / chunkDuration);
|
|
119
|
+
|
|
120
|
+
console.log(`Audio exceeds 30s. Splitting into ${totalChunks} parts...`);
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
123
|
+
const startTime = i * chunkDuration;
|
|
124
|
+
const chunkFileName = `${baseName}_part${i + 1}${ext}`;
|
|
125
|
+
const chunkPath = path.join(dir, `_temp_${chunkFileName}`);
|
|
126
|
+
|
|
127
|
+
console.log(`\n[Part ${i + 1}/${totalChunks}] Cutting ${startTime}s to ${startTime + chunkDuration}s...`);
|
|
128
|
+
|
|
129
|
+
const cutCmd = `ffmpeg -y -ss ${startTime} -t ${chunkDuration} -i "${fullPath}" -c copy "${chunkPath}" -loglevel error`;
|
|
130
|
+
execSync(cutCmd);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
console.log(`Uploading ${chunkFileName}...`);
|
|
134
|
+
const res = await uploadSingleFile(chunkPath);
|
|
135
|
+
console.log(`Part ${i + 1} Uploaded -> File ID: ${res.fileId} | URL ID: ${res.urlId} | URL: ${res.url}`);
|
|
136
|
+
results.push({ part: i + 1, ...res });
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(`Failed to upload part ${i + 1}:`, err.message);
|
|
139
|
+
} finally {
|
|
140
|
+
if (fs.existsSync(chunkPath)) {
|
|
141
|
+
fs.unlinkSync(chunkPath);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(`\n=== All Audio Parts Uploaded ===`);
|
|
147
|
+
console.table(results);
|
|
148
|
+
return results;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (process.argv[1] && process.argv[1].endsWith('ua.js')) {
|
|
152
|
+
const fileArg = process.argv[2];
|
|
153
|
+
if (fileArg) {
|
|
154
|
+
uploadAudio(fileArg);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
package/gd.js
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import readline from 'readline';
|
|
5
|
-
|
|
6
|
-
function getCastleModulePath(subpath) {
|
|
7
|
-
const localPath = path.resolve('./node_modules/castle-cli/dist', subpath);
|
|
8
|
-
if (fs.existsSync(localPath)) return localPath;
|
|
9
|
-
const parentLocalPath = path.resolve('../node_modules/castle-cli/dist', subpath);
|
|
10
|
-
if (fs.existsSync(parentLocalPath)) return parentLocalPath;
|
|
11
|
-
try {
|
|
12
|
-
const npmRoot = execSync('npm root -g', { stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
|
|
13
|
-
const globalPath = path.join(npmRoot, 'castle-cli', 'dist', subpath);
|
|
14
|
-
if (fs.existsSync(globalPath)) return globalPath;
|
|
15
|
-
} catch (e) {}
|
|
16
|
-
throw new Error(`Could not find castle-cli module '${subpath}'. Make sure castle-cli is installed locally or globally.`);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const API = await import(`file://${getCastleModulePath('api.js')}`);
|
|
20
|
-
const { setToken, getToken } = await import(`file://${getCastleModulePath('config.js')}`);
|
|
21
|
-
|
|
22
|
-
function promptInput(query) {
|
|
23
|
-
const rl = readline.createInterface({
|
|
24
|
-
input: process.stdin,
|
|
25
|
-
output: process.stdout,
|
|
26
|
-
});
|
|
27
|
-
return new Promise((resolve) => {
|
|
28
|
-
rl.question(query, (answer) => {
|
|
29
|
-
rl.close();
|
|
30
|
-
resolve(answer.trim());
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function ensureAuth() {
|
|
36
|
-
const savedToken = getToken();
|
|
37
|
-
const promptMsg = savedToken
|
|
38
|
-
? 'Enter Castle Auth Token (or press Enter to use saved token): '
|
|
39
|
-
: 'Enter Castle Auth Token: ';
|
|
40
|
-
|
|
41
|
-
const inputToken = await promptInput(promptMsg);
|
|
42
|
-
if (inputToken) {
|
|
43
|
-
setToken(inputToken);
|
|
44
|
-
} else if (!savedToken) {
|
|
45
|
-
console.error("Error: Auth token is required.");
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function getDeck(deckId, customPath) {
|
|
52
|
-
if (!deckId) {
|
|
53
|
-
console.error("Error: gd need's atleast two info, the deckId, then the Folder. like this: gd <deckId> <Folder>");
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (!(await ensureAuth())) return;
|
|
58
|
-
|
|
59
|
-
const deckDir = path.resolve(customPath || '/sdcard/deck');
|
|
60
|
-
console.log(`Getting ${deckId} to ${deckDir}...`);
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
const deck = await API.deck(deckId);
|
|
64
|
-
if (!deck) {
|
|
65
|
-
console.error("You're not allowed to get this deck or it doesn't exist..");
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
fs.rmSync(deckDir, { recursive: true, force: true });
|
|
70
|
-
fs.mkdirSync(deckDir, { recursive: true });
|
|
71
|
-
|
|
72
|
-
const cards = deck.cards?.length ? deck.cards : deck.initialCard ? [deck.initialCard] : [];
|
|
73
|
-
|
|
74
|
-
fs.writeFileSync(
|
|
75
|
-
path.join(deckDir, 'deck.json'),
|
|
76
|
-
JSON.stringify({
|
|
77
|
-
deckId: deck.deckId,
|
|
78
|
-
title: deck.title ?? '',
|
|
79
|
-
visibility: deck.visibility,
|
|
80
|
-
variables: deck.variables ?? [],
|
|
81
|
-
initialCard: deck.initialCard,
|
|
82
|
-
cards,
|
|
83
|
-
}, null, 2)
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
for (const card of cards) {
|
|
87
|
-
if (!card.cardId || !card.sceneDataUrl) continue;
|
|
88
|
-
console.log(`Decompiling Scene.json for card ${card.cardId}...`);
|
|
89
|
-
|
|
90
|
-
const sceneData = await API.downloadSceneData(card.sceneDataUrl);
|
|
91
|
-
const cardDir = path.join(deckDir, 'cards', card.cardId);
|
|
92
|
-
fs.mkdirSync(cardDir, { recursive: true });
|
|
93
|
-
|
|
94
|
-
fs.writeFileSync(path.join(cardDir, 'card.json'), JSON.stringify(card, null, 2));
|
|
95
|
-
|
|
96
|
-
const sceneDir = path.join(cardDir, 'scene');
|
|
97
|
-
fs.mkdirSync(sceneDir, { recursive: true });
|
|
98
|
-
|
|
99
|
-
const sceneObj = typeof sceneData === 'string' ? JSON.parse(sceneData) : sceneData;
|
|
100
|
-
fs.writeFileSync(path.join(sceneDir, 'scene.json'), JSON.stringify(sceneObj, null, 2));
|
|
101
|
-
|
|
102
|
-
const library = sceneObj.snapshot?.library || {};
|
|
103
|
-
const actors = sceneObj.snapshot?.actors || [];
|
|
104
|
-
const blueprintsDir = path.join(sceneDir, 'blueprints');
|
|
105
|
-
fs.mkdirSync(blueprintsDir, { recursive: true });
|
|
106
|
-
|
|
107
|
-
const slugMap = {};
|
|
108
|
-
for (const [uuid, entry] of Object.entries(library)) {
|
|
109
|
-
if (entry.entryType === 'actorBlueprint') {
|
|
110
|
-
const safeName = (entry.title || uuid).replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
|
|
111
|
-
slugMap[safeName] = uuid;
|
|
112
|
-
fs.writeFileSync(path.join(blueprintsDir, `${safeName}.json`), JSON.stringify(entry, null, 2));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
let actorsYaml = "";
|
|
117
|
-
actors.forEach((actor, index) => {
|
|
118
|
-
actorsYaml += `a${index}:\n`;
|
|
119
|
-
if (actor.title) actorsYaml += ` title: "${actor.title}"\n`;
|
|
120
|
-
if (actor.blueprintId) actorsYaml += ` blueprint: "${actor.blueprintId}"\n`;
|
|
121
|
-
actorsYaml += ` x: ${actor.x || 0}\n`;
|
|
122
|
-
actorsYaml += ` y: ${actor.y || 0}\n`;
|
|
123
|
-
});
|
|
124
|
-
fs.writeFileSync(path.join(sceneDir, 'actors.yaml'), actorsYaml);
|
|
125
|
-
|
|
126
|
-
const castleDir = path.join(sceneDir, '.castle');
|
|
127
|
-
fs.mkdirSync(castleDir, { recursive: true });
|
|
128
|
-
fs.writeFileSync(path.join(castleDir, 'slug-map.json'), JSON.stringify(slugMap, null, 2));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
console.log(`Decompiled deck in ${deckDir}`);
|
|
132
|
-
} catch (err) {
|
|
133
|
-
console.error("Error during the decompilation", err);
|
|
134
|
-
}
|
|
135
|
-
}
|