@kendoo.agentdesk/agentdesk 0.19.8 → 0.20.1

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
- }
42
+ // ---------- Tracker team/project picker helpers ----------
105
43
 
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
- }
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,195 +69,191 @@ 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
+ // --- E3 (was E5): Fetch server config first, so every later prompt can
136
+ // reference the project's known tracker/repo instead of re-asking.
137
+ process.stdout.write(" Fetching project settings from agentdesk.live... ");
138
+ const config = await loadConfig(currentCwd, { apiKey, serverUrl: SERVER, projectName: projectKey, silent: true });
139
+ console.log("✓");
140
+ console.log(` Project: ${projectKey}`);
141
+ if (config.tracker) console.log(` Tracker: ${config.tracker}`);
142
+ if (config.github?.repo) console.log(` Repo: ${config.github.repo}`);
266
143
  console.log("");
267
- return { ok: true, trackerProjectId: manual };
268
- }
269
144
 
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
- }
145
+ // --- E4 (was E3): GitHub token. Names the repo it's for so the user
146
+ // knows why we're asking.
147
+ let env = loadDotEnv(currentCwd);
148
+ let githubToken = env.GITHUB_TOKEN;
149
+ if (!githubToken) {
150
+ const repoHint = config.github?.repo ? ` (needed to push to ${config.github.repo})` : "";
151
+ console.log(` GitHub token${repoHint} create one at https://github.com/settings/tokens, scope: repo`);
152
+ githubToken = await promptRequired("GitHub token");
153
+ saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
154
+ console.log(" ✓ Saved GITHUB_TOKEN to .env");
310
155
  }
156
+ const githubLogin = await fetchGitHubLogin(githubToken);
157
+ if (githubLogin) console.log(` ✓ Token authenticates as @${githubLogin}`);
158
+ else console.log(" ⚠ Token couldn't be verified against the GitHub API (continuing anyway)");
159
+ console.log("");
311
160
 
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;
161
+ // --- E4: Ensure correct repo is locally present ---
162
+ const serverRepo = config.github?.repo ? parseGitHubRef(config.github.repo) : null;
163
+ if (serverRepo) {
164
+ const here = detectGitRepo(currentCwd);
165
+ const expected = `${serverRepo.owner}/${serverRepo.repo}`.toLowerCase();
166
+ const haveHere = (here.ownerRepo || "").toLowerCase();
167
+ if (!here.inRepo || haveHere !== expected) {
168
+ if (here.inRepo && haveHere && haveHere !== expected) {
169
+ console.log(` Current directory holds ${haveHere} — cloning ${expected} into ./${serverRepo.repo} instead.`);
170
+ } else {
171
+ console.log(` Cloning ${expected} into ./${serverRepo.repo}...`);
327
172
  }
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 });
173
+ try {
174
+ currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner: serverRepo.owner, repo: serverRepo.repo, token: githubToken });
175
+ } catch (err) {
176
+ console.log(` ✗ ${err.message}`);
352
177
  return;
353
178
  }
354
- // "new" falls through to the wizard below.
179
+ // Re-save the token into the cloned working tree's .env (previous
180
+ // save was into the parent dir).
181
+ saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
182
+ console.log("");
355
183
  }
356
184
  }
357
185
 
358
- const rl = createInterface({ input: process.stdin, output: process.stdout });
186
+ // Ensure .agentdesk.json in currentCwd carries the projectKey so loadConfig
187
+ // works on subsequent runs without needing discovery.
188
+ writeMinimalLocalConfig(currentCwd, projectKey);
359
189
 
190
+ // --- E6: Fill missing tracker creds + verify (gapScan handles both) ---
191
+ await gapScan({ cwd: currentCwd, apiKey, projectKey });
192
+
193
+ // --- E7: Final access-checks summary ---
194
+ env = loadDotEnv(currentCwd);
195
+ const results = await runAccessChecks({ apiKey, serverUrl: SERVER, cwd: currentCwd, config, creds: env });
196
+ printAccessChecks(results, { tracker: config.tracker });
360
197
  console.log("");
361
- console.log(" AgentDesk — Project Setup");
362
- console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━");
198
+ }
199
+
200
+ // ---------- New-project wizard (steps 1–10) ----------
201
+
202
+ async function runNewProjectWizard(cwd, apiKey) {
203
+ let currentCwd = cwd;
204
+
363
205
  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)"}`);
206
+ console.log(" AgentDesk — new project setup");
207
+ console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
369
208
  console.log("");
