@lightupai/polaris 0.0.5 → 0.0.7

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/src/cli/cli.ts CHANGED
@@ -1,135 +1,132 @@
1
1
  #!/usr/bin/env bun
2
2
  // --- Polaris CLI ---
3
3
  // Usage:
4
- // polaris login authenticate via Google SSO, install local components
5
- // polaris daemon start the local daemon
6
- // polaris status show daemon health and active sessions
7
- // polaris logout — remove credentials and local config
8
-
9
- import { mkdir, writeFile, readFile, rm, exists } from "node:fs/promises";
10
- import { homedir } from "node:os";
4
+ // polaris install + login (default onboarding)
5
+ // polaris install install local components (no auth)
6
+ // polaris login authenticate via Google SSO (production)
7
+ // polaris login --local authenticate against localhost
8
+ // polaris login --url <url> — authenticate against a custom URL
9
+ // polaris login --profile <name> explicit profile name
10
+ // polaris use <profile> switch active profile
11
+ // polaris profiles — list profiles
12
+ // polaris daemon — start the local daemon
13
+ // polaris status — show connection status
14
+ // polaris logout — remove credentials
15
+
16
+ import { mkdir, writeFile, readFile, rm } from "node:fs/promises";
17
+ import { homedir, hostname } from "node:os";
11
18
  import { join } from "node:path";
12
19
 
13
20
  const POLARIS_DIR = join(homedir(), ".polaris");
14
- const CREDENTIALS_FILE = join(POLARIS_DIR, "credentials.json");
21
+ const CONFIG_FILE = join(POLARIS_DIR, "config.json");
22
+ const LEGACY_CREDENTIALS_FILE = join(POLARIS_DIR, "credentials.json");
15
23
  const CLAUDE_DIR = join(homedir(), ".claude");
16
- const CLAUDE_SETTINGS_DIR = join(CLAUDE_DIR);
17
24
 
18
- const SERVICE_URL = process.env.POLARIS_SERVICE_URL ?? "http://localhost:3000";
25
+ const DEFAULT_APP_URL = "https://app.polaris.lightup.ai";
26
+ const LOCAL_APP_URL = "http://localhost:3000";
19
27
 
20
- // --- Login ---
28
+ // --- Config ---
21
29
 
