pushnetgo-cli 1.0.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/README.md +96 -0
- package/bin/pushnetgo-cli.js +38 -0
- package/commands/sync-music.js +63 -0
- package/commands/token.js +37 -0
- package/lib/auth.js +49 -0
- package/lib/firestore.js +23 -0
- package/lib/uploader.js +56 -0
- package/package.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# PushNetGo CLI
|
|
2
|
+
|
|
3
|
+
Command-line tool for PushNetGo App — sync music, validate API keys. Designed for AI agents to invoke directly.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g pushnetgo-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run without installing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx pushnetgo-cli token check --key pushnetgo-xxx
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## HTTP API
|
|
18
|
+
|
|
19
|
+
For AI agents that can't run local commands, use the HTTP API:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Endpoint: https://us-central1-flutter-ai-playground-f8e3b.cloudfunctions.net/unifiedGatewayApi
|
|
23
|
+
Auth: x-api-key header
|
|
24
|
+
Format: JSON POST { "action": "...", ...params }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Actions
|
|
28
|
+
|
|
29
|
+
#### token-check
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
curl -X POST https://us-central1-flutter-ai-playground-f8e3b.cloudfunctions.net/unifiedGatewayApi \
|
|
33
|
+
-H "Content-Type: application/json" \
|
|
34
|
+
-H "x-api-key: pushnetgo-xxx" \
|
|
35
|
+
-d '{"action":"token-check"}'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
#### sync-music
|
|
39
|
+
|
|
40
|
+
Download an MP3 from URL and upload to PushNetGo project.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
curl -X POST https://us-central1-flutter-ai-playground-f8e3b.cloudfunctions.net/unifiedGatewayApi \
|
|
44
|
+
-H "Content-Type: application/json" \
|
|
45
|
+
-H "x-api-key: pushnetgo-xxx" \
|
|
46
|
+
-d '{"action":"sync-music","projectId":"my-project","url":"https://example.com/track.mp3"}'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## CLI Usage
|
|
50
|
+
|
|
51
|
+
Set your API key as an environment variable:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
export MYAPP_API_KEY="pushnetgo-xxx"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or pass it with each command:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pushnetgo-cli --key pushnetgo-xxx <command>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Commands
|
|
64
|
+
|
|
65
|
+
#### sync-music
|
|
66
|
+
|
|
67
|
+
Upload local MP3 files or import from URL:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Upload from local directory
|
|
71
|
+
pushnetgo-cli sync-music --project radio99 --dir /path/to/mp3
|
|
72
|
+
|
|
73
|
+
# Import from URL
|
|
74
|
+
pushnetgo-cli sync-music --project radio99 --url "https://api.mureka.ai/output.mp3"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### token check
|
|
78
|
+
|
|
79
|
+
Validate an API key:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pushnetgo-cli token check
|
|
83
|
+
pushnetgo-cli token check --key pushnetgo-xxx
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## API Key Format
|
|
87
|
+
|
|
88
|
+
PushNetGo API keys use the format:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
pushnetgo-{32-character-hex-uuid}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Example: `pushnetgo-6d122db2132d4a94a91a46e529fd2752`
|
|
95
|
+
|
|
96
|
+
Keys are generated from the PushNetGo App Token Hub.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const program = new Command();
|
|
5
|
+
|
|
6
|
+
program
|
|
7
|
+
.name('pushnetgo-cli')
|
|
8
|
+
.alias('png')
|
|
9
|
+
.description('PushNetGo App open project CLI — AI Agent can invoke via MYAPP_API_KEY env var')
|
|
10
|
+
.version('1.0.0');
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.command('sync-music')
|
|
14
|
+
.alias('sync')
|
|
15
|
+
.description('Sync music to App open project')
|
|
16
|
+
.option('-p, --project <id>', 'target project ID', 'default')
|
|
17
|
+
.option('-d, --dir <path>', 'local MP3 directory')
|
|
18
|
+
.option('-u, --url <url>', 'Mureka AI generated music URL')
|
|
19
|
+
.option('-k, --key <token>', 'API Key (or set MYAPP_API_KEY env var)')
|
|
20
|
+
.action(async (options) => {
|
|
21
|
+
const { syncMusic } = require('../commands/sync-music');
|
|
22
|
+
await syncMusic(options);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command('token <action>')
|
|
27
|
+
.description('Token management: check')
|
|
28
|
+
.option('-k, --key <token>', 'API Key')
|
|
29
|
+
.action(async (action, options) => {
|
|
30
|
+
if (action === 'check') {
|
|
31
|
+
const { checkToken } = require('../commands/token');
|
|
32
|
+
await checkToken(options.key);
|
|
33
|
+
} else {
|
|
34
|
+
console.error(`Unknown action: ${action}. Use: check`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { resolveApiKey } = require('../lib/auth');
|
|
5
|
+
const { uploadFile, saveTrack, downloadUrlToFile } = require('../lib/uploader');
|
|
6
|
+
|
|
7
|
+
async function syncMusic(options) {
|
|
8
|
+
const projectId = options.project || 'default';
|
|
9
|
+
const dir = options.dir;
|
|
10
|
+
const url = options.url;
|
|
11
|
+
|
|
12
|
+
await resolveApiKey(options.key);
|
|
13
|
+
|
|
14
|
+
if (dir) {
|
|
15
|
+
const files = fs.readdirSync(dir)
|
|
16
|
+
.filter(f => f.endsWith('.mp3'))
|
|
17
|
+
.map(f => path.join(dir, f));
|
|
18
|
+
|
|
19
|
+
console.log(`${files.length} MP3 files, project: ${projectId}\n`);
|
|
20
|
+
|
|
21
|
+
let ok = 0;
|
|
22
|
+
for (let i = 0; i < files.length; i++) {
|
|
23
|
+
const fp = files[i];
|
|
24
|
+
const name = path.basename(fp, '.mp3');
|
|
25
|
+
const [artist = 'Unknown', ...titleParts] = name.split(' - ');
|
|
26
|
+
const title = titleParts.join(' - ') || name;
|
|
27
|
+
const trackId = `cli_${Date.now()}_${i}`;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
process.stdout.write(`[${i + 1}/${files.length}] ^ ${artist} - ${title}...`);
|
|
31
|
+
const downloadUrl = await uploadFile(fp, `music_files/${trackId}.mp3`);
|
|
32
|
+
await saveTrack({ trackId, title, artist, downloadUrl, projectId });
|
|
33
|
+
console.log(' OK');
|
|
34
|
+
ok++;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.log(' FAIL', e.message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
console.log(`\nDone: ${ok}/${files.length} uploaded`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (url) {
|
|
43
|
+
console.log(`Download: ${url}`);
|
|
44
|
+
const tmpDir = os.tmpdir();
|
|
45
|
+
const tmpFile = path.join(tmpDir, `mureka_${Date.now()}.mp3`);
|
|
46
|
+
await downloadUrlToFile(url, tmpFile);
|
|
47
|
+
console.log(`Downloaded to: ${tmpFile}`);
|
|
48
|
+
|
|
49
|
+
const trackId = `mureka_${Date.now()}`;
|
|
50
|
+
const artist = 'Mureka AI';
|
|
51
|
+
const title = `Generated ${new Date().toISOString().slice(0, 10)}`;
|
|
52
|
+
|
|
53
|
+
process.stdout.write('Uploading...');
|
|
54
|
+
const downloadUrl = await uploadFile(tmpFile, `music_files/${trackId}.mp3`);
|
|
55
|
+
await saveTrack({ trackId, title, artist, downloadUrl, projectId });
|
|
56
|
+
console.log(' OK');
|
|
57
|
+
|
|
58
|
+
try { fs.unlinkSync(tmpFile); } catch (_) {}
|
|
59
|
+
console.log(`Mureka track published to project ${projectId}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { syncMusic };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { getFirestore } = require('../lib/firestore');
|
|
2
|
+
|
|
3
|
+
async function checkToken(key) {
|
|
4
|
+
const token = key || process.env.MYAPP_API_KEY;
|
|
5
|
+
|
|
6
|
+
if (!token) {
|
|
7
|
+
console.error('Usage: pushnetgo-cli token check --key pushnetgo-xxxx');
|
|
8
|
+
console.error(' or: export MYAPP_API_KEY="pushnetgo-xxxx"');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!token.startsWith('pushnetgo-')) {
|
|
13
|
+
console.error('Invalid prefix. PushNetGo keys must start with pushnetgo-');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const db = getFirestore();
|
|
18
|
+
const snap = await db.collection('tokens')
|
|
19
|
+
.where('tokenValue', '==', token)
|
|
20
|
+
.limit(1)
|
|
21
|
+
.get();
|
|
22
|
+
|
|
23
|
+
if (snap.empty) {
|
|
24
|
+
console.log(`NOT FOUND: ${token.slice(0, 16)}...`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const d = snap.docs[0].data();
|
|
29
|
+
console.log(`OK: ${token}`);
|
|
30
|
+
console.log(` active: ${d.isActive}`);
|
|
31
|
+
console.log(` usage: ${d.usageCount || 0}`);
|
|
32
|
+
console.log(` userId: ${d.userId}`);
|
|
33
|
+
console.log(` perms: ${JSON.stringify(d.permissions || d.scopes || [])}`);
|
|
34
|
+
console.log(` created: ${d.createdAt?.toDate?.() || d.createdAt}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { checkToken };
|
package/lib/auth.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const { getFirestore, admin } = require('./firestore');
|
|
2
|
+
|
|
3
|
+
async function resolveApiKey(cliKey) {
|
|
4
|
+
const key = cliKey || process.env.MYAPP_API_KEY;
|
|
5
|
+
|
|
6
|
+
if (!key) {
|
|
7
|
+
console.error('No API Key found. Set env var or pass --key:');
|
|
8
|
+
console.error(' export MYAPP_API_KEY="pushnetgo-xxxx"');
|
|
9
|
+
console.error(' or: pushnetgo-cli sync-music --key pushnetgo-xxxx ...');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!key.startsWith('pushnetgo-') || key.length < 22) {
|
|
14
|
+
console.error('Invalid key format. Expected pushnetgo- prefix + 32 hex chars');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const db = getFirestore();
|
|
19
|
+
const snap = await db.collection('tokens')
|
|
20
|
+
.where('tokenValue', '==', key)
|
|
21
|
+
.where('isActive', '==', true)
|
|
22
|
+
.limit(1)
|
|
23
|
+
.get();
|
|
24
|
+
|
|
25
|
+
if (snap.empty) {
|
|
26
|
+
console.error('Key not found or inactive');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const data = snap.docs[0].data();
|
|
31
|
+
const perms = data.permissions || data.scopes || [];
|
|
32
|
+
const hasUpload = perms.some(p => p === 'musicUpload' || p === 'music_upload');
|
|
33
|
+
|
|
34
|
+
if (!hasUpload) {
|
|
35
|
+
console.error('This key lacks musicUpload permission');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(`Auth OK — uid: ${data.userId}`);
|
|
40
|
+
|
|
41
|
+
await db.collection('tokens').doc(snap.docs[0].id).update({
|
|
42
|
+
lastUsedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
43
|
+
usageCount: admin.firestore.FieldValue.increment(1),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return key;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { resolveApiKey };
|
package/lib/firestore.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const admin = require('firebase-admin');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
let _initialized = false;
|
|
5
|
+
|
|
6
|
+
function getFirestore() {
|
|
7
|
+
if (!_initialized) {
|
|
8
|
+
const saPath = path.resolve(__dirname, '../../configs/service-account.json');
|
|
9
|
+
admin.initializeApp({
|
|
10
|
+
credential: admin.credential.cert(require(saPath)),
|
|
11
|
+
storageBucket: 'flutter-ai-playground-f8e3b.firebasestorage.app',
|
|
12
|
+
});
|
|
13
|
+
_initialized = true;
|
|
14
|
+
}
|
|
15
|
+
return admin.firestore();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getStorage() {
|
|
19
|
+
getFirestore();
|
|
20
|
+
return admin.storage().bucket();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { getFirestore, getStorage, admin };
|
package/lib/uploader.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const { getStorage, getFirestore, admin } = require('./firestore');
|
|
6
|
+
|
|
7
|
+
async function uploadFile(filePath, remotePath) {
|
|
8
|
+
const bucket = getStorage();
|
|
9
|
+
await bucket.upload(filePath, {
|
|
10
|
+
destination: remotePath,
|
|
11
|
+
metadata: { contentType: 'audio/mpeg' },
|
|
12
|
+
});
|
|
13
|
+
const file = bucket.file(remotePath);
|
|
14
|
+
await file.makePublic();
|
|
15
|
+
return `https://storage.googleapis.com/${bucket.name}/${remotePath}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function saveTrack({ trackId, title, artist, downloadUrl, projectId }) {
|
|
19
|
+
const db = getFirestore();
|
|
20
|
+
await db.collection('musics').doc(trackId).set({
|
|
21
|
+
title,
|
|
22
|
+
artist,
|
|
23
|
+
musicUrl: downloadUrl,
|
|
24
|
+
duration: '0:00',
|
|
25
|
+
plays: 0,
|
|
26
|
+
likesCount: 0,
|
|
27
|
+
projectId: projectId || 'default',
|
|
28
|
+
publisherId: 'cli_upload',
|
|
29
|
+
publisherName: 'PushNetGo CLI',
|
|
30
|
+
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function downloadUrlToFile(url, destPath) {
|
|
35
|
+
const client = url.startsWith('https') ? https : http;
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const file = fs.createWriteStream(destPath);
|
|
39
|
+
client.get(url, (response) => {
|
|
40
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
41
|
+
file.close();
|
|
42
|
+
fs.unlinkSync(destPath);
|
|
43
|
+
downloadUrlToFile(response.headers.location, destPath).then(resolve).catch(reject);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
response.pipe(file);
|
|
47
|
+
file.on('finish', () => { file.close(); resolve(destPath); });
|
|
48
|
+
}).on('error', (err) => {
|
|
49
|
+
file.close();
|
|
50
|
+
try { fs.unlinkSync(destPath); } catch (_) {}
|
|
51
|
+
reject(err);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { uploadFile, saveTrack, downloadUrlToFile };
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pushnetgo-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "PushNetGo App CLI — sync music, validate tokens via HTTP API or local Firebase SDK",
|
|
5
|
+
"bin": {
|
|
6
|
+
"pushnetgo-cli": "./bin/pushnetgo-cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start": "node bin/pushnetgo-cli.js"
|
|
10
|
+
},
|
|
11
|
+
"keywords": ["pushnetgo", "music", "sync", "cli", "ai-agent"],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/pushnetgo/pushnetgo-cli"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"commander": "^12.0.0",
|
|
19
|
+
"firebase-admin": "^12.0.0",
|
|
20
|
+
"chalk": "^5.0.0"
|
|
21
|
+
}
|
|
22
|
+
}
|