@corelayer-ai/cli 0.1.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.
@@ -0,0 +1,48 @@
1
+ import { readConfig, updateConfig } from "../lib/config.js";
2
+ import { fail, printJson } from "../lib/output.js";
3
+ export async function runConfig(args, ctx) {
4
+ const sub = args[0];
5
+ if (sub === "get") {
6
+ const key = args[1];
7
+ const config = readConfig();
8
+ if (!key) {
9
+ if (ctx.json) {
10
+ printJson(config);
11
+ }
12
+ else {
13
+ process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
14
+ }
15
+ return;
16
+ }
17
+ switch (key) {
18
+ case "api-url":
19
+ process.stdout.write(`${config.apiUrl}\n`);
20
+ return;
21
+ case "default-group":
22
+ process.stdout.write(`${config.defaults?.group || ""}\n`);
23
+ return;
24
+ default:
25
+ fail("Unknown key. Supported: api-url, default-group");
26
+ }
27
+ }
28
+ if (sub === "set") {
29
+ const key = args[1];
30
+ const value = args[2];
31
+ if (!key || !value) {
32
+ fail("Usage: corelayer config set <api-url|default-group> <value>");
33
+ }
34
+ switch (key) {
35
+ case "api-url":
36
+ updateConfig({ apiUrl: value });
37
+ process.stdout.write(`api-url set to ${value}\n`);
38
+ return;
39
+ case "default-group":
40
+ updateConfig({ defaults: { group: value } });
41
+ process.stdout.write(`default-group set to ${value}\n`);
42
+ return;
43
+ default:
44
+ fail("Unknown key. Supported: api-url, default-group");
45
+ }
46
+ }
47
+ fail("Usage: corelayer config <get|set> [key] [value]");
48
+ }
@@ -0,0 +1,27 @@
1
+ import { CorelayerClient } from "../lib/api-client.js";
2
+ import { readConfig } from "../lib/config.js";
3
+ import { fail, formatDate, printJson, printTable } from "../lib/output.js";
4
+ export async function runGroups(args, ctx) {
5
+ const sub = args[0];
6
+ if (sub !== "list") {
7
+ fail("Usage: corelayer groups list");
8
+ }
9
+ const config = readConfig();
10
+ const token = config.token;
11
+ if (!token) {
12
+ fail("Not logged in. Run: corelayer login");
13
+ }
14
+ const apiUrl = ctx.apiUrlOverride || config.apiUrl;
15
+ const client = new CorelayerClient(apiUrl, token);
16
+ const result = await client.listGroups();
17
+ if (ctx.json) {
18
+ printJson(result);
19
+ return;
20
+ }
21
+ const rows = result.groups.map(group => [
22
+ group.id,
23
+ group.name,
24
+ formatDate(group.created_at),
25
+ ]);
26
+ printTable(["ID", "NAME", "CREATED"], rows);
27
+ }
@@ -0,0 +1,34 @@
1
+ import { CorelayerClient } from "../lib/api-client.js";
2
+ import { readConfig } from "../lib/config.js";
3
+ import { fail, printJson, printTable } from "../lib/output.js";
4
+ function readFlag(args, flag) {
5
+ const index = args.indexOf(flag);
6
+ if (index === -1)
7
+ return undefined;
8
+ return args[index + 1];
9
+ }
10
+ export async function runIntegrations(args, ctx) {
11
+ const sub = args[0];
12
+ if (sub !== "list") {
13
+ fail("Usage: corelayer integrations list [--group <groupId>]");
14
+ }
15
+ const config = readConfig();
16
+ const token = config.token;
17
+ if (!token) {
18
+ fail("Not logged in. Run: corelayer login");
19
+ }
20
+ const groupId = readFlag(args, "--group") || config.defaults?.group;
21
+ const apiUrl = ctx.apiUrlOverride || config.apiUrl;
22
+ const client = new CorelayerClient(apiUrl, token);
23
+ const result = await client.listIntegrations(groupId);
24
+ if (ctx.json) {
25
+ printJson(result);
26
+ return;
27
+ }
28
+ const rows = result.accounts.map(account => [
29
+ account.id,
30
+ account.provider_code,
31
+ String(account.resources?.length || 0),
32
+ ]);
33
+ printTable(["ACCOUNT ID", "PROVIDER", "RESOURCES"], rows);
34
+ }
@@ -0,0 +1,166 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { CorelayerClient } from "../lib/api-client.js";
4
+ import { readConfig } from "../lib/config.js";
5
+ import { fail, formatDate, printJson, printTable } from "../lib/output.js";
6
+ function readFlag(args, flag) {
7
+ const index = args.indexOf(flag);
8
+ if (index === -1)
9
+ return undefined;
10
+ return args[index + 1];
11
+ }
12
+ function hasFlag(args, flag) {
13
+ return args.includes(flag);
14
+ }
15
+ function removeFlags(args) {
16
+ const out = [];
17
+ let skipNext = false;
18
+ for (const arg of args) {
19
+ if (skipNext) {
20
+ skipNext = false;
21
+ continue;
22
+ }
23
+ if (arg === "--group" ||
24
+ arg === "--status" ||
25
+ arg === "--severity" ||
26
+ arg === "--limit" ||
27
+ arg === "--page" ||
28
+ arg === "--feedback") {
29
+ skipNext = true;
30
+ continue;
31
+ }
32
+ if (arg === "--yes") {
33
+ continue;
34
+ }
35
+ out.push(arg);
36
+ }
37
+ return out;
38
+ }
39
+ async function confirmDelete() {
40
+ if (!process.stdin.isTTY) {
41
+ fail("Cannot confirm in non-interactive mode. Use --yes to skip confirmation.");
42
+ }
43
+ const rl = createInterface({ input, output });
44
+ const answer = await rl.question("Are you sure you want to delete this issue? (y/N) ");
45
+ rl.close();
46
+ return answer.trim().toLowerCase() === "y";
47
+ }
48
+ export async function runIssues(args, ctx) {
49
+ const sub = args[0];
50
+ const config = readConfig();
51
+ const token = config.token;
52
+ if (!token) {
53
+ fail("Not logged in. Run: corelayer login");
54
+ }
55
+ const apiUrl = ctx.apiUrlOverride || config.apiUrl;
56
+ const client = new CorelayerClient(apiUrl, token);
57
+ const commandArgs = removeFlags(args.slice(1));
58
+ if (sub === "list") {
59
+ const groupId = readFlag(args, "--group") || config.defaults?.group;
60
+ if (!groupId) {
61
+ fail("groupId is required. Use --group <groupId> or set default-group via corelayer config set default-group <groupId>");
62
+ }
63
+ const result = await client.listIssues(groupId, {
64
+ status: readFlag(args, "--status"),
65
+ severity: readFlag(args, "--severity"),
66
+ limit: readFlag(args, "--limit"),
67
+ page: readFlag(args, "--page"),
68
+ });
69
+ if (ctx.json) {
70
+ printJson(result);
71
+ return;
72
+ }
73
+ const rows = result.issues.map(issue => [
74
+ issue.id,
75
+ issue.slug || "-",
76
+ issue.severity || "-",
77
+ issue.status || "-",
78
+ issue.title || "-",
79
+ formatDate(issue.last_seen_at || issue.lastSeenAt),
80
+ ]);
81
+ printTable(["ID", "SLUG", "SEVERITY", "STATUS", "TITLE", "LAST SEEN"], rows);
82
+ if (!ctx.quiet) {
83
+ process.stdout.write(`Showing ${result.issues.length} of ${result.total} issues\n`);
84
+ }
85
+ return;
86
+ }
87
+ if (sub === "get") {
88
+ const issueId = commandArgs[0];
89
+ if (!issueId) {
90
+ fail("Usage: corelayer issues get <issueId>");
91
+ }
92
+ const issue = await client.getIssue(issueId);
93
+ if (ctx.json) {
94
+ printJson(issue);
95
+ return;
96
+ }
97
+ process.stdout.write(`Issue ${issue.slug || issue.id}: ${issue.title || ""}\n`);
98
+ process.stdout.write(`Status: ${issue.status || "-"}\n`);
99
+ process.stdout.write(`Severity: ${issue.severity || "-"}\n`);
100
+ process.stdout.write(`Created: ${formatDate(issue.createdAt || issue.created_at)}\n`);
101
+ process.stdout.write(`Last seen: ${formatDate(issue.lastSeenAt || issue.last_seen_at)}\n`);
102
+ process.stdout.write(`Events: ${issue.event_count || 0}\n`);
103
+ if (issue.whatHappened) {
104
+ process.stdout.write(`\nWhat happened:\n${issue.whatHappened}\n`);
105
+ }
106
+ if (issue.rootCause) {
107
+ process.stdout.write(`\nRoot cause:\n${issue.rootCause}\n`);
108
+ }
109
+ if (issue.nextSteps) {
110
+ process.stdout.write(`\nNext steps:\n${issue.nextSteps}\n`);
111
+ }
112
+ return;
113
+ }
114
+ if (sub === "close") {
115
+ const issueId = commandArgs[0];
116
+ if (!issueId) {
117
+ fail('Usage: corelayer issues close <issueId> [--feedback "..."]');
118
+ }
119
+ const feedback = readFlag(args, "--feedback");
120
+ await client.closeIssue(issueId, feedback);
121
+ if (!ctx.quiet) {
122
+ process.stdout.write(`Issue ${issueId} closed.\n`);
123
+ }
124
+ return;
125
+ }
126
+ if (sub === "reopen") {
127
+ const issueId = commandArgs[0];
128
+ if (!issueId) {
129
+ fail("Usage: corelayer issues reopen <issueId>");
130
+ }
131
+ await client.reopenIssue(issueId);
132
+ if (!ctx.quiet) {
133
+ process.stdout.write(`Issue ${issueId} reopened.\n`);
134
+ }
135
+ return;
136
+ }
137
+ if (sub === "delete") {
138
+ const issueId = commandArgs[0];
139
+ if (!issueId) {
140
+ fail("Usage: corelayer issues delete <issueId> [--yes]");
141
+ }
142
+ const yes = hasFlag(args, "--yes");
143
+ if (!yes) {
144
+ const confirmed = await confirmDelete();
145
+ if (!confirmed) {
146
+ process.stdout.write("Aborted.\n");
147
+ return;
148
+ }
149
+ }
150
+ await client.deleteIssue(issueId);
151
+ if (!ctx.quiet) {
152
+ process.stdout.write(`Issue ${issueId} deleted.\n`);
153
+ }
154
+ return;
155
+ }
156
+ if (sub === "summary") {
157
+ const groupId = readFlag(args, "--group") || config.defaults?.group;
158
+ if (!groupId) {
159
+ fail("groupId is required. Use --group <groupId> or set default-group via corelayer config set default-group <groupId>");
160
+ }
161
+ const summary = await client.getIssueSummary(groupId);
162
+ printJson(summary);
163
+ return;
164
+ }
165
+ fail("Usage: corelayer issues <list|get|close|reopen|delete|summary> [options]");
166
+ }
@@ -0,0 +1,36 @@
1
+ import { loginWithBrowser, loginWithCodeDirect } from "../lib/auth.js";
2
+ import { fail } from "../lib/output.js";
3
+ function readFlag(args, flag) {
4
+ const index = args.indexOf(flag);
5
+ if (index === -1)
6
+ return undefined;
7
+ return args[index + 1];
8
+ }
9
+ export async function runLogin(args, ctx) {
10
+ const code = readFlag(args, "--code");
11
+ let email;
12
+ let orgName;
13
+ if (code) {
14
+ const result = await loginWithCodeDirect(code, ctx.apiUrlOverride);
15
+ email = result.email;
16
+ orgName = result.orgName;
17
+ }
18
+ else {
19
+ if (ctx.apiUrlOverride) {
20
+ process.stderr.write("Warning: --api-url is ignored for browser login. The server URL is determined by the auth server.\n");
21
+ }
22
+ try {
23
+ const result = await loginWithBrowser();
24
+ email = result.email;
25
+ orgName = result.orgName;
26
+ }
27
+ catch (err) {
28
+ fail(err instanceof Error
29
+ ? err.message
30
+ : "Login failed. Try: corelayer login --code <AUTH_CODE> --api-url <server-url>");
31
+ }
32
+ }
33
+ if (!ctx.quiet) {
34
+ process.stdout.write(`Logged in as ${email} (org: ${orgName})\n`);
35
+ }
36
+ }
@@ -0,0 +1,7 @@
1
+ import { clearToken } from "../lib/config.js";
2
+ export async function runLogout(_args, ctx) {
3
+ clearToken();
4
+ if (!ctx.quiet) {
5
+ process.stdout.write("Logged out.\n");
6
+ }
7
+ }
package/dist/index.js ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { runConfig } from "./commands/config.js";
4
+ import { runGroups } from "./commands/groups.js";
5
+ import { runIntegrations } from "./commands/integrations.js";
6
+ import { runIssues } from "./commands/issues.js";
7
+ import { runLogin } from "./commands/login.js";
8
+ import { runLogout } from "./commands/logout.js";
9
+ const require = createRequire(import.meta.url);
10
+ const { version } = require("../package.json");
11
+ function printHelp() {
12
+ process.stdout.write(`Corelayer CLI
13
+
14
+ Usage:
15
+ corelayer login (opens browser)
16
+ corelayer login --code <CODE> --api-url <url> (manual)
17
+ corelayer logout
18
+ corelayer issues <list|get|close|reopen|delete|summary> ...
19
+ corelayer groups list
20
+ corelayer integrations list [--group <groupId>]
21
+ corelayer config <get|set> ...
22
+
23
+ Global flags:
24
+ --json
25
+ --quiet, -q
26
+ --api-url <url>
27
+ --no-color
28
+ `);
29
+ }
30
+ function parseGlobalFlags(argv) {
31
+ const ctx = {
32
+ json: false,
33
+ quiet: false,
34
+ noColor: false,
35
+ };
36
+ const args = [];
37
+ let i = 0;
38
+ while (i < argv.length) {
39
+ const arg = argv[i];
40
+ if (arg === "--json") {
41
+ ctx.json = true;
42
+ i += 1;
43
+ continue;
44
+ }
45
+ if (arg === "--quiet" || arg === "-q") {
46
+ ctx.quiet = true;
47
+ i += 1;
48
+ continue;
49
+ }
50
+ if (arg === "--no-color") {
51
+ ctx.noColor = true;
52
+ i += 1;
53
+ continue;
54
+ }
55
+ if (arg === "--api-url") {
56
+ const val = argv[i + 1];
57
+ if (!val || val.startsWith("--")) {
58
+ process.stderr.write("Missing value for --api-url\n");
59
+ process.exit(1);
60
+ }
61
+ ctx.apiUrlOverride = val;
62
+ i += 2;
63
+ continue;
64
+ }
65
+ args.push(arg);
66
+ i += 1;
67
+ }
68
+ return { args, ctx };
69
+ }
70
+ async function main() {
71
+ const parsed = parseGlobalFlags(process.argv.slice(2));
72
+ const { args, ctx } = parsed;
73
+ const command = args[0];
74
+ const rest = args.slice(1);
75
+ if (command === "--version" || command === "-v") {
76
+ process.stdout.write(`${version}\n`);
77
+ return;
78
+ }
79
+ if (!command ||
80
+ command === "help" ||
81
+ command === "--help" ||
82
+ command === "-h") {
83
+ printHelp();
84
+ return;
85
+ }
86
+ switch (command) {
87
+ case "login":
88
+ await runLogin(rest, ctx);
89
+ return;
90
+ case "logout":
91
+ await runLogout(rest, ctx);
92
+ return;
93
+ case "issues":
94
+ await runIssues(rest, ctx);
95
+ return;
96
+ case "groups":
97
+ await runGroups(rest, ctx);
98
+ return;
99
+ case "integrations":
100
+ await runIntegrations(rest, ctx);
101
+ return;
102
+ case "config":
103
+ await runConfig(rest, ctx);
104
+ return;
105
+ default:
106
+ process.stderr.write(`Unknown command: ${command}\n`);
107
+ printHelp();
108
+ process.exit(1);
109
+ }
110
+ }
111
+ main().catch(error => {
112
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
113
+ process.exit(1);
114
+ });
@@ -0,0 +1,118 @@
1
+ async function parseError(res) {
2
+ let body = null;
3
+ try {
4
+ body = (await res.json());
5
+ }
6
+ catch {
7
+ body = null;
8
+ }
9
+ return (body?.error ||
10
+ body?.message ||
11
+ `${res.status} ${res.statusText || "Request failed"}`);
12
+ }
13
+ export class CorelayerClient {
14
+ baseUrl;
15
+ token;
16
+ constructor(baseUrl, token) {
17
+ this.baseUrl = baseUrl;
18
+ this.token = token;
19
+ }
20
+ async request(method, path, body) {
21
+ const controller = new AbortController();
22
+ const timer = setTimeout(() => controller.abort(), 30_000);
23
+ let res;
24
+ try {
25
+ res = await fetch(`${this.baseUrl}${path}`, {
26
+ method,
27
+ headers: {
28
+ "Content-Type": "application/json",
29
+ Authorization: `Bearer ${this.token}`,
30
+ },
31
+ body: body ? JSON.stringify(body) : undefined,
32
+ signal: controller.signal,
33
+ });
34
+ }
35
+ catch (err) {
36
+ clearTimeout(timer);
37
+ if (err instanceof Error && err.name === "AbortError") {
38
+ throw new Error("Request timed out");
39
+ }
40
+ throw err;
41
+ }
42
+ finally {
43
+ clearTimeout(timer);
44
+ }
45
+ if (!res.ok) {
46
+ throw new Error(await parseError(res));
47
+ }
48
+ return (await res.json());
49
+ }
50
+ async listIssues(groupId, filters) {
51
+ const params = new URLSearchParams({ groupId });
52
+ for (const [key, value] of Object.entries(filters)) {
53
+ if (value !== undefined && value !== null && value !== "") {
54
+ params.set(key, String(value));
55
+ }
56
+ }
57
+ return this.request("GET", `/api/v1/issues?${params.toString()}`);
58
+ }
59
+ async getIssue(issueId) {
60
+ return this.request("GET", `/api/v1/issues/${issueId}`);
61
+ }
62
+ async closeIssue(issueId, feedback) {
63
+ await this.request("PATCH", `/api/v1/issues/${issueId}`, {
64
+ status: "Closed",
65
+ feedback,
66
+ });
67
+ }
68
+ async reopenIssue(issueId) {
69
+ await this.request("PATCH", `/api/v1/issues/${issueId}`, {
70
+ status: "Open",
71
+ });
72
+ }
73
+ async deleteIssue(issueId) {
74
+ await this.request("DELETE", `/api/v1/issues/${issueId}`);
75
+ }
76
+ async getIssueSummary(groupId) {
77
+ const params = new URLSearchParams({ groupId });
78
+ return this.request("GET", `/api/v1/issues/summary?${params.toString()}`);
79
+ }
80
+ async listGroups() {
81
+ return this.request("GET", "/api/v1/groups");
82
+ }
83
+ async listIntegrations(groupId) {
84
+ const params = new URLSearchParams();
85
+ if (groupId) {
86
+ params.set("groupId", groupId);
87
+ }
88
+ const suffix = params.toString() ? `?${params.toString()}` : "";
89
+ return this.request("GET", `/api/v1/integrations/accounts${suffix}`);
90
+ }
91
+ }
92
+ export async function exchangeCliCode(baseUrl, code) {
93
+ const controller = new AbortController();
94
+ const timer = setTimeout(() => controller.abort(), 30_000);
95
+ let res;
96
+ try {
97
+ res = await fetch(`${baseUrl}/api/v1/cli/exchange`, {
98
+ method: "POST",
99
+ headers: { "Content-Type": "application/json" },
100
+ body: JSON.stringify({ code }),
101
+ signal: controller.signal,
102
+ });
103
+ }
104
+ catch (err) {
105
+ clearTimeout(timer);
106
+ if (err instanceof Error && err.name === "AbortError") {
107
+ throw new Error("Request timed out");
108
+ }
109
+ throw err;
110
+ }
111
+ finally {
112
+ clearTimeout(timer);
113
+ }
114
+ if (!res.ok) {
115
+ throw new Error(await parseError(res));
116
+ }
117
+ return (await res.json());
118
+ }
@@ -0,0 +1,164 @@
1
+ import http from "node:http";
2
+ import crypto from "node:crypto";
3
+ import { execFile } from "node:child_process";
4
+ import { exchangeCliCode } from "./api-client.js";
5
+ import { readConfig, updateConfig } from "./config.js";
6
+ const AUTH_BASE_URL = process.env.CORELAYER_AUTH_URL || "https://app.corelayer.com";
7
+ export async function loginWithCode(code, serverUrl) {
8
+ const exchanged = await exchangeCliCode(serverUrl, code);
9
+ updateConfig({
10
+ apiUrl: serverUrl,
11
+ token: exchanged.token,
12
+ orgId: exchanged.orgId,
13
+ orgName: exchanged.orgName,
14
+ userEmail: exchanged.email,
15
+ });
16
+ return {
17
+ email: exchanged.email,
18
+ orgName: exchanged.orgName,
19
+ };
20
+ }
21
+ function openBrowser(url) {
22
+ const platform = process.platform;
23
+ if (platform === "win32") {
24
+ execFile("cmd", ["/c", "start", "", url]);
25
+ }
26
+ else {
27
+ const cmd = platform === "darwin" ? "open" : "xdg-open";
28
+ execFile(cmd, [url]);
29
+ }
30
+ }
31
+ function findAvailablePort() {
32
+ return new Promise((resolve, reject) => {
33
+ const server = http.createServer();
34
+ server.listen(0, "127.0.0.1", () => {
35
+ const addr = server.address();
36
+ if (!addr || typeof addr === "string") {
37
+ server.close();
38
+ reject(new Error("Failed to bind to a port"));
39
+ return;
40
+ }
41
+ const port = addr.port;
42
+ server.close(() => resolve(port));
43
+ });
44
+ server.on("error", reject);
45
+ });
46
+ }
47
+ function startCallbackServer(port, state, timeout) {
48
+ return new Promise((resolve, reject) => {
49
+ const timer = setTimeout(() => {
50
+ server.close();
51
+ reject(new Error("Login timed out. Please try again."));
52
+ }, timeout);
53
+ const server = http.createServer((req, res) => {
54
+ const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
55
+ if (url.pathname !== "/callback") {
56
+ res.writeHead(404);
57
+ res.end("Not found");
58
+ return;
59
+ }
60
+ const receivedState = url.searchParams.get("state");
61
+ const code = url.searchParams.get("code");
62
+ const serverUrl = url.searchParams.get("server_url");
63
+ const error = url.searchParams.get("error");
64
+ if (error) {
65
+ res.writeHead(200, { "Content-Type": "text/html" });
66
+ res.end(htmlPage("Login Failed", `Error: ${escapeHtml(error)}. You can close this tab.`));
67
+ clearTimeout(timer);
68
+ server.close();
69
+ reject(new Error(error));
70
+ return;
71
+ }
72
+ if (receivedState !== state) {
73
+ res.writeHead(400, { "Content-Type": "text/html" });
74
+ res.end(htmlPage("Login Failed", "State mismatch. You can close this tab."));
75
+ clearTimeout(timer);
76
+ server.close();
77
+ reject(new Error("State mismatch — possible CSRF attack."));
78
+ return;
79
+ }
80
+ if (!code || !serverUrl) {
81
+ res.writeHead(400, { "Content-Type": "text/html" });
82
+ res.end(htmlPage("Login Failed", "Missing code or server URL. You can close this tab."));
83
+ clearTimeout(timer);
84
+ server.close();
85
+ reject(new Error("Callback missing required parameters."));
86
+ return;
87
+ }
88
+ const decodedServerUrl = decodeURIComponent(serverUrl);
89
+ let parsed;
90
+ try {
91
+ parsed = new URL(decodedServerUrl);
92
+ }
93
+ catch {
94
+ res.writeHead(400, { "Content-Type": "text/html" });
95
+ res.end(htmlPage("Login Failed", "Invalid server URL. You can close this tab."));
96
+ clearTimeout(timer);
97
+ server.close();
98
+ reject(new Error("Invalid server URL received in callback."));
99
+ return;
100
+ }
101
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
102
+ res.writeHead(400, { "Content-Type": "text/html" });
103
+ res.end(htmlPage("Login Failed", "Unsupported server URL protocol. You can close this tab."));
104
+ clearTimeout(timer);
105
+ server.close();
106
+ reject(new Error(`Unsupported protocol "${parsed.protocol}" — only http: and https: are allowed.`));
107
+ return;
108
+ }
109
+ res.writeHead(200, { "Content-Type": "text/html" });
110
+ res.end(htmlPage("Login Successful", "You are now logged in. You can close this tab and return to the terminal."));
111
+ clearTimeout(timer);
112
+ server.close();
113
+ resolve({ code, serverUrl: decodedServerUrl });
114
+ });
115
+ server.listen(port, "127.0.0.1");
116
+ server.on("error", err => {
117
+ clearTimeout(timer);
118
+ reject(err);
119
+ });
120
+ });
121
+ }
122
+ function escapeHtml(str) {
123
+ return str
124
+ .replace(/&/g, "&amp;")
125
+ .replace(/</g, "&lt;")
126
+ .replace(/>/g, "&gt;")
127
+ .replace(/"/g, "&quot;")
128
+ .replace(/'/g, "&#39;");
129
+ }
130
+ function htmlPage(title, body) {
131
+ return `<!DOCTYPE html>
132
+ <html>
133
+ <head><title>${title}</title>
134
+ <style>body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#f8f9fa}
135
+ .card{text-align:center;padding:2rem;border-radius:8px;background:white;box-shadow:0 1px 3px rgba(0,0,0,.1)}
136
+ h1{margin:0 0 .5rem;font-size:1.5rem}p{color:#6b7280;margin:0}</style>
137
+ </head>
138
+ <body><div class="card"><h1>${title}</h1><p>${body}</p></div></body>
139
+ </html>`;
140
+ }
141
+ export async function loginWithBrowser() {
142
+ const port = await findAvailablePort();
143
+ const state = crypto.randomBytes(16).toString("hex");
144
+ const authUrl = `${AUTH_BASE_URL}/cli/auth?port=${port}&state=${state}`;
145
+ process.stdout.write("Opening browser for authentication...\n");
146
+ process.stdout.write(`If the browser doesn't open, visit:\n${authUrl}\n\n`);
147
+ process.stdout.write("Waiting for authorization...\n");
148
+ openBrowser(authUrl);
149
+ const { code, serverUrl } = await startCallbackServer(port, state, 120_000);
150
+ const result = await loginWithCode(code, serverUrl);
151
+ return result;
152
+ }
153
+ function validateServerUrl(url) {
154
+ const parsed = new URL(url);
155
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
156
+ throw new Error(`Unsupported protocol "${parsed.protocol}" — only http: and https: are allowed.`);
157
+ }
158
+ }
159
+ export async function loginWithCodeDirect(code, apiUrlOverride) {
160
+ const current = readConfig();
161
+ const serverUrl = apiUrlOverride || current.apiUrl;
162
+ validateServerUrl(serverUrl);
163
+ return loginWithCode(code, serverUrl);
164
+ }
@@ -0,0 +1,61 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ const CONFIG_DIR = path.join(os.homedir(), ".corelayer");
5
+ const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
6
+ function defaultApiUrl() {
7
+ return process.env.CORELAYER_API_URL || "http://localhost:3000";
8
+ }
9
+ export function getConfigPath() {
10
+ return CONFIG_PATH;
11
+ }
12
+ export function readConfig() {
13
+ if (!fs.existsSync(CONFIG_PATH)) {
14
+ return { apiUrl: defaultApiUrl(), defaults: {} };
15
+ }
16
+ try {
17
+ const content = fs.readFileSync(CONFIG_PATH, "utf8");
18
+ const parsed = JSON.parse(content);
19
+ return {
20
+ apiUrl: parsed.apiUrl || defaultApiUrl(),
21
+ token: parsed.token,
22
+ orgId: parsed.orgId,
23
+ orgName: parsed.orgName,
24
+ userEmail: parsed.userEmail,
25
+ defaults: parsed.defaults || {},
26
+ };
27
+ }
28
+ catch {
29
+ return { apiUrl: defaultApiUrl(), defaults: {} };
30
+ }
31
+ }
32
+ export function writeConfig(config) {
33
+ if (!fs.existsSync(CONFIG_DIR)) {
34
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
35
+ }
36
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), {
37
+ encoding: "utf8",
38
+ mode: 0o600,
39
+ });
40
+ }
41
+ export function updateConfig(partial) {
42
+ const current = readConfig();
43
+ const merged = {
44
+ ...current,
45
+ ...partial,
46
+ defaults: {
47
+ ...(current.defaults || {}),
48
+ ...(partial.defaults || {}),
49
+ },
50
+ };
51
+ writeConfig(merged);
52
+ return merged;
53
+ }
54
+ export function clearToken() {
55
+ const current = readConfig();
56
+ delete current.token;
57
+ delete current.orgId;
58
+ delete current.orgName;
59
+ delete current.userEmail;
60
+ writeConfig(current);
61
+ }
@@ -0,0 +1,31 @@
1
+ export function printJson(value) {
2
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
3
+ }
4
+ export function formatDate(value) {
5
+ if (!value)
6
+ return "-";
7
+ const date = new Date(value);
8
+ if (Number.isNaN(date.getTime())) {
9
+ return value;
10
+ }
11
+ return date.toISOString().replace("T", " ").slice(0, 19);
12
+ }
13
+ export function printTable(headers, rows) {
14
+ const widths = headers.map((header, idx) => Math.max(header.length, ...rows.map(row => (row[idx] || "").length)));
15
+ const renderRow = (cells) => cells
16
+ .map((cell, idx) => cell.padEnd(widths[idx], " "))
17
+ .join(" ")
18
+ .trimEnd();
19
+ process.stdout.write(`${renderRow(headers)}\n`);
20
+ process.stdout.write(`${widths
21
+ .map(width => "-".repeat(width))
22
+ .join(" ")
23
+ .trimEnd()}\n`);
24
+ for (const row of rows) {
25
+ process.stdout.write(`${renderRow(row)}\n`);
26
+ }
27
+ }
28
+ export function fail(message, code = 1) {
29
+ process.stderr.write(`${message}\n`);
30
+ process.exit(code);
31
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@corelayer-ai/cli",
3
+ "version": "0.1.0",
4
+ "description": "Corelayer CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "corelayer": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18.0.0"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "dev": "tsx src/index.ts",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "dependencies": {},
21
+ "devDependencies": {
22
+ "@types/node": "^20.17.57",
23
+ "tsx": "^4.19.4",
24
+ "typescript": "^5.8.3"
25
+ }
26
+ }