swadhinbiswas feat: implement air-gapped mode, UI fixes, and project management integrations · 7/9/2026
e3bd4a1
195 lines · 5.4 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
/**
 * API Commands
 * Make direct API requests to OpenCodeHub
 */

import chalk from "chalk";
import { Command } from "commander";
import { applyTlsConfig } from "../../lib/api.js";
import { getConfig } from "../../lib/config.js";

export const apiCommands = new Command("api")
  .description("Make direct API requests")
  .argument("<endpoint>", "API endpoint (e.g., /user or repos/owner/name)")
  .option("-X, --method <method>", "HTTP method", "GET")
  .option(
    "-F, --field <field>",
    "Add field to request body (key=value)",
    (val, acc: string[]) => [...acc, val],
    [],
  )
  .option(
    "-f, --raw-field <field>",
    "Add raw field (no JSON parsing)",
    (val, acc: string[]) => [...acc, val],
    [],
  )
  .option(
    "-H, --header <header>",
    "Add header",
    (val, acc: string[]) => [...acc, val],
    [],
  )
  .option("--input <file>", "Read body from file")
  .option("-q, --quiet", "Only output response body")
  .option("--jq <query>", "Filter JSON output (basic support)")
  .option("-i, --include", "Include response headers")
  .action(async (endpoint: string, options) => {
    const config = getConfig();
    if (!config.token) {
      console.error(chalk.hex("#ff5555")("Not logged in. Run 'och auth login' first."));
      process.exit(1);
    }

    if (!config.serverUrl) {
      console.error(
        chalk.hex("#ff5555")(
          "Server URL not configured. Run 'och config set serverUrl <url>' or 'och auth login --url <url>'.",
        ),
      );
      process.exit(1);
    }

    try {
      const method = String(options.method || "GET").toUpperCase();
      const allowedMethods = new Set(["GET", "POST", "PATCH", "PUT", "DELETE"]);
      if (!allowedMethods.has(method)) {
        console.error(chalk.hex("#ff5555")(`Unsupported method: ${options.method}`));
        console.error(chalk.hex("#6272a4")("Allowed methods: GET, POST, PATCH, PUT, DELETE"));
        process.exit(1);
      }

      // Normalize endpoint
      if (!endpoint.startsWith("/")) {
        endpoint = "/" + endpoint;
      }
      if (!endpoint.startsWith("/api")) {
        endpoint = "/api" + endpoint;
      }

      // Build headers
      const headers: Record<string, string> = {
        Authorization: `Bearer ${config.token}`,
        "Content-Type": "application/json",
      };

      for (const header of options.header) {
        const [key, ...valueParts] = header.split(":");
        headers[key.trim()] = valueParts.join(":").trim();
      }

      // Build body
      let body: any = undefined;

      if (options.input) {
        const fs = await import("fs");
        body = fs.readFileSync(options.input, "utf-8");
      } else if (options.field.length > 0 || options.rawField.length > 0) {
        body = {};
        for (const field of options.field) {
          const [key, ...valueParts] = field.split("=");
          const value = valueParts.join("=");
          // Try to parse as JSON
          try {
            body[key] = JSON.parse(value);
          } catch {
            body[key] = value;
          }
        }
        for (const field of options.rawField) {
          const [key, ...valueParts] = field.split("=");
          body[key] = valueParts.join("=");
        }
        body = JSON.stringify(body);
      }

      const url = `${config.serverUrl}${endpoint}`;

      if (!options.quiet) {
        console.error(chalk.hex("#6272a4")(`${method} ${url}\n`));
      }

      applyTlsConfig();
      const response = await fetch(url, {
        method,
        headers,
        body,
      });

      // Output headers if requested
      if (options.include) {
        console.log(
          chalk.hex("#8be9fd")(`HTTP/1.1 ${response.status} ${response.statusText}`),
        );
        response.headers.forEach((value, key) => {
          console.log(`${key}: ${value}`);
        });
        console.log("");
      }

      // Parse and output response
      const contentType = response.headers.get("content-type") || "";

      if (contentType.includes("application/json")) {
        const data = await response.json();

        if (options.jq) {
          // Basic jq support
          const output = applyJqFilter(data, options.jq);
          console.log(JSON.stringify(output, null, 2));
        } else {
          console.log(JSON.stringify(data, null, 2));
        }
      } else {
        const text = await response.text();
        console.log(text);
      }

      if (!response.ok && !options.quiet) {
        process.exit(1);
      }
    } catch (error) {
      console.error(chalk.hex("#ff5555")("API request failed"));
      console.error(
        chalk.hex("#ff5555")(error instanceof Error ? error.message : "Unknown error"),
      );
      process.exit(1);
    }
  });

/**
 * Basic jq-like filter support
 */
function applyJqFilter(data: any, query: string): any {
  if (query === ".") return data;

  const parts = query.split(".").filter(Boolean);
  let result = data;

  for (const part of parts) {
    // Handle array index
    const arrayMatch = part.match(/^(\w+)\[(\d+)\]$/);
    if (arrayMatch) {
      result = result[arrayMatch[1]][parseInt(arrayMatch[2])];
    }
    // Handle array access []
    else if (part === "[]") {
      // Already handled - just continue
    }
    // Handle property access
    else {
      if (Array.isArray(result)) {
        result = result.map((item) => item[part]);
      } else {
        result = result[part];
      }
    }

    if (result === undefined) break;
  }

  return result;
}

export default apiCommands;