@kendoo.agentdesk/agentdesk 0.27.0 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/bin/agentdesk.mjs +35 -45
- package/cli/agents.mjs +4 -256
- package/cli/bootstrap.mjs +40 -59
- package/cli/config.mjs +29 -4
- package/cli/daemon.mjs +72 -44
- package/cli/dotenv.mjs +96 -13
- package/cli/engine/agents/index.mjs +152 -0
- package/cli/engine/claude-auth.mjs +72 -0
- package/cli/engine/env.mjs +56 -0
- package/cli/engine/events.mjs +214 -0
- package/cli/engine/hooks.mjs +119 -0
- package/cli/engine/phases/EXECUTION.md +45 -0
- package/cli/engine/phases/INTAKE.md +34 -0
- package/cli/engine/phases/PLAN.md +26 -0
- package/cli/engine/phases/REVIEW.md +21 -0
- package/cli/engine/phases/SOLO.md +115 -0
- package/cli/engine/phases/SUMMARY.md +23 -0
- package/cli/engine/prompts.mjs +181 -0
- package/cli/engine/query.mjs +63 -0
- package/cli/engine/schemas.mjs +180 -0
- package/cli/engine/session.mjs +285 -0
- package/cli/engine/spawn.mjs +83 -0
- package/cli/engine/tracker/github.md +19 -0
- package/cli/engine/tracker/jira.md +23 -0
- package/cli/engine/tracker/linear.md +24 -0
- package/cli/engine/verdict.mjs +83 -0
- package/cli/init.mjs +290 -147
- package/cli/login.mjs +11 -3
- package/cli/phase-loop.mjs +78 -0
- package/cli/proc.mjs +131 -0
- package/cli/project-key.mjs +56 -0
- package/cli/prompt.mjs +9 -503
- package/cli/prompts.mjs +20 -1
- package/cli/security-check.mjs +1 -1
- package/cli/session-isolation.mjs +65 -9
- package/cli/session-sandbox.mjs +13 -1
- package/cli/setup-helpers.mjs +83 -36
- package/cli/team.mjs +41 -34
- package/cli/tracker-check.mjs +12 -2
- package/cli/tracker-project.mjs +93 -0
- package/cli/update-check.mjs +62 -0
- package/package.json +12 -3
- package/cli/orchestrator.mjs +0 -461
- package/cli/stream-parser.mjs +0 -216
- package/prompts/phased.md +0 -549
- package/prompts/team.md +0 -505
package/cli/init.mjs
CHANGED
|
@@ -20,14 +20,18 @@
|
|
|
20
20
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
21
21
|
import { join } from "path";
|
|
22
22
|
import { loadConfig, pushConfig } from "./config.mjs";
|
|
23
|
+
import { deriveProjectKey, resolveProjectKey } from "./project-key.mjs";
|
|
24
|
+
import { getTrackerProject, setTrackerProject, resolveTrackerProject, trackerProjectNoun } from "./tracker-project.mjs";
|
|
23
25
|
import { getStoredApiKey, ensureAccountIdentity } from "./login.mjs";
|
|
24
26
|
import { registerLocalProject } from "./projects.mjs";
|
|
25
27
|
import { checkTrackerPermissions } from "./tracker-check.mjs";
|
|
26
28
|
import { autoMatchProject } from "./bootstrap.mjs";
|
|
27
|
-
import { select as promptSelect, ask, promptRequired } from "./prompts.mjs";
|
|
29
|
+
import { select as promptSelect, ask, promptRequired, promptSecret } from "./prompts.mjs";
|
|
28
30
|
import {
|
|
29
31
|
cloneRepoWithToken,
|
|
30
32
|
detectGitRepo,
|
|
33
|
+
findExistingClone,
|
|
34
|
+
ensureGitignored,
|
|
31
35
|
fetchGitHubLogin,
|
|
32
36
|
loadDotEnv,
|
|
33
37
|
parseGitHubRef,
|
|
@@ -35,10 +39,87 @@ import {
|
|
|
35
39
|
readLocalConfig,
|
|
36
40
|
runAccessChecks,
|
|
37
41
|
saveEnvVar,
|
|
42
|
+
stripRemoteCredentials,
|
|
38
43
|
} from "./setup-helpers.mjs";
|
|
39
44
|
|
|
40
45
|
const SERVER = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
|
|
41
46
|
|
|
47
|
+
// ---------- Credential prompts ----------
|
|
48
|
+
|
|
49
|
+
// Prompt for a GitHub token and verify it before accepting. Returns
|
|
50
|
+
// { githubToken, githubLogin } or { cancelled: true }.
|
|
51
|
+
//
|
|
52
|
+
// The old flow saved the token to .env *before* checking it and, on failure,
|
|
53
|
+
// printed "continuing anyway" — so a mistyped token was persisted and the
|
|
54
|
+
// user only found out when the first session failed to push.
|
|
55
|
+
async function promptGitHubTokenVerified({ ownerRepo }) {
|
|
56
|
+
const repoHint = ownerRepo ? ` (needed to push to ${ownerRepo})` : "";
|
|
57
|
+
console.log(` GitHub token${repoHint} — create one at https://github.com/settings/tokens, scope: repo`);
|
|
58
|
+
while (true) {
|
|
59
|
+
const token = await promptSecret("GitHub token");
|
|
60
|
+
const login = await fetchGitHubLogin(token);
|
|
61
|
+
if (login) {
|
|
62
|
+
console.log(` ✓ Token authenticates as @${login}`);
|
|
63
|
+
return { githubToken: token, githubLogin: login };
|
|
64
|
+
}
|
|
65
|
+
console.log(" ✗ Token did not authenticate with GitHub.");
|
|
66
|
+
const next = await promptSelect({
|
|
67
|
+
message: "What now?",
|
|
68
|
+
choices: [
|
|
69
|
+
{ name: "Re-enter the token", value: "retry" },
|
|
70
|
+
{ name: "Use it anyway (couldn't verify — maybe offline)", value: "use" },
|
|
71
|
+
{ name: "Cancel", value: "cancel" },
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
if (next === "cancel") return { cancelled: true };
|
|
75
|
+
if (next === "use") return { githubToken: token, githubLogin: null };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Verify tracker credentials, offering re-entry on failure. Same loop for new
|
|
80
|
+
// and existing projects — existing mode used to print "failed" and carry on,
|
|
81
|
+
// leaving broken credentials saved with no path to fix them in the wizard.
|
|
82
|
+
//
|
|
83
|
+
// Returns { status: "ok" | "skipped" | "cancelled", creds, identity? }.
|
|
84
|
+
async function verifyTrackerWithRetry({ tracker, config, cwd, creds }) {
|
|
85
|
+
const current = { ...creds };
|
|
86
|
+
process.stdout.write(" Verifying tracker access... ");
|
|
87
|
+
while (true) {
|
|
88
|
+
// Credentials only: the team/project is resolved right after this loop.
|
|
89
|
+
const check = await checkTrackerPermissions({ tracker, config, credentials: current, requireProject: false });
|
|
90
|
+
if (check.ok) {
|
|
91
|
+
console.log("✓");
|
|
92
|
+
if (check.identity?.name || check.identity?.email) {
|
|
93
|
+
console.log(` ✓ Posting as: ${check.identity.name || check.identity.email}`);
|
|
94
|
+
}
|
|
95
|
+
console.log("");
|
|
96
|
+
return { status: "ok", creds: current, identity: check.identity };
|
|
97
|
+
}
|
|
98
|
+
console.log("failed");
|
|
99
|
+
for (const e of check.errors || []) console.log(` • ${e}`);
|
|
100
|
+
const next = await promptSelect({
|
|
101
|
+
message: "What now?",
|
|
102
|
+
choices: [
|
|
103
|
+
{ name: "Re-enter credentials and try again", value: "retry" },
|
|
104
|
+
{ name: "Save anyway (fix later in .env)", value: "skip" },
|
|
105
|
+
{ name: "Cancel", value: "cancel" },
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
if (next === "cancel") return { status: "cancelled", creds: current };
|
|
109
|
+
if (next === "skip") { console.log(""); return { status: "skipped", creds: current }; }
|
|
110
|
+
if (tracker === "linear") {
|
|
111
|
+
current.LINEAR_API_KEY = await promptSecret("Linear API key");
|
|
112
|
+
saveEnvVar(cwd, "LINEAR_API_KEY", current.LINEAR_API_KEY);
|
|
113
|
+
} else if (tracker === "jira") {
|
|
114
|
+
current.JIRA_EMAIL = await promptRequired("Atlassian login email");
|
|
115
|
+
current.JIRA_API_TOKEN = await promptSecret("Jira API token");
|
|
116
|
+
saveEnvVar(cwd, "JIRA_EMAIL", current.JIRA_EMAIL);
|
|
117
|
+
saveEnvVar(cwd, "JIRA_API_TOKEN", current.JIRA_API_TOKEN);
|
|
118
|
+
}
|
|
119
|
+
process.stdout.write(" Re-verifying tracker access... ");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
42
123
|
// ---------- Tracker list helpers (for step 9 in new mode) ----------
|
|
43
124
|
|
|
44
125
|
async function listTrackerProjects({ tracker, creds, location }) {
|
|
@@ -87,11 +168,12 @@ async function fetchAccountProjects(apiKey) {
|
|
|
87
168
|
|
|
88
169
|
// ---------- Legacy migration ----------
|
|
89
170
|
|
|
90
|
-
|
|
171
|
+
// Every project on the account paired with its settings. Settings fetches
|
|
172
|
+
// are best-effort — a project whose settings can't be read is returned with
|
|
173
|
+
// `{}` so the caller still sees it exists.
|
|
174
|
+
async function fetchProjectsWithSettings(apiKey) {
|
|
91
175
|
const projects = await fetchAccountProjects(apiKey);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const settingsList = await Promise.all(projects.map(async p => {
|
|
176
|
+
const settings = await Promise.all(projects.map(async p => {
|
|
95
177
|
try {
|
|
96
178
|
const r = await fetch(`${SERVER}/api/projects/${p.id}/settings`, {
|
|
97
179
|
headers: { "x-api-key": apiKey }, signal: AbortSignal.timeout(5000),
|
|
@@ -99,14 +181,18 @@ async function autoMatchLegacy(cwd, apiKey, localConfig) {
|
|
|
99
181
|
return r.ok ? await r.json() : {};
|
|
100
182
|
} catch { return {}; }
|
|
101
183
|
}));
|
|
184
|
+
return projects.map((project, i) => ({ project, settings: settings[i] || {} }));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function autoMatchLegacy(cwd, apiKey, localConfig) {
|
|
188
|
+
const rows = await fetchProjectsWithSettings(apiKey);
|
|
189
|
+
if (!rows.length) return null;
|
|
102
190
|
|
|
103
191
|
const legacyRepo = (localConfig.github?.repo || "").toLowerCase();
|
|
104
192
|
const legacyTeamKey = (localConfig.linear?.teamKey || "").toLowerCase();
|
|
105
193
|
const legacyJiraProject = (localConfig.jira?.project || "").toLowerCase();
|
|
106
194
|
|
|
107
|
-
for (
|
|
108
|
-
const p = projects[i];
|
|
109
|
-
const s = settingsList[i];
|
|
195
|
+
for (const { project: p, settings: s } of rows) {
|
|
110
196
|
if (legacyRepo && (s?.github?.repo || "").toLowerCase() === legacyRepo) {
|
|
111
197
|
return { projectKey: p.id, name: p.name, reason: `github.repo matches ${s.github.repo}` };
|
|
112
198
|
}
|
|
@@ -138,7 +224,10 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
|
138
224
|
let serverConfig = null;
|
|
139
225
|
if (isExisting) {
|
|
140
226
|
process.stdout.write(" Fetching project settings from agentdesk.live... ");
|
|
141
|
-
|
|
227
|
+
// readOnly: this is a read at the top of a wizard the user may still
|
|
228
|
+
// cancel. Without it, loadConfig rewrote .agentdesk.json and pushed a
|
|
229
|
+
// heal-sync to the server before anything had been confirmed.
|
|
230
|
+
serverConfig = await loadConfig(currentCwd, { apiKey, serverUrl: SERVER, projectName: projectKey, silent: true, readOnly: true });
|
|
142
231
|
console.log("✓");
|
|
143
232
|
console.log(` Project: ${projectKey}`);
|
|
144
233
|
console.log(` Tracker: ${serverConfig.tracker || "(not set — change in agentdesk.live)"}`);
|
|
@@ -150,6 +239,19 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
|
150
239
|
// Step 1 — detect git repo (silent).
|
|
151
240
|
const git = detectGitRepo(currentCwd);
|
|
152
241
|
|
|
242
|
+
// Repair clones made by older versions, which embedded the GitHub token in
|
|
243
|
+
// the origin URL — leaving it in plaintext in .git/config indefinitely.
|
|
244
|
+
// Sessions authenticate through a credential helper; the remote never
|
|
245
|
+
// needed the token.
|
|
246
|
+
if (git.inRepo) {
|
|
247
|
+
try {
|
|
248
|
+
if (stripRemoteCredentials(currentCwd)) {
|
|
249
|
+
console.log(" ✓ Removed embedded credentials from the git remote URL (.git/config)");
|
|
250
|
+
console.log("");
|
|
251
|
+
}
|
|
252
|
+
} catch {}
|
|
253
|
+
}
|
|
254
|
+
|
|
153
255
|
// Step 2 — GitHub repo.
|
|
154
256
|
let ownerRepo = git.ownerRepo;
|
|
155
257
|
if (isExisting) {
|
|
@@ -171,42 +273,35 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
|
171
273
|
}
|
|
172
274
|
|
|
173
275
|
// Step 3 — GitHub token (always editable, per-machine).
|
|
276
|
+
//
|
|
277
|
+
// Nothing is written to disk in this step. The token is persisted once, after
|
|
278
|
+
// step 4, to whichever directory ends up being the project. Previously a
|
|
279
|
+
// freshly entered token was saved to the *current* directory's .env, and
|
|
280
|
+
// then again to the clone's — leaving a copy in a directory that isn't the
|
|
281
|
+
// project at all.
|
|
174
282
|
let env = loadDotEnv(currentCwd);
|
|
175
|
-
let githubToken = env.GITHUB_TOKEN;
|
|
176
|
-
let githubLogin = null;
|
|
283
|
+
let githubToken = env.GITHUB_TOKEN || null;
|
|
284
|
+
let githubLogin = githubToken ? await fetchGitHubLogin(githubToken) : null;
|
|
177
285
|
|
|
178
|
-
if (githubToken) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
});
|
|
189
|
-
if (
|
|
190
|
-
|
|
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}`);
|
|
286
|
+
if (githubToken && githubLogin) {
|
|
287
|
+
const choice = await promptSelect({
|
|
288
|
+
message: `GitHub token — currently authenticates as @${githubLogin}`,
|
|
289
|
+
default: "keep",
|
|
290
|
+
choices: [
|
|
291
|
+
{ name: "Keep current token", value: "keep" },
|
|
292
|
+
{ name: "Replace with a new token", value: "new" },
|
|
293
|
+
],
|
|
294
|
+
});
|
|
295
|
+
if (choice === "new") {
|
|
296
|
+
const r = await promptGitHubTokenVerified({ ownerRepo });
|
|
297
|
+
if (r.cancelled) return;
|
|
298
|
+
({ githubToken, githubLogin } = r);
|
|
201
299
|
}
|
|
202
300
|
} else {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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)");
|
|
301
|
+
if (githubToken) console.log(" ⚠ GITHUB_TOKEN in .env doesn't authenticate with GitHub — enter a new one.");
|
|
302
|
+
const r = await promptGitHubTokenVerified({ ownerRepo });
|
|
303
|
+
if (r.cancelled) return;
|
|
304
|
+
({ githubToken, githubLogin } = r);
|
|
210
305
|
}
|
|
211
306
|
console.log("");
|
|
212
307
|
|
|
@@ -216,11 +311,37 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
|
216
311
|
if (parsed) {
|
|
217
312
|
const expected = `${parsed.owner}/${parsed.repo}`.toLowerCase();
|
|
218
313
|
const haveHere = (git.ownerRepo || "").toLowerCase();
|
|
219
|
-
|
|
314
|
+
const needClone = !git.inRepo || haveHere !== expected;
|
|
315
|
+
const reuse = needClone
|
|
316
|
+
? findExistingClone({ cwd: currentCwd, owner: parsed.owner, repo: parsed.repo })
|
|
317
|
+
: null;
|
|
318
|
+
if (reuse) {
|
|
319
|
+
// The wizard was run from the directory that contains the clone
|
|
320
|
+
// (the parent of ./<repo>). Use it rather than failing with
|
|
321
|
+
// "already exists and is not empty".
|
|
322
|
+
console.log(` ✓ ./${parsed.repo} is already a clone of ${parsed.owner}/${parsed.repo} — using it.`);
|
|
323
|
+
currentCwd = reuse;
|
|
324
|
+
try {
|
|
325
|
+
if (stripRemoteCredentials(currentCwd)) {
|
|
326
|
+
console.log(" ✓ Removed embedded credentials from the git remote URL (.git/config)");
|
|
327
|
+
}
|
|
328
|
+
} catch {}
|
|
329
|
+
console.log("");
|
|
330
|
+
} else if (needClone) {
|
|
331
|
+
if (git.inRepo) {
|
|
332
|
+
// Cloning here nests one git repo inside another — almost always a
|
|
333
|
+
// sign the user ran init from the wrong directory. Confirm first.
|
|
334
|
+
const here = git.ownerRepo ? ` (${git.ownerRepo})` : "";
|
|
335
|
+
console.log(` This directory is already a git repo${here}, not ${parsed.owner}/${parsed.repo}.`);
|
|
336
|
+
const go = (await ask(` Clone ${parsed.owner}/${parsed.repo} into ./${parsed.repo} here anyway? [y/N]: `)).trim().toLowerCase();
|
|
337
|
+
if (go !== "y" && go !== "yes") {
|
|
338
|
+
console.log(" Cancelled — cd to the directory that should contain the clone and re-run `agentdesk init`.");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
220
342
|
console.log(` Cloning ${parsed.owner}/${parsed.repo} into ./${parsed.repo}...`);
|
|
221
343
|
try {
|
|
222
344
|
currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner: parsed.owner, repo: parsed.repo, token: githubToken });
|
|
223
|
-
saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
|
|
224
345
|
} catch (err) {
|
|
225
346
|
console.log(` ✗ ${err.message}`);
|
|
226
347
|
return;
|
|
@@ -230,6 +351,10 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
|
230
351
|
}
|
|
231
352
|
}
|
|
232
353
|
|
|
354
|
+
// Persist the GitHub token exactly once, to the directory that is the
|
|
355
|
+
// project. Also tightens an existing .env to 0600.
|
|
356
|
+
if (githubToken) saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
|
|
357
|
+
|
|
233
358
|
// Step 5 — Tracker type.
|
|
234
359
|
let tracker;
|
|
235
360
|
if (isExisting) {
|
|
@@ -290,12 +415,12 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
|
290
415
|
],
|
|
291
416
|
});
|
|
292
417
|
if (choice === "new") {
|
|
293
|
-
linearApiKey = await
|
|
418
|
+
linearApiKey = await promptSecret("Linear API key");
|
|
294
419
|
saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
|
|
295
420
|
}
|
|
296
421
|
} else {
|
|
297
422
|
console.log(" (Get a Linear API key: https://linear.app/settings/api)");
|
|
298
|
-
linearApiKey = await
|
|
423
|
+
linearApiKey = await promptSecret("Linear API key");
|
|
299
424
|
saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
|
|
300
425
|
}
|
|
301
426
|
console.log("");
|
|
@@ -311,7 +436,7 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
|
311
436
|
});
|
|
312
437
|
if (choice === "new") {
|
|
313
438
|
jiraEmail = await promptRequired("Atlassian login email");
|
|
314
|
-
jiraApiToken = await
|
|
439
|
+
jiraApiToken = await promptSecret("Jira API token");
|
|
315
440
|
saveEnvVar(currentCwd, "JIRA_EMAIL", jiraEmail);
|
|
316
441
|
saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
|
|
317
442
|
}
|
|
@@ -322,7 +447,7 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
|
322
447
|
saveEnvVar(currentCwd, "JIRA_EMAIL", jiraEmail);
|
|
323
448
|
}
|
|
324
449
|
if (!jiraApiToken) {
|
|
325
|
-
jiraApiToken = await
|
|
450
|
+
jiraApiToken = await promptSecret("Jira API token");
|
|
326
451
|
saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
|
|
327
452
|
}
|
|
328
453
|
}
|
|
@@ -356,7 +481,7 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
|
356
481
|
|
|
357
482
|
// Build the in-memory config (used for verification + summary; written
|
|
358
483
|
// to disk only in new mode).
|
|
359
|
-
|
|
484
|
+
let config = {
|
|
360
485
|
tracker,
|
|
361
486
|
github: { repo: ownerRepo || null, login: githubLogin || null },
|
|
362
487
|
};
|
|
@@ -366,85 +491,45 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
|
366
491
|
|
|
367
492
|
let trackerTeamOrProjectId = null;
|
|
368
493
|
let trackerTeamOrProjectName = null;
|
|
494
|
+
// Existing mode: set when the server had no team/project and the user
|
|
495
|
+
// supplied one here — the one project-wide field init is allowed to
|
|
496
|
+
// fill in, because leaving it empty breaks every session.
|
|
497
|
+
let trackerProjectFilledIn = false;
|
|
369
498
|
|
|
370
|
-
// Step 9 — Verify tracker +
|
|
499
|
+
// Step 9 — Verify tracker + resolve team/project.
|
|
371
500
|
if (tracker && tracker !== "github") {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (tracker === "jira" && trackerTeamOrProjectId) config.jira.project = trackerTeamOrProjectId;
|
|
377
|
-
|
|
378
|
-
process.stdout.write(" Verifying tracker access... ");
|
|
379
|
-
const creds = { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken };
|
|
380
|
-
const check = await checkTrackerPermissions({ tracker, config, credentials: creds });
|
|
381
|
-
if (check.ok) {
|
|
382
|
-
console.log("✓");
|
|
383
|
-
if (check.identity?.name || check.identity?.email) {
|
|
384
|
-
console.log(` ✓ Posting as: ${check.identity.name || check.identity.email}`);
|
|
385
|
-
}
|
|
386
|
-
} else {
|
|
387
|
-
console.log("failed");
|
|
388
|
-
for (const e of check.errors || []) console.log(` • ${e}`);
|
|
389
|
-
}
|
|
390
|
-
console.log("");
|
|
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
|
-
],
|
|
414
|
-
});
|
|
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
|
-
}
|
|
501
|
+
const serverProject = isExisting ? getTrackerProject(tracker, serverConfig) : null;
|
|
502
|
+
if (serverProject) {
|
|
503
|
+
config = setTrackerProject(tracker, config, serverProject);
|
|
504
|
+
}
|
|
428
505
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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("");
|
|
446
|
-
}
|
|
506
|
+
// Credentials only at this point — the project is picked right after,
|
|
507
|
+
// so the check must not demand it yet.
|
|
508
|
+
const outcome = await verifyTrackerWithRetry({
|
|
509
|
+
tracker, config, cwd: currentCwd,
|
|
510
|
+
creds: { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken },
|
|
511
|
+
});
|
|
512
|
+
if (outcome.status === "cancelled") return;
|
|
513
|
+
({ LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken } = outcome.creds);
|
|
514
|
+
|
|
515
|
+
if (isExisting && !serverProject) {
|
|
516
|
+
console.log(` ⚠ This project has no ${trackerProjectNoun(tracker)} configured on agentdesk.live — pick one now.`);
|
|
447
517
|
}
|
|
518
|
+
const resolved = await resolveTrackerProject({
|
|
519
|
+
tracker, isExisting, serverProject,
|
|
520
|
+
verified: outcome.status === "ok",
|
|
521
|
+
listProjects: () => listTrackerProjects({ tracker, creds: outcome.creds, location: jiraBaseUrl }),
|
|
522
|
+
promptSelect, promptRequired,
|
|
523
|
+
});
|
|
524
|
+
trackerTeamOrProjectId = resolved.id;
|
|
525
|
+
trackerTeamOrProjectName = resolved.name;
|
|
526
|
+
config = setTrackerProject(tracker, config, resolved.id);
|
|
527
|
+
if (resolved.source === "server") {
|
|
528
|
+
console.log(` ${tracker === "linear" ? "Team: " : "Project: "} ${resolved.id} (configured — change in agentdesk.live)`);
|
|
529
|
+
} else if (isExisting) {
|
|
530
|
+
trackerProjectFilledIn = true;
|
|
531
|
+
}
|
|
532
|
+
console.log("");
|
|
448
533
|
}
|
|
449
534
|
|
|
450
535
|
// Derive projectName + projectKey (only used in new mode).
|
|
@@ -457,8 +542,20 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
|
457
542
|
projectName = trackerTeamOrProjectName
|
|
458
543
|
? `${repoShort} — ${String(trackerTeamOrProjectName).split(" (")[0]}`
|
|
459
544
|
: repoShort;
|
|
460
|
-
|
|
461
|
-
|
|
545
|
+
|
|
546
|
+
// The tracker-derived key is not unique across repos: a second repo on
|
|
547
|
+
// the same Linear team used to get the same key, and its settings push
|
|
548
|
+
// overwrote the first repo's. Check the account before committing to it.
|
|
549
|
+
const baseKey = deriveProjectKey({ trackerId: trackerTeamOrProjectId, repoShort });
|
|
550
|
+
const existing = (await fetchProjectsWithSettings(apiKey))
|
|
551
|
+
.map(({ project, settings }) => ({ id: project.id, repo: settings?.github?.repo || null }));
|
|
552
|
+
const resolved = resolveProjectKey({ baseKey, repo: ownerRepo, repoShort, existing });
|
|
553
|
+
computedKey = resolved.key;
|
|
554
|
+
if (resolved.renamed) {
|
|
555
|
+
console.log(` ⚠ Project id "${baseKey}" already belongs to ${resolved.collidedWith.repo} on your account.`);
|
|
556
|
+
console.log(` Using "${computedKey}" for this repo so the two don't overwrite each other.`);
|
|
557
|
+
console.log("");
|
|
558
|
+
}
|
|
462
559
|
}
|
|
463
560
|
config.projectKey = computedKey;
|
|
464
561
|
|
|
@@ -477,10 +574,24 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
|
477
574
|
console.log(` .env: ${envNames.join(", ") || "(none)"}`);
|
|
478
575
|
console.log("");
|
|
479
576
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
577
|
+
// Default the confirmation to "no" when any access check failed. Saving a
|
|
578
|
+
// config that can't reach GitHub or the tracker just moves the failure to
|
|
579
|
+
// the first session, where it is much harder to diagnose.
|
|
580
|
+
const anyFailed = !results.agentdesk?.ok || !results.claude?.ok || !results.github?.ok
|
|
581
|
+
|| (results.tracker && !results.tracker.ok && !results.tracker.skipped);
|
|
582
|
+
if (anyFailed) {
|
|
583
|
+
console.log(" ⚠ Some access checks failed. Sessions will not run until they pass.");
|
|
584
|
+
const save = (await ask(" Save this config anyway? [y/N]: ")).trim().toLowerCase();
|
|
585
|
+
if (save !== "y" && save !== "yes") {
|
|
586
|
+
console.log(" Cancelled.");
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
const save = (await ask(" Save this config? [Y/n]: ")).trim().toLowerCase();
|
|
591
|
+
if (save && save !== "y" && save !== "yes") {
|
|
592
|
+
console.log(" Cancelled.");
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
484
595
|
}
|
|
485
596
|
console.log("");
|
|
486
597
|
|
|
@@ -497,38 +608,65 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
|
497
608
|
if (existsSync(configPath)) {
|
|
498
609
|
try { existingOnDisk = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
|
|
499
610
|
}
|
|
500
|
-
|
|
611
|
+
let merged = { ...existingOnDisk, projectKey: computedKey };
|
|
612
|
+
if (trackerProjectFilledIn) merged = setTrackerProject(tracker, merged, trackerTeamOrProjectId);
|
|
501
613
|
writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n");
|
|
502
614
|
} else {
|
|
503
615
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
504
616
|
}
|
|
505
617
|
console.log(" ✓ Saved .agentdesk.json");
|
|
506
618
|
|
|
507
|
-
//
|
|
508
|
-
|
|
619
|
+
// .env now holds a GitHub token and tracker credentials; .agentdesk/ is
|
|
620
|
+
// runtime state. Neither may be committed. Only .agentdesk/ used to be
|
|
621
|
+
// added here — on a fresh clone with no .gitignore, the next `git add .`
|
|
622
|
+
// would have committed every token this wizard just collected.
|
|
509
623
|
try {
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
const nl = existing.endsWith("\n") || !existing ? "" : "\n";
|
|
513
|
-
writeFileSync(gitignorePath, `${existing}${nl}.agentdesk/\n`);
|
|
514
|
-
console.log(" ✓ Added .agentdesk/ to .gitignore");
|
|
515
|
-
}
|
|
624
|
+
const added = ensureGitignored(currentCwd, [".env", ".agentdesk/"]);
|
|
625
|
+
for (const entry of added) console.log(` ✓ Added ${entry} to .gitignore`);
|
|
516
626
|
} catch {}
|
|
517
627
|
|
|
518
|
-
// Server sync
|
|
519
|
-
//
|
|
628
|
+
// Server sync.
|
|
629
|
+
// • Existing mode pushes nothing — every project-wide field was locked
|
|
630
|
+
// and unchanged — except when the server had no team/project and the
|
|
631
|
+
// user just filled it in. That one field goes up, merged into the
|
|
632
|
+
// server's own settings so nothing else is touched.
|
|
633
|
+
// • New mode registers the project and pushes the full config.
|
|
634
|
+
if (isExisting && trackerProjectFilledIn) {
|
|
635
|
+
const payload = setTrackerProject(tracker, serverConfig, trackerTeamOrProjectId);
|
|
636
|
+
const push = await pushConfig(apiKey, SERVER, computedKey, payload);
|
|
637
|
+
if (push.ok) {
|
|
638
|
+
console.log(` ✓ Saved ${trackerProjectNoun(tracker)} "${trackerTeamOrProjectId}" to agentdesk.live`);
|
|
639
|
+
} else if (push.error !== "missing_auth") {
|
|
640
|
+
console.log(` ⚠ Could not save the ${trackerProjectNoun(tracker)} to agentdesk.live: ${push.error}`);
|
|
641
|
+
console.log(" It is in .agentdesk.json; the next session will retry the sync.");
|
|
642
|
+
}
|
|
643
|
+
}
|
|
520
644
|
if (!isExisting) {
|
|
521
645
|
try {
|
|
522
|
-
await fetch(`${SERVER}/api/projects`, {
|
|
646
|
+
const reg = await fetch(`${SERVER}/api/projects`, {
|
|
523
647
|
method: "POST",
|
|
524
648
|
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
|
525
649
|
body: JSON.stringify({ id: computedKey, name: projectName, path: currentCwd, tracker }),
|
|
650
|
+
signal: AbortSignal.timeout(8000),
|
|
526
651
|
});
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
652
|
+
if (!reg.ok) {
|
|
653
|
+
// This response used to go unchecked, so a 403 ("id belongs to
|
|
654
|
+
// another account") still ended with "✓ Registered".
|
|
655
|
+
let detail = `HTTP ${reg.status}`;
|
|
656
|
+
try { const d = await reg.json(); if (d?.error) detail = d.error; } catch {}
|
|
657
|
+
console.log(` ✗ Could not register "${computedKey}" on agentdesk.live: ${detail}`);
|
|
658
|
+
if (reg.status === 403) {
|
|
659
|
+
console.log(" That project id belongs to another account. Re-run `agentdesk init --force-full`");
|
|
660
|
+
console.log(" and pick a different tracker team/project, or ask the owner to add you.");
|
|
661
|
+
}
|
|
662
|
+
console.log(" Local config saved; nothing was pushed to the server.");
|
|
530
663
|
} else {
|
|
531
|
-
|
|
664
|
+
const push = await pushConfig(apiKey, SERVER, computedKey, config);
|
|
665
|
+
if (!push.ok && push.error !== "missing_auth") {
|
|
666
|
+
console.log(` ⚠ Registered, but could not push settings to agentdesk.live: ${push.error}`);
|
|
667
|
+
} else {
|
|
668
|
+
console.log(" ✓ Registered with agentdesk.live");
|
|
669
|
+
}
|
|
532
670
|
}
|
|
533
671
|
} catch {
|
|
534
672
|
console.log(" ⚠ Couldn't reach agentdesk.live — local config saved, retry later.");
|
|
@@ -541,9 +679,14 @@ async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
|
541
679
|
registerLocalProject(computedKey, projectName, currentCwd, accountId);
|
|
542
680
|
|
|
543
681
|
console.log("");
|
|
544
|
-
const cmdPrefix = trackerTeamOrProjectId || computedKey.toUpperCase();
|
|
545
682
|
console.log(" Ready. Run a team session:");
|
|
546
|
-
|
|
683
|
+
if (trackerTeamOrProjectId) {
|
|
684
|
+
console.log(` agentdesk team ${trackerTeamOrProjectId}-123`);
|
|
685
|
+
} else if (tracker === "github") {
|
|
686
|
+
console.log(" agentdesk team 123 (a GitHub issue number)");
|
|
687
|
+
} else {
|
|
688
|
+
console.log(' agentdesk team -d "Describe the task"');
|
|
689
|
+
}
|
|
547
690
|
console.log("");
|
|
548
691
|
}
|
|
549
692
|
|
package/cli/login.mjs
CHANGED
|
@@ -185,11 +185,19 @@ export async function runLogin() {
|
|
|
185
185
|
console.log("");
|
|
186
186
|
console.log(" Waiting for authentication...");
|
|
187
187
|
|
|
188
|
-
// Open browser (use execFile to avoid shell injection)
|
|
188
|
+
// Open browser (use execFile to avoid shell injection).
|
|
189
|
+
//
|
|
190
|
+
// On Windows `start` is a cmd.exe builtin, not an executable, so
|
|
191
|
+
// execFile("start", …) failed silently and the browser never opened. Go
|
|
192
|
+
// through cmd, with the empty "" title argument `start` requires when the
|
|
193
|
+
// next argument is a quoted URL.
|
|
189
194
|
const { execFile } = await import("child_process");
|
|
190
195
|
const platform = process.platform;
|
|
191
|
-
|
|
192
|
-
|
|
196
|
+
if (platform === "win32") {
|
|
197
|
+
execFile("cmd", ["/c", "start", "", loginUrl], () => {});
|
|
198
|
+
} else {
|
|
199
|
+
execFile(platform === "darwin" ? "open" : "xdg-open", [loginUrl], () => {});
|
|
200
|
+
}
|
|
193
201
|
|
|
194
202
|
// Timeout after 5 minutes
|
|
195
203
|
setTimeout(() => {
|