@kendoo.agentdesk/agentdesk 0.20.0 → 0.20.2

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/CHANGELOG.md CHANGED
@@ -13,6 +13,23 @@ Internal refactors, infrastructure changes, and architectural notes are not list
13
13
  ### Added
14
14
  - `[UI]` Private-session sharing flow. When someone visits a session URL they don't own, they now see a friendly "Shh… this one's private" page with a one-click "Request access" button instead of a blank error. The session owner sees pending requests in a new header inbox and can grant viewer access for just that session or the whole project. Viewer-granted sessions show up in the teammate's sidebar tagged as a viewer.
15
15
 
16
+ ## [0.20.2] — 2026-04-19
17
+
18
+ ### Changed
19
+ - `[CLI]` `agentdesk init` is now a single mode-aware wizard instead of branching into separate flows. Two modes, picked automatically: **new** (no matching project on the server) walks every step interactively; **existing** (local `.agentdesk.json` has `projectKey`, or the git remote auto-matches a project on your account) shows the same steps but locks the project-wide ones (repo, tracker type/location/team, signature) as read-only with a "change in agentdesk.live" hint. Only per-machine credential steps (GitHub token, tracker credentials) are editable in existing mode — the CLI never mutates project-wide settings for an already-configured project.
20
+ - `[CLI]` Removed the "Refresh tokens / Cancel" menu. With the mode-aware wizard, the existing flow always surfaces every setting so you can see what's there and update credentials cleanly; nothing is hidden behind a "refresh only" shortcut that silently skipped visibility.
21
+ - `[CLI]` When no local config and the git remote doesn't auto-match, `init` shows a picker of the user's account projects plus "Set up as a new project" plus "Cancel" — no ambiguity about whether you're about to create a duplicate.
22
+ - `[CLI]` Per-machine credential prompts (GitHub token, Linear API key, Jira email/token) now offer a "Keep current / Replace" choice when an existing value is present, instead of blindly re-prompting.
23
+ - `[CLI]` `--force-full` still bypasses detection and runs the full new-mode wizard — useful when the auto-match is wrong or you're intentionally re-pushing settings.
24
+
25
+ ## [0.20.1] — 2026-04-19
26
+
27
+ ### Fixed
28
+ - `[CLI]` Removed the "Walk through the full setup again" option from the existing-project refresh menu. Picking it was a footgun — it dropped users into the new-project wizard which re-asked the tracker type from scratch, ignoring the tracker already configured on the server. The refresh menu now only offers "Refresh tokens" and "Cancel". To genuinely reconfigure from scratch, pass `agentdesk init --force-full` or delete `.agentdesk.json` first.
29
+
30
+ ### Changed
31
+ - `[CLI]` Existing-project flow fetches server settings as the first visible action. Subsequent prompts reference the known repo (e.g. "GitHub token — needed to push to owner/repo") instead of asking generically. Nothing is re-asked that's already configured on agentdesk.live.
32
+
16
33
  ## [0.20.0] — 2026-04-19
17
34
 
18
35
  ### Changed
