@kendoo.agentdesk/agentdesk 0.19.8 → 0.20.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 +18 -0
- package/README.md +23 -6
- package/bin/agentdesk.mjs +9 -10
- package/cli/bootstrap.mjs +11 -10
- package/cli/init.mjs +432 -548
- package/cli/setup-helpers.mjs +243 -0
- package/package.json +1 -1
package/cli/init.mjs
CHANGED
|
@@ -1,174 +1,49 @@
|
|
|
1
|
-
// `agentdesk init` —
|
|
1
|
+
// `agentdesk init` — unified setup entry point.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// agentdesk
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// Three possible outcomes, decided automatically:
|
|
4
|
+
// 1. Modern .agentdesk.json with projectKey → "Refresh tokens" is the
|
|
5
|
+
// default option in a small menu; picking it runs gapScan.
|
|
6
|
+
// 2. Legacy .agentdesk.json without projectKey → try to match an account
|
|
7
|
+
// project by github.repo / git remote / tracker team → migrate the
|
|
8
|
+
// file forward and gapScan.
|
|
9
|
+
// 3. No local config → auto-match by git remote. On match, the user
|
|
10
|
+
// confirms "use this project?"; on no match the user picks from
|
|
11
|
+
// their participating projects, or opts into the full new-project
|
|
12
|
+
// wizard.
|
|
13
|
+
//
|
|
14
|
+
// The full wizard is 10 tiny steps (git detect, GitHub repo+token,
|
|
15
|
+
// optional clone, tracker type/location/creds, signature, verify+pick,
|
|
16
|
+
// review+save). Each step does one thing so re-entry after Ctrl-C is sane.
|
|
8
17
|
|
|
9
18
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
10
19
|
import { join } from "path";
|
|
11
|
-
import { createInterface } from "readline";
|
|
12
20
|
import { execSync } from "child_process";
|
|
13
|
-
import { detectProject } from "./detect.mjs";
|
|
14
21
|
import { loadConfig, pushConfig } from "./config.mjs";
|
|
15
22
|
import { getStoredApiKey } from "./login.mjs";
|
|
16
23
|
import { registerLocalProject } from "./projects.mjs";
|
|
17
|
-
import { checkTrackerPermissions
|
|
18
|
-
import { autoMatchProject, discoverProject, gapScan
|
|
19
|
-
import { select as promptSelect } from "./prompts.mjs";
|
|
24
|
+
import { checkTrackerPermissions } from "./tracker-check.mjs";
|
|
25
|
+
import { autoMatchProject, discoverProject, gapScan } from "./bootstrap.mjs";
|
|
26
|
+
import { select as promptSelect, ask, promptRequired } from "./prompts.mjs";
|
|
27
|
+
import {
|
|
28
|
+
cloneRepoWithToken,
|
|
29
|
+
detectGitRepo,
|
|
30
|
+
fetchGitHubLogin,
|
|
31
|
+
loadDotEnv,
|
|
32
|
+
parseGitHubRef,
|
|
33
|
+
printAccessChecks,
|
|
34
|
+
readLocalConfig,
|
|
35
|
+
runAccessChecks,
|
|
36
|
+
saveEnvVar,
|
|
37
|
+
writeMinimalLocalConfig,
|
|
38
|
+
} from "./setup-helpers.mjs";
|
|
20
39
|
|
|
21
40
|
const SERVER = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
|
|
22
41
|
|
|
23
|
-
//
|
|
24
|
-
const TRACKER_GUIDANCE = {
|
|
25
|
-
linear: {
|
|
26
|
-
name: "Linear",
|
|
27
|
-
dedicatedSteps: [
|
|
28
|
-
"1. Open https://linear.app/<your-workspace>/settings/members",
|
|
29
|
-
"2. Invite a new member named \"AgentDesk\" to an email you control",
|
|
30
|
-
"3. Accept the invite from that email, log in as the new user",
|
|
31
|
-
"4. Go to https://linear.app/settings/api → Create key → label it \"AgentDesk\"",
|
|
32
|
-
"5. Paste the key when prompted below",
|
|
33
|
-
],
|
|
34
|
-
personalNote: "Your own Linear account. Comments on tickets will appear under your name.",
|
|
35
|
-
},
|
|
36
|
-
jira: {
|
|
37
|
-
name: "Jira",
|
|
38
|
-
dedicatedSteps: [
|
|
39
|
-
"1. Open https://admin.atlassian.com/ → Directory → Invite users",
|
|
40
|
-
"2. Invite an email you control (e.g. agentdesk@yourdomain.com) as \"AgentDesk\"",
|
|
41
|
-
"3. Accept the invite and sign in as the new user",
|
|
42
|
-
"4. Go to https://id.atlassian.com/manage-profile/security/api-tokens → Create API token",
|
|
43
|
-
"5. Paste the email + token when prompted below",
|
|
44
|
-
],
|
|
45
|
-
personalNote: "Your own Jira account. Comments on tickets will appear under your name.",
|
|
46
|
-
},
|
|
47
|
-
github: {
|
|
48
|
-
name: "GitHub",
|
|
49
|
-
dedicatedSteps: [
|
|
50
|
-
"GitHub enforces one account per human, so a \"dedicated bot user\" must be",
|
|
51
|
-
"a GitHub App (first-class bot identity, shows as agentdesk[bot] on comments).",
|
|
52
|
-
"That's a deeper install — not available yet in this wizard. For now:",
|
|
53
|
-
" • Option A: Use gh CLI authenticated as yourself (quickest to get started)",
|
|
54
|
-
" • Option B: Generate a Personal Access Token scoped to the repo",
|
|
55
|
-
],
|
|
56
|
-
personalNote: "Your own GitHub account via gh CLI or a PAT. Comments appear under your name.",
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
function printDedicatedUserChoice(tracker, quick) {
|
|
61
|
-
if (quick || !tracker) return;
|
|
62
|
-
const g = TRACKER_GUIDANCE[tracker];
|
|
63
|
-
if (!g) return;
|
|
64
|
-
console.log("");
|
|
65
|
-
console.log(` ${g.name} account — who will post the comments?`);
|
|
66
|
-
console.log(" ─────────────────────────────────────────────");
|
|
67
|
-
console.log("");
|
|
68
|
-
console.log(" Recommended: Dedicated \"AgentDesk\" user");
|
|
69
|
-
console.log(" Clean audit trail, survives team changes, clear attribution.");
|
|
70
|
-
for (const line of g.dedicatedSteps) console.log(` ${line}`);
|
|
71
|
-
console.log("");
|
|
72
|
-
console.log(" Quick start: Your own account");
|
|
73
|
-
console.log(` ${g.personalNote}`);
|
|
74
|
-
console.log("");
|
|
75
|
-
console.log(" (This wizard doesn't care which you pick — it only asks for the");
|
|
76
|
-
console.log(" credential. You're deciding whose credential to paste.)");
|
|
77
|
-
console.log("");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function printIdentityEcho(identity) {
|
|
81
|
-
if (!identity) return;
|
|
82
|
-
const label = identity.name || identity.login || identity.email || "(unknown)";
|
|
83
|
-
const extra = identity.email && identity.email !== identity.name ? ` <${identity.email}>` : "";
|
|
84
|
-
console.log(` ✓ Posting as: ${label}${extra}`);
|
|
85
|
-
const looksLikePerson =
|
|
86
|
-
identity.name && !/agent\s*desk|agentdesk|bot|service/i.test(identity.name) &&
|
|
87
|
-
!/(^|[.+])(agentdesk|bot|svc|service)(@|$|[.+])/i.test(identity.email || "");
|
|
88
|
-
if (looksLikePerson) {
|
|
89
|
-
console.log(" (Looks like a personal account — comments will be attributed to this user.");
|
|
90
|
-
console.log(" If you'd rather have a dedicated AgentDesk identity, rotate the credential");
|
|
91
|
-
console.log(" to one generated under a dedicated tracker user and re-run `agentdesk init`.)");
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function loadApiKey(dir) {
|
|
96
|
-
const envPath = join(dir, ".env");
|
|
97
|
-
if (!existsSync(envPath)) return getStoredApiKey();
|
|
98
|
-
const match = readFileSync(envPath, "utf-8").match(/AGENTDESK_API_KEY=(.+)/);
|
|
99
|
-
return match?.[1]?.trim() || getStoredApiKey();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function ask(rl, question) {
|
|
103
|
-
return new Promise(resolve => rl.question(question, resolve));
|
|
104
|
-
}
|
|
42
|
+
// ---------- Tracker team/project picker helpers ----------
|
|
105
43
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const re = new RegExp(`^${key}=.*$`, "m");
|
|
110
|
-
if (re.test(content)) {
|
|
111
|
-
content = content.replace(re, `${key}=${value}`);
|
|
112
|
-
} else {
|
|
113
|
-
content += `${content && !content.endsWith("\n") ? "\n" : ""}${key}=${value}\n`;
|
|
114
|
-
}
|
|
115
|
-
writeFileSync(envPath, content);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function loadDotEnv(dir) {
|
|
119
|
-
const envPath = join(dir, ".env");
|
|
120
|
-
const dotEnv = {};
|
|
121
|
-
if (existsSync(envPath)) {
|
|
122
|
-
for (const line of readFileSync(envPath, "utf-8").split("\n")) {
|
|
123
|
-
const trimmed = line.trim();
|
|
124
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
125
|
-
const eq = trimmed.indexOf("=");
|
|
126
|
-
if (eq !== -1) dotEnv[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return dotEnv;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function fetchServerCreds(apiKey, projectKey) {
|
|
133
|
-
if (!apiKey) return {};
|
|
134
|
-
try {
|
|
135
|
-
const res = await fetch(`${SERVER}/api/projects/${projectKey}/settings/credentials`, {
|
|
136
|
-
headers: { "x-api-key": apiKey },
|
|
137
|
-
signal: AbortSignal.timeout(5000),
|
|
138
|
-
});
|
|
139
|
-
if (res.ok) return await res.json();
|
|
140
|
-
} catch {}
|
|
141
|
-
return {};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Render a numbered-step box of instructions with exact URLs and button names.
|
|
145
|
-
function printSteps(title, steps) {
|
|
146
|
-
const pad = " ";
|
|
147
|
-
console.log(`${pad}${title}`);
|
|
148
|
-
console.log(`${pad}${"─".repeat(title.length)}`);
|
|
149
|
-
for (let i = 0; i < steps.length; i++) {
|
|
150
|
-
console.log(`${pad} ${i + 1}. ${steps[i]}`);
|
|
151
|
-
}
|
|
152
|
-
console.log("");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Try to detect the current git remote (origin) and parse owner/repo from
|
|
156
|
-
// known GitHub URL forms. Returns "owner/repo" or null. Used as a default
|
|
157
|
-
// suggestion in the GitHub step.
|
|
158
|
-
function detectGitRemote(dir) {
|
|
159
|
-
try {
|
|
160
|
-
const url = execSync("git remote get-url origin", { cwd: dir, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
|
|
161
|
-
let m = url.match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
162
|
-
if (m) return `${m[1]}/${m[2]}`;
|
|
163
|
-
} catch {}
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Fetch the set of "things to pick from" per tracker once the auth has been
|
|
168
|
-
// verified. For Linear this is teams; Jira is projects; GitHub Issues is
|
|
169
|
-
// repos (filtered to ones the token can access). Returns an array of
|
|
170
|
-
// { id, name, raw } or null if listing failed — caller falls back to a
|
|
171
|
-
// manual prompt.
|
|
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).
|
|
172
47
|
async function listTrackerProjects({ tracker, creds, location }) {
|
|
173
48
|
if (tracker === "linear") {
|
|
174
49
|
if (!creds.LINEAR_API_KEY) return null;
|
|
@@ -181,12 +56,11 @@ async function listTrackerProjects({ tracker, creds, location }) {
|
|
|
181
56
|
});
|
|
182
57
|
if (!res.ok) return null;
|
|
183
58
|
const data = await res.json();
|
|
184
|
-
|
|
185
|
-
return nodes.map(t => ({ id: t.key, name: `${t.name} (${t.key})`, raw: t }));
|
|
59
|
+
return (data?.data?.teams?.nodes || []).map(t => ({ id: t.key, name: `${t.name} (${t.key})` }));
|
|
186
60
|
} catch { return null; }
|
|
187
61
|
}
|
|
188
|
-
if (tracker === "jira") {
|
|
189
|
-
if (!creds.JIRA_EMAIL || !creds.JIRA_API_TOKEN
|
|
62
|
+
if (tracker === "jira" && location) {
|
|
63
|
+
if (!creds.JIRA_EMAIL || !creds.JIRA_API_TOKEN) return null;
|
|
190
64
|
try {
|
|
191
65
|
const auth = "Basic " + Buffer.from(`${creds.JIRA_EMAIL}:${creds.JIRA_API_TOKEN}`).toString("base64");
|
|
192
66
|
const res = await fetch(`${location.replace(/\/+$/, "")}/rest/api/3/project/search?maxResults=50`, {
|
|
@@ -195,195 +69,191 @@ async function listTrackerProjects({ tracker, creds, location }) {
|
|
|
195
69
|
});
|
|
196
70
|
if (!res.ok) return null;
|
|
197
71
|
const data = await res.json();
|
|
198
|
-
|
|
199
|
-
return values.map(p => ({ id: p.key, name: `${p.name} (${p.key})`, raw: p }));
|
|
72
|
+
return (data?.values || []).map(p => ({ id: p.key, name: `${p.name} (${p.key})` }));
|
|
200
73
|
} catch { return null; }
|
|
201
74
|
}
|
|
202
|
-
if (tracker === "github") {
|
|
203
|
-
// GitHub has potentially thousands of repos; don't try to list — rely on
|
|
204
|
-
// the explicit repo prompt (which has a git-remote default anyway).
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
75
|
return null;
|
|
208
76
|
}
|
|
209
77
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
78
|
+
// ---------- Legacy migration: auto-match a config without projectKey ----------
|
|
79
|
+
|
|
80
|
+
async function autoMatchLegacy(cwd, apiKey, localConfig) {
|
|
81
|
+
// Fetch the user's projects once; filter on server-known fields.
|
|
82
|
+
let projects;
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetch(`${SERVER}/api/projects`, {
|
|
85
|
+
headers: { "x-api-key": apiKey }, signal: AbortSignal.timeout(8000),
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) return null;
|
|
88
|
+
projects = await res.json();
|
|
89
|
+
} catch { return null; }
|
|
90
|
+
if (!projects?.length) return null;
|
|
217
91
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
92
|
+
const settingsList = await Promise.all(projects.map(async p => {
|
|
93
|
+
try {
|
|
94
|
+
const r = await fetch(`${SERVER}/api/projects/${p.id}/settings`, {
|
|
95
|
+
headers: { "x-api-key": apiKey }, signal: AbortSignal.timeout(5000),
|
|
96
|
+
});
|
|
97
|
+
return r.ok ? await r.json() : {};
|
|
98
|
+
} catch { return {}; }
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
const legacyRepo = (localConfig.github?.repo || "").toLowerCase();
|
|
102
|
+
const legacyTeamKey = (localConfig.linear?.teamKey || "").toLowerCase();
|
|
103
|
+
const legacyJiraProject = (localConfig.jira?.project || "").toLowerCase();
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < projects.length; i++) {
|
|
106
|
+
const p = projects[i];
|
|
107
|
+
const s = settingsList[i];
|
|
108
|
+
const serverRepo = (s?.github?.repo || "").toLowerCase();
|
|
109
|
+
const serverTeam = (s?.linear?.teamKey || "").toLowerCase();
|
|
110
|
+
const serverJira = (s?.jira?.project || "").toLowerCase();
|
|
111
|
+
|
|
112
|
+
if (legacyRepo && serverRepo === legacyRepo) {
|
|
113
|
+
return { projectKey: p.id, name: p.name, reason: `github.repo matches ${s.github.repo}` };
|
|
114
|
+
}
|
|
115
|
+
if (legacyTeamKey && serverTeam === legacyTeamKey) {
|
|
116
|
+
return { projectKey: p.id, name: p.name, reason: `Linear team matches ${s.linear.teamKey}` };
|
|
117
|
+
}
|
|
118
|
+
if (legacyJiraProject && serverJira === legacyJiraProject) {
|
|
119
|
+
return { projectKey: p.id, name: p.name, reason: `Jira project matches ${s.jira.project}` };
|
|
120
|
+
}
|
|
225
121
|
}
|
|
122
|
+
return null;
|
|
226
123
|
}
|
|
227
124
|
|
|
228
|
-
//
|
|
229
|
-
// available teams/projects and let the user pick from a list.
|
|
230
|
-
async function verifyAndPickTrackerProject({ rl, cwd, finalProjectKey, tracker, config, location }) {
|
|
231
|
-
console.log(" Verifying tracker access...");
|
|
232
|
-
const dotEnv = loadDotEnv(cwd);
|
|
233
|
-
const apiKey = loadApiKey(cwd);
|
|
234
|
-
const serverCreds = await fetchServerCreds(apiKey, finalProjectKey);
|
|
235
|
-
// .env wins over server credentials — fresh local rotation beats stale server state.
|
|
236
|
-
const credentials = resolveCredentialsFromEnv({ ...serverCreds, ...dotEnv });
|
|
237
|
-
const check = await checkTrackerPermissions({ tracker, config, credentials });
|
|
238
|
-
if (!check.ok) {
|
|
239
|
-
console.log(" ⚠ " + (check.errors[0] || "Verification failed"));
|
|
240
|
-
for (const e of check.errors.slice(1)) console.log(` • ${e}`);
|
|
241
|
-
console.log("");
|
|
242
|
-
return { ok: false };
|
|
243
|
-
}
|
|
244
|
-
console.log(" ✓ Tracker access verified");
|
|
245
|
-
printIdentityEcho(check.identity);
|
|
246
|
-
console.log("");
|
|
125
|
+
// ---------- Existing-project flow (E3–E7) ----------
|
|
247
126
|
|
|
248
|
-
|
|
249
|
-
|
|
127
|
+
async function runExistingFlow(cwd, apiKey, projectKey) {
|
|
128
|
+
let currentCwd = cwd;
|
|
250
129
|
|
|
251
|
-
// Fetch selectable items (Linear teams / Jira projects) and let user pick.
|
|
252
|
-
const items = await listTrackerProjects({ tracker, creds: credentials, location });
|
|
253
|
-
if (!items || items.length === 0) {
|
|
254
|
-
console.log(" Couldn't list projects automatically — type the key manually.");
|
|
255
|
-
const label = tracker === "linear" ? "Linear team key (e.g. KEN)" : "Jira project key (e.g. PROJ)";
|
|
256
|
-
const manual = await promptRequired(rl, label);
|
|
257
|
-
console.log("");
|
|
258
|
-
return { ok: true, trackerProjectId: manual };
|
|
259
|
-
}
|
|
260
130
|
console.log("");
|
|
261
|
-
|
|
131
|
+
console.log(" AgentDesk — connecting to existing project");
|
|
132
|
+
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
262
133
|
console.log("");
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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}`);
|
|
266
143
|
console.log("");
|
|
267
|
-
return { ok: true, trackerProjectId: manual };
|
|
268
|
-
}
|
|
269
144
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
// Fast path A: `.agentdesk.json` already has a projectKey. Offer token
|
|
281
|
-
// refresh (bootstrap) as the default action so second-machine users — or
|
|
282
|
-
// anyone just rotating a token — don't have to walk the whole wizard.
|
|
283
|
-
if (hasConfig && !quick && !forceFull) {
|
|
284
|
-
let localKey = null;
|
|
285
|
-
try { localKey = JSON.parse(readFileSync(configPath, "utf-8"))?.projectKey || null; } catch {}
|
|
286
|
-
if (localKey) {
|
|
287
|
-
console.log("");
|
|
288
|
-
console.log(" AgentDesk — existing project detected");
|
|
289
|
-
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
290
|
-
console.log("");
|
|
291
|
-
const choice = await promptSelect({
|
|
292
|
-
message: "What would you like to do?",
|
|
293
|
-
default: "refresh",
|
|
294
|
-
choices: [
|
|
295
|
-
{ name: "Refresh tokens (update .env only — recommended)", value: "refresh" },
|
|
296
|
-
{ name: "Tracker only (change tracker or credentials)", value: "tracker" },
|
|
297
|
-
{ name: "Full setup (walk the whole wizard)", value: "full" },
|
|
298
|
-
{ name: "Cancel", value: "cancel" },
|
|
299
|
-
],
|
|
300
|
-
});
|
|
301
|
-
console.log("");
|
|
302
|
-
if (choice === "cancel") return;
|
|
303
|
-
if (choice === "refresh") {
|
|
304
|
-
const apiKey = getStoredApiKey();
|
|
305
|
-
await gapScan({ cwd, apiKey, projectKey: localKey });
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
editScope = choice; // "full" or "tracker"
|
|
309
|
-
}
|
|
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");
|
|
310
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)");
|
|
159
|
+
console.log("");
|
|
311
160
|
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (
|
|
320
|
-
console.log(
|
|
321
|
-
|
|
322
|
-
console.log(
|
|
323
|
-
console.log("");
|
|
324
|
-
writeProjectConfig(cwd, match.projectKey);
|
|
325
|
-
await gapScan({ cwd, apiKey, projectKey: match.projectKey });
|
|
326
|
-
return;
|
|
161
|
+
// --- E4: Ensure correct repo is locally present ---
|
|
162
|
+
const serverRepo = config.github?.repo ? parseGitHubRef(config.github.repo) : null;
|
|
163
|
+
if (serverRepo) {
|
|
164
|
+
const here = detectGitRepo(currentCwd);
|
|
165
|
+
const expected = `${serverRepo.owner}/${serverRepo.repo}`.toLowerCase();
|
|
166
|
+
const haveHere = (here.ownerRepo || "").toLowerCase();
|
|
167
|
+
if (!here.inRepo || haveHere !== expected) {
|
|
168
|
+
if (here.inRepo && haveHere && haveHere !== expected) {
|
|
169
|
+
console.log(` Current directory holds ${haveHere} — cloning ${expected} into ./${serverRepo.repo} instead.`);
|
|
170
|
+
} else {
|
|
171
|
+
console.log(` Cloning ${expected} into ./${serverRepo.repo}...`);
|
|
327
172
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
// a duplicate project when they just wanted to attach a second machine.
|
|
333
|
-
console.log("");
|
|
334
|
-
console.log(" AgentDesk — new directory");
|
|
335
|
-
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
336
|
-
console.log("");
|
|
337
|
-
const topChoice = await promptSelect({
|
|
338
|
-
message: "What would you like to do?",
|
|
339
|
-
choices: [
|
|
340
|
-
{ name: "Connect this repo to an existing project on my account (bootstrap)", value: "connect" },
|
|
341
|
-
{ name: "Set up this as a new project (full wizard)", value: "new" },
|
|
342
|
-
{ name: "Cancel", value: "cancel" },
|
|
343
|
-
],
|
|
344
|
-
});
|
|
345
|
-
console.log("");
|
|
346
|
-
if (topChoice === "cancel") return;
|
|
347
|
-
if (topChoice === "connect") {
|
|
348
|
-
const projectKey = await discoverProject(cwd, apiKey);
|
|
349
|
-
if (!projectKey) return;
|
|
350
|
-
console.log("");
|
|
351
|
-
await gapScan({ cwd, apiKey, projectKey });
|
|
173
|
+
try {
|
|
174
|
+
currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner: serverRepo.owner, repo: serverRepo.repo, token: githubToken });
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.log(` ✗ ${err.message}`);
|
|
352
177
|
return;
|
|
353
178
|
}
|
|
354
|
-
//
|
|
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("");
|
|
355
183
|
}
|
|
356
184
|
}
|
|
357
185
|
|
|
358
|
-
|
|
186
|
+
// Ensure .agentdesk.json in currentCwd carries the projectKey so loadConfig
|
|
187
|
+
// works on subsequent runs without needing discovery.
|
|
188
|
+
writeMinimalLocalConfig(currentCwd, projectKey);
|
|
359
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 });
|
|
360
197
|
console.log("");
|
|
361
|
-
|
|
362
|
-
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------- New-project wizard (steps 1–10) ----------
|
|
201
|
+
|
|
202
|
+
async function runNewProjectWizard(cwd, apiKey) {
|
|
203
|
+
let currentCwd = cwd;
|
|
204
|
+
|
|
363
205
|
console.log("");
|
|
364
|
-
console.log(
|
|
365
|
-
console.log(
|
|
366
|
-
console.log(` Directory: ${project.dir}`);
|
|
367
|
-
console.log(` Git: ${project.hasGit ? "yes" : "no"}`);
|
|
368
|
-
console.log(` CLAUDE.md: ${project.hasClaudeMd ? "yes" : "no (recommended)"}`);
|
|
206
|
+
console.log(" AgentDesk — new project setup");
|
|
207
|
+
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
369
208
|
console.log("");
|
|
370
209
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
210
|
+
// --- Step 1: Detect git repo ---
|
|
211
|
+
const git = detectGitRepo(currentCwd);
|
|
212
|
+
let ownerRepo = git.ownerRepo;
|
|
213
|
+
|
|
214
|
+
// --- Step 2: Ask for the GitHub repo (only if no git repo) ---
|
|
215
|
+
if (!git.inRepo) {
|
|
216
|
+
console.log(" No git repo here yet.\n");
|
|
217
|
+
while (!ownerRepo) {
|
|
218
|
+
const input = await promptRequired("GitHub repo (owner/repo or URL)");
|
|
219
|
+
const parsed = parseGitHubRef(input);
|
|
220
|
+
if (parsed) { ownerRepo = `${parsed.owner}/${parsed.repo}`; }
|
|
221
|
+
else console.log(" Not a valid GitHub repo. Only github.com is supported right now.");
|
|
222
|
+
}
|
|
223
|
+
console.log("");
|
|
224
|
+
}
|
|
382
225
|
|
|
383
|
-
|
|
226
|
+
// --- Step 3: GitHub token ---
|
|
227
|
+
let env = loadDotEnv(currentCwd);
|
|
228
|
+
let githubToken = env.GITHUB_TOKEN;
|
|
229
|
+
if (!githubToken) {
|
|
230
|
+
console.log(" GitHub token (create one at https://github.com/settings/tokens — scope: repo)");
|
|
231
|
+
githubToken = await promptRequired("GitHub token");
|
|
232
|
+
saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
|
|
233
|
+
console.log(" ✓ Saved GITHUB_TOKEN to .env");
|
|
234
|
+
}
|
|
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)");
|
|
384
238
|
console.log("");
|
|
239
|
+
|
|
240
|
+
// --- Step 4: Clone (only if no git repo) ---
|
|
241
|
+
if (!git.inRepo) {
|
|
242
|
+
const [owner, repo] = ownerRepo.split("/");
|
|
243
|
+
console.log(` Cloning ${ownerRepo} into ./${repo}...`);
|
|
244
|
+
try {
|
|
245
|
+
currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner, repo, token: githubToken });
|
|
246
|
+
} catch (err) {
|
|
247
|
+
console.log(` ✗ ${err.message}`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
|
|
251
|
+
console.log("");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// --- Step 5: Tracker type ---
|
|
385
255
|
const tracker = await promptSelect({
|
|
386
|
-
message: "Task tracker
|
|
256
|
+
message: "Task tracker",
|
|
387
257
|
choices: [
|
|
388
258
|
{ name: "Linear", value: "linear" },
|
|
389
259
|
{ name: "Jira", value: "jira" },
|
|
@@ -392,284 +262,298 @@ export async function runInit(cwd, opts = {}) {
|
|
|
392
262
|
],
|
|
393
263
|
});
|
|
394
264
|
console.log("");
|
|
395
|
-
if (tracker) config.tracker = tracker;
|
|
396
|
-
|
|
397
|
-
if (tracker) printDedicatedUserChoice(tracker, quick);
|
|
398
265
|
|
|
399
|
-
//
|
|
266
|
+
// --- Step 6: Tracker location ---
|
|
267
|
+
let linearWorkspace = null;
|
|
268
|
+
let jiraBaseUrl = null;
|
|
400
269
|
if (tracker === "linear") {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
"Click \"Create\". Copy the key shown (starts with lin_api_). You will NOT see it again.",
|
|
407
|
-
]);
|
|
408
|
-
const ws = await promptRequired(rl, "Linear workspace slug (the 'kendoo' in linear.app/kendoo)");
|
|
409
|
-
const key = await promptRequired(rl, "Linear API key you just copied");
|
|
410
|
-
saveEnvVar(cwd, "LINEAR_API_KEY", key);
|
|
411
|
-
console.log(" ✓ Saved LINEAR_API_KEY to .env");
|
|
270
|
+
linearWorkspace = await promptRequired("Linear workspace slug (the 'kendoo' in linear.app/kendoo)");
|
|
271
|
+
console.log("");
|
|
272
|
+
} else if (tracker === "jira") {
|
|
273
|
+
const raw = await promptRequired("Jira tenant URL (e.g. https://yourco.atlassian.net)");
|
|
274
|
+
jiraBaseUrl = raw.replace(/\/+$/, "");
|
|
412
275
|
console.log("");
|
|
413
|
-
config.linear = { workspace: ws };
|
|
414
276
|
}
|
|
415
277
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
278
|
+
// --- Step 7: Tracker credentials ---
|
|
279
|
+
env = loadDotEnv(currentCwd);
|
|
280
|
+
let linearApiKey = env.LINEAR_API_KEY;
|
|
281
|
+
let jiraEmail = env.JIRA_EMAIL;
|
|
282
|
+
let jiraApiToken = env.JIRA_API_TOKEN;
|
|
283
|
+
|
|
284
|
+
if (tracker === "linear" && !linearApiKey) {
|
|
285
|
+
console.log(" (Get a Linear API key: https://linear.app/settings/api)");
|
|
286
|
+
linearApiKey = await promptRequired("Linear API key");
|
|
287
|
+
saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
|
|
288
|
+
console.log(" ✓ Saved LINEAR_API_KEY to .env\n");
|
|
289
|
+
} else if (tracker === "jira") {
|
|
290
|
+
if (!jiraEmail) {
|
|
291
|
+
console.log(" (Create a Jira API token: https://id.atlassian.com/manage-profile/security/api-tokens)");
|
|
292
|
+
jiraEmail = await promptRequired("Atlassian login email");
|
|
293
|
+
saveEnvVar(currentCwd, "JIRA_EMAIL", jiraEmail);
|
|
294
|
+
}
|
|
295
|
+
if (!jiraApiToken) {
|
|
296
|
+
jiraApiToken = await promptRequired("Jira API token");
|
|
297
|
+
saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
|
|
298
|
+
}
|
|
299
|
+
console.log(" ✓ Saved JIRA credentials to .env\n");
|
|
432
300
|
}
|
|
433
301
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
"
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
const login = (await ask(rl, ` GitHub username (the @handle that owns the token)${loginDefault ? ` [${loginDefault}]` : ""}: `)).trim() || loginDefault;
|
|
450
|
-
const token = await promptRequired(rl, "GitHub token");
|
|
451
|
-
saveEnvVar(cwd, "GITHUB_TOKEN", token);
|
|
452
|
-
console.log(" ✓ Saved GITHUB_TOKEN to .env");
|
|
453
|
-
console.log("");
|
|
454
|
-
config.github = { repo, login };
|
|
302
|
+
// --- Step 8: Signature ---
|
|
303
|
+
const badgeValue = await promptSelect({
|
|
304
|
+
message: "How should the AI team sign its work?",
|
|
305
|
+
default: "AgentDesk",
|
|
306
|
+
choices: [
|
|
307
|
+
{ name: "AgentDesk", value: "AgentDesk" },
|
|
308
|
+
{ name: "Claude Code", value: "Claude Code" },
|
|
309
|
+
{ name: "Custom…", value: "__custom__" },
|
|
310
|
+
{ name: "Skip (no signature)", value: null },
|
|
311
|
+
],
|
|
312
|
+
});
|
|
313
|
+
let identityBadge = badgeValue;
|
|
314
|
+
if (badgeValue === "__custom__") {
|
|
315
|
+
const v = (await ask(" Custom signature: ")).trim();
|
|
316
|
+
identityBadge = v || null;
|
|
455
317
|
}
|
|
318
|
+
console.log("");
|
|
456
319
|
|
|
457
|
-
// Verify tracker +
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (tracker) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
320
|
+
// --- Step 9: Verify tracker + pick team/project ---
|
|
321
|
+
// Build an in-progress config we mutate as we learn more.
|
|
322
|
+
const config = {
|
|
323
|
+
tracker,
|
|
324
|
+
github: { repo: ownerRepo, login: githubLogin || null },
|
|
325
|
+
};
|
|
326
|
+
if (tracker === "linear") config.linear = { workspace: linearWorkspace };
|
|
327
|
+
if (tracker === "jira") config.jira = { baseUrl: jiraBaseUrl };
|
|
328
|
+
if (identityBadge) config.identityBadge = identityBadge;
|
|
329
|
+
|
|
330
|
+
let trackerTeamOrProjectId = null;
|
|
331
|
+
let trackerTeamOrProjectName = null;
|
|
332
|
+
|
|
333
|
+
if (tracker && tracker !== "github") {
|
|
334
|
+
process.stdout.write(" Verifying tracker access... ");
|
|
335
|
+
let verified = false;
|
|
336
|
+
while (!verified) {
|
|
337
|
+
const creds = { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken };
|
|
338
|
+
const check = await checkTrackerPermissions({ tracker, config, credentials: creds });
|
|
339
|
+
if (check.ok) {
|
|
340
|
+
console.log("✓");
|
|
341
|
+
if (check.identity?.name || check.identity?.email) {
|
|
342
|
+
console.log(` ✓ Posting as: ${check.identity.name || check.identity.email}`);
|
|
343
|
+
}
|
|
344
|
+
verified = true;
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
console.log("failed");
|
|
348
|
+
for (const e of check.errors || []) console.log(` • ${e}`);
|
|
468
349
|
const next = await promptSelect({
|
|
469
350
|
message: "What now?",
|
|
470
351
|
choices: [
|
|
471
352
|
{ name: "Re-enter credentials and try again", value: "retry" },
|
|
472
|
-
{ name: "Save
|
|
473
|
-
{ name: "Cancel
|
|
353
|
+
{ name: "Save anyway (fix later in .env)", value: "skip" },
|
|
354
|
+
{ name: "Cancel", value: "cancel" },
|
|
474
355
|
],
|
|
475
356
|
});
|
|
476
|
-
|
|
477
|
-
if (next === "cancel") {
|
|
478
|
-
rl.close();
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
357
|
+
if (next === "cancel") return;
|
|
481
358
|
if (next === "skip") break;
|
|
482
|
-
// Retry: re-prompt only the tracker auth fields, not the whole wizard.
|
|
483
359
|
if (tracker === "linear") {
|
|
484
|
-
|
|
485
|
-
saveEnvVar(
|
|
486
|
-
console.log(" ✓ Updated LINEAR_API_KEY in .env");
|
|
487
|
-
console.log("");
|
|
360
|
+
linearApiKey = await promptRequired("Linear API key");
|
|
361
|
+
saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
|
|
488
362
|
} else if (tracker === "jira") {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
saveEnvVar(
|
|
493
|
-
saveEnvVar(cwd, "JIRA_API_TOKEN", token);
|
|
494
|
-
if (baseUrl) {
|
|
495
|
-
config.jira = { ...(config.jira || {}), baseUrl: baseUrl.replace(/\/+$/, "") };
|
|
496
|
-
trackerLocation = config.jira.baseUrl;
|
|
497
|
-
}
|
|
498
|
-
console.log(" ✓ Updated JIRA_EMAIL and JIRA_API_TOKEN in .env");
|
|
499
|
-
console.log("");
|
|
500
|
-
} else if (tracker === "github") {
|
|
501
|
-
const token = await promptRequired(rl, "GitHub token");
|
|
502
|
-
saveEnvVar(cwd, "GITHUB_TOKEN", token);
|
|
503
|
-
console.log(" ✓ Updated GITHUB_TOKEN in .env");
|
|
504
|
-
console.log("");
|
|
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);
|
|
505
367
|
}
|
|
368
|
+
process.stdout.write(" Re-verifying tracker access... ");
|
|
506
369
|
}
|
|
507
|
-
if (verified.ok && verified.trackerProjectId) {
|
|
508
|
-
if (tracker === "linear") config.linear = { ...(config.linear || {}), teamKey: verified.trackerProjectId };
|
|
509
|
-
if (tracker === "jira") config.jira = { ...(config.jira || {}), project: verified.trackerProjectId };
|
|
510
|
-
// Use the tracker's project key as the default agentdesk project key.
|
|
511
|
-
if (!existingConfig.projectKey) finalProjectKey = verified.trackerProjectId.toLowerCase();
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
370
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
// above already collected repo+login+token, so skip the duplicate.
|
|
519
|
-
let githubLogin = config.github?.login || null;
|
|
520
|
-
if (tracker !== "github") {
|
|
521
|
-
if (!quick) {
|
|
522
|
-
console.log(" ─".repeat(30));
|
|
523
|
-
console.log(" GitHub access (required — agents push code and open PRs here)");
|
|
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 });
|
|
524
374
|
console.log("");
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const repo = (await ask(rl, ` GitHub repo (owner/repo)${repoDefault ? ` [${repoDefault}]` : ""}: `)).trim() || repoDefault;
|
|
537
|
-
const loginDefault = existingConfig.github?.login || "";
|
|
538
|
-
const login = (await ask(rl, ` GitHub username (the @handle that owns the token)${loginDefault ? ` [${loginDefault}]` : ""}: `)).trim() || loginDefault;
|
|
539
|
-
const token = await promptRequired(rl, "GitHub token");
|
|
540
|
-
saveEnvVar(cwd, "GITHUB_TOKEN", token);
|
|
541
|
-
console.log(" ✓ Saved GITHUB_TOKEN to .env");
|
|
542
|
-
console.log("");
|
|
543
|
-
config.github = { repo, login };
|
|
544
|
-
githubLogin = login;
|
|
545
|
-
|
|
546
|
-
// Verify token matches the user-stated login.
|
|
547
|
-
try {
|
|
548
|
-
const env = { ...process.env, GH_TOKEN: token };
|
|
549
|
-
const who = execSync("gh api user --jq .login", { env, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
|
|
550
|
-
if (who && login && who.toLowerCase() !== login.toLowerCase()) {
|
|
551
|
-
console.log(` ⚠ Token is valid but authenticates as @${who}, not @${login}. Agents will post as @${who}.`);
|
|
552
|
-
} else if (who) {
|
|
553
|
-
console.log(` ✓ GitHub verified as @${who}`);
|
|
375
|
+
if (items && items.length > 0) {
|
|
376
|
+
const pickedId = await promptSelect({
|
|
377
|
+
message: `Pick the ${tracker === "linear" ? "team" : "project"} this project maps to`,
|
|
378
|
+
choices: items.map(it => ({ name: it.name, value: it.id })),
|
|
379
|
+
});
|
|
380
|
+
trackerTeamOrProjectId = pickedId;
|
|
381
|
+
const pickedItem = items.find(it => it.id === pickedId);
|
|
382
|
+
trackerTeamOrProjectName = pickedItem?.name || pickedId;
|
|
383
|
+
} else {
|
|
384
|
+
trackerTeamOrProjectId = await promptRequired(tracker === "linear" ? "Linear team key (e.g. KEN)" : "Jira project key (e.g. PROJ)");
|
|
385
|
+
trackerTeamOrProjectName = trackerTeamOrProjectId;
|
|
554
386
|
}
|
|
555
|
-
|
|
556
|
-
|
|
387
|
+
if (tracker === "linear") config.linear = { ...config.linear, teamKey: trackerTeamOrProjectId };
|
|
388
|
+
if (tracker === "jira") config.jira = { ...config.jira, project: trackerTeamOrProjectId };
|
|
389
|
+
console.log("");
|
|
557
390
|
}
|
|
558
|
-
console.log("");
|
|
559
391
|
}
|
|
560
392
|
|
|
561
|
-
//
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
|
|
393
|
+
// Derive projectName + projectKey silently.
|
|
394
|
+
const repoShort = ownerRepo.split("/").pop();
|
|
395
|
+
const projectName = trackerTeamOrProjectName
|
|
396
|
+
? `${repoShort} — ${String(trackerTeamOrProjectName).split(" (")[0]}`
|
|
397
|
+
: repoShort;
|
|
398
|
+
const keySource = trackerTeamOrProjectId || repoShort;
|
|
399
|
+
const projectKey = String(keySource).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
400
|
+
config.projectKey = projectKey;
|
|
401
|
+
|
|
402
|
+
// --- Step 10: Review + access checks + save ---
|
|
403
|
+
const credsAll = loadDotEnv(currentCwd);
|
|
404
|
+
const results = await runAccessChecks({ apiKey, serverUrl: SERVER, cwd: currentCwd, config, creds: credsAll });
|
|
405
|
+
printAccessChecks(results, { tracker });
|
|
406
|
+
console.log("");
|
|
407
|
+
console.log(" Summary");
|
|
408
|
+
console.log(" ───────");
|
|
409
|
+
console.log(` Project: ${projectName} (${projectKey})`);
|
|
410
|
+
console.log(` Tracker: ${tracker || "none"}${trackerTeamOrProjectId ? ` (${trackerTeamOrProjectId})` : ""}`);
|
|
411
|
+
console.log(` GitHub: ${ownerRepo}${githubLogin ? ` (@${githubLogin})` : ""}`);
|
|
412
|
+
console.log(` Signature: ${identityBadge || "(skipped)"}`);
|
|
413
|
+
const envNames = Object.keys(credsAll).filter(k => ["LINEAR_API_KEY","JIRA_EMAIL","JIRA_API_TOKEN","GITHUB_TOKEN"].includes(k));
|
|
414
|
+
console.log(` .env written: ${envNames.join(", ") || "(none)"}`);
|
|
565
415
|
console.log("");
|
|
566
416
|
|
|
567
|
-
|
|
568
|
-
if (
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
console.log("");
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// --- Summary + confirm before write ---
|
|
575
|
-
let merged = {};
|
|
576
|
-
if (hasConfig) {
|
|
577
|
-
try { merged = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
merged.projectKey = finalProjectKey;
|
|
581
|
-
|
|
582
|
-
if (tracker) merged.tracker = tracker;
|
|
583
|
-
else delete merged.tracker;
|
|
584
|
-
if (config.linear) merged.linear = config.linear; else if (!tracker) delete merged.linear;
|
|
585
|
-
if (config.jira) merged.jira = config.jira; else if (!tracker) delete merged.jira;
|
|
586
|
-
if (config.github) merged.github = config.github;
|
|
587
|
-
if (config.identityBadge) merged.identityBadge = config.identityBadge;
|
|
588
|
-
|
|
589
|
-
if (!quick) {
|
|
590
|
-
console.log(" Review");
|
|
591
|
-
console.log(" ──────");
|
|
592
|
-
console.log(` Project key: ${merged.projectKey}`);
|
|
593
|
-
console.log(` Tracker: ${merged.tracker || "none"}`);
|
|
594
|
-
if (merged.linear) console.log(` Linear: workspace=${merged.linear.workspace || "-"}, team=${merged.linear.teamKey || "-"}`);
|
|
595
|
-
if (merged.jira) console.log(` Jira: ${merged.jira.baseUrl || "-"}, project=${merged.jira.project || "-"}`);
|
|
596
|
-
if (merged.github) console.log(` GitHub: repo=${merged.github.repo || "-"}, user=@${merged.github.login || "-"}`);
|
|
597
|
-
if (merged.identityBadge) console.log(` Identity badge: ${merged.identityBadge}`);
|
|
598
|
-
console.log("");
|
|
599
|
-
const confirm = (await ask(rl, " Save this config? [Y/n]: ")).trim().toLowerCase();
|
|
600
|
-
if (confirm && confirm !== "y" && confirm !== "yes") {
|
|
601
|
-
console.log(" Cancelled — nothing written.");
|
|
602
|
-
rl.close();
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
console.log("");
|
|
417
|
+
const save = (await ask(" Save this config? [Y/n]: ")).trim().toLowerCase();
|
|
418
|
+
if (save && save !== "y" && save !== "yes") {
|
|
419
|
+
console.log(" Cancelled — nothing written to .agentdesk.json.");
|
|
420
|
+
return;
|
|
606
421
|
}
|
|
422
|
+
console.log("");
|
|
607
423
|
|
|
608
|
-
|
|
609
|
-
|
|
424
|
+
// Write .agentdesk.json
|
|
425
|
+
writeFileSync(join(currentCwd, ".agentdesk.json"), JSON.stringify(config, null, 2) + "\n");
|
|
426
|
+
console.log(" ✓ Saved .agentdesk.json");
|
|
610
427
|
|
|
611
|
-
//
|
|
612
|
-
const gitignorePath = join(
|
|
428
|
+
// .gitignore — add .agentdesk/
|
|
429
|
+
const gitignorePath = join(currentCwd, ".gitignore");
|
|
613
430
|
try {
|
|
614
431
|
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
|
|
615
432
|
if (!existing.split("\n").some(line => line.trim() === ".agentdesk/" || line.trim() === ".agentdesk")) {
|
|
616
433
|
const nl = existing.endsWith("\n") || !existing ? "" : "\n";
|
|
617
434
|
writeFileSync(gitignorePath, `${existing}${nl}.agentdesk/\n`);
|
|
618
|
-
console.log(
|
|
435
|
+
console.log(" ✓ Added .agentdesk/ to .gitignore");
|
|
619
436
|
}
|
|
620
437
|
} catch {}
|
|
621
438
|
|
|
622
|
-
// Register
|
|
623
|
-
registerLocalProject(finalProjectKey, project.name || finalProjectKey, project.dir);
|
|
624
|
-
|
|
625
|
-
// --- Register with server ---
|
|
439
|
+
// Register with server + push settings
|
|
626
440
|
try {
|
|
627
|
-
|
|
441
|
+
await fetch(`${SERVER}/api/projects`, {
|
|
628
442
|
method: "POST",
|
|
629
|
-
headers: {
|
|
630
|
-
|
|
631
|
-
...(loadApiKey(cwd) ? { "x-api-key": loadApiKey(cwd) } : {}),
|
|
632
|
-
},
|
|
633
|
-
body: JSON.stringify({
|
|
634
|
-
id: finalProjectKey,
|
|
635
|
-
name: project.name || finalProjectKey,
|
|
636
|
-
path: project.dir,
|
|
637
|
-
type: project.type,
|
|
638
|
-
tracker,
|
|
639
|
-
}),
|
|
443
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
|
444
|
+
body: JSON.stringify({ id: projectKey, name: projectName, path: currentCwd, tracker }),
|
|
640
445
|
});
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
if (key) {
|
|
647
|
-
const push = await pushConfig(key, SERVER, finalProjectKey, merged);
|
|
648
|
-
if (!push.ok && push.error !== "missing_auth") {
|
|
649
|
-
console.log(` ⚠ Could not push settings to agentdesk.live: ${push.error}`);
|
|
650
|
-
console.log(" Your local .agentdesk.json is saved; retry from the UI or re-run init.");
|
|
651
|
-
}
|
|
446
|
+
const push = await pushConfig(apiKey, SERVER, projectKey, config);
|
|
447
|
+
if (!push.ok && push.error !== "missing_auth") {
|
|
448
|
+
console.log(` ⚠ Could not push settings to agentdesk.live: ${push.error}`);
|
|
449
|
+
} else {
|
|
450
|
+
console.log(" ✓ Registered with agentdesk.live");
|
|
652
451
|
}
|
|
653
452
|
} catch {
|
|
654
|
-
|
|
453
|
+
console.log(" ⚠ Couldn't reach agentdesk.live — local config saved, retry from the UI later.");
|
|
655
454
|
}
|
|
656
455
|
|
|
456
|
+
registerLocalProject(projectKey, projectName, currentCwd);
|
|
457
|
+
|
|
657
458
|
console.log("");
|
|
658
|
-
|
|
459
|
+
const cmdPrefix = trackerTeamOrProjectId || projectKey.toUpperCase();
|
|
460
|
+
console.log(" Ready. Run a team session:");
|
|
461
|
+
console.log(` agentdesk team ${cmdPrefix}-123`);
|
|
659
462
|
console.log("");
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ---------- Entry: routing (E1) ----------
|
|
466
|
+
|
|
467
|
+
export async function runInit(cwd, opts = {}) {
|
|
468
|
+
const forceFull = !!opts.forceFull;
|
|
469
|
+
const apiKey = getStoredApiKey();
|
|
470
|
+
if (!apiKey) {
|
|
471
|
+
console.log("\n Not logged in. Run `agentdesk login` first.\n");
|
|
472
|
+
process.exit(1);
|
|
666
473
|
}
|
|
667
|
-
console.log("");
|
|
668
474
|
|
|
669
|
-
|
|
670
|
-
|
|
475
|
+
const local = readLocalConfig(cwd);
|
|
476
|
+
|
|
477
|
+
// --- E1: Detection ---
|
|
478
|
+
if (!forceFull && local?.projectKey) {
|
|
479
|
+
// Modern config — gap-scan by default. No "Full setup" option here:
|
|
480
|
+
// walking the new-project wizard on an already-configured project
|
|
481
|
+
// would re-ask for the tracker as if it weren't on the server, which
|
|
482
|
+
// was the classic footgun. Users who genuinely want to reconfigure
|
|
483
|
+
// from scratch should pass --force-full (or delete .agentdesk.json).
|
|
484
|
+
console.log("");
|
|
485
|
+
console.log(" AgentDesk — existing project detected");
|
|
486
|
+
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
671
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);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (!forceFull && local && !local.projectKey) {
|
|
502
|
+
// Legacy config — try to migrate.
|
|
503
|
+
const match = await autoMatchLegacy(cwd, apiKey, local);
|
|
504
|
+
if (match) {
|
|
505
|
+
console.log(`\n ✓ Legacy .agentdesk.json recognized — ${match.reason}. Migrating to project "${match.name}" (${match.projectKey}).`);
|
|
506
|
+
const migrated = { ...local, projectKey: match.projectKey };
|
|
507
|
+
writeFileSync(join(cwd, ".agentdesk.json"), JSON.stringify(migrated, null, 2) + "\n");
|
|
508
|
+
await runExistingFlow(cwd, apiKey, match.projectKey);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
console.log("\n ⚠ Legacy .agentdesk.json has no projectKey and no confident match found.");
|
|
512
|
+
// Fall through to Connect/New menu.
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (!forceFull) {
|
|
516
|
+
// No local config (or legacy-unmatched). Try git-remote auto-match.
|
|
517
|
+
const match = await autoMatchProject(cwd, apiKey);
|
|
518
|
+
if (match) {
|
|
519
|
+
console.log("");
|
|
520
|
+
console.log(` ✓ Recognized: ${match.name} (${match.projectKey}) — ${match.reason}`);
|
|
521
|
+
const confirm = await promptSelect({
|
|
522
|
+
message: "Use this project?",
|
|
523
|
+
default: "yes",
|
|
524
|
+
choices: [
|
|
525
|
+
{ name: "Yes, continue with this project", value: "yes" },
|
|
526
|
+
{ name: "No, pick another from my account", value: "other" },
|
|
527
|
+
{ name: "Cancel", value: "cancel" },
|
|
528
|
+
],
|
|
529
|
+
});
|
|
530
|
+
if (confirm === "cancel") return;
|
|
531
|
+
if (confirm === "yes") {
|
|
532
|
+
await runExistingFlow(cwd, apiKey, match.projectKey);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
// "other" — fall through to picker below
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
console.log("");
|
|
539
|
+
const top = await promptSelect({
|
|
540
|
+
message: "What would you like to do?",
|
|
541
|
+
choices: [
|
|
542
|
+
{ name: "Connect to an existing project on my account", value: "connect" },
|
|
543
|
+
{ name: "Set up this as a new project (full wizard)", value: "new" },
|
|
544
|
+
{ name: "Cancel", value: "cancel" },
|
|
545
|
+
],
|
|
546
|
+
});
|
|
547
|
+
if (top === "cancel") return;
|
|
548
|
+
if (top === "connect") {
|
|
549
|
+
const projectKey = await discoverProject(cwd, apiKey);
|
|
550
|
+
if (!projectKey) return;
|
|
551
|
+
console.log("");
|
|
552
|
+
await runExistingFlow(cwd, apiKey, projectKey);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
// "new" falls through to the wizard
|
|
672
556
|
}
|
|
673
557
|
|
|
674
|
-
|
|
558
|
+
await runNewProjectWizard(cwd, apiKey);
|
|
675
559
|
}
|