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 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 };
@@ -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 };
@@ -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
+ }