package/README.md CHANGED
@@ -43,28 +43,29 @@ Opens your browser to [agentdesk.live](https://agentdesk.live) where you sign up
43
43
  agentdesk init
44
44
  ```
45
45
 
46
- `agentdesk init` is a single entry point that auto-routes based on what it finds:
46
+ `agentdesk init` is a single mode-aware wizard. It detects whether this clone maps to an existing project on your account and picks a mode accordingly:
47
47
 
48
- - **Modern `.agentdesk.json` (projectKey present)** arrow-key menu: *Refresh tokens* (default) / Full setup / Cancel.
49
- - **Legacy `.agentdesk.json` (no projectKey)** tries to match your account's projects by `github.repo` / git remote / tracker team; migrates the file forward on a confident match.
50
- - **No local config** → auto-matches the git remote against your account's projects. On a single match, prints `✓ Recognized: <name>` and confirms; otherwise a top-level *Connect to existing / New project / Cancel* menu.
48
+ - **Existing mode** — local `.agentdesk.json` has a `projectKey`, or the git remote matches a project on your account. The wizard walks every step, but project-wide steps (repo, tracker type/location/team, signature) are shown as read-only with a "change in agentdesk.live" hint. Only per-machine credential steps (GitHub token, tracker credentials) are interactive. Rationale: tracker/repo/team are shared state — changing them in one CLI would desync teammates and the dashboard, so those changes belong in project settings on agentdesk.live.
49
+ - **New mode** no match found. Every step is interactive. On save, the new project row is POSTed to agentdesk.live and its settings are pushed.
51
50
 
52
- The **new-project wizard** is ten tiny steps, each doing one thing:
51
+ When there's no local config and no unique auto-match but the account has other projects, `init` shows a picker of your projects plus *"Set up as a new project"* and *"Cancel"* so you can't accidentally create a duplicate.
52
+
53
+ The wizard is ten tiny steps, each doing one thing:
53
54
 
54
55
  1. Detect git repo in cwd.
55
- 2. Ask for GitHub repo (`owner/repo` or URL; GitHub only for now) — skipped when a repo is detected.
56
- 3. Ask for `GITHUB_TOKEN`, fetch `@login` from the GitHub API, echo "Token authenticates as @…".
57
- 4. `git clone` the repo into `./<repo>` skipped when already in one.
58
- 5. Tracker type Linear / Jira / GitHub Issues / None.
59
- 6. Tracker location — Linear workspace slug *or* Jira tenant URL.
60
- 7. Tracker credentials — `LINEAR_API_KEY` *or* `JIRA_EMAIL`+`JIRA_API_TOKEN`.
61
- 8. **How should the AI team sign its work?** AgentDesk / Claude Code / Custom… / Skip.
62
- 9. Verify tracker + pick team/project from the fetched list.
56
+ 2. GitHub repo (prompted in new mode; locked-display in existing mode).
57
+ 3. GitHub token per-machine, always editable. If present, offers Keep/Replace; if new, prompts. `@login` is fetched from the GitHub API and echoed.
58
+ 4. `git clone` the repo into `./<repo>` when cwd isn't already that repo.
59
+ 5. Tracker type (locked in existing mode).
60
+ 6. Tracker location — Linear workspace / Jira tenant URL (locked in existing mode).
61
+ 7. Tracker credentials — per-machine, always editable with Keep/Replace.
62
+ 8. "How should the AI team sign its work?" (locked in existing mode).
63
+ 9. Verify tracker + (new mode) pick team/project from the fetched list.
63
64
  10. Review, access-check checklist (`agentdesk.live ✓`, `GitHub ✓`, tracker ✓), confirm save.
64
65
 
65
- The **existing-project flow** (E1–E7) reuses Steps 3, 4 (clone if missing) and Step 10's access checks, plus a server-config fetch and a gap-scan for whichever `.env` credentials are missing. Nothing project-wide is re-asked.
66
+ Legacy `.agentdesk.json` files (without `projectKey`) are auto-migrated: `init` matches your account's projects by `github.repo` / git remote / tracker team, writes the `projectKey` back, and enters existing mode.
66
67
 
67
- Use `agentdesk init --force-full` to bypass every auto-route and always walk the new-project wizard. `agentdesk bootstrap` is an alias that always runs the existing-project flow.
68
+ Use `agentdesk init --force-full` to bypass detection and always run the new-mode wizard (useful when the auto-match is wrong or you're intentionally re-pushing settings). `agentdesk bootstrap` is a compatibility alias.
68
69
 
69
70
  All menus use arrow-key navigation with color highlighting; Ctrl-C exits cleanly.
70
71
 
package/bin/agentdesk.mjs CHANGED
@@ -58,11 +58,12 @@ if (!command || command === "help" || command === "--help") {
58
58
  2. agentdesk init Guided setup — auto-detects new vs existing projects
59
59
  3. agentdesk team TASK-123 Run a team session
60
60
 
61
- 'agentdesk init' branches on what it finds:
62
- Local .agentdesk.json with projectKey Refresh tokens (default) / Full setup / Cancel
63
- No local config, git remote matches → confirms the match, then only asks for tokens
64
- No match → Connect to existing / New project (wizard) / Cancel
65
- Use 'agentdesk init --force-full' to bypass the fast paths.
61
+ 'agentdesk init' is a mode-aware wizard:
62
+ Existing mode (local projectKey / git-remote auto-match) walks every step;
63
+ project-wide fields are locked (change them in agentdesk.live), credentials editable.
64
+ New mode (no match) — every step interactive; project POSTed to the server.
65
+ Ambiguous — picker of your projects + 'Set up as a new project'.
66
+ Use 'agentdesk init --force-full' to bypass detection and always run new mode.
66
67
 
67
68
  Commands:
68
69
  agentdesk login Sign in to AgentDesk
package/cli/init.mjs CHANGED
@@ -1,28 +1,29 @@
1
- // `agentdesk init` — unified setup entry point.
1
+ // `agentdesk init` — always the wizard, mode-aware.
2
2
  //
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.
3
+ // Two modes, decided automatically at entry time:
4
+ // new — server has no project for this clone. Every step is an
5
+ // interactive prompt; save writes a new project row to the
6
+ // server and pushes its settings.
7
+ // existing — server already has a project for this clone (local
8
+ // .agentdesk.json has projectKey, or git remote auto-matches
9
+ // one). Project-wide steps (repo, tracker type/location,
10
+ // team, signature) become read-only display lines with a
11
+ // "configured on agentdesk.live change there" hint;
12
+ // only the per-machine credential steps (GitHub token,
13
+ // tracker creds) are interactive.
13
14
  //
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.
15
+ // Rationale for the locks: tracker/repo/team are shared state. Changing
16
+ // them in one CLI would desync teammates and the dashboard. Those
17
+ // changes belong in project settings on agentdesk.live. CLI init is the
18
+ // single, consistent way to get a machine ready — nothing more.
17
19
 
18
20
  import { existsSync, readFileSync, writeFileSync } from "fs";
19
21
  import { join } from "path";
20
- import { execSync } from "child_process";
21
22
  import { loadConfig, pushConfig } from "./config.mjs";
22
23
  import { getStoredApiKey } from "./login.mjs";
23
24
  import { registerLocalProject } from "./projects.mjs";
24
25
  import { checkTrackerPermissions } from "./tracker-check.mjs";
25
- import { autoMatchProject, discoverProject, gapScan } from "./bootstrap.mjs";
26
+ import { autoMatchProject } from "./bootstrap.mjs";
26
27
  import { select as promptSelect, ask, promptRequired } from "./prompts.mjs";
27
28
  import {
28
29
  cloneRepoWithToken,
@@ -34,16 +35,12 @@ import {
34
35
  readLocalConfig,
35
36
  runAccessChecks,
36
37
  saveEnvVar,
37
- writeMinimalLocalConfig,
38
38
  } from "./setup-helpers.mjs";
39
39
 
40
40
  const SERVER = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
41
41
 
42
- // ---------- Tracker team/project picker helpers ----------
42
+ // ---------- Tracker list helpers (for step 9 in new mode) ----------
43
43
 
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).
47
44
  async function listTrackerProjects({ tracker, creds, location }) {
48
45
  if (tracker === "linear") {
49
46
  if (!creds.LINEAR_API_KEY) return null;
@@ -75,19 +72,24 @@ async function listTrackerProjects({ tracker, creds, location }) {
75
72
  return null;
76
73
  }
77
74
 
78
- // ---------- Legacy migration: auto-match a config without projectKey ----------
75
+ // ---------- Project picker (when auto-match fails and server has projects) ----------
79
76
 
80
- async function autoMatchLegacy(cwd, apiKey, localConfig) {
81
- // Fetch the user's projects once; filter on server-known fields.
82
- let projects;
77
+ async function fetchAccountProjects(apiKey) {
83
78
  try {
84
79
  const res = await fetch(`${SERVER}/api/projects`, {
85
- headers: { "x-api-key": apiKey }, signal: AbortSignal.timeout(8000),
80
+ headers: { "x-api-key": apiKey },
81
+ signal: AbortSignal.timeout(8000),
86
82
  });
87
- if (!res.ok) return null;
88
- projects = await res.json();
89
- } catch { return null; }
90
- if (!projects?.length) return null;
83
+ if (!res.ok) return [];
84
+ return await res.json();
85
+ } catch { return []; }
86
+ }
87
+
88
+ // ---------- Legacy migration ----------
89
+
90
+ async function autoMatchLegacy(cwd, apiKey, localConfig) {
91
+ const projects = await fetchAccountProjects(apiKey);
92
+ if (!projects.length) return null;
91
93
 
92
94
  const settingsList = await Promise.all(projects.map(async p => {
93
95
  try {
@@ -105,218 +107,258 @@ async function autoMatchLegacy(cwd, apiKey, localConfig) {
105
107
  for (let i = 0; i < projects.length; i++) {
106
108
  const p = projects[i];
107
109
  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) {
110
+ if (legacyRepo && (s?.github?.repo || "").toLowerCase() === legacyRepo) {
113
111
  return { projectKey: p.id, name: p.name, reason: `github.repo matches ${s.github.repo}` };
114
112
  }
115
- if (legacyTeamKey && serverTeam === legacyTeamKey) {
113
+ if (legacyTeamKey && (s?.linear?.teamKey || "").toLowerCase() === legacyTeamKey) {
116
114
  return { projectKey: p.id, name: p.name, reason: `Linear team matches ${s.linear.teamKey}` };
117
115
  }
118
- if (legacyJiraProject && serverJira === legacyJiraProject) {
116
+ if (legacyJiraProject && (s?.jira?.project || "").toLowerCase() === legacyJiraProject) {
119
117
  return { projectKey: p.id, name: p.name, reason: `Jira project matches ${s.jira.project}` };
120
118
  }
121
119
  }
122
120
  return null;
123
121
  }
124
122
 
125
- // ---------- Existing-project flow (E3–E7) ----------
123
+ // ---------- The unified wizard ----------
126
124
 
127
- async function runExistingFlow(cwd, apiKey, projectKey) {
125
+ async function runWizard({ cwd, apiKey, mode, projectKey }) {
126
+ const isExisting = mode === "existing";
128
127
  let currentCwd = cwd;
129
128
 
130
129
  console.log("");
131
- console.log(" AgentDesk — connecting to existing project");
132
- console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
133
- console.log("");
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}`);
140
- console.log("");
141
-
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");
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)");
130
+ console.log(isExisting
131
+ ? ` AgentDesk — configuring existing project`
132
+ : ` AgentDesk — setting up a new project`);
133
+ console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
154
134
  console.log("");
