@kendoo.agentdesk/agentdesk 0.19.7 → 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 +15 -0
- package/README.md +23 -6
- package/bin/agentdesk.mjs +9 -10
- package/cli/bootstrap.mjs +11 -10
- package/cli/init.mjs +429 -522
- 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,
|
|
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
|
-
}
|
|
42
|
+
// ---------- Tracker team/project picker helpers ----------
|
|
131
43
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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,168 +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
|
-
const choices = items.map(it => ({ name: it.name, value: it.id }));
|
|
212
|
-
if (allowManual) choices.push({ name: "Type it manually instead", value: "__manual__" });
|
|
213
|
-
const value = await promptSelect({ message: prompt, choices });
|
|
214
|
-
if (value === "__manual__") return null;
|
|
215
|
-
return value;
|
|
216
|
-
}
|
|
78
|
+
// ---------- Legacy migration: auto-match a config without projectKey ----------
|
|
217
79
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
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;
|
|
91
|
+
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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}...`);
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner: serverRepo.owner, repo: serverRepo.repo, token: githubToken });
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.log(` ✗ ${err.message}`);
|
|
326
172
|
return;
|
|
327
173
|
}
|
|
174
|
+
// Re-save the token into the cloned working tree's .env (previous
|
|
175
|
+
// save was into the parent dir).
|
|
176
|
+
saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
|
|
177
|
+
console.log("");
|
|
328
178
|
}
|
|
329
179
|
}
|
|
330
180
|
|
|
331
|
-
|
|
181
|
+
// Ensure .agentdesk.json in currentCwd carries the projectKey so loadConfig
|
|
182
|
+
// works on subsequent runs without needing discovery.
|
|
183
|
+
writeMinimalLocalConfig(currentCwd, projectKey);
|
|
332
184
|
|
|
185
|
+
// --- E6: Fill missing tracker creds + verify (gapScan handles both) ---
|
|
186
|
+
await gapScan({ cwd: currentCwd, apiKey, projectKey });
|
|
187
|
+
|
|
188
|
+
// --- E7: Final access-checks summary ---
|
|
189
|
+
env = loadDotEnv(currentCwd);
|
|
190
|
+
const results = await runAccessChecks({ apiKey, serverUrl: SERVER, cwd: currentCwd, config, creds: env });
|
|
191
|
+
printAccessChecks(results, { tracker: config.tracker });
|
|
333
192
|
console.log("");
|
|
334
|
-
|
|
335
|
-
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------- New-project wizard (steps 1–10) ----------
|
|
196
|
+
|
|
197
|
+
async function runNewProjectWizard(cwd, apiKey) {
|
|
198
|
+
let currentCwd = cwd;
|
|
199
|
+
|
|
336
200
|
console.log("");
|
|
337
|
-
console.log(
|
|
338
|
-
console.log(
|
|
339
|
-
console.log(` Directory: ${project.dir}`);
|
|
340
|
-
console.log(` Git: ${project.hasGit ? "yes" : "no"}`);
|
|
341
|
-
console.log(` CLAUDE.md: ${project.hasClaudeMd ? "yes" : "no (recommended)"}`);
|
|
201
|
+
console.log(" AgentDesk — new project setup");
|
|
202
|
+
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
342
203
|
console.log("");
|
|
343
204
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
+
}
|
|
355
220
|
|
|
356
|
-
|
|
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)");
|
|
357
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 ---
|
|
358
250
|
const tracker = await promptSelect({
|
|
359
|
-
message: "Task tracker
|
|
251
|
+
message: "Task tracker",
|
|
360
252
|
choices: [
|
|
361
253
|
{ name: "Linear", value: "linear" },
|
|
362
254
|
{ name: "Jira", value: "jira" },
|
|
@@ -365,284 +257,299 @@ export async function runInit(cwd, opts = {}) {
|
|
|
365
257
|
],
|
|
366
258
|
});
|
|
367
259
|
console.log("");
|
|
368
|
-
if (tracker) config.tracker = tracker;
|
|
369
|
-
|
|
370
|
-
if (tracker) printDedicatedUserChoice(tracker, quick);
|
|
371
260
|
|
|
372
|
-
//
|
|
261
|
+
// --- Step 6: Tracker location ---
|
|
262
|
+
let linearWorkspace = null;
|
|
263
|
+
let jiraBaseUrl = null;
|
|
373
264
|
if (tracker === "linear") {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
"Click \"Create\". Copy the key shown (starts with lin_api_). You will NOT see it again.",
|
|
380
|
-
]);
|
|
381
|
-
const ws = await promptRequired(rl, "Linear workspace slug (the 'kendoo' in linear.app/kendoo)");
|
|
382
|
-
const key = await promptRequired(rl, "Linear API key you just copied");
|
|
383
|
-
saveEnvVar(cwd, "LINEAR_API_KEY", key);
|
|
384
|
-
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(/\/+$/, "");
|
|
385
270
|
console.log("");
|
|
386
|
-
config.linear = { workspace: ws };
|
|
387
271
|
}
|
|
388
272
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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");
|
|
405
295
|
}
|
|
406
296
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
"
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
const login = (await ask(rl, ` GitHub username (the @handle that owns the token)${loginDefault ? ` [${loginDefault}]` : ""}: `)).trim() || loginDefault;
|
|
423
|
-
const token = await promptRequired(rl, "GitHub token");
|
|
424
|
-
saveEnvVar(cwd, "GITHUB_TOKEN", token);
|
|
425
|
-
console.log(" ✓ Saved GITHUB_TOKEN to .env");
|
|
426
|
-
console.log("");
|
|
427
|
-
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;
|
|
428
312
|
}
|
|
313
|
+
console.log("");
|
|
429
314
|
|
|
430
|
-
// Verify tracker +
|
|
431
|
-
//
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (tracker) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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}`);
|
|
441
344
|
const next = await promptSelect({
|
|
442
345
|
message: "What now?",
|
|
443
346
|
choices: [
|
|
444
347
|
{ name: "Re-enter credentials and try again", value: "retry" },
|
|
445
|
-
{ name: "Save
|
|
446
|
-
{ name: "Cancel
|
|
348
|
+
{ name: "Save anyway (fix later in .env)", value: "skip" },
|
|
349
|
+
{ name: "Cancel", value: "cancel" },
|
|
447
350
|
],
|
|
448
351
|
});
|
|
449
|
-
|
|
450
|
-
if (next === "cancel") {
|
|
451
|
-
rl.close();
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
352
|
+
if (next === "cancel") return;
|
|
454
353
|
if (next === "skip") break;
|
|
455
|
-
// Retry: re-prompt only the tracker auth fields, not the whole wizard.
|
|
456
354
|
if (tracker === "linear") {
|
|
457
|
-
|
|
458
|
-
saveEnvVar(
|
|
459
|
-
console.log(" ✓ Updated LINEAR_API_KEY in .env");
|
|
460
|
-
console.log("");
|
|
355
|
+
linearApiKey = await promptRequired("Linear API key");
|
|
356
|
+
saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
|
|
461
357
|
} else if (tracker === "jira") {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
saveEnvVar(
|
|
466
|
-
saveEnvVar(cwd, "JIRA_API_TOKEN", token);
|
|
467
|
-
if (baseUrl) {
|
|
468
|
-
config.jira = { ...(config.jira || {}), baseUrl: baseUrl.replace(/\/+$/, "") };
|
|
469
|
-
trackerLocation = config.jira.baseUrl;
|
|
470
|
-
}
|
|
471
|
-
console.log(" ✓ Updated JIRA_EMAIL and JIRA_API_TOKEN in .env");
|
|
472
|
-
console.log("");
|
|
473
|
-
} else if (tracker === "github") {
|
|
474
|
-
const token = await promptRequired(rl, "GitHub token");
|
|
475
|
-
saveEnvVar(cwd, "GITHUB_TOKEN", token);
|
|
476
|
-
console.log(" ✓ Updated GITHUB_TOKEN in .env");
|
|
477
|
-
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);
|
|
478
362
|
}
|
|
363
|
+
process.stdout.write(" Re-verifying tracker access... ");
|
|
479
364
|
}
|
|
480
|
-
if (verified.ok && verified.trackerProjectId) {
|
|
481
|
-
if (tracker === "linear") config.linear = { ...(config.linear || {}), teamKey: verified.trackerProjectId };
|
|
482
|
-
if (tracker === "jira") config.jira = { ...(config.jira || {}), project: verified.trackerProjectId };
|
|
483
|
-
// Use the tracker's project key as the default agentdesk project key.
|
|
484
|
-
if (!existingConfig.projectKey) finalProjectKey = verified.trackerProjectId.toLowerCase();
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
365
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
// above already collected repo+login+token, so skip the duplicate.
|
|
492
|
-
let githubLogin = config.github?.login || null;
|
|
493
|
-
if (tracker !== "github") {
|
|
494
|
-
if (!quick) {
|
|
495
|
-
console.log(" ─".repeat(30));
|
|
496
|
-
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 });
|
|
497
369
|
console.log("");
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const repo = (await ask(rl, ` GitHub repo (owner/repo)${repoDefault ? ` [${repoDefault}]` : ""}: `)).trim() || repoDefault;
|
|
510
|
-
const loginDefault = existingConfig.github?.login || "";
|
|
511
|
-
const login = (await ask(rl, ` GitHub username (the @handle that owns the token)${loginDefault ? ` [${loginDefault}]` : ""}: `)).trim() || loginDefault;
|
|
512
|
-
const token = await promptRequired(rl, "GitHub token");
|
|
513
|
-
saveEnvVar(cwd, "GITHUB_TOKEN", token);
|
|
514
|
-
console.log(" ✓ Saved GITHUB_TOKEN to .env");
|
|
515
|
-
console.log("");
|
|
516
|
-
config.github = { repo, login };
|
|
517
|
-
githubLogin = login;
|
|
518
|
-
|
|
519
|
-
// Verify token matches the user-stated login.
|
|
520
|
-
try {
|
|
521
|
-
const env = { ...process.env, GH_TOKEN: token };
|
|
522
|
-
const who = execSync("gh api user --jq .login", { env, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
|
|
523
|
-
if (who && login && who.toLowerCase() !== login.toLowerCase()) {
|
|
524
|
-
console.log(` ⚠ Token is valid but authenticates as @${who}, not @${login}. Agents will post as @${who}.`);
|
|
525
|
-
} else if (who) {
|
|
526
|
-
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;
|
|
527
381
|
}
|
|
528
|
-
|
|
529
|
-
|
|
382
|
+
if (tracker === "linear") config.linear = { ...config.linear, teamKey: trackerTeamOrProjectId };
|
|
383
|
+
if (tracker === "jira") config.jira = { ...config.jira, project: trackerTeamOrProjectId };
|
|
384
|
+
console.log("");
|
|
530
385
|
}
|
|
531
|
-
console.log("");
|
|
532
386
|
}
|
|
533
387
|
|
|
534
|
-
//
|
|
535
|
-
const
|
|
536
|
-
const
|
|
537
|
-
|
|
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)"}`);
|
|
538
410
|
console.log("");
|
|
539
411
|
|
|
540
|
-
|
|
541
|
-
if (
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
console.log("");
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// --- Summary + confirm before write ---
|
|
548
|
-
let merged = {};
|
|
549
|
-
if (hasConfig) {
|
|
550
|
-
try { merged = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
merged.projectKey = finalProjectKey;
|
|
554
|
-
|
|
555
|
-
if (tracker) merged.tracker = tracker;
|
|
556
|
-
else delete merged.tracker;
|
|
557
|
-
if (config.linear) merged.linear = config.linear; else if (!tracker) delete merged.linear;
|
|
558
|
-
if (config.jira) merged.jira = config.jira; else if (!tracker) delete merged.jira;
|
|
559
|
-
if (config.github) merged.github = config.github;
|
|
560
|
-
if (config.identityBadge) merged.identityBadge = config.identityBadge;
|
|
561
|
-
|
|
562
|
-
if (!quick) {
|
|
563
|
-
console.log(" Review");
|
|
564
|
-
console.log(" ──────");
|
|
565
|
-
console.log(` Project key: ${merged.projectKey}`);
|
|
566
|
-
console.log(` Tracker: ${merged.tracker || "none"}`);
|
|
567
|
-
if (merged.linear) console.log(` Linear: workspace=${merged.linear.workspace || "-"}, team=${merged.linear.teamKey || "-"}`);
|
|
568
|
-
if (merged.jira) console.log(` Jira: ${merged.jira.baseUrl || "-"}, project=${merged.jira.project || "-"}`);
|
|
569
|
-
if (merged.github) console.log(` GitHub: repo=${merged.github.repo || "-"}, user=@${merged.github.login || "-"}`);
|
|
570
|
-
if (merged.identityBadge) console.log(` Identity badge: ${merged.identityBadge}`);
|
|
571
|
-
console.log("");
|
|
572
|
-
const confirm = (await ask(rl, " Save this config? [Y/n]: ")).trim().toLowerCase();
|
|
573
|
-
if (confirm && confirm !== "y" && confirm !== "yes") {
|
|
574
|
-
console.log(" Cancelled — nothing written.");
|
|
575
|
-
rl.close();
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
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;
|
|
579
416
|
}
|
|
417
|
+
console.log("");
|
|
580
418
|
|
|
581
|
-
|
|
582
|
-
|
|
419
|
+
// Write .agentdesk.json
|
|
420
|
+
writeFileSync(join(currentCwd, ".agentdesk.json"), JSON.stringify(config, null, 2) + "\n");
|
|
421
|
+
console.log(" ✓ Saved .agentdesk.json");
|
|
583
422
|
|
|
584
|
-
//
|
|
585
|
-
const gitignorePath = join(
|
|
423
|
+
// .gitignore — add .agentdesk/
|
|
424
|
+
const gitignorePath = join(currentCwd, ".gitignore");
|
|
586
425
|
try {
|
|
587
426
|
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
|
|
588
427
|
if (!existing.split("\n").some(line => line.trim() === ".agentdesk/" || line.trim() === ".agentdesk")) {
|
|
589
428
|
const nl = existing.endsWith("\n") || !existing ? "" : "\n";
|
|
590
429
|
writeFileSync(gitignorePath, `${existing}${nl}.agentdesk/\n`);
|
|
591
|
-
console.log(
|
|
430
|
+
console.log(" ✓ Added .agentdesk/ to .gitignore");
|
|
592
431
|
}
|
|
593
432
|
} catch {}
|
|
594
433
|
|
|
595
|
-
// Register
|
|
596
|
-
registerLocalProject(finalProjectKey, project.name || finalProjectKey, project.dir);
|
|
597
|
-
|
|
598
|
-
// --- Register with server ---
|
|
434
|
+
// Register with server + push settings
|
|
599
435
|
try {
|
|
600
|
-
|
|
436
|
+
await fetch(`${SERVER}/api/projects`, {
|
|
601
437
|
method: "POST",
|
|
602
|
-
headers: {
|
|
603
|
-
|
|
604
|
-
...(loadApiKey(cwd) ? { "x-api-key": loadApiKey(cwd) } : {}),
|
|
605
|
-
},
|
|
606
|
-
body: JSON.stringify({
|
|
607
|
-
id: finalProjectKey,
|
|
608
|
-
name: project.name || finalProjectKey,
|
|
609
|
-
path: project.dir,
|
|
610
|
-
type: project.type,
|
|
611
|
-
tracker,
|
|
612
|
-
}),
|
|
438
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
|
439
|
+
body: JSON.stringify({ id: projectKey, name: projectName, path: currentCwd, tracker }),
|
|
613
440
|
});
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
if (key) {
|
|
620
|
-
const push = await pushConfig(key, SERVER, finalProjectKey, merged);
|
|
621
|
-
if (!push.ok && push.error !== "missing_auth") {
|
|
622
|
-
console.log(` ⚠ Could not push settings to agentdesk.live: ${push.error}`);
|
|
623
|
-
console.log(" Your local .agentdesk.json is saved; retry from the UI or re-run init.");
|
|
624
|
-
}
|
|
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");
|
|
625
446
|
}
|
|
626
447
|
} catch {
|
|
627
|
-
|
|
448
|
+
console.log(" ⚠ Couldn't reach agentdesk.live — local config saved, retry from the UI later.");
|
|
628
449
|
}
|
|
629
450
|
|
|
451
|
+
registerLocalProject(projectKey, projectName, currentCwd);
|
|
452
|
+
|
|
630
453
|
console.log("");
|
|
631
|
-
|
|
454
|
+
const cmdPrefix = trackerTeamOrProjectId || projectKey.toUpperCase();
|
|
455
|
+
console.log(" Ready. Run a team session:");
|
|
456
|
+
console.log(` agentdesk team ${cmdPrefix}-123`);
|
|
632
457
|
console.log("");
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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);
|
|
639
468
|
}
|
|
640
|
-
console.log("");
|
|
641
469
|
|
|
642
|
-
|
|
643
|
-
|
|
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(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
644
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
|
|
645
552
|
}
|
|
646
553
|
|
|
647
|
-
|
|
554
|
+
await runNewProjectWizard(cwd, apiKey);
|
|
648
555
|
}
|