370
209
 
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;
210
+ // --- Step 1: Detect git repo ---
211
+ const git = detectGitRepo(currentCwd);
212
+ let ownerRepo = git.ownerRepo;
213
+
214
+ // --- Step 2: Ask for the GitHub repo (only if no git repo) ---
215
+ if (!git.inRepo) {
216
+ console.log(" No git repo here yet.\n");
217
+ while (!ownerRepo) {
218
+ const input = await promptRequired("GitHub repo (owner/repo or URL)");
219
+ const parsed = parseGitHubRef(input);
220
+ if (parsed) { ownerRepo = `${parsed.owner}/${parsed.repo}`; }
221
+ else console.log(" Not a valid GitHub repo. Only github.com is supported right now.");
222
+ }
223
+ console.log("");
224
+ }
382
225
 
383
- if (!quick) console.log(editScope === "tracker" ? " Tracker" : " Step 1 of 3 Task tracker");
226
+ // --- Step 3: GitHub token ---
227
+ let env = loadDotEnv(currentCwd);
228
+ let githubToken = env.GITHUB_TOKEN;
229
+ if (!githubToken) {
230
+ console.log(" GitHub token (create one at https://github.com/settings/tokens — scope: repo)");
231
+ githubToken = await promptRequired("GitHub token");
232
+ saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
233
+ console.log(" ✓ Saved GITHUB_TOKEN to .env");
234
+ }
235
+ const githubLogin = await fetchGitHubLogin(githubToken);
236
+ if (githubLogin) console.log(` ✓ Token authenticates as @${githubLogin}`);
237
+ else console.log(" ⚠ Token couldn't be verified against the GitHub API (continuing anyway)");
384
238
  console.log("");
239
+
240
+ // --- Step 4: Clone (only if no git repo) ---
241
+ if (!git.inRepo) {
242
+ const [owner, repo] = ownerRepo.split("/");
243
+ console.log(` Cloning ${ownerRepo} into ./${repo}...`);
244
+ try {
245
+ currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner, repo, token: githubToken });
246
+ } catch (err) {
247
+ console.log(` ✗ ${err.message}`);
248
+ return;
249
+ }
250
+ saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
251
+ console.log("");
252
+ }
253
+
254
+ // --- Step 5: Tracker type ---
385
255
  const tracker = await promptSelect({
386
- message: "Task tracker:",
256
+ message: "Task tracker",
387
257
  choices: [
388
258
  { name: "Linear", value: "linear" },
389
259
  { name: "Jira", value: "jira" },
@@ -392,284 +262,298 @@ export async function runInit(cwd, opts = {}) {
392
262
  ],
393
263
  });
394
264
  console.log("");
395
- if (tracker) config.tracker = tracker;
396
-
397
- if (tracker) printDedicatedUserChoice(tracker, quick);
398
265
 
399
- // Tracker location + auth with exact instructions, then verify.
266
+ // --- Step 6: Tracker location ---
267
+ let linearWorkspace = null;
268
+ let jiraBaseUrl = null;
400
269
  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");
270
+ linearWorkspace = await promptRequired("Linear workspace slug (the 'kendoo' in linear.app/kendoo)");
271
+ console.log("");
272
+ } else if (tracker === "jira") {
273
+ const raw = await promptRequired("Jira tenant URL (e.g. https://yourco.atlassian.net)");
274
+ jiraBaseUrl = raw.replace(/\/+$/, "");
412
275
  console.log("");
413
- config.linear = { workspace: ws };
414
276
  }
415
277
 
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(/\/+$/, "") };
278
+ // --- Step 7: Tracker credentials ---
279
+ env = loadDotEnv(currentCwd);
280
+ let linearApiKey = env.LINEAR_API_KEY;
281
+ let jiraEmail = env.JIRA_EMAIL;
282
+ let jiraApiToken = env.JIRA_API_TOKEN;
283
+
284
+ if (tracker === "linear" && !linearApiKey) {
285
+ console.log(" (Get a Linear API key: https://linear.app/settings/api)");
286
+ linearApiKey = await promptRequired("Linear API key");
287
+ saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
288
+ console.log(" Saved LINEAR_API_KEY to .env\n");
289
+ } else if (tracker === "jira") {
290
+ if (!jiraEmail) {
291
+ console.log(" (Create a Jira API token: https://id.atlassian.com/manage-profile/security/api-tokens)");
292
+ jiraEmail = await promptRequired("Atlassian login email");
293
+ saveEnvVar(currentCwd, "JIRA_EMAIL", jiraEmail);
294
+ }
295
+ if (!jiraApiToken) {
296
+ jiraApiToken = await promptRequired("Jira API token");
297
+ saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
298
+ }
299
+ console.log(" ✓ Saved JIRA credentials to .env\n");
432
300
  }