155
135
 
156
- // --- E4: Ensure correct repo is locally present ---
157
- const serverRepo = config.github?.repo ? parseGitHubRef(config.github.repo) : null;
158
- if (serverRepo) {
159
- const here = detectGitRepo(currentCwd);
160
- const expected = `${serverRepo.owner}/${serverRepo.repo}`.toLowerCase();
161
- const haveHere = (here.ownerRepo || "").toLowerCase();
162
- if (!here.inRepo || haveHere !== expected) {
163
- if (here.inRepo && haveHere && haveHere !== expected) {
164
- console.log(` Current directory holds ${haveHere}cloning ${expected} into ./${serverRepo.repo} instead.`);
165
- } else {
166
- console.log(` Cloning ${expected} into ./${serverRepo.repo}...`);
167
- }
168
- try {
169
- currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner: serverRepo.owner, repo: serverRepo.repo, token: githubToken });
170
- } catch (err) {
171
- console.log(` ✗ ${err.message}`);
172
- return;
173
- }
174
- // Re-save the token into the cloned working tree's .env (previous
175
- // save was into the parent dir).
176
- saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
177
- console.log("");
178
- }
136
+ // Server config for existing mode. Fetched up front so every subsequent
137
+ // step knows what's already set on agentdesk.live.
138
+ let serverConfig = null;
139
+ if (isExisting) {
140
+ process.stdout.write(" Fetching project settings from agentdesk.live... ");
141
+ serverConfig = await loadConfig(currentCwd, { apiKey, serverUrl: SERVER, projectName: projectKey, silent: true });
142
+ console.log("✓");
143
+ console.log(` Project: ${projectKey}`);
144
+ console.log(` Tracker: ${serverConfig.tracker || "(not setchange in agentdesk.live)"}`);
145
+ console.log(` Repo: ${serverConfig.github?.repo || "(not set)"}`);
146
+ console.log(` Signature: ${serverConfig.identityBadge || "(not set)"}`);
147
+ console.log("");
179
148
  }
