@kendoo.agentdesk/agentdesk 0.20.1 → 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,15 @@ 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
+
16
25
  ## [0.20.1] — 2026-04-19
17
26
 
18
27
  ### Fixed
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,223 +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
- // --- 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}`);
143
- console.log("");
144
-
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");
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)");
130
+ console.log(isExisting
131
+ ? ` AgentDesk — configuring existing project`
132
+ : ` AgentDesk — setting up a new project`);
133
+ console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
159
134
  console.log("");
160
135
 
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}...`);
172
- }
173
- try {
174
- currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner: serverRepo.owner, repo: serverRepo.repo, token: githubToken });
175
- } catch (err) {
176
- console.log(` ✗ ${err.message}`);
177
- return;
178
- }
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("");
183
- }
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("");
184
148
  }
185
149
 
186
- // Ensure .agentdesk.json in currentCwd carries the projectKey so loadConfig
187
- // works on subsequent runs without needing discovery.
188
- writeMinimalLocalConfig(currentCwd, projectKey);
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 });
197
- console.log("");
198
- }
199
-
200
- // ---------- New-project wizard (steps 1–10) ----------
201
-
202
- async function runNewProjectWizard(cwd, apiKey) {
203
- let currentCwd = cwd;
204
-
205
- console.log("");
206
- console.log(" AgentDesk — new project setup");
207
- console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
208
- console.log("");
209
-
210
- // --- Step 1: Detect git repo ---
150
+ // Step 1 detect git repo (silent).
211
151
  const git = detectGitRepo(currentCwd);
212
- let ownerRepo = git.ownerRepo;
213
152
 
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");
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("");
217
164
  while (!ownerRepo) {
218
165
  const input = await promptRequired("GitHub repo (owner/repo or URL)");
219
166
  const parsed = parseGitHubRef(input);
220
- if (parsed) { ownerRepo = `${parsed.owner}/${parsed.repo}`; }
167
+ if (parsed) ownerRepo = `${parsed.owner}/${parsed.repo}`;
221
168
  else console.log(" Not a valid GitHub repo. Only github.com is supported right now.");
222
169
  }
223
170
  console.log("");
224
171
  }
225
172
 
226
- // --- Step 3: GitHub token ---
173
+ // Step 3 GitHub token (always editable, per-machine).
227
174
  let env = loadDotEnv(currentCwd);
228
175
  let githubToken = env.GITHUB_TOKEN;
229
- if (!githubToken) {
230
- 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`);
231
205
  githubToken = await promptRequired("GitHub token");
232
206
  saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
233
- 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)");
234
210
  }
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)");
238
211
  console.log("");
239
212
 
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;
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
+ }
249
230
  }
250
- saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
251
- console.log("");
252
231
  }
253
232
 
254
- // --- Step 5: Tracker type ---
255
- const tracker = await promptSelect({
256
- message: "Task tracker",
257
- choices: [
258
- { name: "Linear", value: "linear" },
259
- { name: "Jira", value: "jira" },
260
- { name: "GitHub Issues", value: "github" },
261
- { name: "None (use descriptions only)", value: null },
262
- ],
263
- });
264
- 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
+ }
265
251
 
266
- // --- Step 6: Tracker location ---
252
+ // Step 6 Tracker location.
267
253
  let linearWorkspace = null;
268
254
  let jiraBaseUrl = null;
269
- if (tracker === "linear") {
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(/\/+$/, "");
275
- 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
+ }
276
274
  }
277
275
 
278
- // --- Step 7: Tracker credentials ---
276
+ // Step 7 Tracker credentials (always editable, per-machine).
279
277
  env = loadDotEnv(currentCwd);
280
278
  let linearApiKey = env.LINEAR_API_KEY;
281
279
  let jiraEmail = env.JIRA_EMAIL;
282
280
  let jiraApiToken = env.JIRA_API_TOKEN;
283
281
 
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);
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);
294
300
  }
295
- if (!jiraApiToken) {
296
- jiraApiToken = await promptRequired("Jira API token");
297
- 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
+ }
298
328
  }
299
- console.log(" ✓ Saved JIRA credentials to .env\n");
329
+ console.log("");
300
330
  }
