channel-worker 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -6,6 +6,10 @@ const os = require('os');
6
6
 
7
7
  const CONFIG_DIR = path.join(os.homedir(), '.channel-worker');
8
8
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
+ const DEFAULT_API_URL = 'https://api.channel.tunasm.art';
10
+ const DEFAULT_EXT_PATH = process.platform === 'win32'
11
+ ? 'C:\\agents\\channel-manager-ext'
12
+ : path.join(os.homedir(), 'channel-manager-ext');
9
13
 
10
14
  function parseArgs(args) {
11
15
  const result = {};
@@ -40,9 +44,9 @@ const cmd = args._cmd || 'start';
40
44
  if (cmd === 'pair') {
41
45
  // Pair with dashboard using one-time code
42
46
  const code = args.code;
43
- const apiUrl = args.api || 'https://api.channel.tunasm.art';
47
+ const apiUrl = args.api || DEFAULT_API_URL;
44
48
  const nstKey = args['nst-key'] || '';
45
- const extensionPath = args.extension || '';
49
+ const extensionPath = args.extension || DEFAULT_EXT_PATH;
46
50
  const maxConcurrent = parseInt(args.concurrent || '2', 10);
47
51
 
48
52
  if (!code) {
@@ -91,10 +95,10 @@ if (cmd === 'pair') {
91
95
  } else if (cmd === 'init') {
92
96
  const config = {
93
97
  worker_id: args.id || `worker-${os.hostname()}`,
94
- api_url: args.api || 'http://localhost:3001',
98
+ api_url: args.api || DEFAULT_API_URL,
95
99
  max_concurrent: parseInt(args.concurrent || '2', 10),
96
100
  nst_api_key: args['nst-key'] || '',
97
- extension_path: args.extension || '',
101
+ extension_path: args.extension || DEFAULT_EXT_PATH,
98
102
  worker_token: args.token || '',
99
103
  };
100
104
  saveConfig(config);
@@ -108,10 +112,10 @@ if (cmd === 'pair') {
108
112
  const saved = loadConfig();
109
113
  const config = {
110
114
  worker_id: args.id || saved.worker_id || `worker-${os.hostname()}`,
111
- api_url: args.api || saved.api_url || 'https://api.channel.tunasm.art',
115
+ api_url: args.api || saved.api_url || DEFAULT_API_URL,
112
116
  max_concurrent: parseInt(args.concurrent || saved.max_concurrent || '2', 10),
113
117
  nst_api_key: args['nst-key'] || saved.nst_api_key || '',
114
- extension_path: args.extension || saved.extension_path || '',
118
+ extension_path: args.extension || saved.extension_path || DEFAULT_EXT_PATH,
115
119
  worker_token: args.token || saved.worker_token || '',
116
120
  verbose: !!args.verbose,
117
121
  };
package/lib/api-client.js CHANGED
@@ -62,12 +62,32 @@ class ApiClient {
62
62
 
63
63
  // Commands
64
64
  async getNextCommand(workerId) {
65
- return this.request('GET', `/workers/commands?worker_id=${workerId}`);
65
+ const workerTypes = 'launch_profile,close_profile,save_file,set_thumbnail,set_tags,set_file_input,click_and_upload,type_text,verify_logins,update_extension';
66
+ return this.request('GET', `/workers/commands?worker_id=${workerId}&types=${encodeURIComponent(workerTypes)}`);
66
67
  }
67
68
 
68
69
  async updateCommand(commandId, data) {
69
70
  return this.request('PUT', `/workers/commands/${commandId}`, data);
70
71
  }
72
+
73
+ // Extension download
74
+ async getExtensionVersion() {
75
+ const data = await this.request('GET', '/extension-download/version');
76
+ return data.version;
77
+ }
78
+
79
+ async downloadExtension(destPath) {
80
+ const url = `${this.baseUrl}/extension-download/zip`;
81
+ const res = await fetch(url, {
82
+ headers: { 'x-worker-token': this.workerToken },
83
+ });
84
+ if (!res.ok) throw new Error(`Download failed: ${res.status}`);
85
+
86
+ const fs = require('fs');
87
+ const buffer = Buffer.from(await res.arrayBuffer());
88
+ fs.writeFileSync(destPath, buffer);
89
+ return destPath;
90
+ }
71
91
  }
72
92
 
73
93
  module.exports = { ApiClient };
@@ -1,4 +1,5 @@
1
1
  const { NstManager } = require('./nst-manager');
2
+ const { checkAndUpdateExtension } = require('./extension-updater');
2
3
 
3
4
  class CommandPoller {
4
5
  constructor(api, config) {
@@ -64,6 +65,9 @@ class CommandPoller {
64
65
  case 'type_text':
65
66
  await this.handleTypeText(command);
66
67
  break;
68
+ case 'update_extension':
69
+ await this.handleUpdateExtension(command);
70
+ break;
67
71
  default:
68
72
  // Other commands (scan_facebook_pages, etc.) handled by extension
69
73
  console.log(`[commands] Skipping ${command.type} — handled by extension`);
@@ -827,6 +831,25 @@ class CommandPoller {
827
831
  await this.api.updateCommand(command._id, { status: 'done' });
828
832
  }
829
833
 
834
+ async handleUpdateExtension(command) {
835
+ console.log('[commands] Updating extension...');
836
+ try {
837
+ if (!this.config.extension_path) {
838
+ throw new Error('No extension_path configured');
839
+ }
840
+ const result = await checkAndUpdateExtension(this.api, this.config.extension_path);
841
+ if (result.updated) {
842
+ console.log(`[commands] Extension updated: ${result.from || 'none'} → ${result.to}`);
843
+ } else {
844
+ console.log(`[commands] Extension already up to date (v${result.local})`);
845
+ }
846
+ await this.api.updateCommand(command._id, { status: 'done', result });
847
+ } catch (err) {
848
+ console.error(`[commands] Extension update failed: ${err.message}`);
849
+ await this.api.updateCommand(command._id, { status: 'failed', error: err.message });
850
+ }
851
+ }
852
+
830
853
  async handleVerifyLogins(command) {
831
854
  const { profile_id, channel_id } = command.payload || {};
832
855
  console.log(`[commands] Verifying logins for profile: ${profile_id}`);
package/lib/daemon.js CHANGED
@@ -3,6 +3,7 @@ const { Heartbeat } = require('./heartbeat');
3
3
  const { JobPoller } = require('./job-poller');
4
4
  const { CommandPoller } = require('./command-poller');
5
5
  const { UpdateChecker, getLocalVersion } = require('./updater');
6
+ const { checkAndUpdateExtension } = require('./extension-updater');
6
7
 
7
8
  class Daemon {
8
9
  constructor(config) {
@@ -34,6 +35,20 @@ class Daemon {
34
35
  max_concurrent: this.config.max_concurrent,
35
36
  });
36
37
  console.log('[daemon] Registered with dashboard ✓');
38
+
39
+ // Auto-update extension on startup
40
+ if (this.config.extension_path) {
41
+ try {
42
+ const result = await checkAndUpdateExtension(this.api, this.config.extension_path);
43
+ if (result.updated) {
44
+ console.log(`[daemon] Extension updated: ${result.from || 'none'} → ${result.to}`);
45
+ } else {
46
+ console.log(`[daemon] Extension up to date (v${result.local})`);
47
+ }
48
+ } catch (err) {
49
+ console.warn(`[daemon] Extension update check failed: ${err.message}`);
50
+ }
51
+ }
37
52
  } catch (err) {
38
53
  console.error(`[daemon] Failed to register: ${err.message}`);
39
54
  console.error('[daemon] Is the dashboard API running? Retrying in 10s...');
@@ -0,0 +1,78 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { execSync } = require('child_process');
5
+
6
+ function getLocalExtensionVersion(extensionPath) {
7
+ try {
8
+ const manifest = JSON.parse(fs.readFileSync(path.join(extensionPath, 'manifest.json'), 'utf8'));
9
+ return manifest.version || null;
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ function isNewer(remote, local) {
16
+ const r = remote.split('.').map(Number);
17
+ const l = local.split('.').map(Number);
18
+ for (let i = 0; i < 3; i++) {
19
+ if ((r[i] || 0) > (l[i] || 0)) return true;
20
+ if ((r[i] || 0) < (l[i] || 0)) return false;
21
+ }
22
+ return false;
23
+ }
24
+
25
+ async function checkAndUpdateExtension(api, extensionPath) {
26
+ const local = getLocalExtensionVersion(extensionPath);
27
+ const remote = await api.getExtensionVersion();
28
+
29
+ if (local && !isNewer(remote, local)) {
30
+ return { updated: false, local, remote };
31
+ }
32
+
33
+ console.log(`[ext-updater] ${local ? `Update available: ${local} → ${remote}` : `Downloading extension v${remote}...`}`);
34
+
35
+ // Download to temp
36
+ const tmpArchive = path.join(os.tmpdir(), `channel-manager-ext-${Date.now()}.tar.gz`);
37
+ const tmpExtract = path.join(os.tmpdir(), `channel-manager-ext-new-${Date.now()}`);
38
+
39
+ try {
40
+ await api.downloadExtension(tmpArchive);
41
+
42
+ // Extract
43
+ fs.mkdirSync(tmpExtract, { recursive: true });
44
+ execSync(`tar -xzf "${tmpArchive}" -C "${tmpExtract}"`, { timeout: 30000 });
45
+
46
+ // tar extracts into a subfolder named "channel-manager-ext"
47
+ const extracted = path.join(tmpExtract, 'channel-manager-ext');
48
+ const sourceDir = fs.existsSync(extracted) ? extracted : tmpExtract;
49
+
50
+ // Replace existing extension dir
51
+ if (fs.existsSync(extensionPath)) {
52
+ fs.rmSync(extensionPath, { recursive: true, force: true });
53
+ }
54
+ fs.mkdirSync(extensionPath, { recursive: true });
55
+
56
+ // Copy files
57
+ const files = fs.readdirSync(sourceDir);
58
+ for (const f of files) {
59
+ const src = path.join(sourceDir, f);
60
+ const dest = path.join(extensionPath, f);
61
+ const stat = fs.statSync(src);
62
+ if (stat.isFile()) {
63
+ fs.copyFileSync(src, dest);
64
+ } else if (stat.isDirectory()) {
65
+ fs.cpSync(src, dest, { recursive: true });
66
+ }
67
+ }
68
+
69
+ console.log(`[ext-updater] Extension updated to v${remote}`);
70
+ return { updated: true, from: local, to: remote };
71
+ } finally {
72
+ // Cleanup temp files
73
+ try { fs.unlinkSync(tmpArchive); } catch {}
74
+ try { fs.rmSync(tmpExtract, { recursive: true, force: true }); } catch {}
75
+ }
76
+ }
77
+
78
+ module.exports = { checkAndUpdateExtension, getLocalExtensionVersion };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Channel Manager worker daemon — runs on remote machines to execute video pipeline jobs",
5
5
  "main": "lib/daemon.js",
6
6
  "bin": {