@kendoo.agentdesk/agentdesk 0.19.8 → 0.20.0
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 +10 -0
- package/README.md +23 -6
- package/bin/agentdesk.mjs +9 -10
- package/cli/bootstrap.mjs +11 -10
- package/cli/init.mjs +428 -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
|
-
}
|
|
105
|
-
|
|
106
|
-
function saveEnvVar(dir, key, value) {
|
|
107
|
-
const envPath = join(dir, ".env");
|
|
108
|
-
let content = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
|
|
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
|
-
}
|
|
42
|
+
// ---------- Tracker team/project picker helpers ----------
|
|
166
43
|
|
|
167
|
-
// Fetch the
|
|
168
|
-
//
|
|
169
|
-
//
|
|
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,186 @@ 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
|
-
const
|
|
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}`);
|
|
266
140
|
console.log("");
|
|
267
|
-
return { ok: true, trackerProjectId: manual };
|
|
268
|
-
}
|
|
269
141
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
let editScope = "full";
|
|
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
|
-
}
|
|
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");
|
|
310
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)");
|
|
154
|
+
console.log("");
|
|
311
155
|
|
|
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;
|
|
156
|
+
// --- E4: Ensure correct repo is locally present ---
|
|
157
|
+
const serverRepo = config.github?.repo ? parseGitHubRef(config.github.repo) : null;
|
|
158
|
+
if (serverRepo) {
|
|
159
|
+
const here = detectGitRepo(currentCwd);
|
|
160
|
+
const expected = `${serverRepo.owner}/${serverRepo.repo}`.toLowerCase();
|
|
161
|
+
const haveHere = (here.ownerRepo || "").toLowerCase();
|
|
162
|
+
if (!here.inRepo || haveHere !== expected) {
|
|
163
|
+
if (here.inRepo && haveHere && haveHere !== expected) {
|
|
164
|
+
console.log(` Current directory holds ${haveHere} — cloning ${expected} into ./${serverRepo.repo} instead.`);
|
|
165
|
+
} else {
|
|
166
|
+
console.log(` Cloning ${expected} into ./${serverRepo.repo}...`);
|
|
327
167
|
}
|
|
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 });
|
|
168
|
+
try {
|
|
169
|
+
currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner: serverRepo.owner, repo: serverRepo.repo, token: githubToken });
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.log(` ✗ ${err.message}`);
|
|
352
172
|
return;
|
|
353
173
|
}
|
|
354
|
-
//
|
|
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("");
|
|
355
178
|
}
|
|
356
179
|
}
|
|
357
180
|
|
|
358
|
-
|
|
181
|
+
// Ensure .agentdesk.json in currentCwd carries the projectKey so loadConfig
|
|
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 });
|
|
359
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 });
|
|
360
192
|
console.log("");
|
|
361
|
-
|
|
362
|
-
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------- New-project wizard (steps 1–10) ----------
|
|
196
|
+
|
|
197
|
+
async function runNewProjectWizard(cwd, apiKey) {
|
|
198
|
+
let currentCwd = cwd;
|
|
199
|
+
|
|
363
200
|
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)"}`);
|
|
201
|
+
console.log(" AgentDesk — new project setup");
|
|
202
|
+
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
369
203
|
console.log("");
|
|
370
204
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
205
|
+
// --- Step 1: Detect git repo ---
|
|
206
|
+
const git = detectGitRepo(currentCwd);
|
|
207
|
+
let ownerRepo = git.ownerRepo;
|
|
208
|
+
|
|
209
|
+
// --- Step 2: Ask for the GitHub repo (only if no git repo) ---
|
|
210
|
+
if (!git.inRepo) {
|
|
211
|
+
console.log(" No git repo here yet.\n");
|
|
212
|
+
while (!ownerRepo) {
|
|
213
|
+
const input = await promptRequired("GitHub repo (owner/repo or URL)");
|
|
214
|
+
const parsed = parseGitHubRef(input);
|
|
215
|
+
if (parsed) { ownerRepo = `${parsed.owner}/${parsed.repo}`; }
|
|
216
|
+
else console.log(" Not a valid GitHub repo. Only github.com is supported right now.");
|
|
217
|
+
}
|
|
218
|
+
console.log("");
|
|
219
|
+
}
|
|
382
220
|
|
|
383
|
-
|
|
221
|
+
// --- Step 3: GitHub token ---
|
|
222
|
+
let env = loadDotEnv(currentCwd);
|
|
223
|
+
let githubToken = env.GITHUB_TOKEN;
|
|
224
|
+
if (!githubToken) {
|
|
225
|
+
console.log(" GitHub token (create one at https://github.com/settings/tokens — scope: repo)");
|
|
226
|
+
githubToken = await promptRequired("GitHub token");
|
|
227
|
+
saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
|
|
228
|
+
console.log(" ✓ Saved GITHUB_TOKEN to .env");
|
|
229
|
+
}
|
|
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)");
|
|
384
233
|
console.log("");
|
|
234
|
+
|
|
235
|
+
// --- Step 4: Clone (only if no git repo) ---
|
|
236
|
+
if (!git.inRepo) {
|
|
237
|
+
const [owner, repo] = ownerRepo.split("/");
|
|
238
|
+
console.log(` Cloning ${ownerRepo} into ./${repo}...`);
|
|
239
|
+
try {
|
|
240
|
+
currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner, repo, token: githubToken });
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.log(` ✗ ${err.message}`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
|
|
246
|
+
console.log("");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- Step 5: Tracker type ---
|
|
385
250
|
const tracker = await promptSelect({
|
|
386
|
-
message: "Task tracker
|
|
251
|
+
message: "Task tracker",
|
|
387
252
|
choices: [
|
|
388
253
|
{ name: "Linear", value: "linear" },
|
|
389
254
|
{ name: "Jira", value: "jira" },
|
|
@@ -392,284 +257,299 @@ export async function runInit(cwd, opts = {}) {
|
|
|
392
257
|
],
|
|
393
258
|
});
|
|
394
259
|
console.log("");
|
|
395
|
-
if (tracker) config.tracker = tracker;
|
|
396
|
-
|
|
397
|
-
if (tracker) printDedicatedUserChoice(tracker, quick);
|
|
398
260
|
|
|
399
|
-
//
|
|
261
|
+
// --- Step 6: Tracker location ---
|
|
262
|
+
let linearWorkspace = null;
|
|
263
|
+
let jiraBaseUrl = null;
|
|
400
264
|
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");
|
|
265
|
+
linearWorkspace = await promptRequired("Linear workspace slug (the 'kendoo' in linear.app/kendoo)");
|
|
266
|
+
console.log("");
|
|
267
|
+
} else if (tracker === "jira") {
|
|
268
|
+
const raw = await promptRequired("Jira tenant URL (e.g. https://yourco.atlassian.net)");
|
|
269
|
+
jiraBaseUrl = raw.replace(/\/+$/, "");
|
|
412
270
|
console.log("");
|
|
413
|
-
config.linear = { workspace: ws };
|
|
414
271
|
}
|
|
415
272
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
273
|
+
// --- Step 7: Tracker credentials ---
|
|
274
|
+
env = loadDotEnv(currentCwd);
|
|
275
|
+
let linearApiKey = env.LINEAR_API_KEY;
|
|
276
|
+
let jiraEmail = env.JIRA_EMAIL;
|
|
277
|
+
let jiraApiToken = env.JIRA_API_TOKEN;
|
|
278
|
+
|
|
279
|
+
if (tracker === "linear" && !linearApiKey) {
|
|
280
|
+
console.log(" (Get a Linear API key: https://linear.app/settings/api)");
|
|
281
|
+
linearApiKey = await promptRequired("Linear API key");
|
|
282
|
+
saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
|
|
283
|
+
console.log(" ✓ Saved LINEAR_API_KEY to .env\n");
|
|
284
|
+
} else if (tracker === "jira") {
|
|
285
|
+
if (!jiraEmail) {
|
|
286
|
+
console.log(" (Create a Jira API token: https://id.atlassian.com/manage-profile/security/api-tokens)");
|
|
287
|
+
jiraEmail = await promptRequired("Atlassian login email");
|
|
288
|
+
saveEnvVar(currentCwd, "JIRA_EMAIL", jiraEmail);
|
|
289
|
+
}
|
|
290
|
+
if (!jiraApiToken) {
|
|
291
|
+
jiraApiToken = await promptRequired("Jira API token");
|
|
292
|
+
saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
|
|
293
|
+
}
|
|
294
|
+
console.log(" ✓ Saved JIRA credentials to .env\n");
|
|
432
295
|
}
|
|
433
296
|
|
|
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 };
|
|
297
|
+
// --- Step 8: Signature ---
|
|
298
|
+
const badgeValue = await promptSelect({
|
|
299
|
+
message: "How should the AI team sign its work?",
|
|
300
|
+
default: "AgentDesk",
|
|
301
|
+
choices: [
|
|
302
|
+
{ name: "AgentDesk", value: "AgentDesk" },
|
|
303
|
+
{ name: "Claude Code", value: "Claude Code" },
|
|
304
|
+
{ name: "Custom…", value: "__custom__" },
|
|
305
|
+
{ name: "Skip (no signature)", value: null },
|
|
306
|
+
],
|
|
307
|
+
});
|
|
308
|
+
let identityBadge = badgeValue;
|
|
309
|
+
if (badgeValue === "__custom__") {
|
|
310
|
+
const v = (await ask(" Custom signature: ")).trim();
|
|
311
|
+
identityBadge = v || null;
|
|
455
312
|
}
|
|
313
|
+
console.log("");
|
|
456
314
|
|
|
457
|
-
// Verify tracker +
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (tracker) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
315
|
+
// --- Step 9: Verify tracker + pick team/project ---
|
|
316
|
+
// Build an in-progress config we mutate as we learn more.
|
|
317
|
+
const config = {
|
|
318
|
+
tracker,
|
|
319
|
+
github: { repo: ownerRepo, login: githubLogin || null },
|
|
320
|
+
};
|
|
321
|
+
if (tracker === "linear") config.linear = { workspace: linearWorkspace };
|
|
322
|
+
if (tracker === "jira") config.jira = { baseUrl: jiraBaseUrl };
|
|
323
|
+
if (identityBadge) config.identityBadge = identityBadge;
|
|
324
|
+
|
|
325
|
+
let trackerTeamOrProjectId = null;
|
|
326
|
+
let trackerTeamOrProjectName = null;
|
|
327
|
+
|
|
328
|
+
if (tracker && tracker !== "github") {
|
|
329
|
+
process.stdout.write(" Verifying tracker access... ");
|
|
330
|
+
let verified = false;
|
|
331
|
+
while (!verified) {
|
|
332
|
+
const creds = { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken };
|
|
333
|
+
const check = await checkTrackerPermissions({ tracker, config, credentials: creds });
|
|
334
|
+
if (check.ok) {
|
|
335
|
+
console.log("✓");
|
|
336
|
+
if (check.identity?.name || check.identity?.email) {
|
|
337
|
+
console.log(` ✓ Posting as: ${check.identity.name || check.identity.email}`);
|
|
338
|
+
}
|
|
339
|
+
verified = true;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
console.log("failed");
|
|
343
|
+
for (const e of check.errors || []) console.log(` • ${e}`);
|
|
468
344
|
const next = await promptSelect({
|
|
469
345
|
message: "What now?",
|
|
470
346
|
choices: [
|
|
471
347
|
{ name: "Re-enter credentials and try again", value: "retry" },
|
|
472
|
-
{ name: "Save
|
|
473
|
-
{ name: "Cancel
|
|
348
|
+
{ name: "Save anyway (fix later in .env)", value: "skip" },
|
|
349
|
+
{ name: "Cancel", value: "cancel" },
|
|
474
350
|
],
|
|
475
351
|
});
|
|
476
|
-
|
|
477
|
-
if (next === "cancel") {
|
|
478
|
-
rl.close();
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
352
|
+
if (next === "cancel") return;
|
|
481
353
|
if (next === "skip") break;
|
|
482
|
-
// Retry: re-prompt only the tracker auth fields, not the whole wizard.
|
|
483
354
|
if (tracker === "linear") {
|
|
484
|
-
|
|
485
|
-
saveEnvVar(
|
|
486
|
-
console.log(" ✓ Updated LINEAR_API_KEY in .env");
|
|
487
|
-
console.log("");
|
|
355
|
+
linearApiKey = await promptRequired("Linear API key");
|
|
356
|
+
saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
|
|
488
357
|
} 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("");
|
|
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);
|
|
505
362
|
}
|
|
363
|
+
process.stdout.write(" Re-verifying tracker access... ");
|
|
506
364
|
}
|
|
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
365
|
|
|
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)");
|
|
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 });
|
|
524
369
|
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}`);
|
|
370
|
+
if (items && items.length > 0) {
|
|
371
|
+
const pickedId = await promptSelect({
|
|
372
|
+
message: `Pick the ${tracker === "linear" ? "team" : "project"} this project maps to`,
|
|
373
|
+
choices: items.map(it => ({ name: it.name, value: it.id })),
|
|
374
|
+
});
|
|
375
|
+
trackerTeamOrProjectId = pickedId;
|
|
376
|
+
const pickedItem = items.find(it => it.id === pickedId);
|
|
377
|
+
trackerTeamOrProjectName = pickedItem?.name || pickedId;
|
|
378
|
+
} else {
|
|
379
|
+
trackerTeamOrProjectId = await promptRequired(tracker === "linear" ? "Linear team key (e.g. KEN)" : "Jira project key (e.g. PROJ)");
|
|
380
|
+
trackerTeamOrProjectName = trackerTeamOrProjectId;
|
|
554
381
|
}
|
|
555
|
-
|
|
556
|
-
|
|
382
|
+
if (tracker === "linear") config.linear = { ...config.linear, teamKey: trackerTeamOrProjectId };
|
|
383
|
+
if (tracker === "jira") config.jira = { ...config.jira, project: trackerTeamOrProjectId };
|
|
384
|
+
console.log("");
|
|
557
385
|
}
|
|
558
|
-
console.log("");
|
|
559
386
|
}
|
|
560
387
|
|
|
561
|
-
//
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
|
|
388
|
+
// Derive projectName + projectKey silently.
|
|
389
|
+
const repoShort = ownerRepo.split("/").pop();
|
|
390
|
+
const projectName = trackerTeamOrProjectName
|
|
391
|
+
? `${repoShort} — ${String(trackerTeamOrProjectName).split(" (")[0]}`
|
|
392
|
+
: repoShort;
|
|
393
|
+
const keySource = trackerTeamOrProjectId || repoShort;
|
|
394
|
+
const projectKey = String(keySource).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
395
|
+
config.projectKey = projectKey;
|
|
396
|
+
|
|
397
|
+
// --- Step 10: Review + access checks + save ---
|
|
398
|
+
const credsAll = loadDotEnv(currentCwd);
|
|
399
|
+
const results = await runAccessChecks({ apiKey, serverUrl: SERVER, cwd: currentCwd, config, creds: credsAll });
|
|
400
|
+
printAccessChecks(results, { tracker });
|
|
401
|
+
console.log("");
|
|
402
|
+
console.log(" Summary");
|
|
403
|
+
console.log(" ───────");
|
|
404
|
+
console.log(` Project: ${projectName} (${projectKey})`);
|
|
405
|
+
console.log(` Tracker: ${tracker || "none"}${trackerTeamOrProjectId ? ` (${trackerTeamOrProjectId})` : ""}`);
|
|
406
|
+
console.log(` GitHub: ${ownerRepo}${githubLogin ? ` (@${githubLogin})` : ""}`);
|
|
407
|
+
console.log(` Signature: ${identityBadge || "(skipped)"}`);
|
|
408
|
+
const envNames = Object.keys(credsAll).filter(k => ["LINEAR_API_KEY","JIRA_EMAIL","JIRA_API_TOKEN","GITHUB_TOKEN"].includes(k));
|
|
409
|
+
console.log(` .env written: ${envNames.join(", ") || "(none)"}`);
|
|
565
410
|
console.log("");
|
|
566
411
|
|
|
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("");
|
|
412
|
+
const save = (await ask(" Save this config? [Y/n]: ")).trim().toLowerCase();
|
|
413
|
+
if (save && save !== "y" && save !== "yes") {
|
|
414
|
+
console.log(" Cancelled — nothing written to .agentdesk.json.");
|
|
415
|
+
return;
|
|
606
416
|
}
|
|
417
|
+
console.log("");
|
|
607
418
|
|
|
608
|
-
|
|
609
|
-
|
|
419
|
+
// Write .agentdesk.json
|
|
420
|
+
writeFileSync(join(currentCwd, ".agentdesk.json"), JSON.stringify(config, null, 2) + "\n");
|
|
421
|
+
console.log(" ✓ Saved .agentdesk.json");
|
|
610
422
|
|
|
611
|
-
//
|
|
612
|
-
const gitignorePath = join(
|
|
423
|
+
// .gitignore — add .agentdesk/
|
|
424
|
+
const gitignorePath = join(currentCwd, ".gitignore");
|
|
613
425
|
try {
|
|
614
426
|
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
|
|
615
427
|
if (!existing.split("\n").some(line => line.trim() === ".agentdesk/" || line.trim() === ".agentdesk")) {
|
|
616
428
|
const nl = existing.endsWith("\n") || !existing ? "" : "\n";
|
|
617
429
|
writeFileSync(gitignorePath, `${existing}${nl}.agentdesk/\n`);
|
|
618
|
-
console.log(
|
|
430
|
+
console.log(" ✓ Added .agentdesk/ to .gitignore");
|
|
619
431
|
}
|
|
620
432
|
} catch {}
|
|
621
433
|
|
|
622
|
-
// Register
|
|
623
|
-
registerLocalProject(finalProjectKey, project.name || finalProjectKey, project.dir);
|
|
624
|
-
|
|
625
|
-
// --- Register with server ---
|
|
434
|
+
// Register with server + push settings
|
|
626
435
|
try {
|
|
627
|
-
|
|
436
|
+
await fetch(`${SERVER}/api/projects`, {
|
|
628
437
|
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
|
-
}),
|
|
438
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
|
439
|
+
body: JSON.stringify({ id: projectKey, name: projectName, path: currentCwd, tracker }),
|
|
640
440
|
});
|
|
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
|
-
}
|
|
441
|
+
const push = await pushConfig(apiKey, SERVER, projectKey, config);
|
|
442
|
+
if (!push.ok && push.error !== "missing_auth") {
|
|
443
|
+
console.log(` ⚠ Could not push settings to agentdesk.live: ${push.error}`);
|
|
444
|
+
} else {
|
|
445
|
+
console.log(" ✓ Registered with agentdesk.live");
|
|
652
446
|
}
|
|
653
447
|
} catch {
|
|
654
|
-
|
|
448
|
+
console.log(" ⚠ Couldn't reach agentdesk.live — local config saved, retry from the UI later.");
|
|
655
449
|
}
|
|
656
450
|
|
|
451
|
+
registerLocalProject(projectKey, projectName, currentCwd);
|
|
452
|
+
|
|
657
453
|
console.log("");
|
|
658
|
-
|
|
454
|
+
const cmdPrefix = trackerTeamOrProjectId || projectKey.toUpperCase();
|
|
455
|
+
console.log(" Ready. Run a team session:");
|
|
456
|
+
console.log(` agentdesk team ${cmdPrefix}-123`);
|
|
659
457
|
console.log("");
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ---------- Entry: routing (E1) ----------
|
|
461
|
+
|
|
462
|
+
export async function runInit(cwd, opts = {}) {
|
|
463
|
+
const forceFull = !!opts.forceFull;
|
|
464
|
+
const apiKey = getStoredApiKey();
|
|
465
|
+
if (!apiKey) {
|
|
466
|
+
console.log("\n Not logged in. Run `agentdesk login` first.\n");
|
|
467
|
+
process.exit(1);
|
|
666
468
|
}
|
|
667
|
-
console.log("");
|
|
668
469
|
|
|
669
|
-
|
|
670
|
-
|
|
470
|
+
const local = readLocalConfig(cwd);
|
|
471
|
+
|
|
472
|
+
// --- E1: Detection ---
|
|
473
|
+
if (!forceFull && local?.projectKey) {
|
|
474
|
+
// Modern config — gap-scan by default.
|
|
475
|
+
console.log("");
|
|
476
|
+
console.log(" AgentDesk — existing project detected");
|
|
477
|
+
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
671
478
|
console.log("");
|
|
479
|
+
const choice = await promptSelect({
|
|
480
|
+
message: "What would you like to do?",
|
|
481
|
+
default: "refresh",
|
|
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);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (!forceFull && local && !local.projectKey) {
|
|
498
|
+
// Legacy config — try to migrate.
|
|
499
|
+
const match = await autoMatchLegacy(cwd, apiKey, local);
|
|
500
|
+
if (match) {
|
|
501
|
+
console.log(`\n ✓ Legacy .agentdesk.json recognized — ${match.reason}. Migrating to project "${match.name}" (${match.projectKey}).`);
|
|
502
|
+
const migrated = { ...local, projectKey: match.projectKey };
|
|
503
|
+
writeFileSync(join(cwd, ".agentdesk.json"), JSON.stringify(migrated, null, 2) + "\n");
|
|
504
|
+
await runExistingFlow(cwd, apiKey, match.projectKey);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
console.log("\n ⚠ Legacy .agentdesk.json has no projectKey and no confident match found.");
|
|
508
|
+
// Fall through to Connect/New menu.
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!forceFull) {
|
|
512
|
+
// No local config (or legacy-unmatched). Try git-remote auto-match.
|
|
513
|
+
const match = await autoMatchProject(cwd, apiKey);
|
|
514
|
+
if (match) {
|
|
515
|
+
console.log("");
|
|
516
|
+
console.log(` ✓ Recognized: ${match.name} (${match.projectKey}) — ${match.reason}`);
|
|
517
|
+
const confirm = await promptSelect({
|
|
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
|
+
}
|
|
533
|
+
|
|
534
|
+
console.log("");
|
|
535
|
+
const top = await promptSelect({
|
|
536
|
+
message: "What would you like to do?",
|
|
537
|
+
choices: [
|
|
538
|
+
{ name: "Connect to an existing project on my account", value: "connect" },
|
|
539
|
+
{ name: "Set up this as a new project (full wizard)", value: "new" },
|
|
540
|
+
{ name: "Cancel", value: "cancel" },
|
|
541
|
+
],
|
|
542
|
+
});
|
|
543
|
+
if (top === "cancel") return;
|
|
544
|
+
if (top === "connect") {
|
|
545
|
+
const projectKey = await discoverProject(cwd, apiKey);
|
|
546
|
+
if (!projectKey) return;
|
|
547
|
+
console.log("");
|
|
548
|
+
await runExistingFlow(cwd, apiKey, projectKey);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
// "new" falls through to the wizard
|
|
672
552
|
}
|
|
673
553
|
|
|
674
|
-
|
|
554
|
+
await runNewProjectWizard(cwd, apiKey);
|
|
675
555
|
}
|