@epic-cloudcontrol/daemon 0.3.1 → 0.4.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.
Files changed (2) hide show
  1. package/dist/cli.js +88 -90
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -27,113 +27,111 @@ program
27
27
  // ── login ─────────────────────────────────────────────
28
28
  program
29
29
  .command("login")
30
- .description("Log in and sync all your company profiles")
31
- .option("--email <email>", "Log in with email (syncs all companies automatically)")
32
- .option("--profile <name>", "Profile name for single-key setup", "default")
30
+ .description("Log in via browser (device code flow)")
33
31
  .option("--api-url <url>", "CloudControl API URL")
34
- .option("--api-key <key>", "API key (for single-company manual setup)")
32
+ .option("--api-key <key>", "Manual API key setup (skip browser)")
33
+ .option("--no-browser", "Don't open browser automatically (print URL instead)")
35
34
  .option("--name <name>", "Worker name")
35
+ .option("--profile <name>", "Profile name for manual key setup", "default")
36
36
  .action(async (opts) => {
37
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
38
- const ask = (q, defaultVal) => new Promise((resolve) => {
39
- const prompt = defaultVal ? `${q} [${defaultVal}]: ` : `${q}: `;
40
- rl.question(prompt, (answer) => resolve(answer.trim() || defaultVal || ""));
41
- });
42
- let apiUrl = opts.apiUrl;
43
- if (!apiUrl) {
44
- const existing = loadProfile();
45
- apiUrl = await ask("CloudControl API URL", existing?.apiUrl || "https://cloudcontrol.onrender.com");
46
- }
47
- // Email login mode — sync all companies
48
- const email = opts.email || await ask("Email (or leave blank for API key setup)");
49
- if (email) {
50
- const password = await ask("Password");
37
+ const existing = loadProfile();
38
+ const apiUrl = opts.apiUrl || existing?.apiUrl || "https://cloudcontrol.onrender.com";
39
+ const workerName = opts.name || existing?.workerName || `worker-${os.hostname()}`;
40
+ // Manual API key mode (backwards compat)
41
+ if (opts.apiKey) {
42
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
43
+ const ask = (q, def) => new Promise((r) => rl.question(def ? `${q} [${def}]: ` : `${q}: `, (a) => r(a.trim() || def || "")));
44
+ const teamName = await ask("Company name", opts.profile);
51
45
  rl.close();
52
- if (!password) {
53
- console.error("Error: Password required.");
54
- process.exit(1);
46
+ saveProfile({ apiUrl, apiKey: opts.apiKey, workerName, teamName }, opts.profile);
47
+ console.log(`\nProfile "${opts.profile}" saved.`);
48
+ return;
49
+ }
50
+ // Device code flow
51
+ console.log("\nRequesting device code...");
52
+ let code;
53
+ let deviceUrl;
54
+ try {
55
+ const res = await fetch(`${apiUrl}/api/auth/device`, {
56
+ method: "POST",
57
+ headers: { "content-type": "application/json" },
58
+ body: JSON.stringify({ action: "create" }),
59
+ });
60
+ if (!res.ok)
61
+ throw new Error(`HTTP ${res.status}`);
62
+ const data = await res.json();
63
+ code = data.code;
64
+ deviceUrl = data.url;
65
+ }
66
+ catch (err) {
67
+ console.error(`Failed to connect to ${apiUrl}: ${err.message}`);
68
+ process.exit(1);
69
+ }
70
+ // Open browser
71
+ const fullUrl = `${deviceUrl}?code=${code}`;
72
+ if (opts.browser !== false) {
73
+ console.log("\nOpening browser for authentication...\n");
74
+ try {
75
+ const { exec } = await import("child_process");
76
+ const cmd = process.platform === "darwin" ? `open "${fullUrl}"`
77
+ : process.platform === "win32" ? `start "" "${fullUrl}"`
78
+ : `xdg-open "${fullUrl}"`;
79
+ exec(cmd);
55
80
  }
56
- console.log("\nAuthenticating...");
81
+ catch {
82
+ // Browser open failed — user will use the URL
83
+ }
84
+ }
85
+ console.log(`If the browser didn't open, go to:`);
86
+ console.log(` ${deviceUrl}\n`);
87
+ console.log(`Enter code: ${code}\n`);
88
+ console.log("Waiting for authorization...");
89
+ // Poll until authorized or expired
90
+ const pollInterval = 5000;
91
+ const maxAttempts = 120; // 10 minutes at 5s intervals
92
+ for (let i = 0; i < maxAttempts; i++) {
93
+ await new Promise((r) => setTimeout(r, pollInterval));
57
94
  try {
58
- const res = await fetch(`${apiUrl}/api/auth/teams`, {
95
+ const res = await fetch(`${apiUrl}/api/auth/device`, {
59
96
  method: "POST",
60
97
  headers: { "content-type": "application/json" },
61
- body: JSON.stringify({ email, password, generateKeys: true }),
98
+ body: JSON.stringify({ action: "poll", code }),
62
99
  });
63
100
  if (!res.ok) {
64
- const data = await res.json();
65
- console.error(`Login failed: ${data.error || res.status}`);
66
- process.exit(1);
101
+ if (res.status === 404) {
102
+ console.error("\nCode expired. Run 'cloudcontrol login' again.");
103
+ process.exit(1);
104
+ }
105
+ continue;
67
106
  }
68
107
  const data = await res.json();
69
- const workerName = opts.name || `worker-${os.hostname()}`;
70
- console.log(`\nLogged in as ${data.user.email}`);
71
- console.log(`Found ${data.teams.length} company(s):\n`);
72
- for (const team of data.teams) {
73
- // Generate slug from team name
74
- const slug = team.teamName
75
- .toLowerCase()
76
- .replace(/[^a-z0-9]+/g, "-")
77
- .replace(/^-|-$/g, "");
78
- saveProfile({
79
- apiUrl,
80
- apiKey: team.apiKey,
81
- workerName,
82
- teamName: team.teamName,
83
- }, slug);
84
- console.log(` ✓ ${team.teamName} → profile "${slug}" (${team.role})`);
108
+ if (data.status === "pending") {
109
+ continue;
85
110
  }
86
- // Also save the first team as default
87
- if (data.teams.length > 0) {
88
- const first = data.teams[0];
89
- saveProfile({
90
- apiUrl,
91
- apiKey: first.apiKey,
92
- workerName,
93
- teamName: first.teamName,
94
- });
111
+ if (data.status === "authorized" && data.user && data.teams) {
112
+ console.log(`\n✓ Logged in as ${data.user.email}`);
113
+ console.log(` Found ${data.teams.length} company(s):\n`);
114
+ for (const team of data.teams) {
115
+ const slug = team.teamName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
116
+ saveProfile({ apiUrl, apiKey: team.apiKey, workerName, teamName: team.teamName }, slug);
117
+ console.log(` ✓ ${team.teamName} → profile "${slug}" (${team.role})`);
118
+ }
119
+ // Save first team as default
120
+ if (data.teams.length > 0) {
121
+ const first = data.teams[0];
122
+ saveProfile({ apiUrl, apiKey: first.apiKey, workerName, teamName: first.teamName });
123
+ }
124
+ console.log(`\n${data.teams.length} profile(s) saved to ${getConfigDir()}/`);
125
+ console.log("Run 'cloudcontrol start --all' to begin processing tasks.");
126
+ return;
95
127
  }
96
- console.log(`\n${data.teams.length} profile(s) saved to ${getConfigDir()}/`);
97
- console.log("Run 'cloudcontrol profiles' to see them.");
98
- console.log("Run 'cloudcontrol start --profile <name>' to start working.");
99
- }
100
- catch (err) {
101
- console.error(`Error: ${err.message}`);
102
- process.exit(1);
103
128
  }
104
- }
105
- else {
106
- // Manual API key mode (original flow)
107
- const profileName = opts.profile;
108
- let apiKey = opts.apiKey;
109
- let workerName = opts.name;
110
- let teamName;
111
- const existing = loadProfile(profileName);
112
- if (!teamName)
113
- teamName = await ask("Company name", existing?.teamName || profileName);
114
- if (!apiKey)
115
- apiKey = await ask("API Key (cc_...)", existing?.apiKey);
116
- if (!workerName)
117
- workerName = await ask("Worker name", existing?.workerName || `worker-${os.hostname()}`);
118
- rl.close();
119
- if (!apiKey) {
120
- console.error("Error: API key is required.");
121
- process.exit(1);
122
- }
123
- console.log("\nVerifying connection...");
124
- try {
125
- const res = await fetch(`${apiUrl}/api/health`);
126
- if (!res.ok)
127
- throw new Error(`HTTP ${res.status}`);
128
- console.log("Connected to CloudControl.");
129
- }
130
- catch (err) {
131
- console.error(`Warning: Could not reach ${apiUrl} — ${err.message}`);
129
+ catch {
130
+ // Poll error — retry
132
131
  }
133
- saveProfile({ apiUrl, apiKey, workerName, teamName }, profileName);
134
- console.log(`\nProfile "${profileName}" saved to ${getConfigDir()}/`);
135
- console.log("Run 'cloudcontrol start' to begin processing tasks.");
136
132
  }
133
+ console.error("\nAuthorization timed out. Run 'cloudcontrol login' again.");
134
+ process.exit(1);
137
135
  });
138
136
  // ── profiles ──────────────────────────────────────────
139
137
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epic-cloudcontrol/daemon",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "CloudControl local daemon — executes AI agent tasks on worker machines",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",