433
301
 
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 };
302
+ // --- Step 8: Signature ---
303
+ const badgeValue = await promptSelect({
304
+ message: "How should the AI team sign its work?",
305
+ default: "AgentDesk",
306
+ choices: [
307
+ { name: "AgentDesk", value: "AgentDesk" },
308
+ { name: "Claude Code", value: "Claude Code" },
309
+ { name: "Custom…", value: "__custom__" },
310
+ { name: "Skip (no signature)", value: null },
311
+ ],
312
+ });
313
+ let identityBadge = badgeValue;
314
+ if (badgeValue === "__custom__") {
315
+ const v = (await ask(" Custom signature: ")).trim();
316
+ identityBadge = v || null;
455
317
  }
318
+ console.log("");
456
319
 
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("");
320
+ // --- Step 9: Verify tracker + pick team/project ---
321
+ // Build an in-progress config we mutate as we learn more.
322
+ const config = {
323
+ tracker,
324
+ github: { repo: ownerRepo, login: githubLogin || null },
325
+ };
326
+ if (tracker === "linear") config.linear = { workspace: linearWorkspace };
327
+ if (tracker === "jira") config.jira = { baseUrl: jiraBaseUrl };
328
+ if (identityBadge) config.identityBadge = identityBadge;
329
+
330
+ let trackerTeamOrProjectId = null;
331
+ let trackerTeamOrProjectName = null;
332
+
333
+ if (tracker && tracker !== "github") {
334
+ process.stdout.write(" Verifying tracker access... ");
335
+ let verified = false;
336
+ while (!verified) {
337
+ const creds = { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken };
338
+ const check = await checkTrackerPermissions({ tracker, config, credentials: creds });
339
+ if (check.ok) {
340
+ console.log("✓");
341
+ if (check.identity?.name || check.identity?.email) {
342
+ console.log(` ✓ Posting as: ${check.identity.name || check.identity.email}`);
343
+ }
344
+ verified = true;
345
+ break;
346
+ }
347
+ console.log("failed");
348
+ for (const e of check.errors || []) console.log(` • ${e}`);
468
349
  const next = await promptSelect({
469
350
  message: "What now?",
470
351
  choices: [
471
352
  { 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" },
353
+ { name: "Save anyway (fix later in .env)", value: "skip" },
354
+ { name: "Cancel", value: "cancel" },
474
355
  ],
475
356
  });
476
- console.log("");
477
- if (next === "cancel") {
478
- rl.close();
479
- return;
480
- }
357
+ if (next === "cancel") return;
481
358
  if (next === "skip") break;
482
- // Retry: re-prompt only the tracker auth fields, not the whole wizard.
483
359
  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("");
360
+ linearApiKey = await promptRequired("Linear API key");
361
+ saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
488
362
  } 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("");
363
+ jiraEmail = await promptRequired("Atlassian login email");
364
+ jiraApiToken = await promptRequired("Jira API token");
365
+ saveEnvVar(currentCwd, "JIRA_EMAIL", jiraEmail);
366
+ saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
505
367
  }
368
+ process.stdout.write(" Re-verifying tracker access... ");
506
369
  }
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
370
 
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)");
371
+ if (verified) {
372
+ const creds = { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken };
373
+ const items = await listTrackerProjects({ tracker, creds, location: jiraBaseUrl });
524
374
  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}`);
375
+ if (items && items.length > 0) {
376
+ const pickedId = await promptSelect({
377
+ message: `Pick the ${tracker === "linear" ? "team" : "project"} this project maps to`,
378
+ choices: items.map(it => ({ name: it.name, value: it.id })),
379
+ });
380
+ trackerTeamOrProjectId = pickedId;
381
+ const pickedItem = items.find(it => it.id === pickedId);
382
+ trackerTeamOrProjectName = pickedItem?.name || pickedId;
383
+ } else {
384
+ trackerTeamOrProjectId = await promptRequired(tracker === "linear" ? "Linear team key (e.g. KEN)" : "Jira project key (e.g. PROJ)");
385
+ trackerTeamOrProjectName = trackerTeamOrProjectId;
554
386
  }
555
- } catch {
556
- console.log(" ⚠ Could not verify the GitHub token (gh CLI missing or network unreachable). Saved anyway verify later in the dashboard.");
387
+ if (tracker === "linear") config.linear = { ...config.linear, teamKey: trackerTeamOrProjectId };
388
+ if (tracker === "jira") config.jira = { ...config.jira, project: trackerTeamOrProjectId };
389
+ console.log("");
557
390
  }
558
- console.log("");
559
391
  }
560
392
 
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;
393
+ // Derive projectName + projectKey silently.
394
+ const repoShort = ownerRepo.split("/").pop();
395
+ const projectName = trackerTeamOrProjectName
396
+ ? `${repoShort} — ${String(trackerTeamOrProjectName).split(" (")[0]}`
397
+ : repoShort;
398
+ const keySource = trackerTeamOrProjectId || repoShort;
399
+ const projectKey = String(keySource).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
400
+ config.projectKey = projectKey;
401
+
402
+ // --- Step 10: Review + access checks + save ---
403
+ const credsAll = loadDotEnv(currentCwd);
404
+ const results = await runAccessChecks({ apiKey, serverUrl: SERVER, cwd: currentCwd, config, creds: credsAll });
405
+ printAccessChecks(results, { tracker });
406
+ console.log("");
407
+ console.log(" Summary");
408
+ console.log(" ───────");
409
+ console.log(` Project: ${projectName} (${projectKey})`);
410
+ console.log(` Tracker: ${tracker || "none"}${trackerTeamOrProjectId ? ` (${trackerTeamOrProjectId})` : ""}`);
411
+ console.log(` GitHub: ${ownerRepo}${githubLogin ? ` (@${githubLogin})` : ""}`);
412
+ console.log(` Signature: ${identityBadge || "(skipped)"}`);
413
+ const envNames = Object.keys(credsAll).filter(k => ["LINEAR_API_KEY","JIRA_EMAIL","JIRA_API_TOKEN","GITHUB_TOKEN"].includes(k));
414
+ console.log(` .env written: ${envNames.join(", ") || "(none)"}`);
565
415
  console.log("");
566
416
 
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("");
417
+ const save = (await ask(" Save this config? [Y/n]: ")).trim().toLowerCase();
418
+ if (save && save !== "y" && save !== "yes") {
419
+ console.log(" Cancelled nothing written to .agentdesk.json.");
420
+ return;
606
421
  }
422
+ console.log("");
607
423
 
608
- writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n");
609
- console.log(` Saved .agentdesk.json`);
424
+ // Write .agentdesk.json
425
+ writeFileSync(join(currentCwd, ".agentdesk.json"), JSON.stringify(config, null, 2) + "\n");
426
+ console.log(" ✓ Saved .agentdesk.json");
610
427
 
611
- // Ensure .agentdesk/ is gitignored (local runtime state memory, cache, etc.)
612
- const gitignorePath = join(project.dir, ".gitignore");
428
+ // .gitignoreadd .agentdesk/
429
+ const gitignorePath = join(currentCwd, ".gitignore");
613
430
  try {
614
431
  const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
615
432
  if (!existing.split("\n").some(line => line.trim() === ".agentdesk/" || line.trim() === ".agentdesk")) {
616
433
  const nl = existing.endsWith("\n") || !existing ? "" : "\n";
617
434
  writeFileSync(gitignorePath, `${existing}${nl}.agentdesk/\n`);
618
- console.log(` Added .agentdesk/ to .gitignore`);
435
+ console.log(" Added .agentdesk/ to .gitignore");
619
436
  }
620
437
  } catch {}
621
438
 
622
- // Register in local project index (for daemon discovery)
623
- registerLocalProject(finalProjectKey, project.name || finalProjectKey, project.dir);
624
-
625
- // --- Register with server ---
439
+ // Register with server + push settings
626
440
  try {
627
- const res = await fetch(`${SERVER}/api/projects`, {
441
+ await fetch(`${SERVER}/api/projects`, {
628
442
  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
- }),
443
+ headers: { "Content-Type": "application/json", "x-api-key": apiKey },
444
+ body: JSON.stringify({ id: projectKey, name: projectName, path: currentCwd, tracker }),
640
445
  });
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
- }
446
+ const push = await pushConfig(apiKey, SERVER, projectKey, config);
447
+ if (!push.ok && push.error !== "missing_auth") {
448
+ console.log(` ⚠ Could not push settings to agentdesk.live: ${push.error}`);
449
+ } else {
450
+ console.log(" ✓ Registered with agentdesk.live");
652
451
  }
653
452
  } catch {
654
- // Server not available
453
+ console.log(" ⚠ Couldn't reach agentdesk.live — local config saved, retry from the UI later.");
655
454
  }
656
455
 
456
+ registerLocalProject(projectKey, projectName, currentCwd);
457
+
657
458
  console.log("");
658
- console.log(" Ready! Run a team session:");
459
+ const cmdPrefix = trackerTeamOrProjectId || projectKey.toUpperCase();
460
+ console.log(" Ready. Run a team session:");
461
+ console.log(` agentdesk team ${cmdPrefix}-123`);
659
462
  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"`);