180
149
 
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 });
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 });
192
- console.log("");
193
- }
194
-
195
- // ---------- New-project wizard (steps 1–10) ----------
196
-
197
- async function runNewProjectWizard(cwd, apiKey) {
198
- let currentCwd = cwd;
199
-
200
- console.log("");
201
- console.log(" AgentDesk — new project setup");
202
- console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
203
- console.log("");
204
-
205
- // --- Step 1: Detect git repo ---
150
+ // Step 1 detect git repo (silent).
206
151
  const git = detectGitRepo(currentCwd);
207
- let ownerRepo = git.ownerRepo;
208
152
 
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");
153
+ // Step 2 GitHub repo.
154
+ let ownerRepo = git.ownerRepo;
155
+ if (isExisting) {
156
+ if (serverConfig.github?.repo) {
157
+ ownerRepo = serverConfig.github.repo;
158
+ console.log(` Repo: ${ownerRepo} (configured — change in agentdesk.live)`);
159
+ console.log("");
160
+ }
161
+ } else if (!git.inRepo) {
162
+ console.log(" No git repo here yet.");
163
+ console.log("");
212
164
  while (!ownerRepo) {
213
165
  const input = await promptRequired("GitHub repo (owner/repo or URL)");
214
166
  const parsed = parseGitHubRef(input);
215
- if (parsed) { ownerRepo = `${parsed.owner}/${parsed.repo}`; }
167
+ if (parsed) ownerRepo = `${parsed.owner}/${parsed.repo}`;
216
168
  else console.log(" Not a valid GitHub repo. Only github.com is supported right now.");
217
169
  }
218
170
  console.log("");
219
171
  }
220
172
 
221
- // --- Step 3: GitHub token ---
173
+ // Step 3 GitHub token (always editable, per-machine).
222
174
  let env = loadDotEnv(currentCwd);
223
175
  let githubToken = env.GITHUB_TOKEN;
224
- if (!githubToken) {
225
- console.log(" GitHub token (create one at https://github.com/settings/tokens — scope: repo)");
176
+ let githubLogin = null;
177
+
178
+ if (githubToken) {
179
+ githubLogin = await fetchGitHubLogin(githubToken);
180
+ if (githubLogin) {
181
+ const choice = await promptSelect({
182
+ message: `GitHub token — currently authenticates as @${githubLogin}`,
183
+ default: "keep",
184
+ choices: [
185
+ { name: "Keep current token", value: "keep" },
186
+ { name: "Replace with a new token", value: "new" },
187
+ ],
188
+ });
189
+ if (choice === "new") {
190
+ githubToken = await promptRequired("GitHub token");
191
+ saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
192
+ githubLogin = await fetchGitHubLogin(githubToken);
193
+ if (githubLogin) console.log(` ✓ Token authenticates as @${githubLogin}`);
194
+ }
195
+ } else {
196
+ console.log(" ⚠ GITHUB_TOKEN in .env doesn't authenticate with GitHub — enter a new one.");
197
+ githubToken = await promptRequired("GitHub token");
198
+ saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
199
+ githubLogin = await fetchGitHubLogin(githubToken);
200
+ if (githubLogin) console.log(` ✓ Token authenticates as @${githubLogin}`);
201
+ }
202
+ } else {
203
+ const repoHint = ownerRepo ? ` (needed to push to ${ownerRepo})` : "";
204
+ console.log(` GitHub token${repoHint} — create one at https://github.com/settings/tokens, scope: repo`);
226
205
  githubToken = await promptRequired("GitHub token");
227
206
  saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
228
- console.log(" ✓ Saved GITHUB_TOKEN to .env");
207
+ githubLogin = await fetchGitHubLogin(githubToken);
208
+ if (githubLogin) console.log(` ✓ Token authenticates as @${githubLogin}`);
209
+ else console.log(" ⚠ Token couldn't be verified against the GitHub API (continuing anyway)");
229
210
  }
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)");
233
211
  console.log("");
234
212
 
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;
213
+ // Step 4 Clone if the correct repo isn't here locally.
214
+ if (ownerRepo) {
215
+ const parsed = parseGitHubRef(ownerRepo);
216
+ if (parsed) {
217
+ const expected = `${parsed.owner}/${parsed.repo}`.toLowerCase();
218
+ const haveHere = (git.ownerRepo || "").toLowerCase();
219
+ if (!git.inRepo || haveHere !== expected) {
220
+ console.log(` Cloning ${parsed.owner}/${parsed.repo} into ./${parsed.repo}...`);
221
+ try {
222
+ currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner: parsed.owner, repo: parsed.repo, token: githubToken });
223
+ saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
224
+ } catch (err) {
225
+ console.log(` ✗ ${err.message}`);
226
+ return;
227
+ }
228
+ console.log("");
229
+ }
244
230
  }
245
- saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
246
- console.log("");
247
231
  }
248
232
 
