@kendoo.agentdesk/agentdesk 0.19.8 → 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, discoverProject, 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
- }
131
-
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
- }
42
+ // ---------- Tracker team/project picker helpers ----------
166
43
 
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,195 +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 ----------
79
+
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;
217
91
 
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.`);
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 });
326
- return;
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}...`);
327
167
  }
328
-
329
- // No auto-match. Offer an explicit choice between connecting this
330
- // clone to an existing server-side project (bootstrap) and creating
331
- // a new one (full wizard). Prevents users from accidentally creating
332
- // a duplicate project when they just wanted to attach a second machine.
333
- console.log("");
334
- console.log(" AgentDesk — new directory");
335
- console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━");
336
- console.log("");
337
- const topChoice = await promptSelect({
338
- message: "What would you like to do?",
339
- choices: [
340
- { name: "Connect this repo to an existing project on my account (bootstrap)", value: "connect" },
341
- { name: "Set up this as a new project (full wizard)", value: "new" },
342
- { name: "Cancel", value: "cancel" },
343
- ],
344
- });
345
- console.log("");
346
- if (topChoice === "cancel") return;
347
- if (topChoice === "connect") {
348
- const projectKey = await discoverProject(cwd, apiKey);
349
- if (!projectKey) return;
350
- console.log("");
351
- await gapScan({ cwd, apiKey, projectKey });
168
+ try {
169
+ currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner: serverRepo.owner, repo: serverRepo.repo, token: githubToken });
170
+ } catch (err) {
171
+ console.log(` ✗ ${err.message}`);
352
172
  return;
353
173
  }
354
- // "new" falls through to the wizard below.
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("");
355
178
  }
356
179
  }
357
180
 
358
- 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);
184
+
185
+ // --- E6: Fill missing tracker creds + verify (gapScan handles both) ---
186
+ await gapScan({ cwd: currentCwd, apiKey, projectKey });
359
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 });
360
192
  console.log("");
361
- console.log(" AgentDesk — Project Setup");
362
- console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━");
193
+ }
194
+
195
+ // ---------- New-project wizard (steps 1–10) ----------
196
+
197
+ async function runNewProjectWizard(cwd, apiKey) {
198
+ let currentCwd = cwd;
199
+
363
200
  console.log("");
364
- console.log(` Project: ${project.name || "unknown"}`);
365
- console.log(` Type: ${project.label}`);
366
- console.log(` Directory: ${project.dir}`);
367
- console.log(` Git: ${project.hasGit ? "yes" : "no"}`);
368
- console.log(` CLAUDE.md: ${project.hasClaudeMd ? "yes" : "no (recommended)"}`);
201
+ console.log(" AgentDesk — new project setup");
202
+ console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
369
203
  console.log("");
370
204
 
371
- if (project.testCommand) console.log(` Test: ${project.testCommand}`);
372
- if (project.buildCommand) console.log(` Build: ${project.buildCommand}`);
373
- if (project.lintCommand) console.log(` Lint: ${project.lintCommand}`);
374
- if (project.testCommand || project.buildCommand || project.lintCommand) console.log("");
375
-
376
- // --- Tracker step: platform → location → auth → verify → pick project ---
377
- const config = {};
378
- // Initial default for the agentdesk project key. Gets overwritten with the
379
- // tracker's project/team key if we learn one during verification, and the
380
- // user can override at the final review screen.
381
- 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
+ }
382
220
 
