@kendoo.agentdesk/agentdesk 0.19.7 → 0.20.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.
package/cli/init.mjs CHANGED
@@ -1,174 +1,49 @@
1
- // `agentdesk init` — interactive project setup with tracker configuration.
1
+ // `agentdesk init` — unified setup entry point.
2
2
  //
3
- // Run modes:
4
- // agentdesk init full wizard with inline guidance
5
- // agentdesk init --quick → skip narrative copy for power users / re-runs
6
- // (automatic) re-run → when .agentdesk.json already exists, offer
7
- // to edit only a specific section
3
+ // Three possible outcomes, decided automatically:
4
+ // 1. Modern .agentdesk.json with projectKey "Refresh tokens" is the
5
+ // default option in a small menu; picking it runs gapScan.
6
+ // 2. Legacy .agentdesk.json without projectKey → try to match an account
7
+ // project by github.repo / git remote / tracker team → migrate the
8
+ // file forward and gapScan.
9
+ // 3. No local config → auto-match by git remote. On match, the user
10
+ // confirms "use this project?"; on no match the user picks from
11
+ // their participating projects, or opts into the full new-project
12
+ // wizard.
13
+ //
14
+ // The full wizard is 10 tiny steps (git detect, GitHub repo+token,
15
+ // optional clone, tracker type/location/creds, signature, verify+pick,
16
+ // review+save). Each step does one thing so re-entry after Ctrl-C is sane.
8
17
 
9
18
  import { existsSync, readFileSync, writeFileSync } from "fs";
10
19
  import { join } from "path";
11
- import { createInterface } from "readline";
12
20
  import { execSync } from "child_process";
13
- import { detectProject } from "./detect.mjs";
14
21
  import { loadConfig, pushConfig } from "./config.mjs";
15
22
  import { getStoredApiKey } from "./login.mjs";
16
23
  import { registerLocalProject } from "./projects.mjs";
17
- import { checkTrackerPermissions, resolveCredentialsFromEnv } from "./tracker-check.mjs";
18
- import { autoMatchProject, gapScan, writeProjectConfig } from "./bootstrap.mjs";
19
- import { select as promptSelect } from "./prompts.mjs";
24
+ import { checkTrackerPermissions } from "./tracker-check.mjs";
25
+ import { autoMatchProject, discoverProject, gapScan } from "./bootstrap.mjs";
26
+ import { select as promptSelect, ask, promptRequired } from "./prompts.mjs";
27
+ import {
28
+ cloneRepoWithToken,
29
+ detectGitRepo,
30
+ fetchGitHubLogin,
31
+ loadDotEnv,
32
+ parseGitHubRef,
33
+ printAccessChecks,
34
+ readLocalConfig,
35
+ runAccessChecks,
36
+ saveEnvVar,
37
+ writeMinimalLocalConfig,
38
+ } from "./setup-helpers.mjs";
20
39
 
21
40
  const SERVER = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
22
41
 
