@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 +9 -0
- package/README.md +16 -15
- package/bin/agentdesk.mjs +6 -5
- package/cli/init.mjs +384 -333
- package/package.json +1 -1
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
|
|
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
|
-
- **
|
|
49
|
-
- **
|
|
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
|
-
|
|
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.
|
|
56
|
-
3.
|
|
57
|
-
4. `git clone` the repo into `./<repo>`
|
|
58
|
-
5. Tracker type
|
|
59
|
-
6. Tracker location — Linear workspace
|
|
60
|
-
7. Tracker credentials —
|
|
61
|
-
8.
|
|
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
|
-
|
|
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
|
|
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'
|
|
62
|
-
•
|
|
63
|
-
|
|
64
|
-
•
|
|
65
|
-
|
|
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` —
|
|
1
|
+
// `agentdesk init` — always the wizard, mode-aware.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
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
|
|
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
|
|
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
|
-
// ----------
|
|
75
|
+
// ---------- Project picker (when auto-match fails and server has projects) ----------
|
|
79
76
|
|
|
80
|
-
async function
|
|
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 },
|
|
80
|
+
headers: { "x-api-key": apiKey },
|
|
81
|
+
signal: AbortSignal.timeout(8000),
|
|
86
82
|
});
|
|
87
|
-
if (!res.ok) return
|
|
88
|
-
|
|
89
|
-
} catch { return
|
|
90
|
-
|
|
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
|
-
|
|
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 &&
|
|
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 &&
|
|
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
|
-
// ----------
|
|
123
|
+
// ---------- The unified wizard ----------
|
|
126
124
|
|
|
127
|
-
async function
|
|
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(
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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 set — change 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
|
-
//
|
|
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
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
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)
|
|
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
|
-
//
|
|
173
|
+
// Step 3 — GitHub token (always editable, per-machine).
|
|
227
174
|
let env = loadDotEnv(currentCwd);
|
|
228
175
|
let githubToken = env.GITHUB_TOKEN;
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
241
|
-
if (
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
//
|
|
252
|
+
// Step 6 — Tracker location.
|
|
267
253
|
let linearWorkspace = null;
|
|
268
254
|
let jiraBaseUrl = null;
|
|
269
|
-
if (
|
|
270
|
-
linearWorkspace =
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
//
|
|
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"
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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("
|
|
329
|
+
console.log("");
|
|
300
330
|
}
|
|
301
331
|
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
//
|
|
321
|
-
//
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
345
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
//
|
|
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} (${
|
|
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 || "(
|
|
413
|
-
const envNames = Object.keys(credsAll).filter(k => ["LINEAR_API_KEY","JIRA_EMAIL","JIRA_API_TOKEN","GITHUB_TOKEN"].includes(k));
|
|
414
|
-
console.log(` .env
|
|
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
|
|
482
|
+
console.log(" Cancelled.");
|
|
420
483
|
return;
|
|
421
484
|
}
|
|
422
485
|
console.log("");
|
|
423
486
|
|
|
424
|
-
// Write .agentdesk.json
|
|
425
|
-
|
|
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
|
-
// .
|
|
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
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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(
|
|
529
|
+
registerLocalProject(computedKey, projectName, currentCwd);
|
|
457
530
|
|
|
458
531
|
console.log("");
|
|
459
|
-
const cmdPrefix = trackerTeamOrProjectId ||
|
|
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:
|
|
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
|
-
//
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
502
|
-
|
|
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}"
|
|
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
|
|
571
|
+
await runWizard({ cwd, apiKey, mode: "existing", projectKey: match.projectKey });
|
|
509
572
|
return;
|
|
510
573
|
}
|
|
511
|
-
|
|
512
|
-
// Fall through to Connect/New menu.
|
|
574
|
+
// Fall through to git-remote auto-match / picker.
|
|
513
575
|
}
|
|
514
576
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
|
540
|
-
message: "
|
|
590
|
+
const picked = await promptSelect({
|
|
591
|
+
message: "Which project is this clone?",
|
|
541
592
|
choices: [
|
|
542
|
-
{ name:
|
|
543
|
-
{ name: "Set up
|
|
544
|
-
{ name: "Cancel", value: "
|
|
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
|
-
|
|
548
|
-
if (
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
604
|
+
await runWizard({ cwd, apiKey, mode: "existing", projectKey: picked });
|
|
605
|
+
return;
|
|
556
606
|
}
|
|
557
607
|
|
|
558
|
-
|
|
608
|
+
// Account has no projects — must be a first-time user.
|
|
609
|
+
await runWizard({ cwd, apiKey, mode: "new" });
|
|
559
610
|
}
|