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.
- package/dist/commands/init.js +193 -101
- package/package.json +3 -2
package/dist/commands/init.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* kodingo init
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
50
|
-
const
|
|
51
|
-
const
|
|
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 (
|
|
93
|
+
if (fs.existsSync(path.join(current, ".git")))
|
|
56
94
|
return current;
|
|
57
|
-
const parent =
|
|
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 =
|
|
103
|
+
const gitignorePath = path.join(repoRoot, ".gitignore");
|
|
77
104
|
const line = entry.endsWith("\n") ? entry : `${entry}\n`;
|
|
78
|
-
if (
|
|
79
|
-
const contents =
|
|
80
|
-
if (contents.split("\n").some(
|
|
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
|
-
|
|
109
|
+
fs.appendFileSync(gitignorePath, `\n# Kortex\n${line}`, "utf-8");
|
|
83
110
|
}
|
|
84
111
|
else {
|
|
85
|
-
|
|
112
|
+
fs.writeFileSync(gitignorePath, `# Kortex\n${line}`, "utf-8");
|
|
86
113
|
}
|
|
87
114
|
}
|
|
88
|
-
// ──
|
|
89
|
-
async function
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
105
|
-
.option("--
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
console.log("✔
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
//
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
if (!
|
|
157
|
-
console.error("✖
|
|
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
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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(
|
|
176
|
-
console.log(
|
|
177
|
-
console.log(`
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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✖
|
|
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.
|
|
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.
|
|
33
|
+
"@types/node": "^22.19.20",
|
|
33
34
|
"@types/uuid": "^10.0.0",
|
|
34
35
|
"typescript": "^5.6.0"
|
|
35
36
|
}
|