23
- // Pretty dividers and tracker-specific "dedicated user" guidance.
24
- const TRACKER_GUIDANCE = {
25
- linear: {
26
- name: "Linear",
27
- dedicatedSteps: [
28
- "1. Open https://linear.app/<your-workspace>/settings/members",
29
- "2. Invite a new member named \"AgentDesk\" to an email you control",
30
- "3. Accept the invite from that email, log in as the new user",
31
- "4. Go to https://linear.app/settings/api → Create key → label it \"AgentDesk\"",
32
- "5. Paste the key when prompted below",
33
- ],
34
- personalNote: "Your own Linear account. Comments on tickets will appear under your name.",
35
- },
36
- jira: {
37
- name: "Jira",
38
- dedicatedSteps: [
39
- "1. Open https://admin.atlassian.com/ → Directory → Invite users",
40
- "2. Invite an email you control (e.g. agentdesk@yourdomain.com) as \"AgentDesk\"",
41
- "3. Accept the invite and sign in as the new user",
42
- "4. Go to https://id.atlassian.com/manage-profile/security/api-tokens → Create API token",
43
- "5. Paste the email + token when prompted below",
44
- ],
45
- personalNote: "Your own Jira account. Comments on tickets will appear under your name.",
46
- },
47
- github: {
48
- name: "GitHub",
49
- dedicatedSteps: [
50
- "GitHub enforces one account per human, so a \"dedicated bot user\" must be",
51
- "a GitHub App (first-class bot identity, shows as agentdesk[bot] on comments).",
52
- "That's a deeper install — not available yet in this wizard. For now:",
53
- " • Option A: Use gh CLI authenticated as yourself (quickest to get started)",
54
- " • Option B: Generate a Personal Access Token scoped to the repo",
55
- ],
56
- personalNote: "Your own GitHub account via gh CLI or a PAT. Comments appear under your name.",
57
- },
58
- };
59
-
60
- function printDedicatedUserChoice(tracker, quick) {
61
- if (quick || !tracker) return;
62
- const g = TRACKER_GUIDANCE[tracker];
63
- if (!g) return;
64
- console.log("");
65
- console.log(` ${g.name} account — who will post the comments?`);
66
- console.log(" ─────────────────────────────────────────────");
67
- console.log("");
68
- console.log(" Recommended: Dedicated \"AgentDesk\" user");
69
- console.log(" Clean audit trail, survives team changes, clear attribution.");
70
- for (const line of g.dedicatedSteps) console.log(` ${line}`);
71
- console.log("");
72
- console.log(" Quick start: Your own account");
73
- console.log(` ${g.personalNote}`);
74
- console.log("");
75
- console.log(" (This wizard doesn't care which you pick — it only asks for the");
76
- console.log(" credential. You're deciding whose credential to paste.)");
77
- console.log("");
78
- }
79
-
80
- function printIdentityEcho(identity) {
81
- if (!identity) return;
82
- const label = identity.name || identity.login || identity.email || "(unknown)";
83
- const extra = identity.email && identity.email !== identity.name ? ` <${identity.email}>` : "";
84
- console.log(` ✓ Posting as: ${label}${extra}`);
85
- const looksLikePerson =
86
- identity.name && !/agent\s*desk|agentdesk|bot|service/i.test(identity.name) &&
87
- !/(^|[.+])(agentdesk|bot|svc|service)(@|$|[.+])/i.test(identity.email || "");
88
- if (looksLikePerson) {
89
- console.log(" (Looks like a personal account — comments will be attributed to this user.");
90
- console.log(" If you'd rather have a dedicated AgentDesk identity, rotate the credential");
91
- console.log(" to one generated under a dedicated tracker user and re-run `agentdesk init`.)");
92
- }
93
- }
94
-
95
- function loadApiKey(dir) {
96
- const envPath = join(dir, ".env");
97
- if (!existsSync(envPath)) return getStoredApiKey();
98
- const match = readFileSync(envPath, "utf-8").match(/AGENTDESK_API_KEY=(.+)/);
99
- return match?.[1]?.trim() || getStoredApiKey();
100
- }
101
-
102
- function ask(rl, question) {
103
- return new Promise(resolve => rl.question(question, resolve));
104
- }
105
-
106
- function saveEnvVar(dir, key, value) {
107
- const envPath = join(dir, ".env");
108
- let content = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
109
- const re = new RegExp(`^${key}=.*$`, "m");
110
- if (re.test(content)) {
111
- content = content.replace(re, `${key}=${value}`);
112
- } else {
113
- content += `${content && !content.endsWith("\n") ? "\n" : ""}${key}=${value}\n`;
114
- }
115
- writeFileSync(envPath, content);
116
- }
117
-
118
- function loadDotEnv(dir) {
119
- const envPath = join(dir, ".env");
120
- const dotEnv = {};
121
- if (existsSync(envPath)) {
122
- for (const line of readFileSync(envPath, "utf-8").split("\n")) {
123
- const trimmed = line.trim();
124
- if (!trimmed || trimmed.startsWith("#")) continue;
125
- const eq = trimmed.indexOf("=");
126
- if (eq !== -1) dotEnv[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
127
- }
128
- }
129
- return dotEnv;
130
- }
42
+ // ---------- Tracker team/project picker helpers ----------
131
43
 
132
- async function fetchServerCreds(apiKey, projectKey) {
133
- if (!apiKey) return {};
134
- try {
135
- const res = await fetch(`${SERVER}/api/projects/${projectKey}/settings/credentials`, {
136
- headers: { "x-api-key": apiKey },
137
- signal: AbortSignal.timeout(5000),
138
- });
139
- if (res.ok) return await res.json();
140
- } catch {}
141
- return {};
142
- }
143
-
144
- // Render a numbered-step box of instructions with exact URLs and button names.
145
- function printSteps(title, steps) {
146
- const pad = " ";
147
- console.log(`${pad}${title}`);
148
- console.log(`${pad}${"─".repeat(title.length)}`);
149
- for (let i = 0; i < steps.length; i++) {
150
- console.log(`${pad} ${i + 1}. ${steps[i]}`);
151
- }
152
- console.log("");
153
- }
154
-
155
- // Try to detect the current git remote (origin) and parse owner/repo from
156
- // known GitHub URL forms. Returns "owner/repo" or null. Used as a default
157
- // suggestion in the GitHub step.
158
- function detectGitRemote(dir) {
159
- try {
160
- const url = execSync("git remote get-url origin", { cwd: dir, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
161
- let m = url.match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/);
162
- if (m) return `${m[1]}/${m[2]}`;
163
- } catch {}
164
- return null;
165
- }
166
-
167
- // Fetch the set of "things to pick from" per tracker once the auth has been
168
- // verified. For Linear this is teams; Jira is projects; GitHub Issues is
169
- // repos (filtered to ones the token can access). Returns an array of
170
- // { id, name, raw } or null if listing failed — caller falls back to a
171
- // manual prompt.
44
+ // Fetch the pickable list of teams/projects after tracker creds are known.
45
+ // Returns [{ id, name }] or null when the API couldn't answer (caller
46
+ // falls back to a manual key prompt).
172
47
  async function listTrackerProjects({ tracker, creds, location }) {
173
48
  if (tracker === "linear") {
174
49
  if (!creds.LINEAR_API_KEY) return null;
@@ -181,12 +56,11 @@ async function listTrackerProjects({ tracker, creds, location }) {
181
56
  });
182
57
  if (!res.ok) return null;
183
58
  const data = await res.json();
184
- const nodes = data?.data?.teams?.nodes || [];
185
- return nodes.map(t => ({ id: t.key, name: `${t.name} (${t.key})`, raw: t }));
59
+ return (data?.data?.teams?.nodes || []).map(t => ({ id: t.key, name: `${t.name} (${t.key})` }));
186
60
  } catch { return null; }
187
61
  }
188
- if (tracker === "jira") {
189
- if (!creds.JIRA_EMAIL || !creds.JIRA_API_TOKEN || !location) return null;
62
+ if (tracker === "jira" && location) {
63
+ if (!creds.JIRA_EMAIL || !creds.JIRA_API_TOKEN) return null;
190
64
  try {
191
65
  const auth = "Basic " + Buffer.from(`${creds.JIRA_EMAIL}:${creds.JIRA_API_TOKEN}`).toString("base64");
192
66
  const res = await fetch(`${location.replace(/\/+$/, "")}/rest/api/3/project/search?maxResults=50`, {
@@ -195,168 +69,186 @@ async function listTrackerProjects({ tracker, creds, location }) {
195
69
  });
196
70
  if (!res.ok) return null;
197
71
  const data = await res.json();
198
- const values = data?.values || [];
199
- return values.map(p => ({ id: p.key, name: `${p.name} (${p.key})`, raw: p }));
72
+ return (data?.values || []).map(p => ({ id: p.key, name: `${p.name} (${p.key})` }));
200
73
  } catch { return null; }
201
74
  }
202
- if (tracker === "github") {
203
- // GitHub has potentially thousands of repos; don't try to list — rely on
204
- // the explicit repo prompt (which has a git-remote default anyway).
205
- return null;
206
- }
207
75
  return null;
208
76
  }
209
77
 
210
- async function pickFromList(_rl, prompt, items, { allowManual = true } = {}) {
211
- const choices = items.map(it => ({ name: it.name, value: it.id }));
212
- if (allowManual) choices.push({ name: "Type it manually instead", value: "__manual__" });
213
- const value = await promptSelect({ message: prompt, choices });
214
- if (value === "__manual__") return null;
215
- return value;
216
- }
78
+ // ---------- Legacy migration: auto-match a config without projectKey ----------
217
79
 
218
- // Prompt for a value, showing instructions first, looping until non-empty.
219
- async function promptRequired(rl, label, instructions) {
220
- if (instructions) printSteps(instructions.title, instructions.steps);
221
- while (true) {
222
- const v = (await ask(rl, ` ${label}: `)).trim();
223
- if (v) return v;
224
- console.log(` ${label} is required. Ctrl+C to abort.`);
80
+ async function autoMatchLegacy(cwd, apiKey, localConfig) {
81
+ // Fetch the user's projects once; filter on server-known fields.
82
+ let projects;
83
+ try {
84
+ const res = await fetch(`${SERVER}/api/projects`, {
85
+ headers: { "x-api-key": apiKey }, signal: AbortSignal.timeout(8000),
86
+ });
87
+ if (!res.ok) return null;
88
+ projects = await res.json();
89
+ } catch { return null; }
90
+ if (!projects?.length) return null;
91
+
92
+ const settingsList = await Promise.all(projects.map(async p => {
93
+ try {
94
+ const r = await fetch(`${SERVER}/api/projects/${p.id}/settings`, {
95
+ headers: { "x-api-key": apiKey }, signal: AbortSignal.timeout(5000),
96
+ });
97
+ return r.ok ? await r.json() : {};
98
+ } catch { return {}; }
99
+ }));
100
+
101
+ const legacyRepo = (localConfig.github?.repo || "").toLowerCase();
102
+ const legacyTeamKey = (localConfig.linear?.teamKey || "").toLowerCase();
103
+ const legacyJiraProject = (localConfig.jira?.project || "").toLowerCase();
104
+
105
+ for (let i = 0; i < projects.length; i++) {
106
+ const p = projects[i];
107
+ const s = settingsList[i];
108
+ const serverRepo = (s?.github?.repo || "").toLowerCase();
109
+ const serverTeam = (s?.linear?.teamKey || "").toLowerCase();
110
+ const serverJira = (s?.jira?.project || "").toLowerCase();
111
+
112
+ if (legacyRepo && serverRepo === legacyRepo) {
113
+ return { projectKey: p.id, name: p.name, reason: `github.repo matches ${s.github.repo}` };
114
+ }
115
+ if (legacyTeamKey && serverTeam === legacyTeamKey) {
116
+ return { projectKey: p.id, name: p.name, reason: `Linear team matches ${s.linear.teamKey}` };
117
+ }
118
+ if (legacyJiraProject && serverJira === legacyJiraProject) {
119
+ return { projectKey: p.id, name: p.name, reason: `Jira project matches ${s.jira.project}` };
120
+ }
225
121
  }
122
+ return null;
226
123
  }
227
124
 
228
- // Verify tracker auth with what we have in .env + server, then fetch
229
- // available teams/projects and let the user pick from a list.
230
- async function verifyAndPickTrackerProject({ rl, cwd, finalProjectKey, tracker, config, location }) {
231
- console.log(" Verifying tracker access...");
232
- const dotEnv = loadDotEnv(cwd);
233
- const apiKey = loadApiKey(cwd);
234
- const serverCreds = await fetchServerCreds(apiKey, finalProjectKey);
235
- // .env wins over server credentials — fresh local rotation beats stale server state.
236
- const credentials = resolveCredentialsFromEnv({ ...serverCreds, ...dotEnv });
237
- const check = await checkTrackerPermissions({ tracker, config, credentials });
238
- if (!check.ok) {
239
- console.log(" ⚠ " + (check.errors[0] || "Verification failed"));
240
- for (const e of check.errors.slice(1)) console.log(` • ${e}`);
241
- console.log("");
242
- return { ok: false };
243
- }
244
- console.log(" ✓ Tracker access verified");
245
- printIdentityEcho(check.identity);
246
- console.log("");
125
+ // ---------- Existing-project flow (E3–E7) ----------
247
126
 
248
- // GitHub Issues — project = repo, already collected above. Skip the pick.
249
- if (tracker === "github") return { ok: true, trackerProjectId: null };
127
+ async function runExistingFlow(cwd, apiKey, projectKey) {
128
+ let currentCwd = cwd;
250
129
 
251
- // Fetch selectable items (Linear teams / Jira projects) and let user pick.
252
- const items = await listTrackerProjects({ tracker, creds: credentials, location });
253
- if (!items || items.length === 0) {
254
- console.log(" Couldn't list projects automatically — type the key manually.");
255
- const label = tracker === "linear" ? "Linear team key (e.g. KEN)" : "Jira project key (e.g. PROJ)";
256
- const manual = await promptRequired(rl, label);
257
- console.log("");
258
- return { ok: true, trackerProjectId: manual };
259
- }
260
130
  console.log("");
261
- const picked = await pickFromList(rl, `Pick the ${tracker === "linear" ? "team" : "project"} this agentdesk project maps to:`, items);
131
+ console.log(" AgentDesk connecting to existing project");
132
+ console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
262
133
  console.log("");
263
- if (picked) return { ok: true, trackerProjectId: picked };
264
- const label = tracker === "linear" ? "Linear team key" : "Jira project key";
265
- const manual = await promptRequired(rl, label);
134
+
135
+ // Fetch server config early so we know the project's github.repo and tracker.
136
+ const config = await loadConfig(currentCwd, { apiKey, serverUrl: SERVER, projectName: projectKey, silent: true });
137
+ console.log(` Project: ${projectKey}`);
138
+ if (config.tracker) console.log(` Tracker: ${config.tracker}`);
139
+ if (config.github?.repo) console.log(` Repo: ${config.github.repo}`);
266
140
  console.log("");
267
- return { ok: true, trackerProjectId: manual };
268
- }
269
141
 
270
- export async function runInit(cwd, opts = {}) {
271
- const quick = !!opts.quick;
272
- const forceFull = !!opts.forceFull;
273
- const project = detectProject(cwd);
274
- const existingConfig = loadConfig(cwd);
275
- const projectId = project.name || cwd.split("/").pop();
276
- const configPath = join(cwd, ".agentdesk.json");
277
- const hasConfig = existsSync(configPath);
278
- let editScope = "full";
279
-
280
- // Fast path A: `.agentdesk.json` already has a projectKey. Offer token
281
- // refresh (bootstrap) as the default action so second-machine users — or
282
- // anyone just rotating a token — don't have to walk the whole wizard.
283
- if (hasConfig && !quick && !forceFull) {
284
- let localKey = null;
285
- try { localKey = JSON.parse(readFileSync(configPath, "utf-8"))?.projectKey || null; } catch {}
286
- if (localKey) {
287
- console.log("");
288
- console.log(" AgentDesk — existing project detected");
289
- console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
290
- console.log("");
291
- const choice = await promptSelect({
292
- message: "What would you like to do?",
293
- default: "refresh",
294
- choices: [
295
- { name: "Refresh tokens (update .env only — recommended)", value: "refresh" },
296
- { name: "Tracker only (change tracker or credentials)", value: "tracker" },
297
- { name: "Full setup (walk the whole wizard)", value: "full" },
298
- { name: "Cancel", value: "cancel" },
299
- ],
300
- });
301
- console.log("");
302
- if (choice === "cancel") return;
303
- if (choice === "refresh") {
304
- const apiKey = getStoredApiKey();
305
- await gapScan({ cwd, apiKey, projectKey: localKey });
306
- return;
307
- }
308
- editScope = choice; // "full" or "tracker"
309
- }
142
+ // --- E3: GitHub token ---
143
+ let env = loadDotEnv(currentCwd);
144
+ let githubToken = env.GITHUB_TOKEN;
145
+ if (!githubToken) {
146
+ console.log(" GitHub token (needed for clone + agent pushes — https://github.com/settings/tokens, scope: repo)");
147
+ githubToken = await promptRequired("GitHub token");
148
+ saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
149
+ console.log(" Saved GITHUB_TOKEN to .env");
310
150
  }
151
+ const githubLogin = await fetchGitHubLogin(githubToken);
152
+ if (githubLogin) console.log(` ✓ Token authenticates as @${githubLogin}`);
153
+ else console.log(" ⚠ Token couldn't be verified against the GitHub API (continuing anyway)");
154
+ console.log("");
311
155
 
312
- // Fast path B: no local config, but the user's account has a project that
313
- // silently matches this clone (git remote → project id/name). Switch to
314
- // bootstrap mode without opening the wizard.
315
- if (!hasConfig && !forceFull) {
316
- const apiKey = getStoredApiKey();
317
- if (apiKey) {
318
- const match = await autoMatchProject(cwd, apiKey);
319
- if (match) {
320
- console.log("");
321
- console.log(` ✓ Detected project "${match.name}" on your account — ${match.reason}`);
322
- console.log(" Switching to token-refresh mode. Use `agentdesk init --force-full` to walk the wizard instead.");
323
- console.log("");
324
- writeProjectConfig(cwd, match.projectKey);
325
- await gapScan({ cwd, apiKey, projectKey: match.projectKey });
156
+ // --- E4: Ensure correct repo is locally present ---
157
+ const serverRepo = config.github?.repo ? parseGitHubRef(config.github.repo) : null;
158
+ if (serverRepo) {
159
+ const here = detectGitRepo(currentCwd);
160
+ const expected = `${serverRepo.owner}/${serverRepo.repo}`.toLowerCase();
161
+ const haveHere = (here.ownerRepo || "").toLowerCase();
162
+ if (!here.inRepo || haveHere !== expected) {
163
+ if (here.inRepo && haveHere && haveHere !== expected) {
164
+ console.log(` Current directory holds ${haveHere} — cloning ${expected} into ./${serverRepo.repo} instead.`);
165
+ } else {
166
+ console.log(` Cloning ${expected} into ./${serverRepo.repo}...`);
167
+ }
168
+ try {
169
+ currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner: serverRepo.owner, repo: serverRepo.repo, token: githubToken });
170
+ } catch (err) {
171
+ console.log(` ✗ ${err.message}`);
326
172
  return;
327
173
  }
174
+ // Re-save the token into the cloned working tree's .env (previous
175
+ // save was into the parent dir).
176
+ saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
177
+ console.log("");
328
178
  }
329
179
  }
330
180
 
331
- const rl = createInterface({ input: process.stdin, output: process.stdout });
181
+ // Ensure .agentdesk.json in currentCwd carries the projectKey so loadConfig
182
+ // works on subsequent runs without needing discovery.
183
+ writeMinimalLocalConfig(currentCwd, projectKey);
332
184
 
185
+ // --- E6: Fill missing tracker creds + verify (gapScan handles both) ---
186
+ await gapScan({ cwd: currentCwd, apiKey, projectKey });
187
+
188
+ // --- E7: Final access-checks summary ---
189
+ env = loadDotEnv(currentCwd);
190
+ const results = await runAccessChecks({ apiKey, serverUrl: SERVER, cwd: currentCwd, config, creds: env });
191
+ printAccessChecks(results, { tracker: config.tracker });
333
192
  console.log("");
334
- console.log(" AgentDesk — Project Setup");
335
- console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━");
193
+ }
194
+
195
+ // ---------- New-project wizard (steps 1–10) ----------
196
+
197
+ async function runNewProjectWizard(cwd, apiKey) {
198
+ let currentCwd = cwd;
199
+
336
200
  console.log("");
337
- console.log(` Project: ${project.name || "unknown"}`);
338
- console.log(` Type: ${project.label}`);
339
- console.log(` Directory: ${project.dir}`);
340
- console.log(` Git: ${project.hasGit ? "yes" : "no"}`);
341
- console.log(` CLAUDE.md: ${project.hasClaudeMd ? "yes" : "no (recommended)"}`);
201
+ console.log(" AgentDesk — new project setup");
202
+ console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
342
203
  console.log("");
343
204
 
344
- if (project.testCommand) console.log(` Test: ${project.testCommand}`);
345
- if (project.buildCommand) console.log(` Build: ${project.buildCommand}`);
346
- if (project.lintCommand) console.log(` Lint: ${project.lintCommand}`);
347
- if (project.testCommand || project.buildCommand || project.lintCommand) console.log("");
348
-
349
- // --- Tracker step: platform → location → auth → verify → pick project ---
350
- const config = {};
351
- // Initial default for the agentdesk project key. Gets overwritten with the
352
- // tracker's project/team key if we learn one during verification, and the
353
- // user can override at the final review screen.
354
- let finalProjectKey = existingConfig.projectKey || projectId;
205
+ // --- Step 1: Detect git repo ---
206
+ const git = detectGitRepo(currentCwd);
207
+ let ownerRepo = git.ownerRepo;
208
+
209
+ // --- Step 2: Ask for the GitHub repo (only if no git repo) ---
210
+ if (!git.inRepo) {
211
+ console.log(" No git repo here yet.\n");
212
+ while (!ownerRepo) {
213
+ const input = await promptRequired("GitHub repo (owner/repo or URL)");
214
+ const parsed = parseGitHubRef(input);
215
+ if (parsed) { ownerRepo = `${parsed.owner}/${parsed.repo}`; }
216
+ else console.log(" Not a valid GitHub repo. Only github.com is supported right now.");
217
+ }
218
+ console.log("");
219
+ }
355
220
 
356
- if (!quick) console.log(editScope === "tracker" ? " Tracker" : " Step 1 of 3 Task tracker");
221
+ // --- Step 3: GitHub token ---
222
+ let env = loadDotEnv(currentCwd);
223
+ let githubToken = env.GITHUB_TOKEN;
224
+ if (!githubToken) {
225
+ console.log(" GitHub token (create one at https://github.com/settings/tokens — scope: repo)");
226
+ githubToken = await promptRequired("GitHub token");
227
+ saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
228
+ console.log(" ✓ Saved GITHUB_TOKEN to .env");
229
+ }
230
+ const githubLogin = await fetchGitHubLogin(githubToken);
231
+ if (githubLogin) console.log(` ✓ Token authenticates as @${githubLogin}`);
232
+ else console.log(" ⚠ Token couldn't be verified against the GitHub API (continuing anyway)");
357
233
  console.log("");
234
+
235
+ // --- Step 4: Clone (only if no git repo) ---
236
+ if (!git.inRepo) {
237
+ const [owner, repo] = ownerRepo.split("/");
238
+ console.log(` Cloning ${ownerRepo} into ./${repo}...`);
239
+ try {
240
+ currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner, repo, token: githubToken });
241
+ } catch (err) {
242
+ console.log(` ✗ ${err.message}`);
243
+ return;
244
+ }
245
+ saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
246
+ console.log("");
247
+ }
248
+
249
+ // --- Step 5: Tracker type ---
358
250
  const tracker = await promptSelect({
359
- message: "Task tracker:",
251
+ message: "Task tracker",
360
252
  choices: [
361
253
  { name: "Linear", value: "linear" },
362
254
  { name: "Jira", value: "jira" },
@@ -365,284 +257,299 @@ export async function runInit(cwd, opts = {}) {
365
257
  ],
366
258
  });
367
259
  console.log("");
368
- if (tracker) config.tracker = tracker;
369
-
370
- if (tracker) printDedicatedUserChoice(tracker, quick);
371
260
 
372
- // Tracker location + auth with exact instructions, then verify.
261
+ // --- Step 6: Tracker location ---
262
+ let linearWorkspace = null;
263
+ let jiraBaseUrl = null;
373
264
  if (tracker === "linear") {
374
- if (!quick) printSteps("Create a Linear API key", [
375
- "Open https://linear.app/settings/api in your browser (log in first if needed).",
376
- "Click \"Create key\" (top right of the API page).",
377
- "Label the key: AgentDesk",
378
- "Expiration: set a value matching your policy, or choose \"No expiration\".",
379
- "Click \"Create\". Copy the key shown (starts with lin_api_). You will NOT see it again.",
380
- ]);
381
- const ws = await promptRequired(rl, "Linear workspace slug (the 'kendoo' in linear.app/kendoo)");
382
- const key = await promptRequired(rl, "Linear API key you just copied");
383
- saveEnvVar(cwd, "LINEAR_API_KEY", key);
384
- console.log(" ✓ Saved LINEAR_API_KEY to .env");
265
+ linearWorkspace = await promptRequired("Linear workspace slug (the 'kendoo' in linear.app/kendoo)");
266
+ console.log("");
267
+ } else if (tracker === "jira") {
268
+ const raw = await promptRequired("Jira tenant URL (e.g. https://yourco.atlassian.net)");
269
+ jiraBaseUrl = raw.replace(/\/+$/, "");
385
270
  console.log("");
386
- config.linear = { workspace: ws };
387
271
  }
388
272
 
389
- if (tracker === "jira") {
390
- if (!quick) printSteps("Create a Jira API token", [
391
- "Open https://id.atlassian.com/manage-profile/security/api-tokens",
392
- "Click \"Create API token\".",
393
- "Label: AgentDesk",
394
- "Click \"Create\". Copy the token (shown once).",
395
- "You'll also need the email you sign into Atlassian with.",
396
- ]);
397
- const baseUrl = await promptRequired(rl, "Jira tenant URL (e.g. https://yourco.atlassian.net)");
398
- const email = await promptRequired(rl, "Your Atlassian login email");
399
- const token = await promptRequired(rl, "Jira API token you just created");
400
- saveEnvVar(cwd, "JIRA_EMAIL", email);
401
- saveEnvVar(cwd, "JIRA_API_TOKEN", token);
402
- console.log(" Saved JIRA_EMAIL and JIRA_API_TOKEN to .env");
403
- console.log("");
404
- config.jira = { baseUrl: baseUrl.replace(/\/+$/, "") };
273
+ // --- Step 7: Tracker credentials ---
274
+ env = loadDotEnv(currentCwd);
275
+ let linearApiKey = env.LINEAR_API_KEY;
276
+ let jiraEmail = env.JIRA_EMAIL;
277
+ let jiraApiToken = env.JIRA_API_TOKEN;
278
+
279
+ if (tracker === "linear" && !linearApiKey) {
280
+ console.log(" (Get a Linear API key: https://linear.app/settings/api)");
281
+ linearApiKey = await promptRequired("Linear API key");
282
+ saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
283
+ console.log(" Saved LINEAR_API_KEY to .env\n");
284
+ } else if (tracker === "jira") {
285
+ if (!jiraEmail) {
286
+ console.log(" (Create a Jira API token: https://id.atlassian.com/manage-profile/security/api-tokens)");
287
+ jiraEmail = await promptRequired("Atlassian login email");
288
+ saveEnvVar(currentCwd, "JIRA_EMAIL", jiraEmail);
289
+ }
290
+ if (!jiraApiToken) {
291
+ jiraApiToken = await promptRequired("Jira API token");
292
+ saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
293
+ }
294
+ console.log(" ✓ Saved JIRA credentials to .env\n");
405
295
  }
406
296
 
407
- if (tracker === "github") {
408
- // The GitHub-issues credential doubles as the git-push credential below,
409
- // so we collect a full repo/login/token block up front.
410
- if (!quick) printSteps("Create a GitHub personal access token", [
411
- "Open https://github.com/settings/tokens (classic tokens).",
412
- "Click \"Generate new token\" → \"Generate new token (classic)\".",
413
- "Name: AgentDesk",
414
- "Expiration: match your policy.",
415
- "Scopes: check \"repo\" (full control of private repositories).",
416
- "Click \"Generate token\" at the bottom. Copy the token (shown once).",
417
- ]);
418
- const detected = detectGitRemote(cwd);
419
- const repoDefault = existingConfig.github?.repo || detected || "";
420
- const repo = (await ask(rl, ` GitHub repo (owner/repo)${repoDefault ? ` [${repoDefault}]` : ""}: `)).trim() || repoDefault;
421
- const loginDefault = existingConfig.github?.login || "";
422
- const login = (await ask(rl, ` GitHub username (the @handle that owns the token)${loginDefault ? ` [${loginDefault}]` : ""}: `)).trim() || loginDefault;
423
- const token = await promptRequired(rl, "GitHub token");
424
- saveEnvVar(cwd, "GITHUB_TOKEN", token);
425
- console.log(" ✓ Saved GITHUB_TOKEN to .env");
426
- console.log("");
427
- config.github = { repo, login };
297
+ // --- Step 8: Signature ---
298
+ const badgeValue = await promptSelect({
299
+ message: "How should the AI team sign its work?",
300
+ default: "AgentDesk",
301
+ choices: [
302
+ { name: "AgentDesk", value: "AgentDesk" },
303
+ { name: "Claude Code", value: "Claude Code" },
304
+ { name: "Custom…", value: "__custom__" },
305
+ { name: "Skip (no signature)", value: null },
306
+ ],
307
+ });
308
+ let identityBadge = badgeValue;
309
+ if (badgeValue === "__custom__") {
310
+ const v = (await ask(" Custom signature: ")).trim();
311
+ identityBadge = v || null;
428
312
  }
313
+ console.log("");
429
314
 
430
- // Verify tracker + fetch available projects to pick from. If verification
431
- // fails (bad email, revoked token, typo in base URL), loop back and let the
432
- // user re-enter credentials rather than silently saving broken config.
433
- let trackerLocation = null;
434
- if (tracker === "jira") trackerLocation = config.jira?.baseUrl;
435
- let verified = { ok: true };
436
- if (tracker) {
437
- while (true) {
438
- verified = await verifyAndPickTrackerProject({ rl, cwd, finalProjectKey, tracker, config, location: trackerLocation });
439
- if (verified.ok) break;
440
- console.log("");
315
+ // --- Step 9: Verify tracker + pick team/project ---
316
+ // Build an in-progress config we mutate as we learn more.
317
+ const config = {
318
+ tracker,
319
+ github: { repo: ownerRepo, login: githubLogin || null },
320
+ };
321
+ if (tracker === "linear") config.linear = { workspace: linearWorkspace };
322
+ if (tracker === "jira") config.jira = { baseUrl: jiraBaseUrl };
323
+ if (identityBadge) config.identityBadge = identityBadge;
324
+
325
+ let trackerTeamOrProjectId = null;
326
+ let trackerTeamOrProjectName = null;
327
+
328
+ if (tracker && tracker !== "github") {
329
+ process.stdout.write(" Verifying tracker access... ");
330
+ let verified = false;
331
+ while (!verified) {
332
+ const creds = { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken };
333
+ const check = await checkTrackerPermissions({ tracker, config, credentials: creds });
334
+ if (check.ok) {
335
+ console.log("✓");
336
+ if (check.identity?.name || check.identity?.email) {
337
+ console.log(` ✓ Posting as: ${check.identity.name || check.identity.email}`);
338
+ }
339
+ verified = true;
340
+ break;
341
+ }
342
+ console.log("failed");
343
+ for (const e of check.errors || []) console.log(` • ${e}`);
441
344
  const next = await promptSelect({
442
345
  message: "What now?",
443
346
  choices: [
444
347
  { name: "Re-enter credentials and try again", value: "retry" },
445
- { name: "Save config anyway (will need to fix before `agentdesk team`)", value: "skip" },
446
- { name: "Cancel init", value: "cancel" },
348
+ { name: "Save anyway (fix later in .env)", value: "skip" },
349
+ { name: "Cancel", value: "cancel" },
447
350
  ],
448
351
  });
449
- console.log("");
450
- if (next === "cancel") {
451
- rl.close();
452
- return;
453
- }
352
+ if (next === "cancel") return;
454
353
  if (next === "skip") break;
455
- // Retry: re-prompt only the tracker auth fields, not the whole wizard.
456
354
  if (tracker === "linear") {
457
- const key = await promptRequired(rl, "Linear API key");
458
- saveEnvVar(cwd, "LINEAR_API_KEY", key);
459
- console.log(" ✓ Updated LINEAR_API_KEY in .env");
460
- console.log("");
355
+ linearApiKey = await promptRequired("Linear API key");
356
+ saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
461
357
  } else if (tracker === "jira") {
462
- const baseUrl = (await ask(rl, ` Jira tenant URL [${config.jira?.baseUrl || ""}]: `)).trim() || config.jira?.baseUrl;
463
- const email = await promptRequired(rl, "Your Atlassian login email");
464
- const token = await promptRequired(rl, "Jira API token");
465
- saveEnvVar(cwd, "JIRA_EMAIL", email);
466
- saveEnvVar(cwd, "JIRA_API_TOKEN", token);
467
- if (baseUrl) {
468
- config.jira = { ...(config.jira || {}), baseUrl: baseUrl.replace(/\/+$/, "") };
469
- trackerLocation = config.jira.baseUrl;
470
- }
471
- console.log(" ✓ Updated JIRA_EMAIL and JIRA_API_TOKEN in .env");
472
- console.log("");
473
- } else if (tracker === "github") {
474
- const token = await promptRequired(rl, "GitHub token");
475
- saveEnvVar(cwd, "GITHUB_TOKEN", token);
476
- console.log(" ✓ Updated GITHUB_TOKEN in .env");
477
- console.log("");
358
+ jiraEmail = await promptRequired("Atlassian login email");
359
+ jiraApiToken = await promptRequired("Jira API token");
360
+ saveEnvVar(currentCwd, "JIRA_EMAIL", jiraEmail);
361
+ saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
478
362
  }
363
+ process.stdout.write(" Re-verifying tracker access... ");
479
364
  }
480
- if (verified.ok && verified.trackerProjectId) {
481
- if (tracker === "linear") config.linear = { ...(config.linear || {}), teamKey: verified.trackerProjectId };
482
- if (tracker === "jira") config.jira = { ...(config.jira || {}), project: verified.trackerProjectId };
483
- // Use the tracker's project key as the default agentdesk project key.
484
- if (!existingConfig.projectKey) finalProjectKey = verified.trackerProjectId.toLowerCase();
485
- }
486
- }
487
365
 
488
- // --- GitHub block (always — even when tracker is Linear/Jira/None) ---
489
- // This configures the credential agents use for git push, PRs, and
490
- // GitHub API calls. When tracker is already GitHub Issues the block
491
- // above already collected repo+login+token, so skip the duplicate.
492
- let githubLogin = config.github?.login || null;
493
- if (tracker !== "github") {
494
- if (!quick) {
495
- console.log(" ─".repeat(30));
496
- console.log(" GitHub access (required — agents push code and open PRs here)");
366
+ if (verified) {
367
+ const creds = { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken };
368
+ const items = await listTrackerProjects({ tracker, creds, location: jiraBaseUrl });
497
369
  console.log("");
498
- }
499
- if (!quick) printSteps("Create a GitHub personal access token", [
500
- "Open https://github.com/settings/tokens (classic tokens).",
501
- "Click \"Generate new token\" \"Generate new token (classic)\".",
502
- "Name: AgentDesk",
503
- "Expiration: match your policy.",
504
- "Scopes: check \"repo\" (full control of private repositories).",
505
- "Click \"Generate token\" at the bottom. Copy the token (shown once).",
506
- ]);
507
- const detected = detectGitRemote(cwd);
508
- const repoDefault = existingConfig.github?.repo || detected || "";
509
- const repo = (await ask(rl, ` GitHub repo (owner/repo)${repoDefault ? ` [${repoDefault}]` : ""}: `)).trim() || repoDefault;
510
- const loginDefault = existingConfig.github?.login || "";
511
- const login = (await ask(rl, ` GitHub username (the @handle that owns the token)${loginDefault ? ` [${loginDefault}]` : ""}: `)).trim() || loginDefault;
512
- const token = await promptRequired(rl, "GitHub token");
513
- saveEnvVar(cwd, "GITHUB_TOKEN", token);
514
- console.log(" ✓ Saved GITHUB_TOKEN to .env");
515
- console.log("");
516
- config.github = { repo, login };
517
- githubLogin = login;
518
-
519
- // Verify token matches the user-stated login.
520
- try {
521
- const env = { ...process.env, GH_TOKEN: token };
522
- const who = execSync("gh api user --jq .login", { env, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
523
- if (who && login && who.toLowerCase() !== login.toLowerCase()) {
524
- console.log(` ⚠ Token is valid but authenticates as @${who}, not @${login}. Agents will post as @${who}.`);
525
- } else if (who) {
526
- console.log(` ✓ GitHub verified as @${who}`);
370
+ if (items && items.length > 0) {
371
+ const pickedId = await promptSelect({
372
+ message: `Pick the ${tracker === "linear" ? "team" : "project"} this project maps to`,
373
+ choices: items.map(it => ({ name: it.name, value: it.id })),
374
+ });
375
+ trackerTeamOrProjectId = pickedId;
376
+ const pickedItem = items.find(it => it.id === pickedId);
377
+ trackerTeamOrProjectName = pickedItem?.name || pickedId;
378
+ } else {
379
+ trackerTeamOrProjectId = await promptRequired(tracker === "linear" ? "Linear team key (e.g. KEN)" : "Jira project key (e.g. PROJ)");
380
+ trackerTeamOrProjectName = trackerTeamOrProjectId;
527
381
  }
528
- } catch {
529
- console.log(" ⚠ Could not verify the GitHub token (gh CLI missing or network unreachable). Saved anyway verify later in the dashboard.");
382
+ if (tracker === "linear") config.linear = { ...config.linear, teamKey: trackerTeamOrProjectId };
383
+ if (tracker === "jira") config.jira = { ...config.jira, project: trackerTeamOrProjectId };
384
+ console.log("");
530
385
  }
531
- console.log("");
532
386
  }
533
387
 
534
- // Identity badge always ask (optional).
535
- const badgeDefault = existingConfig.identityBadge || "";
536
- const badge = (await ask(rl, ` Identity badge (display name for AgentDesk)${badgeDefault ? ` [${badgeDefault}]` : ""} [optional]: `)).trim() || badgeDefault;
537
- if (badge) config.identityBadge = badge;
388
+ // Derive projectName + projectKey silently.
389
+ const repoShort = ownerRepo.split("/").pop();
390
+ const projectName = trackerTeamOrProjectName
391
+ ? `${repoShort} — ${String(trackerTeamOrProjectName).split(" (")[0]}`
392
+ : repoShort;
393
+ const keySource = trackerTeamOrProjectId || repoShort;
394
+ const projectKey = String(keySource).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
395
+ config.projectKey = projectKey;
396
+
397
+ // --- Step 10: Review + access checks + save ---
398
+ const credsAll = loadDotEnv(currentCwd);
399
+ const results = await runAccessChecks({ apiKey, serverUrl: SERVER, cwd: currentCwd, config, creds: credsAll });
400
+ printAccessChecks(results, { tracker });
401
+ console.log("");
402
+ console.log(" Summary");
403
+ console.log(" ───────");
404
+ console.log(` Project: ${projectName} (${projectKey})`);
405
+ console.log(` Tracker: ${tracker || "none"}${trackerTeamOrProjectId ? ` (${trackerTeamOrProjectId})` : ""}`);
406
+ console.log(` GitHub: ${ownerRepo}${githubLogin ? ` (@${githubLogin})` : ""}`);
407
+ console.log(` Signature: ${identityBadge || "(skipped)"}`);
408
+ const envNames = Object.keys(credsAll).filter(k => ["LINEAR_API_KEY","JIRA_EMAIL","JIRA_API_TOKEN","GITHUB_TOKEN"].includes(k));
409
+ console.log(` .env written: ${envNames.join(", ") || "(none)"}`);
538
410
  console.log("");
539
411
 
540
- // --- Final step: agentdesk project key (with the tracker-derived default) ---
541
- if (!quick) {
542
- const keyAnswer = await ask(rl, ` AgentDesk project key [${finalProjectKey}]: `);
543
- if (keyAnswer.trim()) finalProjectKey = keyAnswer.trim();
544
- console.log("");
545
- }
546
-
547
- // --- Summary + confirm before write ---
548
- let merged = {};
549
- if (hasConfig) {
550
- try { merged = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
551
- }
552
-
553
- merged.projectKey = finalProjectKey;
554
-
555
- if (tracker) merged.tracker = tracker;
556
- else delete merged.tracker;
557
- if (config.linear) merged.linear = config.linear; else if (!tracker) delete merged.linear;
558
- if (config.jira) merged.jira = config.jira; else if (!tracker) delete merged.jira;
559
- if (config.github) merged.github = config.github;
560
- if (config.identityBadge) merged.identityBadge = config.identityBadge;
561
-
562
- if (!quick) {
563
- console.log(" Review");
564
- console.log(" ──────");
565
- console.log(` Project key: ${merged.projectKey}`);
566
- console.log(` Tracker: ${merged.tracker || "none"}`);
567
- if (merged.linear) console.log(` Linear: workspace=${merged.linear.workspace || "-"}, team=${merged.linear.teamKey || "-"}`);
568
- if (merged.jira) console.log(` Jira: ${merged.jira.baseUrl || "-"}, project=${merged.jira.project || "-"}`);
569
- if (merged.github) console.log(` GitHub: repo=${merged.github.repo || "-"}, user=@${merged.github.login || "-"}`);
570
- if (merged.identityBadge) console.log(` Identity badge: ${merged.identityBadge}`);
571
- console.log("");
572
- const confirm = (await ask(rl, " Save this config? [Y/n]: ")).trim().toLowerCase();
573
- if (confirm && confirm !== "y" && confirm !== "yes") {
574
- console.log(" Cancelled — nothing written.");
575
- rl.close();
576
- return;
577
- }
578
- console.log("");
412
+ const save = (await ask(" Save this config? [Y/n]: ")).trim().toLowerCase();
413
+ if (save && save !== "y" && save !== "yes") {
414
+ console.log(" Cancelled nothing written to .agentdesk.json.");
415
+ return;
579
416
  }
417
+ console.log("");
580
418
 
581
- writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n");
582
- console.log(` Saved .agentdesk.json`);
419
+ // Write .agentdesk.json
420
+ writeFileSync(join(currentCwd, ".agentdesk.json"), JSON.stringify(config, null, 2) + "\n");
421
+ console.log(" ✓ Saved .agentdesk.json");
583
422
 
584
- // Ensure .agentdesk/ is gitignored (local runtime state memory, cache, etc.)
585
- const gitignorePath = join(project.dir, ".gitignore");
423
+ // .gitignoreadd .agentdesk/
424
+ const gitignorePath = join(currentCwd, ".gitignore");
586
425
  try {
587
426
  const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
588
427
  if (!existing.split("\n").some(line => line.trim() === ".agentdesk/" || line.trim() === ".agentdesk")) {
589
428
  const nl = existing.endsWith("\n") || !existing ? "" : "\n";
590
429
  writeFileSync(gitignorePath, `${existing}${nl}.agentdesk/\n`);
591
- console.log(` Added .agentdesk/ to .gitignore`);
430
+ console.log(" Added .agentdesk/ to .gitignore");
592
431
  }
593
432
  } catch {}
594
433
 
595
- // Register in local project index (for daemon discovery)
596
- registerLocalProject(finalProjectKey, project.name || finalProjectKey, project.dir);
597
-
598
- // --- Register with server ---
434
+ // Register with server + push settings
599
435
  try {
600
- const res = await fetch(`${SERVER}/api/projects`, {
436
+ await fetch(`${SERVER}/api/projects`, {
601
437
  method: "POST",
602
- headers: {
603
- "Content-Type": "application/json",
604
- ...(loadApiKey(cwd) ? { "x-api-key": loadApiKey(cwd) } : {}),
605
- },
606
- body: JSON.stringify({
607
- id: finalProjectKey,
608
- name: project.name || finalProjectKey,
609
- path: project.dir,
610
- type: project.type,
611
- tracker,
612
- }),
438
+ headers: { "Content-Type": "application/json", "x-api-key": apiKey },
439
+ body: JSON.stringify({ id: projectKey, name: projectName, path: currentCwd, tracker }),
613
440
  });
614
- if (res.ok) {
615
- console.log(" Registered with agentdesk.live");
616
- }
617
- // Push settings to server via the shared helper so error surfacing is consistent.
618
- const key = loadApiKey(cwd);
619
- if (key) {
620
- const push = await pushConfig(key, SERVER, finalProjectKey, merged);
621
- if (!push.ok && push.error !== "missing_auth") {
622
- console.log(` ⚠ Could not push settings to agentdesk.live: ${push.error}`);
623
- console.log(" Your local .agentdesk.json is saved; retry from the UI or re-run init.");
624
- }
441
+ const push = await pushConfig(apiKey, SERVER, projectKey, config);
442
+ if (!push.ok && push.error !== "missing_auth") {
443
+ console.log(` ⚠ Could not push settings to agentdesk.live: ${push.error}`);
444
+ } else {
445
+ console.log(" ✓ Registered with agentdesk.live");
625
446
  }
626
447
  } catch {
627
- // Server not available
448
+ console.log(" ⚠ Couldn't reach agentdesk.live — local config saved, retry from the UI later.");
628
449
  }
629
450
 
451
+ registerLocalProject(projectKey, projectName, currentCwd);
452
+
630
453
  console.log("");
631
- console.log(" Ready! Run a team session:");
454
+ const cmdPrefix = trackerTeamOrProjectId || projectKey.toUpperCase();
455
+ console.log(" Ready. Run a team session:");
456
+ console.log(` agentdesk team ${cmdPrefix}-123`);
632
457
  console.log("");
633
- if (tracker) {
634
- const prefix = config.linear?.teamKey || config.jira?.project || finalProjectKey.toUpperCase();
635
- console.log(` agentdesk team ${prefix}-123`);
636
- console.log(` agentdesk team ${prefix}-123 -d "Optional extra context"`);
637
- } else {
638
- console.log(` agentdesk team my-feature -d "Add dark mode support"`);
458
+ }
459
+
460
+ // ---------- Entry: routing (E1) ----------
461
+
462
+ export async function runInit(cwd, opts = {}) {
463
+ const forceFull = !!opts.forceFull;
464
+ const apiKey = getStoredApiKey();
465
+ if (!apiKey) {
466
+ console.log("\n Not logged in. Run `agentdesk login` first.\n");
467
+ process.exit(1);
639
468
  }
640
- console.log("");
641
469
 
642
- if (!project.hasClaudeMd) {
643
- console.log(" Tip: Create a CLAUDE.md to help the agents understand your codebase.");
470
+ const local = readLocalConfig(cwd);
471
+
472
+ // --- E1: Detection ---
473
+ if (!forceFull && local?.projectKey) {
474
+ // Modern config — gap-scan by default.
475
+ console.log("");
476
+ console.log(" AgentDesk — existing project detected");
477
+ console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
644
478
  console.log("");
479
+ const choice = await promptSelect({
480
+ message: "What would you like to do?",
481
+ default: "refresh",
482
+ choices: [
483
+ { name: "Refresh tokens (update .env only — recommended)", value: "refresh" },
484
+ { name: "Walk through the full setup again", value: "full" },
485
+ { name: "Cancel", value: "cancel" },
486
+ ],
487
+ });
488
+ if (choice === "cancel") return;
489
+ if (choice === "refresh") {
490
+ await runExistingFlow(cwd, apiKey, local.projectKey);
491
+ return;
492
+ }
493
+ // choice === "full" → fall through to new-project wizard
494
+ return await runNewProjectWizard(cwd, apiKey);
495
+ }
496
+
497
+ if (!forceFull && local && !local.projectKey) {
498
+ // Legacy config — try to migrate.
499
+ const match = await autoMatchLegacy(cwd, apiKey, local);
500
+ if (match) {
501
+ console.log(`\n ✓ Legacy .agentdesk.json recognized — ${match.reason}. Migrating to project "${match.name}" (${match.projectKey}).`);
502
+ const migrated = { ...local, projectKey: match.projectKey };
503
+ writeFileSync(join(cwd, ".agentdesk.json"), JSON.stringify(migrated, null, 2) + "\n");
504
+ await runExistingFlow(cwd, apiKey, match.projectKey);
505
+ return;
506
+ }
507
+ console.log("\n ⚠ Legacy .agentdesk.json has no projectKey and no confident match found.");
508
+ // Fall through to Connect/New menu.
509
+ }
510
+
511
+ if (!forceFull) {
512
+ // No local config (or legacy-unmatched). Try git-remote auto-match.
513
+ const match = await autoMatchProject(cwd, apiKey);
514
+ if (match) {
515
+ console.log("");
516
+ console.log(` ✓ Recognized: ${match.name} (${match.projectKey}) — ${match.reason}`);
517
+ const confirm = await promptSelect({
518
+ message: "Use this project?",
519
+ default: "yes",
520
+ choices: [
521
+ { name: "Yes, continue with this project", value: "yes" },
522
+ { name: "No, pick another from my account", value: "other" },
523
+ { name: "Cancel", value: "cancel" },
524
+ ],
525
+ });
526
+ if (confirm === "cancel") return;
527
+ if (confirm === "yes") {
528
+ await runExistingFlow(cwd, apiKey, match.projectKey);
529
+ return;
530
+ }
531
+ // "other" — fall through to picker below
532
+ }
533
+
534
+ console.log("");
535
+ const top = await promptSelect({
536
+ message: "What would you like to do?",
537
+ choices: [
538
+ { name: "Connect to an existing project on my account", value: "connect" },
539
+ { name: "Set up this as a new project (full wizard)", value: "new" },
540
+ { name: "Cancel", value: "cancel" },
541
+ ],
542
+ });
543
+ if (top === "cancel") return;
544
+ if (top === "connect") {
545
+ const projectKey = await discoverProject(cwd, apiKey);
546
+ if (!projectKey) return;
547
+ console.log("");
548
+ await runExistingFlow(cwd, apiKey, projectKey);
549
+ return;
550
+ }
551
+ // "new" falls through to the wizard
645
552
  }
646
553
 
647
- rl.close();
554
+ await runNewProjectWizard(cwd, apiKey);
648
555
  }