301
331
 
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;
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("");
317
355
  }
318
- console.log("");
319
356
 
320
- // --- Step 9: Verify tracker + pick team/project ---
321
- // 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).
322
359
  const config = {
323
360
  tracker,
324
- github: { repo: ownerRepo, login: githubLogin || null },
361
+ github: { repo: ownerRepo || null, login: githubLogin || null },
325
362
  };
326
363
  if (tracker === "linear") config.linear = { workspace: linearWorkspace };
327
364
  if (tracker === "jira") config.jira = { baseUrl: jiraBaseUrl };
@@ -330,10 +367,15 @@ async function runNewProjectWizard(cwd, apiKey) {
330
367
  let trackerTeamOrProjectId = null;
331
368
  let trackerTeamOrProjectName = null;
332
369
 
370
+ // Step 9 — Verify tracker + (new mode) pick team/project.
333
371
  if (tracker && tracker !== "github") {
334
- process.stdout.write(" Verifying tracker access... ");
335
- let verified = false;
336
- 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... ");
337
379
  const creds = { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken };
338
380
  const check = await checkTrackerPermissions({ tracker, config, credentials: creds });
339
381
  if (check.ok) {
@@ -341,91 +383,119 @@ async function runNewProjectWizard(cwd, apiKey) {
341
383
  if (check.identity?.name || check.identity?.email) {
342
384
  console.log(` ✓ Posting as: ${check.identity.name || check.identity.email}`);
343
385
  }
344
- verified = true;
345
- break;
346
- }
347
- console.log("failed");
348
- for (const e of check.errors || []) console.log(` • ${e}`);
349
- const next = await promptSelect({
350
- message: "What now?",
351
- choices: [
352
- { name: "Re-enter credentials and try again", value: "retry" },
353
- { name: "Save anyway (fix later in .env)", value: "skip" },
354
- { name: "Cancel", value: "cancel" },
355
- ],
356
- });
357
- if (next === "cancel") return;
358
- if (next === "skip") break;
359
- if (tracker === "linear") {
360
- linearApiKey = await promptRequired("Linear API key");
361
- saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
362
- } else if (tracker === "jira") {
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);
386
+ } else {
387
+ console.log("failed");
388
+ for (const e of check.errors || []) console.log(` • ${e}`);
367
389
  }
368
- process.stdout.write(" Re-verifying tracker access... ");
369
- }
370
-
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 });
374
390
  console.log("");
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 })),
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
+ ],
379
414
  });
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;
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("");
386
446
  }
387
- if (tracker === "linear") config.linear = { ...config.linear, teamKey: trackerTeamOrProjectId };
388
- if (tracker === "jira") config.jira = { ...config.jira, project: trackerTeamOrProjectId };
389
- console.log("");
390
447
  }
391
448
  }
392
449
 
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;
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;
401
464
 
402
- // --- Step 10: Review + access checks + save ---
465
+ // Step 10 Review + access checks + save.
403
466
  const credsAll = loadDotEnv(currentCwd);
404
467
  const results = await runAccessChecks({ apiKey, serverUrl: SERVER, cwd: currentCwd, config, creds: credsAll });
405
468
  printAccessChecks(results, { tracker });
406
469
  console.log("");
407
470
  console.log(" Summary");
408
471
  console.log(" ───────");
409
- console.log(` Project: ${projectName} (${projectKey})`);
472
+ console.log(` Project: ${projectName} (${computedKey})`);
410
473
  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)"}`);
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)"}`);
415
478
  console.log("");
416
479
 
417
480
  const save = (await ask(" Save this config? [Y/n]: ")).trim().toLowerCase();
418
481
  if (save && save !== "y" && save !== "yes") {
419
- console.log(" Cancelled — nothing written to .agentdesk.json.");
482
+ console.log(" Cancelled.");
420
483
  return;
421
484
  }
422
485
  console.log("");
423
486
 
424
- // Write .agentdesk.json
425
- 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
+ }
426
496
  console.log(" ✓ Saved .agentdesk.json");
427
497
 