249
- // --- Step 5: Tracker type ---
250
- const tracker = await promptSelect({
251
- message: "Task tracker",
252
- choices: [
253
- { name: "Linear", value: "linear" },
254
- { name: "Jira", value: "jira" },
255
- { name: "GitHub Issues", value: "github" },
256
- { name: "None (use descriptions only)", value: null },
257
- ],
258
- });
259
- console.log("");
233
+ // Step 5 Tracker type.
234
+ let tracker;
235
+ if (isExisting) {
236
+ tracker = serverConfig.tracker || null;
237
+ console.log(` Tracker: ${tracker || "none"} (configured change in agentdesk.live)`);
238
+ console.log("");
239
+ } else {
240
+ tracker = await promptSelect({
241
+ message: "Task tracker",
242
+ choices: [
243
+ { name: "Linear", value: "linear" },
244
+ { name: "Jira", value: "jira" },
245
+ { name: "GitHub Issues", value: "github" },
246
+ { name: "None (use descriptions only)", value: null },
247
+ ],
248
+ });
249
+ console.log("");
250
+ }
260
251
 
261
- // --- Step 6: Tracker location ---
252
+ // Step 6 Tracker location.
262
253
  let linearWorkspace = null;
263
254
  let jiraBaseUrl = null;
264
- if (tracker === "linear") {
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(/\/+$/, "");
270
- console.log("");
255
+ if (isExisting) {
256
+ linearWorkspace = serverConfig.linear?.workspace || null;
257
+ jiraBaseUrl = serverConfig.jira?.baseUrl || null;
258
+ if (tracker === "linear") {
259
+ console.log(` Linear workspace: ${linearWorkspace || "(not set)"} (configured change in agentdesk.live)`);
260
+ console.log("");
261
+ } else if (tracker === "jira") {
262
+ console.log(` Jira URL: ${jiraBaseUrl || "(not set)"} (configured — change in agentdesk.live)`);
263
+ console.log("");
264
+ }
265
+ } else {
266
+ if (tracker === "linear") {
267
+ linearWorkspace = await promptRequired("Linear workspace slug (the 'kendoo' in linear.app/kendoo)");
268
+ console.log("");
269
+ } else if (tracker === "jira") {
270
+ const raw = await promptRequired("Jira tenant URL (e.g. https://yourco.atlassian.net)");
271
+ jiraBaseUrl = raw.replace(/\/+$/, "");
272
+ console.log("");
273
+ }
271
274
  }
272
275
 
273
- // --- Step 7: Tracker credentials ---
276
+ // Step 7 Tracker credentials (always editable, per-machine).
274
277
  env = loadDotEnv(currentCwd);
275
278
  let linearApiKey = env.LINEAR_API_KEY;
276
279
  let jiraEmail = env.JIRA_EMAIL;
277
280
  let jiraApiToken = env.JIRA_API_TOKEN;
278
281
 
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);
282
+ if (tracker === "linear") {
283
+ if (linearApiKey) {
284
+ const choice = await promptSelect({
285
+ message: "Linear API key — already in .env",
286
+ default: "keep",
287
+ choices: [
288
+ { name: "Keep current", value: "keep" },
289
+ { name: "Replace with a new one", value: "new" },
290
+ ],
291
+ });
292
+ if (choice === "new") {
293
+ linearApiKey = await promptRequired("Linear API key");
294
+ saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
295
+ }
296
+ } else {
297
+ console.log(" (Get a Linear API key: https://linear.app/settings/api)");
298
+ linearApiKey = await promptRequired("Linear API key");
299
+ saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
289
300
  }
290
- if (!jiraApiToken) {
291
- jiraApiToken = await promptRequired("Jira API token");
292
- saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
301
+ console.log("");
302
+ } else if (tracker === "jira") {
303
+ if (jiraEmail && jiraApiToken) {
304
+ const choice = await promptSelect({
305
+ message: `Jira credentials — ${jiraEmail} / token in .env`,
306
+ default: "keep",
307
+ choices: [
308
+ { name: "Keep current", value: "keep" },
309
+ { name: "Replace with new credentials", value: "new" },
310
+ ],
311
+ });
312
+ if (choice === "new") {
313
+ jiraEmail = await promptRequired("Atlassian login email");
314
+ jiraApiToken = await promptRequired("Jira API token");
315
+ saveEnvVar(currentCwd, "JIRA_EMAIL", jiraEmail);
316
+ saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
317
+ }
318
+ } else {
319
+ if (!jiraEmail) {
320
+ console.log(" (Create a Jira API token: https://id.atlassian.com/manage-profile/security/api-tokens)");
321
+ jiraEmail = await promptRequired("Atlassian login email");
322
+ saveEnvVar(currentCwd, "JIRA_EMAIL", jiraEmail);
323
+ }
324
+ if (!jiraApiToken) {
325
+ jiraApiToken = await promptRequired("Jira API token");
326
+ saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
327
+ }
293
328
  }
294
- console.log(" ✓ Saved JIRA credentials to .env\n");
329
+ console.log("");
295
330
  }
296
331
 
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;
332
+ // Step 8 Signature.
333
+ let identityBadge;
334
+ if (isExisting) {
335
+ identityBadge = serverConfig.identityBadge || null;
336
+ console.log(` Signature: ${identityBadge || "(not set)"} (configured — change in agentdesk.live)`);
337
+ console.log("");
338
+ } else {
339
+ const badgeValue = await promptSelect({
340
+ message: "How should the AI team sign its work?",
341
+ default: "AgentDesk",
342
+ choices: [
343
+ { name: "AgentDesk", value: "AgentDesk" },
344
+ { name: "Claude Code", value: "Claude Code" },
345
+ { name: "Custom…", value: "__custom__" },
346
+ { name: "Skip (no signature)", value: null },
347
+ ],
348
+ });
349
+ identityBadge = badgeValue;
350
+ if (badgeValue === "__custom__") {
351
+ const v = (await ask(" Custom signature: ")).trim();
352
+ identityBadge = v || null;
353
+ }
354
+ console.log("");
312
355
  }
313
- console.log("");
314
356
 
315
- // --- Step 9: Verify tracker + pick team/project ---
316
- // Build an in-progress config we mutate as we learn more.
357
+ // Build the in-memory config (used for verification + summary; written
358
+ // to disk only in new mode).
317
359
  const config = {
318
360
  tracker,
319
- github: { repo: ownerRepo, login: githubLogin || null },
361
+ github: { repo: ownerRepo || null, login: githubLogin || null },
320
362
  };
321
363
  if (tracker === "linear") config.linear = { workspace: linearWorkspace };
322
364
  if (tracker === "jira") config.jira = { baseUrl: jiraBaseUrl };
@@ -325,10 +367,15 @@ async function runNewProjectWizard(cwd, apiKey) {
325
367
  let trackerTeamOrProjectId = null;
326
368
  let trackerTeamOrProjectName = null;
327
369
 
370
+ // Step 9 — Verify tracker + (new mode) pick team/project.
328
371
  if (tracker && tracker !== "github") {
329
- process.stdout.write(" Verifying tracker access... ");
330
- let verified = false;
331
- while (!verified) {
372
+ if (isExisting) {
373
+ trackerTeamOrProjectId = serverConfig.linear?.teamKey || serverConfig.jira?.project || null;
374
+ trackerTeamOrProjectName = trackerTeamOrProjectId;
375
+ if (tracker === "linear" && trackerTeamOrProjectId) config.linear.teamKey = trackerTeamOrProjectId;
376
+ if (tracker === "jira" && trackerTeamOrProjectId) config.jira.project = trackerTeamOrProjectId;
377
+
378
+ process.stdout.write(" Verifying tracker access... ");
332
379
  const creds = { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken };
333
380
  const check = await checkTrackerPermissions({ tracker, config, credentials: creds });
334
381
  if (check.ok) {
@@ -336,91 +383,119 @@ async function runNewProjectWizard(cwd, apiKey) {
336
383
  if (check.identity?.name || check.identity?.email) {
337
384
  console.log(` ✓ Posting as: ${check.identity.name || check.identity.email}`);
338
385
  }
339
- verified = true;
340
- break;
341
- }
342
- console.log("failed");
343
- for (const e of check.errors || []) console.log(` • ${e}`);
344
- const next = await promptSelect({
345
- message: "What now?",
346
- choices: [
347
- { name: "Re-enter credentials and try again", value: "retry" },
348
- { name: "Save anyway (fix later in .env)", value: "skip" },
349
- { name: "Cancel", value: "cancel" },
350
- ],
351
- });
352
- if (next === "cancel") return;
353
- if (next === "skip") break;
354
- if (tracker === "linear") {
355
- linearApiKey = await promptRequired("Linear API key");
356
- saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
357
- } else if (tracker === "jira") {
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);
386
+ } else {
387
+ console.log("failed");
388
+ for (const e of check.errors || []) console.log(` • ${e}`);
362
389
  }
363
- process.stdout.write(" Re-verifying tracker access... ");
364
- }
365
-
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 });
369
390
  console.log("");
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 })),
391
+ } else {
392
+ process.stdout.write(" Verifying tracker access... ");
393
+ let verified = false;
394
+ while (!verified) {
395
+ const creds = { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken };
396
+ const check = await checkTrackerPermissions({ tracker, config, credentials: creds });
397
+ if (check.ok) {
398
+ console.log("✓");
399
+ if (check.identity?.name || check.identity?.email) {
400
+ console.log(` ✓ Posting as: ${check.identity.name || check.identity.email}`);
401
+ }
402
+ verified = true;
403
+ break;
404
+ }
405
+ console.log("failed");
406
+ for (const e of check.errors || []) console.log(` • ${e}`);
407
+ const next = await promptSelect({
408
+ message: "What now?",
409
+ choices: [
410
+ { name: "Re-enter credentials and try again", value: "retry" },
411
+ { name: "Save anyway (fix later in .env)", value: "skip" },
412
+ { name: "Cancel", value: "cancel" },
413
+ ],
374
414
  });
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;
415
+ if (next === "cancel") return;
416
+ if (next === "skip") break;
417
+ if (tracker === "linear") {
418
+ linearApiKey = await promptRequired("Linear API key");
419
+ saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
420
+ } else if (tracker === "jira") {
421
+ jiraEmail = await promptRequired("Atlassian login email");
422
+ jiraApiToken = await promptRequired("Jira API token");
423
+ saveEnvVar(currentCwd, "JIRA_EMAIL", jiraEmail);
424
+ saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
425
+ }
426
+ process.stdout.write(" Re-verifying tracker access... ");
427
+ }
428
+
429
+ if (verified) {
430
+ const creds = { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken };
431
+ const items = await listTrackerProjects({ tracker, creds, location: jiraBaseUrl });
432
+ if (items && items.length > 0) {
433
+ const pickedId = await promptSelect({
434
+ message: `Pick the ${tracker === "linear" ? "team" : "project"} this project maps to`,
435
+ choices: items.map(it => ({ name: it.name, value: it.id })),
436
+ });
437
+ trackerTeamOrProjectId = pickedId;
438
+ trackerTeamOrProjectName = items.find(it => it.id === pickedId)?.name || pickedId;
439
+ } else {
440
+ trackerTeamOrProjectId = await promptRequired(tracker === "linear" ? "Linear team key (e.g. KEN)" : "Jira project key (e.g. PROJ)");
441
+ trackerTeamOrProjectName = trackerTeamOrProjectId;
442
+ }
443
+ if (tracker === "linear") config.linear = { ...config.linear, teamKey: trackerTeamOrProjectId };
444
+ if (tracker === "jira") config.jira = { ...config.jira, project: trackerTeamOrProjectId };
445
+ console.log("");
381
446
  }
382
- if (tracker === "linear") config.linear = { ...config.linear, teamKey: trackerTeamOrProjectId };
383
- if (tracker === "jira") config.jira = { ...config.jira, project: trackerTeamOrProjectId };
384
- console.log("");
385
447
  }
