kodingo-cli 1.0.13 → 1.0.14

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/commands/init.js +193 -101
  2. package/package.json +3 -2
@@ -2,9 +2,11 @@
2
2
  /**
3
3
  * kodingo init
4
4
  *
5
- * Interactively configures ~/.kodingo/config.json.
6
- * Supports both local (psql) and cloud (kodingo-api) modes.
7
- * On successful cloud init, ensures .kortex/ is in .gitignore.
5
+ * GitHub-style browser-based authentication and project connection.
6
+ * - First time on a machine: opens browser to login, saves auth token globally
7
+ * - Subsequent runs: skips login, goes straight to project selection
8
+ * - Project selection: type project name, case-insensitive match
9
+ * - Create new project if name not found
8
10
  */
9
11
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
12
  if (k2 === undefined) k2 = k;
@@ -45,142 +47,232 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
45
47
  Object.defineProperty(exports, "__esModule", { value: true });
46
48
  exports.registerInitCommand = registerInitCommand;
47
49
  const readline = __importStar(require("node:readline"));
50
+ const fs = __importStar(require("fs"));
51
+ const path = __importStar(require("path"));
52
+ const os = __importStar(require("os"));
53
+ const open_1 = __importDefault(require("open"));
48
54
  const persistence_config_1 = require("../utils/persistence-config");
49
- const path_1 = __importDefault(require("path"));
50
- const scan_repo_1 = require("./scan-repo");
51
- const fs_1 = __importDefault(require("fs"));
55
+ const API_BASE = "https://kodingo-api.onrender.com";
56
+ const APP_URL = "https://kodingo.xyz";
57
+ const CONFIG_DIR = path.join(os.homedir(), ".kodingo");
58
+ const AUTH_TOKEN_PATH = path.join(CONFIG_DIR, "auth.json");
59
+ // ── Auth token storage ────────────────────────────────────────────────────────
60
+ function readAuthToken() {
61
+ try {
62
+ if (fs.existsSync(AUTH_TOKEN_PATH)) {
63
+ const data = JSON.parse(fs.readFileSync(AUTH_TOKEN_PATH, "utf-8"));
64
+ return data.token ?? null;
65
+ }
66
+ }
67
+ catch { }
68
+ return null;
69
+ }
70
+ function saveAuthToken(token, email) {
71
+ if (!fs.existsSync(CONFIG_DIR))
72
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
73
+ fs.writeFileSync(AUTH_TOKEN_PATH, JSON.stringify({ token, email }, null, 2), "utf-8");
74
+ }
75
+ function readAuthEmail() {
76
+ try {
77
+ if (fs.existsSync(AUTH_TOKEN_PATH)) {
78
+ const data = JSON.parse(fs.readFileSync(AUTH_TOKEN_PATH, "utf-8"));
79
+ return data.email ?? '';
80
+ }
81
+ }
82
+ catch { }
83
+ return '';
84
+ }
85
+ // ── Prompt helper ─────────────────────────────────────────────────────────────
86
+ function prompt(rl, question) {
87
+ return new Promise(resolve => rl.question(question, ans => resolve(ans.trim())));
88
+ }
89
+ // ── Repo root finder ──────────────────────────────────────────────────────────
52
90
  function findRepoRoot(startPath) {
53
91
  let current = startPath;
54
92
  while (true) {
55
- if (fs_1.default.existsSync(path_1.default.join(current, ".git")))
93
+ if (fs.existsSync(path.join(current, ".git")))
56
94
  return current;
57
- const parent = path_1.default.dirname(current);
95
+ const parent = path.dirname(current);
58
96
  if (parent === current)
59
97
  return startPath;
60
98
  current = parent;
61
99
  }
62
100
  }
63
- // ── Prompt helper ─────────────────────────────────────────────────────────────
64
- function prompt(rl, question) {
65
- return new Promise((resolve) => rl.question(question, (ans) => resolve(ans.trim())));
66
- }
67
- // ── Health check ──────────────────────────────────────────────────────────────
68
- async function verifyCloudConnection(apiUrl, token) {
69
- const url = `${apiUrl.replace(/\/$/, "")}/health`;
70
- const res = await fetch(url, { headers: { "X-Kodingo-Token": token } });
71
- if (!res.ok)
72
- throw new Error(`Health check failed (${res.status}) — check your API URL`);
73
- }
74
101
  // ── .gitignore helper ─────────────────────────────────────────────────────────
