@kendoo.agentdesk/agentdesk 0.19.3 → 0.19.5
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 +3 -0
- package/bin/agentdesk.mjs +11 -0
- package/cli/bootstrap.mjs +399 -0
- package/cli/login.mjs +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,16 @@ Internal refactors, infrastructure changes, and architectural notes are not list
|
|
|
13
13
|
### Added
|
|
14
14
|
- `[UI]` Private-session sharing flow. When someone visits a session URL they don't own, they now see a friendly "Shh… this one's private" page with a one-click "Request access" button instead of a blank error. The session owner sees pending requests in a new header inbox and can grant viewer access for just that session or the whole project. Viewer-granted sessions show up in the teammate's sidebar tagged as a viewer.
|
|
15
15
|
|
|
16
|
+
## [0.19.5] — 2026-04-19
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `[CLI]` `agentdesk bootstrap` now works even when the cloned repo has no committed `.agentdesk.json`. If the project config file is absent, bootstrap fetches the user's project list from the server, tries to auto-match by the git remote (`origin`), and falls back to a numbered picker of the account's projects. Once picked, a minimal `.agentdesk.json` is written locally (prompting the user to commit it so future clones skip the picker) and the rest of the gap-scan continues unchanged. Previous behavior required the file to already be present.
|
|
20
|
+
|
|
21
|
+
## [0.19.4] — 2026-04-19
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `[CLI]` `agentdesk bootstrap` — fresh-machine setup for repos already configured on another machine. Reads the committed `.agentdesk.json` for project identity, tracker type, team/project key, and GitHub repo, then only prompts for the per-machine secrets (Linear API key, Jira email + token, GitHub token) that belong in the local `.env`. Previously the only post-clone path was `agentdesk init`, which walked the full wizard and risked overwriting project-wide settings when the user just wanted to paste in their own tokens. Bootstrap never writes `.agentdesk.json` and never pushes to the server, so the first machine's setup stays untouched. `agentdesk login` now points at both `init` (new project) and `bootstrap` (cloned repo) as next steps.
|
|
25
|
+
|
|
16
26
|
## [0.19.3] — 2026-04-19
|
|
17
27
|
|
|
18
28
|
### Fixed
|
package/README.md
CHANGED
|
@@ -47,6 +47,8 @@ A guided wizard walks you through it: picks up your project type, asks which tas
|
|
|
47
47
|
|
|
48
48
|
Re-running `agentdesk init` in a project that already has config offers a "tracker only" shortcut. Use `agentdesk init --quick` to skip the narrative copy for scripted setups.
|
|
49
49
|
|
|
50
|
+
**On a second machine** (repo cloned from a teammate's setup): run `agentdesk bootstrap` instead of `init`. It reads the committed `.agentdesk.json` for the project-wide settings (tracker type, team/project key, repo) and only prompts for per-machine secrets (Linear API key, Jira email + token, GitHub token) that end up in `.env`. The other machine's `.env` and the server config are left untouched.
|
|
51
|
+
|
|
50
52
|
`.agentdesk.json` is kept in sync with the server: every CLI run fetches the authoritative config and rewrites the local file as a credential-free snapshot. Credentials live only on the server, encrypted. If a local edit disagreed with the server's value, a one-line warning lists the overridden fields.
|
|
51
53
|
|
|
52
54
|
### 4. Run a team session
|
|
@@ -72,6 +74,7 @@ Watch the session live at [agentdesk.live](https://agentdesk.live). Each session
|
|
|
72
74
|
agentdesk login Sign in to AgentDesk
|
|
73
75
|
agentdesk logout Sign out and remove credentials
|
|
74
76
|
agentdesk init Set up project and configure tracker
|
|
77
|
+
agentdesk bootstrap Fill in per-machine tokens on a cloned repo
|
|
75
78
|
agentdesk team <TASK-ID> Run a team session on an existing task
|
|
76
79
|
agentdesk team -d "..." Describe what you want — task created automatically
|
|
77
80
|
agentdesk <agent> -d "..." Run a single agent (jane, dennis, sam, bart, vera, luna, mark, nora)
|
package/bin/agentdesk.mjs
CHANGED
|
@@ -58,10 +58,16 @@ if (!command || command === "help" || command === "--help") {
|
|
|
58
58
|
2. agentdesk init Set up your project (choose tracker, save config)
|
|
59
59
|
3. agentdesk team TASK-123 Run a team session
|
|
60
60
|
|
|
61
|
+
On a freshly cloned repo (already set up on another machine):
|
|
62
|
+
1. agentdesk login Sign in
|
|
63
|
+
2. agentdesk bootstrap Fill in per-machine secrets (tokens)
|
|
64
|
+
3. agentdesk team TASK-123 Run a team session
|
|
65
|
+
|
|
61
66
|
Commands:
|
|
62
67
|
agentdesk login Sign in to AgentDesk
|
|
63
68
|
agentdesk logout Sign out and remove credentials
|
|
64
69
|
agentdesk init [--quick] Set up project and configure tracker
|
|
70
|
+
agentdesk bootstrap Fill in per-machine tokens on a cloned repo
|
|
65
71
|
agentdesk team <TASK-ID> Run a team session on an existing task
|
|
66
72
|
agentdesk team -d "..." Create a task and run a session
|
|
67
73
|
agentdesk <agent> -d "..." Run a single agent (jane, dennis, sam, bart, vera, luna, mark, nora)
|
|
@@ -146,6 +152,11 @@ else if (command === "init") {
|
|
|
146
152
|
await runInit(process.cwd(), { quick });
|
|
147
153
|
}
|
|
148
154
|
|
|
155
|
+
else if (command === "bootstrap") {
|
|
156
|
+
const { runBootstrap } = await import("../cli/bootstrap.mjs");
|
|
157
|
+
await runBootstrap(process.cwd());
|
|
158
|
+
}
|
|
159
|
+
|
|
149
160
|
else if (command === "team") {
|
|
150
161
|
// Parse options first to find -d, --cwd, --child-strategy
|
|
151
162
|
let description = "";
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// `agentdesk bootstrap` — fill in per-machine secrets after cloning a repo
|
|
2
|
+
// that's already been set up on a different machine.
|
|
3
|
+
//
|
|
4
|
+
// Intent: on a fresh clone, `.agentdesk.json` carries the project identity,
|
|
5
|
+
// tracker binding, and GitHub repo — everything that's project-wide. What's
|
|
6
|
+
// missing are the per-machine secrets that live in `.env` (LINEAR_API_KEY,
|
|
7
|
+
// JIRA_EMAIL/JIRA_API_TOKEN, GITHUB_TOKEN). Bootstrap detects which are
|
|
8
|
+
// missing or invalid and prompts only for those.
|
|
9
|
+
//
|
|
10
|
+
// Hard constraints:
|
|
11
|
+
// - Never write `.agentdesk.json` (besides the cached-server-state rewrite
|
|
12
|
+
// that loadConfig already does on every CLI run — we don't add new ones).
|
|
13
|
+
// - Never push to the server. Project-wide config on machine 1 is untouched.
|
|
14
|
+
// - Only writes to `.env`.
|
|
15
|
+
// - Idempotent. Running on a fully-configured machine prints "all set" and
|
|
16
|
+
// exits 0.
|
|
17
|
+
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
import { createInterface } from "readline";
|
|
21
|
+
import { execSync } from "child_process";
|
|
22
|
+
import { loadConfig } from "./config.mjs";
|
|
23
|
+
import { getStoredApiKey } from "./login.mjs";
|
|
24
|
+
import { checkTrackerPermissions, resolveCredentialsFromEnv } from "./tracker-check.mjs";
|
|
25
|
+
import { assertPushable, PreflightError } from "./session-preflight.mjs";
|
|
26
|
+
|
|
27
|
+
const SERVER = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
|
|
28
|
+
|
|
29
|
+
function detectGitRemote(dir) {
|
|
30
|
+
try {
|
|
31
|
+
const url = execSync("git remote get-url origin", { cwd: dir, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
|
|
32
|
+
const m = url.match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
33
|
+
if (m) return `${m[1]}/${m[2]}`;
|
|
34
|
+
} catch {}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function fetchProjects(apiKey) {
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(`${SERVER}/api/projects`, {
|
|
41
|
+
headers: { "x-api-key": apiKey },
|
|
42
|
+
signal: AbortSignal.timeout(8000),
|
|
43
|
+
});
|
|
44
|
+
if (!res.ok) return null;
|
|
45
|
+
return await res.json();
|
|
46
|
+
} catch { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function fetchProjectSettings(apiKey, projectId) {
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(`${SERVER}/api/projects/${projectId}/settings`, {
|
|
52
|
+
headers: { "x-api-key": apiKey },
|
|
53
|
+
signal: AbortSignal.timeout(5000),
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) return {};
|
|
56
|
+
return await res.json();
|
|
57
|
+
} catch { return {}; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Discover the server-side project for this clone when .agentdesk.json is
|
|
61
|
+
// absent. Strategy:
|
|
62
|
+
// 1. Fetch the user's projects list.
|
|
63
|
+
// 2. For each, fetch settings so we can see github.repo.
|
|
64
|
+
// 3. If we can match the local git remote to exactly one project's repo,
|
|
65
|
+
// auto-select (confirmation prompt).
|
|
66
|
+
// 4. Otherwise show a numbered picker of all the user's projects.
|
|
67
|
+
// 5. Write a minimal `.agentdesk.json` locally so loadConfig takes over
|
|
68
|
+
// from here.
|
|
69
|
+
// Returns the chosen projectKey, or null if the user aborts.
|
|
70
|
+
async function discoverProject(rl, cwd, apiKey) {
|
|
71
|
+
const remote = detectGitRemote(cwd);
|
|
72
|
+
if (remote) console.log(` Detected git remote: ${remote}`);
|
|
73
|
+
console.log(" Fetching your projects from the server...");
|
|
74
|
+
|
|
75
|
+
const projects = await fetchProjects(apiKey);
|
|
76
|
+
if (!projects || projects.length === 0) {
|
|
77
|
+
console.log("");
|
|
78
|
+
console.log(" No projects found on your account.");
|
|
79
|
+
console.log(" Run `agentdesk init` on the primary machine first, then retry here.");
|
|
80
|
+
console.log("");
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Fetch settings in parallel so we can display/match the configured repo.
|
|
85
|
+
const settings = await Promise.all(projects.map(p => fetchProjectSettings(apiKey, p.id)));
|
|
86
|
+
const rows = projects.map((p, i) => {
|
|
87
|
+
const repo = settings[i]?.github?.repo || null;
|
|
88
|
+
const matches = remote && repo && repo.toLowerCase() === remote.toLowerCase();
|
|
89
|
+
return { project: p, settings: settings[i] || {}, repo, matches };
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const exactMatches = rows.filter(r => r.matches);
|
|
93
|
+
let chosen = null;
|
|
94
|
+
|
|
95
|
+
if (exactMatches.length === 1) {
|
|
96
|
+
const r = exactMatches[0];
|
|
97
|
+
console.log(` ✓ Match: ${r.project.name} (${r.project.id}) — repo ${r.repo}`);
|
|
98
|
+
console.log("");
|
|
99
|
+
const ans = (await askInline(rl, ` Use this project? [Y/n]: `)).trim().toLowerCase();
|
|
100
|
+
if (ans === "" || ans === "y" || ans === "yes") chosen = r;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!chosen) {
|
|
104
|
+
console.log("");
|
|
105
|
+
console.log(" Pick the project this repo maps to:");
|
|
106
|
+
console.log("");
|
|
107
|
+
rows.forEach((r, i) => {
|
|
108
|
+
const tag = r.matches ? " ← matches git remote" : "";
|
|
109
|
+
const repoStr = r.repo ? ` [${r.repo}]` : "";
|
|
110
|
+
console.log(` ${i + 1}) ${r.project.name} (${r.project.id})${repoStr}${tag}`);
|
|
111
|
+
});
|
|
112
|
+
console.log("");
|
|
113
|
+
while (true) {
|
|
114
|
+
const answer = await askInline(rl, ` Choose (1-${rows.length}, or 'c' to cancel): `);
|
|
115
|
+
const trimmed = answer.trim().toLowerCase();
|
|
116
|
+
if (trimmed === "c" || trimmed === "cancel") return null;
|
|
117
|
+
const num = parseInt(trimmed, 10);
|
|
118
|
+
if (num >= 1 && num <= rows.length) { chosen = rows[num - 1]; break; }
|
|
119
|
+
console.log(` Please enter a number between 1 and ${rows.length}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Write a minimal .agentdesk.json — loadConfig will merge server state
|
|
124
|
+
// into it on the next call, producing the full cached view.
|
|
125
|
+
const minimal = { projectKey: chosen.project.id };
|
|
126
|
+
writeFileSync(join(cwd, ".agentdesk.json"), JSON.stringify(minimal, null, 2) + "\n");
|
|
127
|
+
console.log("");
|
|
128
|
+
console.log(` ✓ Wrote .agentdesk.json (projectKey: ${chosen.project.id})`);
|
|
129
|
+
console.log(" (commit this file so future clones skip the picker)");
|
|
130
|
+
return chosen.project.id;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function askInline(rl, question) {
|
|
134
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ask(rl, question) {
|
|
138
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function promptRequired(rl, label) {
|
|
142
|
+
while (true) {
|
|
143
|
+
const v = (await ask(rl, ` ${label}: `)).trim();
|
|
144
|
+
if (v) return v;
|
|
145
|
+
console.log(` ${label} is required. Ctrl+C to abort.`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function saveEnvVar(dir, key, value) {
|
|
150
|
+
const envPath = join(dir, ".env");
|
|
151
|
+
let content = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
|
|
152
|
+
const re = new RegExp(`^${key}=.*$`, "m");
|
|
153
|
+
if (re.test(content)) {
|
|
154
|
+
content = content.replace(re, `${key}=${value}`);
|
|
155
|
+
} else {
|
|
156
|
+
content += `${content && !content.endsWith("\n") ? "\n" : ""}${key}=${value}\n`;
|
|
157
|
+
}
|
|
158
|
+
writeFileSync(envPath, content);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function loadDotEnvLocal(dir) {
|
|
162
|
+
const envPath = join(dir, ".env");
|
|
163
|
+
const out = {};
|
|
164
|
+
if (!existsSync(envPath)) return out;
|
|
165
|
+
for (const line of readFileSync(envPath, "utf-8").split("\n")) {
|
|
166
|
+
const trimmed = line.trim();
|
|
167
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
168
|
+
const eq = trimmed.indexOf("=");
|
|
169
|
+
if (eq !== -1) out[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function readLocalConfig(cwd) {
|
|
175
|
+
const path = join(cwd, ".agentdesk.json");
|
|
176
|
+
if (!existsSync(path)) return null;
|
|
177
|
+
try { return JSON.parse(readFileSync(path, "utf-8")); } catch { return null; }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Compact step list — the init wizard carries the full narrative; bootstrap
|
|
181
|
+
// assumes the user has already seen it on their first machine.
|
|
182
|
+
function printTrackerHint(tracker) {
|
|
183
|
+
if (tracker === "linear") {
|
|
184
|
+
console.log(" (Get a Linear API key at https://linear.app/settings/api)");
|
|
185
|
+
} else if (tracker === "jira") {
|
|
186
|
+
console.log(" (Create a Jira API token at https://id.atlassian.com/manage-profile/security/api-tokens)");
|
|
187
|
+
} else if (tracker === "github") {
|
|
188
|
+
console.log(" (Create a GitHub token at https://github.com/settings/tokens — scope: repo)");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Walk missing tracker credentials and prompt for each. Writes to .env.
|
|
193
|
+
// Returns the merged credentials map so the caller can verify.
|
|
194
|
+
async function promptTrackerCreds(rl, cwd, tracker, existing) {
|
|
195
|
+
const creds = { ...existing };
|
|
196
|
+
if (tracker === "linear") {
|
|
197
|
+
if (!creds.LINEAR_API_KEY) {
|
|
198
|
+
console.log("");
|
|
199
|
+
printTrackerHint("linear");
|
|
200
|
+
const v = await promptRequired(rl, "Linear API key");
|
|
201
|
+
saveEnvVar(cwd, "LINEAR_API_KEY", v);
|
|
202
|
+
creds.LINEAR_API_KEY = v;
|
|
203
|
+
console.log(" ✓ Saved LINEAR_API_KEY to .env");
|
|
204
|
+
}
|
|
205
|
+
} else if (tracker === "jira") {
|
|
206
|
+
if (!creds.JIRA_EMAIL) {
|
|
207
|
+
console.log("");
|
|
208
|
+
printTrackerHint("jira");
|
|
209
|
+
const v = await promptRequired(rl, "Your Atlassian login email");
|
|
210
|
+
saveEnvVar(cwd, "JIRA_EMAIL", v);
|
|
211
|
+
creds.JIRA_EMAIL = v;
|
|
212
|
+
console.log(" ✓ Saved JIRA_EMAIL to .env");
|
|
213
|
+
}
|
|
214
|
+
if (!creds.JIRA_API_TOKEN) {
|
|
215
|
+
if (creds.JIRA_EMAIL) console.log("");
|
|
216
|
+
const v = await promptRequired(rl, "Jira API token");
|
|
217
|
+
saveEnvVar(cwd, "JIRA_API_TOKEN", v);
|
|
218
|
+
creds.JIRA_API_TOKEN = v;
|
|
219
|
+
console.log(" ✓ Saved JIRA_API_TOKEN to .env");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return creds;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function promptGitHubToken(rl, cwd) {
|
|
226
|
+
console.log("");
|
|
227
|
+
printTrackerHint("github");
|
|
228
|
+
const v = await promptRequired(rl, "GitHub token");
|
|
229
|
+
saveEnvVar(cwd, "GITHUB_TOKEN", v);
|
|
230
|
+
console.log(" ✓ Saved GITHUB_TOKEN to .env");
|
|
231
|
+
return v;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function selectOption(rl, prompt, options) {
|
|
235
|
+
console.log(` ${prompt}`);
|
|
236
|
+
console.log("");
|
|
237
|
+
options.forEach((opt, i) => console.log(` ${i + 1}) ${opt.label}`));
|
|
238
|
+
console.log("");
|
|
239
|
+
while (true) {
|
|
240
|
+
const answer = await ask(rl, ` Choose (1-${options.length}): `);
|
|
241
|
+
const num = parseInt(answer.trim(), 10);
|
|
242
|
+
if (num >= 1 && num <= options.length) return options[num - 1];
|
|
243
|
+
console.log(` Please enter a number between 1 and ${options.length}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function runBootstrap(cwd = process.cwd()) {
|
|
248
|
+
console.log("");
|
|
249
|
+
console.log(" AgentDesk — Bootstrap this machine");
|
|
250
|
+
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
251
|
+
console.log("");
|
|
252
|
+
|
|
253
|
+
// 1. Must be logged in.
|
|
254
|
+
const apiKey = getStoredApiKey();
|
|
255
|
+
if (!apiKey) {
|
|
256
|
+
console.log(" Not logged in. Run `agentdesk login` first.");
|
|
257
|
+
console.log("");
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
262
|
+
|
|
263
|
+
// 2. Find the server-side project for this clone. Prefer a committed
|
|
264
|
+
// .agentdesk.json (clean path); fall back to discovering from the
|
|
265
|
+
// user's server-side project list when the file is missing.
|
|
266
|
+
let localConfig = readLocalConfig(cwd);
|
|
267
|
+
let projectKey = localConfig?.projectKey || null;
|
|
268
|
+
|
|
269
|
+
if (!projectKey) {
|
|
270
|
+
if (localConfig) {
|
|
271
|
+
console.log(" .agentdesk.json has no projectKey — discovering project from server.");
|
|
272
|
+
} else {
|
|
273
|
+
console.log(" No .agentdesk.json in this directory — discovering project from server.");
|
|
274
|
+
}
|
|
275
|
+
console.log("");
|
|
276
|
+
projectKey = await discoverProject(rl, cwd, apiKey);
|
|
277
|
+
if (!projectKey) { rl.close(); process.exit(1); }
|
|
278
|
+
localConfig = readLocalConfig(cwd);
|
|
279
|
+
console.log("");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 3. Load merged config (server-authoritative when reachable). This is the
|
|
283
|
+
// same call `agentdesk team` makes, so we see the same view.
|
|
284
|
+
const config = await loadConfig(cwd, { apiKey, serverUrl: SERVER, projectName: projectKey, silent: true });
|
|
285
|
+
|
|
286
|
+
const tracker = config.tracker || null;
|
|
287
|
+
const repo = config.github?.repo || null;
|
|
288
|
+
const login = config.github?.login || null;
|
|
289
|
+
|
|
290
|
+
console.log(` Project: ${projectKey}`);
|
|
291
|
+
console.log(` Tracker: ${tracker || "none"}`);
|
|
292
|
+
if (tracker === "linear" && config.linear?.teamKey) console.log(` Team: ${config.linear.teamKey}`);
|
|
293
|
+
if (tracker === "jira" && config.jira?.project) console.log(` Project: ${config.jira.project}`);
|
|
294
|
+
if (repo) console.log(` Repo: ${repo}${login ? ` (@${login})` : ""}`);
|
|
295
|
+
console.log("");
|
|
296
|
+
|
|
297
|
+
// 4. Gap scan + prompt loop. We retry tracker + github verification up to
|
|
298
|
+
// three times, matching init's retry/skip/cancel UX.
|
|
299
|
+
let creds = resolveCredentialsFromEnv(loadDotEnvLocal(cwd));
|
|
300
|
+
let trackerOk = !tracker; // no tracker → nothing to verify
|
|
301
|
+
let githubOk = false;
|
|
302
|
+
let attempts = 0;
|
|
303
|
+
|
|
304
|
+
while (attempts < 5) {
|
|
305
|
+
attempts += 1;
|
|
306
|
+
|
|
307
|
+
// Tracker
|
|
308
|
+
if (tracker && !trackerOk) {
|
|
309
|
+
const missing =
|
|
310
|
+
(tracker === "linear" && !creds.LINEAR_API_KEY) ||
|
|
311
|
+
(tracker === "jira" && (!creds.JIRA_EMAIL || !creds.JIRA_API_TOKEN));
|
|
312
|
+
if (missing) {
|
|
313
|
+
creds = await promptTrackerCreds(rl, cwd, tracker, creds);
|
|
314
|
+
}
|
|
315
|
+
process.stdout.write(" Verifying tracker access... ");
|
|
316
|
+
const check = await checkTrackerPermissions({ tracker, config, credentials: creds });
|
|
317
|
+
if (check.ok) {
|
|
318
|
+
console.log("✓");
|
|
319
|
+
if (check.identity?.name || check.identity?.email) {
|
|
320
|
+
const label = check.identity.name || check.identity.login || check.identity.email;
|
|
321
|
+
console.log(` ✓ Posting as: ${label}`);
|
|
322
|
+
}
|
|
323
|
+
trackerOk = true;
|
|
324
|
+
} else {
|
|
325
|
+
console.log("failed");
|
|
326
|
+
for (const e of check.errors || []) console.log(` • ${e}`);
|
|
327
|
+
console.log("");
|
|
328
|
+
const next = await selectOption(rl, "What now?", [
|
|
329
|
+
{ label: "Re-enter credentials", value: "retry" },
|
|
330
|
+
{ label: "Skip tracker (fix later in .env)", value: "skip" },
|
|
331
|
+
{ label: "Cancel", value: "cancel" },
|
|
332
|
+
]);
|
|
333
|
+
console.log("");
|
|
334
|
+
if (next.value === "cancel") { rl.close(); process.exit(1); }
|
|
335
|
+
if (next.value === "skip") { trackerOk = true; break; }
|
|
336
|
+
// retry: clear the tracker-specific creds so the next loop re-prompts
|
|
337
|
+
if (tracker === "linear") delete creds.LINEAR_API_KEY;
|
|
338
|
+
if (tracker === "jira") { delete creds.JIRA_EMAIL; delete creds.JIRA_API_TOKEN; }
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// GitHub — always required (agents push code regardless of tracker)
|
|
344
|
+
if (!githubOk) {
|
|
345
|
+
if (!creds.GITHUB_TOKEN) {
|
|
346
|
+
creds.GITHUB_TOKEN = await promptGitHubToken(rl, cwd);
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
assertPushable({ cwd, creds: { GITHUB_TOKEN: creds.GITHUB_TOKEN }, projectName: projectKey });
|
|
350
|
+
// Best-effort gh API check to confirm the token is valid and matches
|
|
351
|
+
// the configured login. Mirrors init.mjs:495-506.
|
|
352
|
+
try {
|
|
353
|
+
const { execSync } = await import("child_process");
|
|
354
|
+
const env = { ...process.env, GH_TOKEN: creds.GITHUB_TOKEN };
|
|
355
|
+
const who = execSync("gh api user --jq .login", { env, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
|
|
356
|
+
if (who && login && who.toLowerCase() !== login.toLowerCase()) {
|
|
357
|
+
console.log(` ⚠ GitHub token authenticates as @${who}, not @${login} (from .agentdesk.json). Agents will post as @${who}.`);
|
|
358
|
+
} else if (who) {
|
|
359
|
+
console.log(` ✓ GitHub verified as @${who}`);
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
console.log(" ✓ GITHUB_TOKEN present (couldn't verify via gh — install gh CLI for identity check)");
|
|
363
|
+
}
|
|
364
|
+
githubOk = true;
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (err instanceof PreflightError) {
|
|
367
|
+
console.log(` ✗ ${err.message}`);
|
|
368
|
+
console.log("");
|
|
369
|
+
const next = await selectOption(rl, "What now?", [
|
|
370
|
+
{ label: "Re-enter GITHUB_TOKEN", value: "retry" },
|
|
371
|
+
{ label: "Skip (sessions will fail until fixed)", value: "skip" },
|
|
372
|
+
{ label: "Cancel", value: "cancel" },
|
|
373
|
+
]);
|
|
374
|
+
console.log("");
|
|
375
|
+
if (next.value === "cancel") { rl.close(); process.exit(1); }
|
|
376
|
+
if (next.value === "skip") { githubOk = true; break; }
|
|
377
|
+
delete creds.GITHUB_TOKEN;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
throw err;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (trackerOk && githubOk) break;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
rl.close();
|
|
388
|
+
|
|
389
|
+
console.log("");
|
|
390
|
+
if (trackerOk && githubOk) {
|
|
391
|
+
console.log(" Ready. Try:");
|
|
392
|
+
const prefix = config.linear?.teamKey || config.jira?.project || projectKey.toUpperCase();
|
|
393
|
+
console.log(` agentdesk team ${prefix}-123`);
|
|
394
|
+
console.log("");
|
|
395
|
+
} else {
|
|
396
|
+
console.log(" Bootstrap finished with missing pieces — edit .env directly or re-run.");
|
|
397
|
+
console.log("");
|
|
398
|
+
}
|
|
399
|
+
}
|
package/cli/login.mjs
CHANGED
|
@@ -114,7 +114,8 @@ export async function runLogin() {
|
|
|
114
114
|
console.log(` API key saved to ~/.agentdesk/credentials.json`);
|
|
115
115
|
console.log("");
|
|
116
116
|
console.log(" Next steps:");
|
|
117
|
-
console.log(" agentdesk init");
|
|
117
|
+
console.log(" agentdesk init (new project on this machine)");
|
|
118
|
+
console.log(" agentdesk bootstrap (repo cloned from another machine)");
|
|
118
119
|
console.log(" agentdesk team TASK-123");
|
|
119
120
|
console.log("");
|
|
120
121
|
|