kodingo-cli 1.0.13 → 1.0.15

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 +192 -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,231 @@ 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(rl) {
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
+ rl.question("", () => resolve());
126
+ });
127
+ // Open browser
90
128
  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
- }
129
+ await (0, open_1.default)(loginUrl);
96
130
  }
97
- catch {
98
- // Never fail init because of a scan error
131
+ catch { }
132
+ console.log("Waiting for login...");
133
+ // Poll for completion
134
+ const start = Date.now();
135
+ while (Date.now() - start < 5 * 60 * 1000) {
136
+ await new Promise(r => setTimeout(r, 2000));
137
+ const pollRes = await fetch(`${API_BASE}/cli/auth/poll/${state}`);
138
+ if (!pollRes.ok)
139
+ continue;
140
+ const data = await pollRes.json();
141
+ if (data.status === "complete" && data.token) {
142
+ return { token: data.token, email: data.email ?? "" };
143
+ }
99
144
  }
145
+ throw new Error("Login timed out. Please try again.");
100
146
  }
147
+ // ── Fetch accessible projects ─────────────────────────────────────────────────
148
+ async function fetchProjects(authToken) {
149
+ const res = await fetch(`${API_BASE}/cli/projects`, {
150
+ headers: { Authorization: `Bearer ${authToken}` },
151
+ });
152
+ if (!res.ok)
153
+ throw new Error("Failed to fetch projects. Please run `kodingo init` again.");
154
+ const data = await res.json();
155
+ return data.projects ?? [];
156
+ }
157
+ // ── Create a new project ──────────────────────────────────────────────────────
158
+ async function createProject(authToken, name, orgId) {
159
+ const res = await fetch(`${API_BASE}/cli/projects`, {
160
+ method: "POST",
161
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}` },
162
+ body: JSON.stringify({ name, orgId }),
163
+ });
164
+ const data = await res.json();
165
+ if (!res.ok)
166
+ throw new Error(data.error ?? "Failed to create project");
167
+ return data;
168
+ }
169
+ // ── Command ───────────────────────────────────────────────────────────────────
101
170
  function registerInitCommand(program) {
102
171
  program
103
172
  .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)")
173
+ .description("Connect this repository to a Kodingo project")
174
+ .option("--logout", "Log out and clear saved credentials")
109
175
  .action(async (opts) => {
110
- const rl = readline.createInterface({
111
- input: process.stdin,
112
- output: process.stdout,
113
- });
176
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
114
177
  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.");
178
+ // ── Logout ──────────────────────────────────────────────────────────
179
+ if (opts.logout) {
180
+ if (fs.existsSync(AUTH_TOKEN_PATH))
181
+ fs.unlinkSync(AUTH_TOKEN_PATH);
182
+ console.log("✔ Logged out of Kodingo.");
120
183
  return;
121
184
  }
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;
185
+ // ── Check if already logged in ───────────────────────────────────────
186
+ let authToken = readAuthToken();
187
+ let email = readAuthEmail();
188
+ if (authToken) {
189
+ console.log(`\n✔ Already logged in as ${email || "your Kodingo account"}\n`);
136
190
  }
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;
191
+ else {
192
+ console.log("\nKodingo Connect this repository\n");
193
+ const result = await browserLogin(rl);
194
+ authToken = result.token;
195
+ email = result.email;
196
+ saveAuthToken(authToken, email);
197
+ console.log(`\n✔ Logged in as ${email}\n`);
151
198
  }
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.");
199
+ // ── Fetch projects ───────────────────────────────────────────────────
200
+ const projects = await fetchProjects(authToken);
201
+ // ── Ask for project name ─────────────────────────────────────────────
202
+ const projectName = await prompt(rl, "Enter the project name to connect to this repo: ");
203
+ if (!projectName) {
204
+ console.error("✖ Project name is required.");
158
205
  process.exit(1);
159
206
  }
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);
207
+ // ── Case-insensitive search ──────────────────────────────────────────
208
+ const normalised = projectName.toLowerCase().trim();
209
+ const matches = projects.filter(p => p.name.toLowerCase().trim() === normalised);
210
+ let selectedProject = null;
211
+ if (matches.length === 1) {
212
+ selectedProject = matches[0];
213
+ console.log(`\n✔ Found project: ${selectedProject.name} (${matches[0].org_name} · ${matches[0].plan} plan)\n`);
214
+ }
215
+ else if (matches.length > 1) {
216
+ // Multiple orgs with same project name — ask which one
217
+ console.log("\nMultiple projects found with that name:");
218
+ matches.forEach((p, i) => console.log(` ${i + 1}. ${p.name} — ${p.org_name}`));
219
+ const choice = await prompt(rl, "Enter number: ");
220
+ const idx = parseInt(choice) - 1;
221
+ if (idx < 0 || idx >= matches.length) {
222
+ console.error("✖ Invalid choice.");
223
+ process.exit(1);
224
+ }
225
+ selectedProject = matches[idx];
165
226
  }
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
227
+ else {
228
+ // No match — offer to create
229
+ console.log(`\n No project named "${projectName}" found in your account.`);
230
+ const create = await prompt(rl, "Would you like to create it? (y/n): ");
231
+ if (!create.toLowerCase().startsWith("y")) {
232
+ console.log("✖ Cancelled.");
233
+ process.exit(0);
234
+ }
235
+ // If user has multiple orgs, ask which one
236
+ const orgs = [...new Map(projects.map(p => [p.org_id, { id: p.org_id, name: p.org_name }])).values()];
237
+ let orgId = orgs[0]?.id;
238
+ if (orgs.length > 1) {
239
+ console.log("\nSelect an organisation:");
240
+ orgs.forEach((o, i) => console.log(` ${i + 1}. ${o.name}`));
241
+ const orgChoice = await prompt(rl, "Enter number: ");
242
+ const orgIdx = parseInt(orgChoice) - 1;
243
+ if (orgIdx < 0 || orgIdx >= orgs.length) {
244
+ console.error("✖ Invalid choice.");
245
+ process.exit(1);
246
+ }
247
+ orgId = orgs[orgIdx].id;
248
+ }
249
+ const created = await createProject(authToken, projectName, orgId);
250
+ selectedProject = created;
251
+ console.log(`\n✔ Project "${created.name}" created\n`);
252
+ }
253
+ // ── Save config ──────────────────────────────────────────────────────
171
254
  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
255
+ const apiUrl = API_BASE;
256
+ (0, persistence_config_1.writeConfig)({ mode: "cloud", apiUrl, token: selectedProject.token });
257
+ (0, persistence_config_1.writeWorkspaceConfig)(repoRoot, { mode: "cloud", apiUrl, token: selectedProject.token });
174
258
  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);
259
+ console.log(`✔ Connected. Token saved to ${persistence_config_1.CONFIG_PATH}`);
260
+ console.log("\nKortex is now watching this repo.");
261
+ console.log("Run `kodingo install-hook` to capture memories on every commit.\n");
262
+ // Auto-scan existing codebase
263
+ try {
264
+ const { scanRepoCommand } = await Promise.resolve().then(() => __importStar(require("./scan-repo")));
265
+ console.log("🔍 Scanning existing codebase for functions without memory...");
266
+ const captured = await scanRepoCommand({ repo: repoRoot, silent: false });
267
+ if (captured === 0) {
268
+ console.log(" No existing functions found — Kortex will capture them as you write and commit code.");
269
+ }
270
+ }
271
+ catch { }
181
272
  }
182
273
  catch (err) {
183
- console.error(`\n✖ Init failed: ${err.message}`);
274
+ console.error(`\n✖ ${err.message}`);
184
275
  process.exit(1);
185
276
  }
186
277
  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.15",
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
  }