75
102
  function ensureGitignoreEntry(repoRoot, entry) {
76
- const gitignorePath = path_1.default.join(repoRoot, ".gitignore");
103
+ const gitignorePath = path.join(repoRoot, ".gitignore");
77
104
  const line = entry.endsWith("\n") ? entry : `${entry}\n`;
78
- if (fs_1.default.existsSync(gitignorePath)) {
79
- const contents = fs_1.default.readFileSync(gitignorePath, "utf-8");
80
- if (contents.split("\n").some((l) => l.trim() === entry.trim()))
105
+ if (fs.existsSync(gitignorePath)) {
106
+ const contents = fs.readFileSync(gitignorePath, "utf-8");
107
+ if (contents.split("\n").some(l => l.trim() === entry.trim()))
81
108
  return;
82
- fs_1.default.appendFileSync(gitignorePath, `\n# Kortex — auto-generated context file\n${line}`, "utf-8");
109
+ fs.appendFileSync(gitignorePath, `\n# Kortex\n${line}`, "utf-8");
83
110
  }
84
111
  else {
85
- fs_1.default.writeFileSync(gitignorePath, `# Kortex — auto-generated context file\n${line}`, "utf-8");
112
+ fs.writeFileSync(gitignorePath, `# Kortex\n${line}`, "utf-8");
86
113
  }
87
114
  }
88
- // ── Command ───────────────────────────────────────────────────────────────────
89
- async function runInitialScan(repoRoot) {
115
+ // ── Browser login flow ────────────────────────────────────────────────────────
116
+ async function browserLogin() {
117
+ // Request a state token from the API
118
+ const initRes = await fetch(`${API_BASE}/cli/auth/init`, { method: "POST" });
119
+ if (!initRes.ok)
120
+ throw new Error("Failed to start login flow");
121
+ const { state, loginUrl } = await initRes.json();
122
+ console.log("\nPress Enter to open kodingo.xyz in your browser...");
123
+ console.log(`Or open this URL manually: ${loginUrl}\n`);
124
+ await new Promise(resolve => {
125
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
126
+ rl.question("", () => { rl.close(); resolve(); });
127
+ });
128
+ // Open browser
90
129
  try {
91
- console.log("\n🔍 Scanning existing codebase for functions without memory...");
92
- const captured = await (0, scan_repo_1.scanRepoCommand)({ repo: repoRoot, silent: false });
93
- if (captured === 0) {
94
- console.log(" No existing functions found — Kortex will capture them as you write and commit code.");
95
- }
130
+ await (0, open_1.default)(loginUrl);
96
131
  }
97
- catch {
98
- // Never fail init because of a scan error
132
+ catch { }
133
+ console.log("Waiting for login...");
134
+ // Poll for completion
135
+ const start = Date.now();
136
+ while (Date.now() - start < 5 * 60 * 1000) {
137
+ await new Promise(r => setTimeout(r, 2000));
138
+ const pollRes = await fetch(`${API_BASE}/cli/auth/poll/${state}`);
139
+ if (!pollRes.ok)
140
+ continue;
141
+ const data = await pollRes.json();
142
+ if (data.status === "complete" && data.token) {
143
+ return { token: data.token, email: data.email ?? "" };
144
+ }
99
145
  }
146
+ throw new Error("Login timed out. Please try again.");
100
147
  }
148
+ // ── Fetch accessible projects ─────────────────────────────────────────────────
149
+ async function fetchProjects(authToken) {
150
+ const res = await fetch(`${API_BASE}/cli/projects`, {
151
+ headers: { Authorization: `Bearer ${authToken}` },
152
+ });
153
+ if (!res.ok)
154
+ throw new Error("Failed to fetch projects. Please run `kodingo init` again.");
155
+ const data = await res.json();
156
+ return data.projects ?? [];
157
+ }
158
+ // ── Create a new project ──────────────────────────────────────────────────────
159
+ async function createProject(authToken, name, orgId) {
160
+ const res = await fetch(`${API_BASE}/cli/projects`, {
161
+ method: "POST",
162
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}` },
163
+ body: JSON.stringify({ name, orgId }),
164
+ });
165
+ const data = await res.json();
166
+ if (!res.ok)
167
+ throw new Error(data.error ?? "Failed to create project");
168
+ return data;
169
+ }
170
+ // ── Command ───────────────────────────────────────────────────────────────────
101
171
  function registerInitCommand(program) {
102
172
  program
103
173
  .command("init")
104
- .description("Configure kodingo CLI (local psql or cloud API)")
105
- .option("--local", "Switch to local mode (psql)")
106
- .option("--cloud", "Switch to cloud mode (kodingo-api)")
107
- .option("--api-url <url>", "Cloud API URL (skips prompt)")
108
- .option("--token <token>", "Cloud API token (skips prompt)")
174
+ .description("Connect this repository to a Kodingo project")
175
+ .option("--logout", "Log out and clear saved credentials")
109
176
  .action(async (opts) => {
110
- const rl = readline.createInterface({
111
- input: process.stdin,
112
- output: process.stdout,
113
- });
177
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
114
178
  try {
115
- const current = (0, persistence_config_1.readConfig)();
116
- // ── Non-interactive flags ─────────────────────────────────────────────
117
- if (opts.local) {
118
- (0, persistence_config_1.writeConfig)({ mode: "local" });
119
- console.log("✔ Switched to local mode — using local psql database.");
179
+ // ── Logout ──────────────────────────────────────────────────────────
180
+ if (opts.logout) {
181
+ if (fs.existsSync(AUTH_TOKEN_PATH))
182
+ fs.unlinkSync(AUTH_TOKEN_PATH);
183
+ console.log("✔ Logged out of Kodingo.");
120
184
  return;
121
185
  }
122
- if (opts.cloud && opts.apiUrl && opts.token) {
123
- console.log("Verifying connection to kodingo-api...");
124
- await verifyCloudConnection(opts.apiUrl, opts.token);
125
- (0, persistence_config_1.writeConfig)({
126
- mode: "cloud",
127
- apiUrl: opts.apiUrl,
128
- token: opts.token,
129
- });
130
- const repoRoot = findRepoRoot(process.cwd());
131
- ensureGitignoreEntry(repoRoot, ".kortex/");
132
- console.log(`✔ Cloud mode configured. Config saved to ${persistence_config_1.CONFIG_PATH}`);
133
- // Auto-scan existing codebase silently
134
- await runInitialScan(repoRoot);
135
- return;
186
+ // ── Check if already logged in ───────────────────────────────────────
187
+ let authToken = readAuthToken();
188
+ let email = readAuthEmail();
189
+ if (authToken) {
190
+ console.log(`\n✔ Already logged in as ${email || "your Kodingo account"}\n`);
136
191
  }
137
- // ── Interactive flow ──────────────────────────────────────────────────
138
- console.log("\nKodingo CLI Setup\n");
139
- console.log(`Current mode: ${current.mode}`);
140
- if (current.apiUrl)
141
- console.log(`Current API URL: ${current.apiUrl}`);
142
- console.log();
143
- const modeInput = await prompt(rl, "Choose mode (l)ocal or (c)loud [default: local]: ");
144
- const mode = modeInput.toLowerCase().startsWith("c")
145
- ? "cloud"
146
- : "local";
147
- if (mode === "local") {
148
- (0, persistence_config_1.writeConfig)({ mode: "local" });
149
- console.log("\n✔ Local mode configured. Using local psql database.");
150
- return;
192
+ else {
193
+ console.log("\nKodingo Connect this repository\n");
194
+ const result = await browserLogin();
195
+ authToken = result.token;
196
+ email = result.email;
197
+ saveAuthToken(authToken, email);
198
+ console.log(`\n✔ Logged in as ${email}\n`);
151
199
  }
152
- // Cloud mode collect API URL and token
153
- const defaultUrl = current.apiUrl ?? "";
154
- const apiUrlInput = await prompt(rl, `API URL${defaultUrl ? ` [${defaultUrl}]` : ""}: `);
155
- const apiUrl = apiUrlInput || defaultUrl;
156
- if (!apiUrl) {
157
- console.error("✖ API URL is required for cloud mode.");
200
+ // ── Fetch projects ───────────────────────────────────────────────────
201
+ const projects = await fetchProjects(authToken);
202
+ // ── Ask for project name ─────────────────────────────────────────────
203
+ const projectName = await prompt(rl, "Enter the project name to connect to this repo: ");
204
+ if (!projectName) {
205
+ console.error("✖ Project name is required.");
158
206
  process.exit(1);
159
207
  }
160
- const tokenInput = await prompt(rl, "API Token (X-Kodingo-Token): ");
161
- const token = tokenInput || current.token || "";
162
- if (!token) {
163
- console.error("✖ API token is required for cloud mode.");
164
- process.exit(1);
208
+ // ── Case-insensitive search ──────────────────────────────────────────
209
+ const normalised = projectName.toLowerCase().trim();
210
+ const matches = projects.filter(p => p.name.toLowerCase().trim() === normalised);
211
+ let selectedProject = null;
212
+ if (matches.length === 1) {
213
+ selectedProject = matches[0];
214
+ console.log(`\n✔ Found project: ${selectedProject.name} (${matches[0].org_name} · ${matches[0].plan} plan)\n`);
215
+ }
216
+ else if (matches.length > 1) {
217
+ // Multiple orgs with same project name — ask which one
218
+ console.log("\nMultiple projects found with that name:");
219
+ matches.forEach((p, i) => console.log(` ${i + 1}. ${p.name} — ${p.org_name}`));
220
+ const choice = await prompt(rl, "Enter number: ");
221
+ const idx = parseInt(choice) - 1;
222
+ if (idx < 0 || idx >= matches.length) {
223
+ console.error("✖ Invalid choice.");
224
+ process.exit(1);
225
+ }
226
+ selectedProject = matches[idx];
165
227
  }
166
- console.log("\nVerifying connection to kodingo-api...");
167
- await verifyCloudConnection(apiUrl, token);
168
- const config = { mode: "cloud", apiUrl, token };
169
- (0, persistence_config_1.writeConfig)(config);
170
- // Save workspace-specific config scoped to this repo
228
+ else {
229
+ // No match — offer to create
230
+ console.log(`\n No project named "${projectName}" found in your account.`);
231
+ const create = await prompt(rl, "Would you like to create it? (y/n): ");
232
+ if (!create.toLowerCase().startsWith("y")) {
233
+ console.log("✖ Cancelled.");
234
+ process.exit(0);
235
+ }
236
+ // If user has multiple orgs, ask which one
237
+ const orgs = [...new Map(projects.map(p => [p.org_id, { id: p.org_id, name: p.org_name }])).values()];
238
+ let orgId = orgs[0]?.id;
239
+ if (orgs.length > 1) {
240
+ console.log("\nSelect an organisation:");
241
+ orgs.forEach((o, i) => console.log(` ${i + 1}. ${o.name}`));
242
+ const orgChoice = await prompt(rl, "Enter number: ");
243
+ const orgIdx = parseInt(orgChoice) - 1;
244
+ if (orgIdx < 0 || orgIdx >= orgs.length) {
245
+ console.error("✖ Invalid choice.");
246
+ process.exit(1);
247
+ }
248
+ orgId = orgs[orgIdx].id;
249
+ }
250
+ const created = await createProject(authToken, projectName, orgId);
251
+ selectedProject = created;
252
+ console.log(`\n✔ Project "${created.name}" created\n`);
253
+ }
254
+ // ── Save config ──────────────────────────────────────────────────────
171
255
  const repoRoot = findRepoRoot(process.cwd());
172
- (0, persistence_config_1.writeWorkspaceConfig)(repoRoot, { mode: "cloud", apiUrl, token });
173
- // Ensure .kortex/ is gitignored it's a generated file, not source
256
+ const apiUrl = API_BASE;
257
+ (0, persistence_config_1.writeConfig)({ mode: "cloud", apiUrl, token: selectedProject.token });
258
+ (0, persistence_config_1.writeWorkspaceConfig)(repoRoot, { mode: "cloud", apiUrl, token: selectedProject.token });
174
259
  ensureGitignoreEntry(repoRoot, ".kortex/");
175
- console.log(`\n✔ Cloud mode configured successfully.`);
176
- console.log(` API URL : ${apiUrl}`);
177
- console.log(` Config : ${persistence_config_1.CONFIG_PATH}`);
178
- console.log("\nYou're ready to use kodingo with the cloud API.");
179
- // Auto-scan existing codebase for functions without memory
180
- await runInitialScan(repoRoot);
260
+ console.log(`✔ Connected. Token saved to ${persistence_config_1.CONFIG_PATH}`);
261
+ console.log("\nKortex is now watching this repo.");
262
+ console.log("Run `kodingo install-hook` to capture memories on every commit.\n");
263
+ // Auto-scan existing codebase
264
+ try {
265
+ const { scanRepoCommand } = await Promise.resolve().then(() => __importStar(require("./scan-repo")));
266
+ console.log("🔍 Scanning existing codebase for functions without memory...");
267
+ const captured = await scanRepoCommand({ repo: repoRoot, silent: false });
268
+ if (captured === 0) {
269
+ console.log(" No existing functions found — Kortex will capture them as you write and commit code.");
270
+ }
271
+ }
272
+ catch { }
181
273
  }
182
274
  catch (err) {
183
- console.error(`\n✖ Init failed: ${err.message}`);
275
+ console.error(`\n✖ ${err.message}`);
184
276
  process.exit(1);
185
277
  }
186
278
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodingo-cli",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Kodingo CLI",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -24,12 +24,13 @@
24
24
  "@babel/traverse": "^7.29.0",
25
25
  "commander": "^12.1.0",
26
26
  "kodingo-core": "^0.1.0",
27
+ "open": "^11.0.0",
27
28
  "simple-git": "^3.30.0",
28
29
  "uuid": "^10.0.0"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@types/babel__traverse": "^7.28.0",
32
- "@types/node": "^22.0.0",
33
+ "@types/node": "^22.19.20",
33
34
  "@types/uuid": "^10.0.0",
34
35
  "typescript": "^5.6.0"
35
36
  }