428
- // .gitignore add .agentdesk/
498
+ // Ensure .agentdesk/ is gitignored (runtime state).
429
499
  const gitignorePath = join(currentCwd, ".gitignore");
430
500
  try {
431
501
  const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
@@ -436,124 +506,105 @@ async function runNewProjectWizard(cwd, apiKey) {
436
506
  }
437
507
  } catch {}
438
508
 
439
- // Register with server + push settings
440
- try {
441
- await fetch(`${SERVER}/api/projects`, {
442
- method: "POST",
443
- headers: { "Content-Type": "application/json", "x-api-key": apiKey },
444
- body: JSON.stringify({ id: projectKey, name: projectName, path: currentCwd, tracker }),
445
- });
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");
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.");
451
526
  }
452
- } catch {
453
- console.log(" ⚠ Couldn't reach agentdesk.live — local config saved, retry from the UI later.");
454
527
  }
455
528
 
456
- registerLocalProject(projectKey, projectName, currentCwd);
529
+ registerLocalProject(computedKey, projectName, currentCwd);
457
530
 
458
531
  console.log("");
459
- const cmdPrefix = trackerTeamOrProjectId || projectKey.toUpperCase();
532
+ const cmdPrefix = trackerTeamOrProjectId || computedKey.toUpperCase();
460
533
  console.log(" Ready. Run a team session:");
461
534
  console.log(` agentdesk team ${cmdPrefix}-123`);
462
535
  console.log("");
463
536
  }
464
537
 
465
- // ---------- Entry: routing (E1) ----------
538
+ // ---------- Entry: mode detection ----------
466
539
 
467
540
  export async function runInit(cwd, opts = {}) {
468
541
  const forceFull = !!opts.forceFull;
542
+ const forceNew = !!opts.forceNew;
469
543
  const apiKey = getStoredApiKey();
470
544
  if (!apiKey) {
471
545
  console.log("\n Not logged in. Run `agentdesk login` first.\n");
472
546
  process.exit(1);
473
547
  }
474
548
 
549
+ // Step 1 of mode detection: look at local config.
475
550
  const local = readLocalConfig(cwd);
476
551
 
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(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
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);
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 });
498
561
  return;
499
562
  }
500
563
 
501
- if (!forceFull && local && !local.projectKey) {
502
- // Legacy config try to migrate.
564
+ // Legacy local config — try to migrate.
565
+ if (local && !local.projectKey) {
503
566
  const match = await autoMatchLegacy(cwd, apiKey, local);
504
567
  if (match) {
505
- 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}".`);
506
569
  const migrated = { ...local, projectKey: match.projectKey };
507
570
  writeFileSync(join(cwd, ".agentdesk.json"), JSON.stringify(migrated, null, 2) + "\n");
508
- await runExistingFlow(cwd, apiKey, match.projectKey);
571
+ await runWizard({ cwd, apiKey, mode: "existing", projectKey: match.projectKey });
509
572
  return;
510
573
  }
511
- console.log("\n ⚠ Legacy .agentdesk.json has no projectKey and no confident match found.");
512
- // Fall through to Connect/New menu.
574
+ // Fall through to git-remote auto-match / picker.
513
575
  }
514
576
 
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
- }
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
+ }
537
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) {
538
589
  console.log("");
539
- const top = await promptSelect({
540
- message: "What would you like to do?",
590
+ const picked = await promptSelect({
591
+ message: "Which project is this clone?",
541
592
  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" },
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__" },
545
596
  ],
546
597
  });
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);
598
+ console.log("");
599
+ if (picked === "__cancel__") return;
600
+ if (picked === "__new__") {
601
+ await runWizard({ cwd, apiKey, mode: "new" });
553
602
  return;
554
603
  }
555
- // "new" falls through to the wizard
604
+ await runWizard({ cwd, apiKey, mode: "existing", projectKey: picked });
605
+ return;
556
606
  }
557
607
 
558
- await runNewProjectWizard(cwd, apiKey);
608
+ // Account has no projects — must be a first-time user.
609
+ await runWizard({ cwd, apiKey, mode: "new" });
559
610
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kendoo.agentdesk/agentdesk",
3
- "version": "0.20.1",
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": {