386
448
  }
387
449
 
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;
450
+ // Derive projectName + projectKey (only used in new mode).
451
+ let projectName, computedKey;
452
+ if (isExisting) {
453
+ projectName = serverConfig.name || projectKey;
454
+ computedKey = projectKey;
455
+ } else {
456
+ const repoShort = ownerRepo ? ownerRepo.split("/").pop() : "project";
457
+ projectName = trackerTeamOrProjectName
458
+ ? `${repoShort} — ${String(trackerTeamOrProjectName).split(" (")[0]}`
459
+ : repoShort;
460
+ const keySource = trackerTeamOrProjectId || repoShort;
461
+ computedKey = String(keySource).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
462
+ }
463
+ config.projectKey = computedKey;
396
464
 
397
- // --- Step 10: Review + access checks + save ---
465
+ // Step 10 Review + access checks + save.
398
466
  const credsAll = loadDotEnv(currentCwd);
399
467
  const results = await runAccessChecks({ apiKey, serverUrl: SERVER, cwd: currentCwd, config, creds: credsAll });
400
468
  printAccessChecks(results, { tracker });
401
469
  console.log("");
402
470
  console.log(" Summary");
403
471
  console.log(" ───────");
404
- console.log(` Project: ${projectName} (${projectKey})`);
472
+ console.log(` Project: ${projectName} (${computedKey})`);
405
473
  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)"}`);