383
- 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)");
384
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 ---
385
250
  const tracker = await promptSelect({
386
- message: "Task tracker:",
251
+ message: "Task tracker",
387
252
  choices: [
388
253
  { name: "Linear", value: "linear" },
389
254
  { name: "Jira", value: "jira" },
@@ -392,284 +257,299 @@ export async function runInit(cwd, opts = {}) {
392
257
  ],
393
258
  });
394
259
  console.log("");
395
- if (tracker) config.tracker = tracker;
396
-
397
- if (tracker) printDedicatedUserChoice(tracker, quick);
398
260
 
399
- // Tracker location + auth with exact instructions, then verify.
261
+ // --- Step 6: Tracker location ---
262
+ let linearWorkspace = null;
263
+ let jiraBaseUrl = null;
400
264
  if (tracker === "linear") {
401
- if (!quick) printSteps("Create a Linear API key", [
402
- "Open https://linear.app/settings/api in your browser (log in first if needed).",
403
- "Click \"Create key\" (top right of the API page).",
404
- "Label the key: AgentDesk",
405
- "Expiration: set a value matching your policy, or choose \"No expiration\".",
406
- "Click \"Create\". Copy the key shown (starts with lin_api_). You will NOT see it again.",
407
- ]);
408
- const ws = await promptRequired(rl, "Linear workspace slug (the 'kendoo' in linear.app/kendoo)");
409
- const key = await promptRequired(rl, "Linear API key you just copied");
410
- saveEnvVar(cwd, "LINEAR_API_KEY", key);
411
- 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(/\/+$/, "");
412
270
  console.log("");
413
- config.linear = { workspace: ws };
414
271
  }
415
272
 
416
- if (tracker === "jira") {
417
- if (!quick) printSteps("Create a Jira API token", [
418
- "Open https://id.atlassian.com/manage-profile/security/api-tokens",
419
- "Click \"Create API token\".",
420
- "Label: AgentDesk",
421
- "Click \"Create\". Copy the token (shown once).",
422
- "You'll also need the email you sign into Atlassian with.",
423
- ]);
424
- const baseUrl = await promptRequired(rl, "Jira tenant URL (e.g. https://yourco.atlassian.net)");
425
- const email = await promptRequired(rl, "Your Atlassian login email");
426
- const token = await promptRequired(rl, "Jira API token you just created");
427
- saveEnvVar(cwd, "JIRA_EMAIL", email);
428
- saveEnvVar(cwd, "JIRA_API_TOKEN", token);
429
- console.log(" Saved JIRA_EMAIL and JIRA_API_TOKEN to .env");
430
- console.log("");
431
- 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");
432
295
  }
433
296
 
434
- if (tracker === "github") {
435
- // The GitHub-issues credential doubles as the git-push credential below,
436
- // so we collect a full repo/login/token block up front.
437
- if (!quick) printSteps("Create a GitHub personal access token", [
438
- "Open https://github.com/settings/tokens (classic tokens).",
439
- "Click \"Generate new token\" → \"Generate new token (classic)\".",
440
- "Name: AgentDesk",
441
- "Expiration: match your policy.",
442
- "Scopes: check \"repo\" (full control of private repositories).",
443
- "Click \"Generate token\" at the bottom. Copy the token (shown once).",
444
- ]);
445
- const detected = detectGitRemote(cwd);
446
- const repoDefault = existingConfig.github?.repo || detected || "";
447
- const repo = (await ask(rl, ` GitHub repo (owner/repo)${repoDefault ? ` [${repoDefault}]` : ""}: `)).trim() || repoDefault;
448
- const loginDefault = existingConfig.github?.login || "";
449
- const login = (await ask(rl, ` GitHub username (the @handle that owns the token)${loginDefault ? ` [${loginDefault}]` : ""}: `)).trim() || loginDefault;
450
- const token = await promptRequired(rl, "GitHub token");
451
- saveEnvVar(cwd, "GITHUB_TOKEN", token);
452
- console.log(" ✓ Saved GITHUB_TOKEN to .env");
453
- console.log("");
454
- 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;
455
312
  }
313
+ console.log("");
456
314
 
457
- // Verify tracker + fetch available projects to pick from. If verification
458
- // fails (bad email, revoked token, typo in base URL), loop back and let the
459
- // user re-enter credentials rather than silently saving broken config.
460
- let trackerLocation = null;
461
- if (tracker === "jira") trackerLocation = config.jira?.baseUrl;
462
- let verified = { ok: true };
463
- if (tracker) {
464
- while (true) {
465
- verified = await verifyAndPickTrackerProject({ rl, cwd, finalProjectKey, tracker, config, location: trackerLocation });
466
- if (verified.ok) break;
467
- 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}`);
468
344
  const next = await promptSelect({
469
345
  message: "What now?",
470
346
  choices: [
471
347
  { name: "Re-enter credentials and try again", value: "retry" },
472
- { name: "Save config anyway (will need to fix before `agentdesk team`)", value: "skip" },
473
- { name: "Cancel init", value: "cancel" },
348
+ { name: "Save anyway (fix later in .env)", value: "skip" },
349
+ { name: "Cancel", value: "cancel" },
474
350
  ],
475
351
  });
476
- console.log("");
477
- if (next === "cancel") {
478
- rl.close();
479
- return;
480
- }
352
+ if (next === "cancel") return;
481
353
  if (next === "skip") break;
482
- // Retry: re-prompt only the tracker auth fields, not the whole wizard.
483
354
  if (tracker === "linear") {
484
- const key = await promptRequired(rl, "Linear API key");
485
- saveEnvVar(cwd, "LINEAR_API_KEY", key);
486
- console.log(" ✓ Updated LINEAR_API_KEY in .env");
487
- console.log("");
355
+ linearApiKey = await promptRequired("Linear API key");
356
+ saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
488
357
  } else if (tracker === "jira") {
489
- const baseUrl = (await ask(rl, ` Jira tenant URL [${config.jira?.baseUrl || ""}]: `)).trim() || config.jira?.baseUrl;
490
- const email = await promptRequired(rl, "Your Atlassian login email");
491
- const token = await promptRequired(rl, "Jira API token");
492
- saveEnvVar(cwd, "JIRA_EMAIL", email);
493
- saveEnvVar(cwd, "JIRA_API_TOKEN", token);
494
- if (baseUrl) {
495
- config.jira = { ...(config.jira || {}), baseUrl: baseUrl.replace(/\/+$/, "") };
496
- trackerLocation = config.jira.baseUrl;
497
- }
498
- console.log(" ✓ Updated JIRA_EMAIL and JIRA_API_TOKEN in .env");
499
- console.log("");
500
- } else if (tracker === "github") {
501
- const token = await promptRequired(rl, "GitHub token");
502
- saveEnvVar(cwd, "GITHUB_TOKEN", token);
503
- console.log(" ✓ Updated GITHUB_TOKEN in .env");
504
- 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);
505
362
  }
363
+ process.stdout.write(" Re-verifying tracker access... ");
506
364
  }
507
- if (verified.ok && verified.trackerProjectId) {
508
- if (tracker === "linear") config.linear = { ...(config.linear || {}), teamKey: verified.trackerProjectId };
509
- if (tracker === "jira") config.jira = { ...(config.jira || {}), project: verified.trackerProjectId };
510
- // Use the tracker's project key as the default agentdesk project key.
511
- if (!existingConfig.projectKey) finalProjectKey = verified.trackerProjectId.toLowerCase();
512
- }
513
- }
514
365
 
515
- // --- GitHub block (always — even when tracker is Linear/Jira/None) ---
516
- // This configures the credential agents use for git push, PRs, and
517
- // GitHub API calls. When tracker is already GitHub Issues the block
518
- // above already collected repo+login+token, so skip the duplicate.
519
- let githubLogin = config.github?.login || null;
520
- if (tracker !== "github") {
521
- if (!quick) {
522
- console.log(" ─".repeat(30));
523
- 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 });
524
369
  console.log("");
525
- }
526
- if (!quick) printSteps("Create a GitHub personal access token", [
527
- "Open https://github.com/settings/tokens (classic tokens).",
528
- "Click \"Generate new token\" \"Generate new token (classic)\".",
529
- "Name: AgentDesk",
530
- "Expiration: match your policy.",
531
- "Scopes: check \"repo\" (full control of private repositories).",
532
- "Click \"Generate token\" at the bottom. Copy the token (shown once).",
533
- ]);
534
- const detected = detectGitRemote(cwd);
535
- const repoDefault = existingConfig.github?.repo || detected || "";
536
- const repo = (await ask(rl, ` GitHub repo (owner/repo)${repoDefault ? ` [${repoDefault}]` : ""}: `)).trim() || repoDefault;
537
- const loginDefault = existingConfig.github?.login || "";
538
- const login = (await ask(rl, ` GitHub username (the @handle that owns the token)${loginDefault ? ` [${loginDefault}]` : ""}: `)).trim() || loginDefault;
539
- const token = await promptRequired(rl, "GitHub token");
540
- saveEnvVar(cwd, "GITHUB_TOKEN", token);
541
- console.log(" ✓ Saved GITHUB_TOKEN to .env");
542
- console.log("");
543
- config.github = { repo, login };
544
- githubLogin = login;
545
-
546
- // Verify token matches the user-stated login.
547
- try {
548
- const env = { ...process.env, GH_TOKEN: token };
549
- const who = execSync("gh api user --jq .login", { env, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
550
- if (who && login && who.toLowerCase() !== login.toLowerCase()) {
551
- console.log(` ⚠ Token is valid but authenticates as @${who}, not @${login}. Agents will post as @${who}.`);
552
- } else if (who) {
553
- 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;
554
381
  }
555
- } catch {
556
- 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("");
557
385
  }
558
- console.log("");
559
386
  }
560
387
 
561
- // Identity badge always ask (optional).
562
- const badgeDefault = existingConfig.identityBadge || "";
563
- const badge = (await ask(rl, ` Identity badge (display name for AgentDesk)${badgeDefault ? ` [${badgeDefault}]` : ""} [optional]: `)).trim() || badgeDefault;
564
- 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)"}`);
565
410
  console.log("");
566
411
 
567
- // --- Final step: agentdesk project key (with the tracker-derived default) ---
568
- if (!quick) {
569
- const keyAnswer = await ask(rl, ` AgentDesk project key [${finalProjectKey}]: `);
570
- if (keyAnswer.trim()) finalProjectKey = keyAnswer.trim();
571
- console.log("");
572
- }
573
-
574
- // --- Summary + confirm before write ---
575
- let merged = {};
576
- if (hasConfig) {
577
- try { merged = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
578
- }
579
-
580
- merged.projectKey = finalProjectKey;
581
-
582
- if (tracker) merged.tracker = tracker;
583
- else delete merged.tracker;
584
- if (config.linear) merged.linear = config.linear; else if (!tracker) delete merged.linear;
585
- if (config.jira) merged.jira = config.jira; else if (!tracker) delete merged.jira;
586
- if (config.github) merged.github = config.github;
587
- if (config.identityBadge) merged.identityBadge = config.identityBadge;
588
-
589
- if (!quick) {
590
- console.log(" Review");
591
- console.log(" ──────");
592
- console.log(` Project key: ${merged.projectKey}`);
593
- console.log(` Tracker: ${merged.tracker || "none"}`);
594
- if (merged.linear) console.log(` Linear: workspace=${merged.linear.workspace || "-"}, team=${merged.linear.teamKey || "-"}`);
595
- if (merged.jira) console.log(` Jira: ${merged.jira.baseUrl || "-"}, project=${merged.jira.project || "-"}`);
596
- if (merged.github) console.log(` GitHub: repo=${merged.github.repo || "-"}, user=@${merged.github.login || "-"}`);
597
- if (merged.identityBadge) console.log(` Identity badge: ${merged.identityBadge}`);
598
- console.log("");
599
- const confirm = (await ask(rl, " Save this config? [Y/n]: ")).trim().toLowerCase();
600
- if (confirm && confirm !== "y" && confirm !== "yes") {
601
- console.log(" Cancelled — nothing written.");
602
- rl.close();
603
- return;
604
- }
605
- 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;
606
416
  }
417
+ console.log("");
607
418
 
608
- writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n");
609
- 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");
610
422
 
611
- // Ensure .agentdesk/ is gitignored (local runtime state memory, cache, etc.)
612
- const gitignorePath = join(project.dir, ".gitignore");
423
+ // .gitignoreadd .agentdesk/
424
+ const gitignorePath = join(currentCwd, ".gitignore");
613
425
  try {
614
426
  const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
615
427
  if (!existing.split("\n").some(line => line.trim() === ".agentdesk/" || line.trim() === ".agentdesk")) {
616
428
  const nl = existing.endsWith("\n") || !existing ? "" : "\n";
617
429
  writeFileSync(gitignorePath, `${existing}${nl}.agentdesk/\n`);
618
- console.log(` Added .agentdesk/ to .gitignore`);
430
+ console.log(" Added .agentdesk/ to .gitignore");
619
431
  }
620
432
  } catch {}
621
433
 
622
- // Register in local project index (for daemon discovery)
623
- registerLocalProject(finalProjectKey, project.name || finalProjectKey, project.dir);
624
-
625
- // --- Register with server ---
434
+ // Register with server + push settings
626
435
  try {
627
- const res = await fetch(`${SERVER}/api/projects`, {
436
+ await fetch(`${SERVER}/api/projects`, {
628
437
  method: "POST",
629
- headers: {
630
- "Content-Type": "application/json",
631
- ...(loadApiKey(cwd) ? { "x-api-key": loadApiKey(cwd) } : {}),
632
- },
633
- body: JSON.stringify({
634
- id: finalProjectKey,
635
- name: project.name || finalProjectKey,
636
- path: project.dir,
637
- type: project.type,
638
- tracker,
639
- }),
438
+ headers: { "Content-Type": "application/json", "x-api-key": apiKey },
439
+ body: JSON.stringify({ id: projectKey, name: projectName, path: currentCwd, tracker }),
640
440
  });
641
- if (res.ok) {
642
- console.log(" Registered with agentdesk.live");
643
- }
644
- // Push settings to server via the shared helper so error surfacing is consistent.
645
- const key = loadApiKey(cwd);
646
- if (key) {
647
- const push = await pushConfig(key, SERVER, finalProjectKey, merged);
648
- if (!push.ok && push.error !== "missing_auth") {
649
- console.log(` ⚠ Could not push settings to agentdesk.live: ${push.error}`);
650
- console.log(" Your local .agentdesk.json is saved; retry from the UI or re-run init.");
651
- }
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");
652
446
  }
653
447
  } catch {
654
- // Server not available
448
+ console.log(" ⚠ Couldn't reach agentdesk.live — local config saved, retry from the UI later.");
655
449
  }
656
450
 
451
+ registerLocalProject(projectKey, projectName, currentCwd);
452
+
657
453
  console.log("");
658
- 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`);
659
457
  console.log("");
660
- if (tracker) {
661
- const prefix = config.linear?.teamKey || config.jira?.project || finalProjectKey.toUpperCase();
662
- console.log(` agentdesk team ${prefix}-123`);
663
- console.log(` agentdesk team ${prefix}-123 -d "Optional extra context"`);
664
- } else {
665
- 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);
666
468
  }
667
- console.log("");
668
469
 
669
- if (!project.hasClaudeMd) {
670
- 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(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
671
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
672
552
  }
673
553
 
674
- rl.close();
554
+ await runNewProjectWizard(cwd, apiKey);
675
555
  }