22
- async function login() {
23
- console.log("Polaris — setting up your machine\n");
30
+ interface Profile {
31
+ api: string;
32
+ app: string;
33
+ token: string;
34
+ email: string;
35
+ name: string;
36
+ org_id: string;
37
+ participant_id: string;
38
+ }
24
39
 
25
- // 1. Start a local HTTP server to receive the token callback
26
- let resolveToken: (token: string) => void;
27
- const tokenPromise = new Promise<string>((resolve) => { resolveToken = resolve; });
40
+ interface Config {
41
+ active: string;
42
+ profiles: Record<string, Profile>;
43
+ }
28
44
 
29
- const callbackServer = Bun.serve({
30
- port: 0,
31
- hostname: "127.0.0.1",
32
- fetch(req) {
33
- const url = new URL(req.url);
34
- if (url.pathname === "/callback") {
35
- const token = url.searchParams.get("token");
36
- if (token) {
37
- resolveToken!(token);
38
- return new Response(
39
- `<!DOCTYPE html><html><head><title>Polaris</title><style>body{font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;color:#1a1a1a;}</style></head><body><div style="text-align:center"><h2>Authenticated!</h2><p>You can close this tab and return to the terminal.</p></div></body></html>`,
40
- { headers: { "Content-Type": "text/html" } }
41
- );
42
- }
45
+ async function loadConfig(): Promise<Config> {
46
+ try {
47
+ return JSON.parse(await readFile(CONFIG_FILE, "utf-8"));
48
+ } catch {
49
+ // Migrate from legacy credentials.json if it exists
50
+ try {
51
+ const legacy = JSON.parse(await readFile(LEGACY_CREDENTIALS_FILE, "utf-8"));
52
+ if (legacy.token) {
53
+ const appUrl = legacy.service_url ?? DEFAULT_APP_URL;
54
+ const profileName = deriveProfileName(appUrl);
55
+ const config: Config = {
56
+ active: profileName,
57
+ profiles: {
58
+ [profileName]: {
59
+ api: appToApi(appUrl),
60
+ app: appUrl,
61
+ token: legacy.token,
62
+ email: legacy.email ?? "",
63
+ name: legacy.name ?? "",
64
+ org_id: legacy.org_id ?? "",
65
+ participant_id: legacy.participant_id ?? "",
66
+ },
67
+ },
68
+ };
69
+ await saveConfig(config);
70
+ return config;
43
71
  }
44
- return new Response("Not found", { status: 404 });
45
- },
46
- });
47
-
48
- const callbackPort = callbackServer.port;
49
- const authUrl = `${SERVICE_URL}/auth/cli?port=${callbackPort}`;
50
-
51
- console.log("Opening browser for Google sign-in...");
52
- console.log(`If the browser doesn't open, visit: ${authUrl}\n`);
53
-
54
- // Open browser
55
- const proc = Bun.spawn(
56
- process.platform === "darwin" ? ["open", authUrl] :
57
- process.platform === "win32" ? ["cmd", "/c", "start", authUrl] :
58
- ["xdg-open", authUrl],
59
- { stdout: "ignore", stderr: "ignore" }
60
- );
61
- await proc.exited;
72
+ } catch { /* no legacy file either */ }
73
+ return { active: "", profiles: {} };
74
+ }
75
+ }
62
76
 
63
- // 2. Wait for the token
64
- console.log("Waiting for authentication...");
65
- const token = await tokenPromise;
66
- // Keep server alive briefly so the browser can render the success page
67
- setTimeout(() => callbackServer.stop(true), 3000);
77
+ async function saveConfig(config: Config): Promise<void> {
78
+ await mkdir(POLARIS_DIR, { recursive: true });
79
+ await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
80
+ }
68
81
 
69
- // 3. Validate the token and get user info
70
- const res = await fetch(`${SERVICE_URL}/auth/token?token=${token}`);
71
- if (!res.ok) {
72
- console.error("Failed to validate token. Please try again.");
73
- process.exit(1);
74
- }
75
- const userInfo = (await res.json()) as {
76
- sub: string;
77
- email: string;
78
- name: string;
79
- org_id: string;
80
- participant_id: string;
81
- };
82
+ function getActiveProfile(config: Config): Profile | null {
83
+ if (!config.active || !config.profiles[config.active]) return null;
84
+ return config.profiles[config.active];
85
+ }
82
86
 
83
- // Fetch org name
84
- let orgName = userInfo.org_id;
87
+ function deriveProfileName(appUrl: string): string {
88
+ if (appUrl.includes("localhost") || appUrl.includes("127.0.0.1")) return "local";
85
89
  try {
86
- const orgRes = await fetch(`${SERVICE_URL}/auth/token?token=${token}`);
87
- if (orgRes.ok) {
88
- const orgData = (await orgRes.json()) as { org_id: string };
89
- // The org name isn't in the token — use the email domain as display
90
- orgName = userInfo.email.split("@")[1].split(".")[0];
91
- orgName = orgName.charAt(0).toUpperCase() + orgName.slice(1);
92
- }
93
- } catch { /* use org_id as fallback */ }
90
+ const host = new URL(appUrl).hostname;
91
+ // app.polaris.lightup.ai → prod
92
+ if (host.includes("polaris.lightup.ai")) return "prod";
93
+ // strip common prefixes
94
+ return host.replace(/^app\./, "").replace(/\./g, "-");
95
+ } catch {
96
+ return "default";
97
+ }
98
+ }
94
99
 
95
- console.log(`\nAuthenticated as ${userInfo.name} (${userInfo.email})`);
96
- console.log(`Organization: ${orgName}`);
97
- console.log(`Participant ID: ${userInfo.participant_id}\n`);
100
+ function appToApi(appUrl: string): string {
101
+ if (appUrl.includes("localhost") || appUrl.includes("127.0.0.1")) {
102
+ return appUrl.replace(":3000", ":4321");
103
+ }
104
+ return appUrl.replace("app.", "api.");
105
+ }
98
106
 
99
- // 4. Store credentials
100
- await mkdir(POLARIS_DIR, { recursive: true });
101
- await writeFile(CREDENTIALS_FILE, JSON.stringify({
102
- token,
103
- ...userInfo,
104
- service_url: SERVICE_URL,
105
- }, null, 2));
106
- console.log(`Credentials saved to ${CREDENTIALS_FILE}`);
107
+ // --- Install ---
107
108
 
108
- // 5. Install MCP server config
109
+ async function install(participantId?: string) {
109
110
  await mkdir(CLAUDE_DIR, { recursive: true });
110
111
 
111
- // Find the path to the client.ts relative to this CLI
112
+ // MCP server config
112
113
  const clientPath = join(import.meta.dir, "..", "client", "client.ts");
113
-
114
114
  const mcpConfig = {
115
115
  mcpServers: {
116
116
  polaris: {
117
- command: "bun",
118
- args: [clientPath],
117
+ command: "npx",
118
+ args: ["bun", clientPath],
119
119
  env: {
120
- POLARIS_DAEMON_URL: "http://127.0.0.1:4321",
121
- POLARIS_SERVICE_URL: SERVICE_URL.replace(":3000", ":4321"), // API port
120
+ POLARIS_DAEMON_URL: "http://127.0.0.1:4322",
122
121
  },
123
122
  },
124
123
  },
125
124
  };
126
125
 
127
126
  const mcpConfigPath = join(CLAUDE_DIR, ".mcp.json");
128
- // Merge with existing config if present
129
127
  let existingMcp: Record<string, unknown> = {};
130
128
  try {
131
- const existing = await readFile(mcpConfigPath, "utf-8");
132
- existingMcp = JSON.parse(existing);
129
+ existingMcp = JSON.parse(await readFile(mcpConfigPath, "utf-8"));
133
130
  } catch { /* doesn't exist yet */ }
134
131
 
135
132
  const mergedMcp = {
@@ -140,9 +137,9 @@ async function login() {
140
137
  },
141
138
  };
142
139
  await writeFile(mcpConfigPath, JSON.stringify(mergedMcp, null, 2));
143
- console.log(`MCP server config written to ${mcpConfigPath}`);
140
+ console.log(" ✓ MCP server config written");
144
141
 
145
- // 6. Install hooks
142
+ // Hooks
146
143
  const captureShPath = join(import.meta.dir, "..", "..", "hooks", "capture.sh");
147
144
  const hooksConfig = {
148
145
  UserPromptSubmit: [{ hooks: [{ type: "command", command: captureShPath }] }],
@@ -154,28 +151,34 @@ async function login() {
154
151
  const settingsPath = join(CLAUDE_DIR, "settings.json");
155
152
  let existingSettings: Record<string, unknown> = {};
156
153
  try {
157
- const existing = await readFile(settingsPath, "utf-8");
158
- existingSettings = JSON.parse(existing);
154
+ existingSettings = JSON.parse(await readFile(settingsPath, "utf-8"));
159
155
  } catch { /* doesn't exist yet */ }
160
156
 
157
+ // Status line
158
+ const statusLinePath = join(import.meta.dir, "..", "..", "hooks", "statusline.sh");
161
159
  const mergedSettings = {
162
160
  ...existingSettings,
163
161
  hooks: {
164
162
  ...(existingSettings as { hooks?: Record<string, unknown> }).hooks,
165
163
  ...hooksConfig,
166
164
  },
165
+ statusLine: {
166
+ type: "command",
167
+ command: statusLinePath,
168
+ },
167
169
  };
168
170
  await writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2));
169
- console.log(`Hooks config written to ${settingsPath}`);
171
+ console.log(" ✓ Hooks + status line config written");
170
172
 
171
- // 7. Install /polaris skill
173
+ // /polaris skill
172
174
  const skillDir = join(CLAUDE_DIR, "skills", "polaris");
173
175
  await mkdir(skillDir, { recursive: true });
176
+ const identity = participantId ? `\`${participantId}\`` : "the user's participant ID (ask them if unknown)";
174
177
  const skillContent = `---
175
178
  name: polaris
176
179
  description: Connect to a Polaris multiplayer collaboration session
177
- allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context
178
- argument-hint: [join <project> <session> | disconnect | (no args for status)]
180
+ allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename
181
+ argument-hint: [join <project> <session> | rename <new-name> | disconnect | (no args for status)]
179
182
  ---
180
183
 
181
184
  ## Polaris — Multiplayer Collaboration
@@ -187,9 +190,13 @@ Manage your connection to a Polaris collaboration session.
187
190
  Based on the arguments provided, do ONE of the following:
188
191
 
189
192
  **\`/polaris join <project> <session>\`** — Connect to a session:
190
- 1. Call \`polaris_connect\` with the given project, session, and user identity \`${userInfo.participant_id}\`
193
+ 1. Call \`polaris_connect\` with the given project, session, and user identity ${identity}
191
194
  2. Report the connection status
192
195
 
196
+ **\`/polaris rename <new-name>\`** — Rename the current project:
197
+ 1. Call \`polaris_rename\` with the new name
198
+ 2. Report the result
199
+
193
200
  **\`/polaris disconnect\`** — Disconnect:
194
201
  1. Call \`polaris_disconnect\`
195
202
  2. Confirm disconnection
@@ -201,24 +208,142 @@ Based on the arguments provided, do ONE of the following:
201
208
  ### Arguments: $ARGUMENTS
202
209
  `;
203
210
  await writeFile(join(skillDir, "SKILL.md"), skillContent);
204
- console.log(`/polaris skill written to ${skillDir}/SKILL.md`);
211
+ console.log(" ✓ /polaris skill written");
212
+ }
205
213
 
206
- // 8. Install status line
207
- const statusLinePath = join(import.meta.dir, "..", "..", "hooks", "statusline.sh");
208
- const mergedSettingsWithStatusLine = {
209
- ...mergedSettings,
210
- statusLine: {
211
- type: "command",
212
- command: statusLinePath,
214
+ // --- Login ---
215
+
216
+ async function login(appUrl: string, profileName?: string) {
217
+ const derivedName = profileName ?? deriveProfileName(appUrl);
218
+
219
+ // Browser OAuth
220
+ let resolveToken: (token: string) => void;
221
+ const tokenPromise = new Promise<string>((resolve) => { resolveToken = resolve; });
222
+
223
+ const callbackServer = Bun.serve({
224
+ port: 0,
225
+ hostname: "127.0.0.1",
226
+ fetch(req) {
227
+ const url = new URL(req.url);
228
+ if (url.pathname === "/callback") {
229
+ const token = url.searchParams.get("token");
230
+ if (token) {
231
+ resolveToken!(token);
232
+ return new Response(
233
+ `<!DOCTYPE html><html><head><title>Polaris</title><style>body{font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;color:#1a1a1a;}</style></head><body><div style="text-align:center"><h2>Authenticated!</h2><p>You can close this tab and return to the terminal.</p></div></body></html>`,
234
+ { headers: { "Content-Type": "text/html" } }
235
+ );
236
+ }
237
+ }
238
+ return new Response("Not found", { status: 404 });
213
239
  },
240
+ });
241
+
242
+ const callbackPort = callbackServer.port;
243
+ const authUrl = `${appUrl}/auth/cli?port=${callbackPort}`;
244
+
245
+ console.log(` Opening browser for Google sign-in (${derivedName})...`);
246
+ console.log(` If the browser doesn't open, visit: ${authUrl}\n`);
247
+
248
+ const proc = Bun.spawn(
249
+ process.platform === "darwin" ? ["open", authUrl] :
250
+ process.platform === "win32" ? ["cmd", "/c", "start", authUrl] :
251
+ ["xdg-open", authUrl],
252
+ { stdout: "ignore", stderr: "ignore" }
253
+ );
254
+ await proc.exited;
255
+
256
+ console.log(" Waiting for authentication...");
257
+ const token = await tokenPromise;
258
+ setTimeout(() => callbackServer.stop(true), 3000);
259
+
260
+ // Validate token
261
+ const res = await fetch(`${appUrl}/auth/token?token=${token}`);
262
+ if (!res.ok) {
263
+ console.error(" ✗ Failed to validate token. Please try again.");
264
+ process.exit(1);
265
+ }
266
+ const userInfo = (await res.json()) as {
267
+ sub: string;
268
+ email: string;
269
+ name: string;
270
+ org_id: string;
271
+ participant_id: string;
214
272
  };
215
- await writeFile(settingsPath, JSON.stringify(mergedSettingsWithStatusLine, null, 2));
216
- console.log(`Status line config written to ${settingsPath}`);
217
273
 
218
- // 9. Post system event (device connected)
219
- const hostname = (await import("node:os")).hostname();
274
+ let orgName = userInfo.email.split("@")[1].split(".")[0];
275
+ orgName = orgName.charAt(0).toUpperCase() + orgName.slice(1);
276
+
277
+ console.log(`\n Authenticated as ${userInfo.name} (${userInfo.email})`);
278
+ console.log(` Organization: ${orgName}`);
279
+ console.log(` Participant ID: ${userInfo.participant_id}`);
280
+
281
+ // Save to profile
282
+ const config = await loadConfig();
283
+ config.profiles[derivedName] = {
284
+ api: appToApi(appUrl),
285
+ app: appUrl,
286
+ token,
287
+ email: userInfo.email,
288
+ name: userInfo.name,
289
+ org_id: userInfo.org_id,
290
+ participant_id: userInfo.participant_id,
291
+ };
292
+ config.active = derivedName;
293
+ await saveConfig(config);
294
+
295
+ // Also write legacy credentials.json for backward compat (daemon reads it)
296
+ await writeFile(LEGACY_CREDENTIALS_FILE, JSON.stringify({
297
+ token,
298
+ ...userInfo,
299
+ service_url: appUrl,
300
+ }, null, 2));
301
+
302
+ console.log(` ✓ Profile "${derivedName}" saved and set as active`);
303
+
304
+ // Re-install skill with personalized participant ID
305
+ await mkdir(join(CLAUDE_DIR, "skills", "polaris"), { recursive: true });
306
+ const identity = `\`${userInfo.participant_id}\``;
307
+ const skillDir = join(CLAUDE_DIR, "skills", "polaris");
308
+ const skillContent = `---
309
+ name: polaris
310
+ description: Connect to a Polaris multiplayer collaboration session
311
+ allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename
312
+ argument-hint: [join <project> <session> | rename <new-name> | disconnect | (no args for status)]
313
+ ---
314
+
315
+ ## Polaris — Multiplayer Collaboration
316
+
317
+ Manage your connection to a Polaris collaboration session.
318
+
319
+ ### Commands
320
+
321
+ Based on the arguments provided, do ONE of the following:
322
+
323
+ **\`/polaris join <project> <session>\`** — Connect to a session:
324
+ 1. Call \`polaris_connect\` with the given project, session, and user identity ${identity}
325
+ 2. Report the connection status
326
+
327
+ **\`/polaris rename <new-name>\`** — Rename the current project:
328
+ 1. Call \`polaris_rename\` with the new name
329
+ 2. Report the result
330
+
331
+ **\`/polaris disconnect\`** — Disconnect:
332
+ 1. Call \`polaris_disconnect\`
333
+ 2. Confirm disconnection
334
+
335
+ **\`/polaris\`** (no arguments) — Show status:
336
+ 1. Call \`polaris_status\`
337
+ 2. Display the current connection state
338
+
339
+ ### Arguments: $ARGUMENTS
340
+ `;
341
+ await writeFile(join(skillDir, "SKILL.md"), skillContent);
342
+
343
+ // Post system event (device connected)
344
+ const apiUrl = appToApi(appUrl);
220
345
  try {
221
- await fetch(`${SERVICE_URL.replace(":3000", ":4321")}/projects/_system/sessions/_system/events`, {
346
+ await fetch(`${apiUrl}/projects/_system/sessions/_system/events`, {
222
347
  method: "POST",
223
348
  headers: {
224
349
  "Content-Type": "application/json",
@@ -229,21 +354,63 @@ Based on the arguments provided, do ONE of the following:
229
354
  payload: {
230
355
  hook_event_name: "Stop",
231
356
  session_id: "_system",
232
- stop_response: `Device connected: ${hostname} (${process.platform})`,
357
+ stop_response: `Device connected: ${hostname()} (${process.platform})`,
233
358
  },
234
359
  }),
235
360
  });
236
- // Notify web app dashboard to refresh
237
- await fetch(`${SERVICE_URL}/api/notify-dashboard`, {
238
- method: "POST",
239
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
240
- });
241
361
  } catch { /* non-fatal */ }
362
+ }
363
+
364
+ // --- Use ---
365
+
366
+ async function use(profileName: string) {
367
+ const config = await loadConfig();
368
+ if (!config.profiles[profileName]) {
369
+ console.error(`Profile "${profileName}" not found.`);
370
+ const names = Object.keys(config.profiles);
371
+ if (names.length > 0) {
372
+ console.error(`Available profiles: ${names.join(", ")}`);
373
+ } else {
374
+ console.error("No profiles configured. Run: polaris login");
375
+ }
376
+ process.exit(1);
377
+ }
378
+ config.active = profileName;
379
+ await saveConfig(config);
380
+
381
+ // Update legacy credentials.json for daemon
382
+ const profile = config.profiles[profileName];
383
+ await writeFile(LEGACY_CREDENTIALS_FILE, JSON.stringify({
384
+ token: profile.token,
385
+ email: profile.email,
386
+ name: profile.name,
387
+ org_id: profile.org_id,
388
+ participant_id: profile.participant_id,
389
+ service_url: profile.app,
390
+ }, null, 2));
242
391
 
243
- console.log("\n✓ Polaris is set up on this machine!");
244
- console.log("\nNext steps:");
245
- console.log(" 1. Start the daemon: polaris daemon");
246
- console.log(" 2. Open your AI agent and run: /polaris join <project> <session>");
392
+ console.log(`Active profile: ${profileName} (${profile.api})`);
393
+ console.log("Restart the daemon for this to take effect.");
394
+ }
395
+
396
+ // --- Profiles ---
397
+
398
+ async function profiles() {
399
+ const config = await loadConfig();
400
+ const names = Object.keys(config.profiles);
401
+ if (names.length === 0) {
402
+ console.log("No profiles configured. Run: polaris login");
403
+ return;
404
+ }
405
+ console.log("Profiles:\n");
406
+ for (const name of names) {
407
+ const p = config.profiles[name];
408
+ const active = name === config.active ? " (active)" : "";
409
+ console.log(` ${name}${active}`);
410
+ console.log(` API: ${p.api}`);
411
+ console.log(` User: ${p.name} (${p.email})`);
412
+ console.log("");
413
+ }
247
414
  }
248
415
 
249
416
  // --- Daemon ---
@@ -256,7 +423,6 @@ async function daemon() {
256
423
  stderr: "inherit",
257
424
  env: {
258
425
  ...process.env,
259
- POLARIS_SERVICE_URL: SERVICE_URL.replace(":3000", ":4321"),
260
426
  },
261
427
  });
262
428
  await proc.exited;
@@ -265,68 +431,143 @@ async function daemon() {
265
431
  // --- Status ---
266
432
 
267
433
  async function status() {
434
+ // Active profile
435
+ const config = await loadConfig();
436
+ const profile = getActiveProfile(config);
437
+ if (profile) {
438
+ console.log(`Profile: ${config.active} (${profile.api})`);
439
+ console.log(`User: ${profile.name} (${profile.email})`);
440
+ } else {
441
+ console.log("Not logged in. Run: polaris login");
442
+ }
443
+
444
+ // Daemon
268
445
  try {
269
- const res = await fetch("http://127.0.0.1:4321/status");
446
+ const res = await fetch("http://127.0.0.1:4322/status");
270
447
  const data = (await res.json()) as { ok: boolean; sessions?: Array<{ ccSessionId: string; project: string; session: string; user: string }> };
271
448
  if (data.ok) {
272
- console.log("Daemon: running");
449
+ console.log("\nDaemon: running");
273
450
  if (data.sessions && data.sessions.length > 0) {
274
451
  console.log(`Active sessions (${data.sessions.length}):`);
275
452
  for (const s of data.sessions) {
276
- console.log(` ${s.project}/${s.session} as ${s.user} (cc: ${s.ccSessionId})`);
453
+ console.log(` ${s.project}/${s.session} as ${s.user}`);
277
454
  }
278
455
  } else {
279
456
  console.log("No active sessions");
280
457
  }
281
458
  }
282
459
  } catch {
283
- console.log("Daemon: not running");
284
- }
285
-
286
- // Check credentials
287
- try {
288
- const creds = JSON.parse(await readFile(CREDENTIALS_FILE, "utf-8"));
289
- console.log(`\nLogged in as: ${creds.name} (${creds.email})`);
290
- console.log(`Org: ${creds.org_id}`);
291
- console.log(`Service: ${creds.service_url}`);
292
- } catch {
293
- console.log("\nNot logged in. Run: polaris login");
460
+ console.log("\nDaemon: not running");
294
461
  }
295
462
  }
296
463
 
297
464
  // --- Logout ---
298
465
 
299
- async function logout() {
300
- try {
301
- await rm(POLARIS_DIR, { recursive: true });
302
- console.log("Credentials removed.");
303
- } catch { /* already gone */ }
304
- console.log("Logged out. MCP config and hooks are still installed — remove them manually if needed.");
466
+ async function logout(all = false) {
467
+ if (all) {
468
+ try {
469
+ await rm(POLARIS_DIR, { recursive: true });
470
+ console.log("All profiles and credentials removed.");
471
+ } catch { /* already gone */ }
472
+ } else {
473
+ const config = await loadConfig();
474
+ if (!config.active || !config.profiles[config.active]) {
475
+ console.log("No active profile to remove.");
476
+ return;
477
+ }
478
+ const name = config.active;
479
+ delete config.profiles[name];
480
+ // Set active to first remaining profile, or empty
481
+ const remaining = Object.keys(config.profiles);
482
+ config.active = remaining.length > 0 ? remaining[0] : "";
483
+ await saveConfig(config);
484
+ console.log(`Profile "${name}" removed.`);
485
+ if (config.active) {
486
+ console.log(`Active profile switched to: ${config.active}`);
487
+ }
488
+ }
489
+ console.log("MCP config and hooks are still installed — run `polaris install` to reset them.");
305
490
  }
306
491
 
307
492
  // --- Main ---
308
493
 
309
- const command = process.argv[2];
494
+ const args = process.argv.slice(2);
495
+ const command = args[0];
496
+
497
+ function getFlag(name: string): string | undefined {
498
+ const idx = args.indexOf(`--${name}`);
499
+ if (idx >= 0 && idx + 1 < args.length) return args[idx + 1];
500
+ return undefined;
501
+ }
502
+
503
+ function hasFlag(name: string): boolean {
504
+ return args.includes(`--${name}`);
505
+ }
310
506
 
311
507
  switch (command) {
312
- case "login":
313
- await login();
508
+ case "install":
509
+ console.log("Polaris — installing local components\n");
510
+ await install();
511
+ console.log("\nInstall complete.");
314
512
  break;
513
+
514
+ case "login": {
515
+ const appUrl = hasFlag("local") ? LOCAL_APP_URL : (getFlag("url") ?? DEFAULT_APP_URL);
516
+ const profileName = getFlag("profile");
517
+ console.log("Polaris — authenticating\n");
518
+ await login(appUrl, profileName);
519
+ console.log("\n✓ Login complete!");
520
+ console.log("\nNext: start the daemon with `polaris daemon`, then `/polaris join <project> <session>` in your AI agent.");
521
+ break;
522
+ }
523
+
524
+ case "use":
525
+ if (!args[1]) {
526
+ console.error("Usage: polaris use <profile>");
527
+ process.exit(1);
528
+ }
529
+ await use(args[1]);
530
+ break;
531
+
532
+ case "profiles":
533
+ await profiles();
534
+ break;
535
+
315
536
  case "daemon":
316
537
  await daemon();
317
538
  break;
539
+
318
540
  case "status":
319
541
  await status();
320
542
  break;
543
+
321
544
  case "logout":
322
- await logout();
545
+ await logout(hasFlag("all"));
546
+ break;
547
+
548
+ case undefined:
549
+ // Default: install + login
550
+ console.log("Polaris — setting up your machine\n");
551
+ console.log("[1/2] Installing local components...\n");
552
+ await install();
553
+ console.log("\n Install complete. Run `polaris install` to repeat this step independently.\n");
554
+ console.log("[2/2] Authenticating...\n");
555
+ await login(DEFAULT_APP_URL);
556
+ console.log("\n✓ Polaris is set up on this machine!");
557
+ console.log("\nNext: start the daemon with `polaris daemon`, then `/polaris join <project> <session>` in your AI agent.");
323
558
  break;
559
+
324
560
  default:
325
- console.log("Polaris CLI");
326
- console.log("");
561
+ console.log("Polaris CLI\n");
327
562
  console.log("Usage:");
328
- console.log(" polaris login authenticate and set up this machine");
329
- console.log(" polaris daemon start the local daemon");
330
- console.log(" polaris status show connection status");
331
- console.log(" polaris logout remove credentials");
563
+ console.log(" polaris install + login (default setup)");
564
+ console.log(" polaris install install local components (no auth)");
565
+ console.log(" polaris login authenticate (production)");
566
+ console.log(" polaris login --local authenticate (local dev)");
567
+ console.log(" polaris use <profile> — switch active profile");
568
+ console.log(" polaris profiles — list all profiles");
569
+ console.log(" polaris daemon — start the local daemon");
570
+ console.log(" polaris status — show connection status");
571
+ console.log(" polaris logout — remove active profile");
572
+ console.log(" polaris logout --all — remove all credentials");
332
573
  }