474
+ console.log(` GitHub: ${ownerRepo || "(not set)"}${githubLogin ? ` (@${githubLogin})` : ""}`);
475
+ console.log(` Signature: ${identityBadge || "(none)"}`);
476
+ const envNames = Object.keys(credsAll).filter(k => ["LINEAR_API_KEY", "JIRA_EMAIL", "JIRA_API_TOKEN", "GITHUB_TOKEN"].includes(k));
477
+ console.log(` .env: ${envNames.join(", ") || "(none)"}`);
410
478
  console.log("");
411
479
 
412
480
  const save = (await ask(" Save this config? [Y/n]: ")).trim().toLowerCase();
413
481
  if (save && save !== "y" && save !== "yes") {
414
- console.log(" Cancelled — nothing written to .agentdesk.json.");
482
+ console.log(" Cancelled.");
415
483
  return;
416
484
  }
417
485
  console.log("");
418
486
 
419
- // Write .agentdesk.json
420
- writeFileSync(join(currentCwd, ".agentdesk.json"), JSON.stringify(config, null, 2) + "\n");
487
+ // Write local .agentdesk.json. Existing mode writes only projectKey
488
+ // (server owns the rest and loadConfig will re-populate on the next
489
+ // run); new mode writes the full config.
490
+ const configPath = join(currentCwd, ".agentdesk.json");
491
+ if (isExisting) {
492
+ writeFileSync(configPath, JSON.stringify({ projectKey: computedKey }, null, 2) + "\n");
493
+ } else {
494
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
495
+ }
421
496
  console.log(" ✓ Saved .agentdesk.json");
422
497
 
423
- // .gitignore add .agentdesk/
498
+ // Ensure .agentdesk/ is gitignored (runtime state).
424
499
  const gitignorePath = join(currentCwd, ".gitignore");
425
500
  try {
426
501
  const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
@@ -431,125 +506,105 @@ async function runNewProjectWizard(cwd, apiKey) {
431
506
  }
432
507
  } catch {}
433
508
 
