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