swadhinbiswas feat: implement air-gapped mode, UI fixes, and project management integrations Β· 7/9/2026
e3bd4a1
236 lines Β· 6.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/**
 * Auth Commands
 * GitHub-style authentication for OpenCodeHub CLI
 */

import chalk from "chalk";
import { Command } from "commander";
import inquirer from "inquirer";
import ora from "ora";
import { applyTlsConfig } from "../lib/api.js";
import {
  getConfig,
  normalizeServerUrl,
  saveConfig,
  supportsSecureTokenStorage,
} from "../lib/config.js";

export const authCommands = new Command("auth").description(
  "Authentication commands",
);

// Login - GitHub style
authCommands
  .command("login")
  .description("Login to OpenCodeHub")
  .option("-u, --url <url>", "OpenCodeHub server URL")
  .option("--ca-file <path>", "Path to custom CA bundle (self-signed TLS)")
  .option("--insecure", "Disable TLS certificate verification")
  .option("--with-token", "Authenticate using a Personal Access Token")
  .option(
    "-t, --token <token>",
    "Personal access token (for non-interactive use)",
  )
  .action(async (options) => {
    console.log(chalk.blue("\nπŸ” OpenCodeHub Login\n"));

    try {
      const existingConfig = getConfig();
      let token: string;
      let serverUrl = options.url || existingConfig.serverUrl;

      if (!serverUrl) {
        const answers = await inquirer.prompt([
          {
            type: "input",
            name: "serverUrl",
            message: "OpenCodeHub server URL:",
            validate: (input) => input.length > 0 || "Server URL is required",
          },
        ]);
        serverUrl = answers.serverUrl;
      }
      serverUrl = normalizeServerUrl(serverUrl);

      if (options.token) {
        // Token provided directly via CLI
        token = options.token;
      } else if (options.withToken) {
        // Interactive token entry (GitHub style: gh auth login --with-token)
        console.log(
          chalk.hex("#6272a4")("Tip: You can create a Personal Access Token at:"),
        );
        console.log(chalk.hex("#8be9fd")(`${serverUrl}/settings/tokens\n`));

        const answers = await inquirer.prompt([
          {
            type: "password",
            name: "token",
            message: "Paste your token:",
            mask: "*",
          },
        ]);
        token = answers.token;
      } else {
        // Default: Open browser for token creation (like gh auth login)
        console.log(chalk.hex("#f1fa8c")("! No token provided."));
        console.log(
          chalk.hex("#6272a4")("\nTo authenticate, you need a Personal Access Token."),
        );
        console.log(
          chalk.hex("#6272a4")("Create one at: ") +
            chalk.hex("#8be9fd")(`${serverUrl}/settings/tokens`),
        );
        console.log();

        const { useToken } = await inquirer.prompt([
          {
            type: "confirm",
            name: "useToken",
            message: "Do you have a token to paste?",
            default: true,
          },
        ]);

        if (!useToken) {
          console.log(
            chalk.hex("#6272a4")("\nRun ") +
              chalk.hex("#8be9fd")("och auth login --with-token") +
              chalk.hex("#6272a4")(" after creating a token."),
          );
          process.exit(0);
        }

        const answers = await inquirer.prompt([
          {
            type: "password",
            name: "token",
            message: "Paste your token:",
            mask: "*",
          },
        ]);
        token = answers.token;
      }

      // Validate token
      const spinner = ora("Validating token...").start();

      if (token.trim().length < 8) {
        spinner.fail("Invalid token format");
        console.error(chalk.hex("#ff5555")("Token looks too short"));
        process.exit(1);
      }

      // Test the token by getting user info
      saveConfig({
        serverUrl,
        caFile: options.caFile ?? existingConfig.caFile,
        insecure: options.insecure ?? existingConfig.insecure,
      });

      applyTlsConfig();

      const response = await fetch(`${serverUrl}/api/user`, {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });

      if (!response.ok) {
        spinner.fail("Invalid or expired token");
        process.exit(1);
      }

      const userData = await response.json();
      const username = userData.data?.username || "unknown";

      spinner.succeed(`Authenticated as ${chalk.hex("#50fa7b")(username)}`);

      // Save config
      saveConfig({
        serverUrl,
        token,
        username,
        caFile: options.caFile ?? existingConfig.caFile,
        insecure: options.insecure ?? existingConfig.insecure,
      });

      console.log(chalk.hex("#50fa7b")("\nβœ“ Logged in to " + serverUrl));
      console.log(
        chalk.hex("#6272a4")(
          supportsSecureTokenStorage()
            ? "  Token saved to system keychain"
            : "  Token saved to CLI config fallback (set OCH_TOKEN to avoid local persistence)",
        ),
      );
    } catch (error) {
      console.error(chalk.hex("#ff5555")("Login failed"));
      console.error(
        chalk.hex("#6272a4")(error instanceof Error ? error.message : "Unknown error"),
      );
      process.exit(1);
    }
  });

// Logout
authCommands
  .command("logout")
  .description("Logout from OpenCodeHub")
  .action(async () => {
    saveConfig({ serverUrl: "", token: "" });
    console.log(chalk.hex("#50fa7b")("βœ“ Logged out successfully"));
  });

// Status
authCommands
  .command("status")
  .description("Show authentication status")
  .action(async () => {
    const config = getConfig();

    if (config.token) {
      const spinner = ora("Checking authentication...").start();

      try {
        applyTlsConfig();
        const response = await fetch(`${config.serverUrl}/api/user`, {
          headers: {
            Authorization: `Bearer ${config.token}`,
          },
        });

        if (response.ok) {
          const userData = await response.json();
          spinner.succeed("Authenticated");
          console.log(
            chalk.hex("#6272a4")("  User: ") +
              chalk.hex("#8be9fd")(userData.data?.username || "unknown"),
          );
          console.log(chalk.hex("#6272a4")("  Server: ") + config.serverUrl);
          console.log(
            chalk.hex("#6272a4")("  Token: ") + config.token.slice(0, 12) + "...",
          );
        } else {
          spinner.fail("Token is invalid or expired");
          console.log(
            chalk.hex("#6272a4")("  Run ") +
              chalk.hex("#8be9fd")("och auth login") +
              chalk.hex("#6272a4")(" to re-authenticate"),
          );
        }
      } catch (error) {
        spinner.fail("Could not connect to server");
        console.log(chalk.hex("#6272a4")("  Server: ") + config.serverUrl);
      }
    } else {
      console.log(chalk.hex("#f1fa8c")("βœ— Not authenticated"));
      console.log(
        chalk.hex("#6272a4")("  Run ") +
          chalk.hex("#8be9fd")("och auth login") +
          chalk.hex("#6272a4")(" to authenticate"),
      );
    }
  });

export default authCommands;