434
- // Register with server + push settings
435
- try {
436
- await fetch(`${SERVER}/api/projects`, {
437
- method: "POST",
438
- headers: { "Content-Type": "application/json", "x-api-key": apiKey },
439
- body: JSON.stringify({ id: projectKey, name: projectName, path: currentCwd, tracker }),
440
- });
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");
509
+ // Server sync new mode only. Existing mode doesn't push anything
510
+ // because every project-wide field was locked and unchanged.
511
+ if (!isExisting) {
512
+ try {
513
+ await fetch(`${SERVER}/api/projects`, {
514
+ method: "POST",
515
+ headers: { "Content-Type": "application/json", "x-api-key": apiKey },
516
+ body: JSON.stringify({ id: computedKey, name: projectName, path: currentCwd, tracker }),
517
+ });
518
+ const push = await pushConfig(apiKey, SERVER, computedKey, config);
519
+ if (!push.ok && push.error !== "missing_auth") {
520
+ console.log(` ⚠ Could not push settings to agentdesk.live: ${push.error}`);
521
+ } else {
522
+ console.log(" ✓ Registered with agentdesk.live");
523
+ }
524
+ } catch {
525
+ console.log(" ⚠ Couldn't reach agentdesk.live — local config saved, retry later.");
446
526
  }
447
- } catch {
448
- console.log(" ⚠ Couldn't reach agentdesk.live — local config saved, retry from the UI later.");
449
527
  }
450
528
 
451
- registerLocalProject(projectKey, projectName, currentCwd);
529
+ registerLocalProject(computedKey, projectName, currentCwd);
452
530
 
453
531
  console.log("");
454
- const cmdPrefix = trackerTeamOrProjectId || projectKey.toUpperCase();
532
+ const cmdPrefix = trackerTeamOrProjectId || computedKey.toUpperCase();
455
533
  console.log(" Ready. Run a team session:");
456
534
  console.log(` agentdesk team ${cmdPrefix}-123`);
457
535
  console.log("");
458
536
  }
459
537
 
460
- // ---------- Entry: routing (E1) ----------
538
+ // ---------- Entry: mode detection ----------
461
539
 
462
540
  export async function runInit(cwd, opts = {}) {
463
541
  const forceFull = !!opts.forceFull;
542
+ const forceNew = !!opts.forceNew;
464
543
  const apiKey = getStoredApiKey();
465
544
  if (!apiKey) {
466
545
  console.log("\n Not logged in. Run `agentdesk login` first.\n");
467
546
  process.exit(1);
468
547
  }
469
548
 
549
+ // Step 1 of mode detection: look at local config.
470
550
  const local = readLocalConfig(cwd);
471
551
 
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(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
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);
552
+ // --force-new / --force-full → treat as new no matter what the local/remote state says.
553
+ if (forceFull || forceNew) {
554
+ await runWizard({ cwd, apiKey, mode: "new" });
555
+ return;
556
+ }
557
+
558
+ // Modern local config with projectKey → existing mode.
559
+ if (local?.projectKey) {
560
+ await runWizard({ cwd, apiKey, mode: "existing", projectKey: local.projectKey });
561
+ return;
495
562
  }
496
563
 
497
- if (!forceFull && local && !local.projectKey) {
498
- // Legacy config try to migrate.
564
+ // Legacy local config — try to migrate.
565
+ if (local && !local.projectKey) {
499
566
  const match = await autoMatchLegacy(cwd, apiKey, local);
500
567
  if (match) {
501
- console.log(`\n ✓ Legacy .agentdesk.json recognized — ${match.reason}. Migrating to project "${match.name}" (${match.projectKey}).`);
568
+ console.log(`\n ✓ Legacy .agentdesk.json recognized — ${match.reason}. Migrating to project "${match.name}".`);
502
569
  const migrated = { ...local, projectKey: match.projectKey };
503
570
  writeFileSync(join(cwd, ".agentdesk.json"), JSON.stringify(migrated, null, 2) + "\n");
504
- await runExistingFlow(cwd, apiKey, match.projectKey);
571
+ await runWizard({ cwd, apiKey, mode: "existing", projectKey: match.projectKey });
505
572
  return;
506
573
  }
507
- console.log("\n ⚠ Legacy .agentdesk.json has no projectKey and no confident match found.");
508
- // Fall through to Connect/New menu.
574
+ // Fall through to git-remote auto-match / picker.
509
575
  }
510
576
 
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
- }
577
+ // No local config (or legacy-unmatched). Try auto-match by git remote.
578
+ const match = await autoMatchProject(cwd, apiKey);
579
+ if (match) {
580
+ console.log(`\n ✓ Recognized "${match.name}" (${match.projectKey}) — ${match.reason}.`);
581
+ await runWizard({ cwd, apiKey, mode: "existing", projectKey: match.projectKey });
582
+ return;
583
+ }
533
584
 
585
+ // No unique auto-match. If the account has other projects, offer to
586
+ // pick one; otherwise go straight to new mode.
587
+ const projects = await fetchAccountProjects(apiKey);
588
+ if (projects.length > 0) {
534
589
  console.log("");
535
- const top = await promptSelect({
536
- message: "What would you like to do?",
590
+ const picked = await promptSelect({
591
+ message: "Which project is this clone?",
537
592
  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" },
593
+ ...projects.map(p => ({ name: `${p.name} (${p.id})`, value: p.id })),
594
+ { name: "Set up as a new project", value: "__new__" },
595
+ { name: "Cancel", value: "__cancel__" },
541
596
  ],
542
597
  });
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);
598
+ console.log("");
599
+ if (picked === "__cancel__") return;
600
+ if (picked === "__new__") {
601
+ await runWizard({ cwd, apiKey, mode: "new" });
549
602
  return;
550
603
  }
551
- // "new" falls through to the wizard
604
+ await runWizard({ cwd, apiKey, mode: "existing", projectKey: picked });
605
+ return;
552
606
  }
553
607
 
554
- await runNewProjectWizard(cwd, apiKey);
608
+ // Account has no projects — must be a first-time user.
609
+ await runWizard({ cwd, apiKey, mode: "new" });
555
610
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kendoo.agentdesk/agentdesk",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
5
5
  "type": "module",
6
6
  "bin": {