463
+ }
464
+
465
+ // ---------- Entry: routing (E1) ----------
466
+
467
+ export async function runInit(cwd, opts = {}) {
468
+ const forceFull = !!opts.forceFull;
469
+ const apiKey = getStoredApiKey();
470
+ if (!apiKey) {
471
+ console.log("\n Not logged in. Run `agentdesk login` first.\n");
472
+ process.exit(1);
666
473
  }
667
- console.log("");
668
474
 
669
- if (!project.hasClaudeMd) {
670
- console.log(" Tip: Create a CLAUDE.md to help the agents understand your codebase.");
475
+ const local = readLocalConfig(cwd);
476
+
477
+ // --- E1: Detection ---
478
+ if (!forceFull && local?.projectKey) {
479
+ // Modern config — gap-scan by default. No "Full setup" option here:
480
+ // walking the new-project wizard on an already-configured project
481
+ // would re-ask for the tracker as if it weren't on the server, which
482
+ // was the classic footgun. Users who genuinely want to reconfigure
483
+ // from scratch should pass --force-full (or delete .agentdesk.json).
484
+ console.log("");
485
+ console.log(" AgentDesk — existing project detected");
486
+ console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
671
487
  console.log("");
488
+ const choice = await promptSelect({
489
+ message: "What would you like to do?",
490
+ default: "refresh",
491
+ choices: [
492
+ { name: "Refresh tokens (update .env only)", value: "refresh" },
493
+ { name: "Cancel", value: "cancel" },
494
+ ],
495
+ });
496
+ if (choice === "cancel") return;
497
+ await runExistingFlow(cwd, apiKey, local.projectKey);
498
+ return;
499
+ }
500
+
501
+ if (!forceFull && local && !local.projectKey) {
502
+ // Legacy config — try to migrate.
503
+ const match = await autoMatchLegacy(cwd, apiKey, local);
504
+ if (match) {
505
+ console.log(`\n ✓ Legacy .agentdesk.json recognized — ${match.reason}. Migrating to project "${match.name}" (${match.projectKey}).`);
506
+ const migrated = { ...local, projectKey: match.projectKey };
507
+ writeFileSync(join(cwd, ".agentdesk.json"), JSON.stringify(migrated, null, 2) + "\n");
508
+ await runExistingFlow(cwd, apiKey, match.projectKey);
509
+ return;
510
+ }
511
+ console.log("\n ⚠ Legacy .agentdesk.json has no projectKey and no confident match found.");
512
+ // Fall through to Connect/New menu.
513
+ }
514
+
515
+ if (!forceFull) {
516
+ // No local config (or legacy-unmatched). Try git-remote auto-match.
517
+ const match = await autoMatchProject(cwd, apiKey);
518
+ if (match) {
519
+ console.log("");
520
+ console.log(` ✓ Recognized: ${match.name} (${match.projectKey}) — ${match.reason}`);
521
+ const confirm = await promptSelect({
522
+ message: "Use this project?",
523
+ default: "yes",
524
+ choices: [
525
+ { name: "Yes, continue with this project", value: "yes" },
526
+ { name: "No, pick another from my account", value: "other" },
527
+ { name: "Cancel", value: "cancel" },
528
+ ],
529
+ });
530
+ if (confirm === "cancel") return;
531
+ if (confirm === "yes") {
532
+ await runExistingFlow(cwd, apiKey, match.projectKey);
533
+ return;
534
+ }
535
+ // "other" — fall through to picker below
536
+ }
537
+
538
+ console.log("");
539
+ const top = await promptSelect({
540
+ message: "What would you like to do?",
541
+ choices: [
542
+ { name: "Connect to an existing project on my account", value: "connect" },
543
+ { name: "Set up this as a new project (full wizard)", value: "new" },
544
+ { name: "Cancel", value: "cancel" },
545
+ ],
546
+ });
547
+ if (top === "cancel") return;
548
+ if (top === "connect") {
549
+ const projectKey = await discoverProject(cwd, apiKey);
550
+ if (!projectKey) return;
551
+ console.log("");
552
+ await runExistingFlow(cwd, apiKey, projectKey);
553
+ return;
554
+ }
555
+ // "new" falls through to the wizard
672
556
  }
673
557
 
674
- rl.close();
558
+ await runNewProjectWizard(cwd, apiKey);
675
559
  }