@kendoo.agentdesk/agentdesk 0.19.6 → 0.19.7
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 +6 -0
- package/README.md +4 -2
- package/bin/agentdesk.mjs +7 -4
- package/cli/bootstrap.mjs +120 -141
- package/cli/init.mjs +78 -54
- package/cli/prompts.mjs +64 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,12 @@ 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.7] — 2026-04-19
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `[CLI]` `agentdesk init` now auto-detects the second-machine / cloned-repo case and switches to a token-refresh flow without opening the wizard. Two fast paths land before the full wizard: (1) when `.agentdesk.json` already has a `projectKey`, the first menu offers "Refresh tokens" as the default action (along with "Tracker only", "Full setup", "Cancel"); (2) when there's no local config but the user's account has a project whose id or name matches the git remote, init writes a minimal `.agentdesk.json` and goes straight to the token-refresh. Pass `--force-full` to bypass both and walk the full wizard. The standalone `agentdesk bootstrap` command is now a thin alias over the same flow.
|
|
20
|
+
- `[CLI]` All interactive menus (tracker picker, re-run action, retry/skip/cancel prompts, team/project picker) now use arrow-key navigation with color highlighting via `@inquirer/prompts` instead of numbered text prompts. Ctrl-C during a menu exits cleanly with status 130.
|
|
21
|
+
|
|
16
22
|
## [0.19.6] — 2026-04-19
|
|
17
23
|
|
|
18
24
|
### Changed
|
package/README.md
CHANGED
|
@@ -45,9 +45,11 @@ agentdesk init
|
|
|
45
45
|
|
|
46
46
|
A guided wizard walks you through it: picks up your project type, asks which task tracker you use (Linear, Jira, GitHub Issues, or none), shows the tradeoff between a **dedicated AgentDesk user** and your personal account with step-by-step setup for either, verifies the credentials, echoes "Posting as: …" so you can confirm which identity will appear on tickets, then writes `.agentdesk.json`.
|
|
47
47
|
|
|
48
|
-
Re-running `agentdesk init` in a project that already has config offers a "
|
|
48
|
+
Re-running `agentdesk init` in a project that already has config offers a four-way menu with **"Refresh tokens"** as the default — so rotating a token or setting up a second machine is a single enter-press, no wizard walk. The other options are "Tracker only", "Full setup", and "Cancel". Use `agentdesk init --quick` to skip the narrative copy for scripted setups, or `--force-full` to bypass the fast paths and walk the full wizard.
|
|
49
49
|
|
|
50
|
-
**On a second machine** (repo cloned from a teammate's setup): run `agentdesk
|
|
50
|
+
**On a second machine** (repo cloned from a teammate's setup): just run `agentdesk init`. If no `.agentdesk.json` is committed, `init` fetches your account's project list, auto-matches this clone against the git remote, and switches to token-refresh mode — you only paste the per-machine tokens (Linear API key, Jira email + token, GitHub token) into `.env`. The explicit `agentdesk bootstrap` command still exists as an alias that always runs that flow. The other machine's `.env` and the server config are left untouched.
|
|
51
|
+
|
|
52
|
+
All menus use arrow-key navigation with color highlighting.
|
|
51
53
|
|
|
52
54
|
`.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.
|
|
53
55
|
|
package/bin/agentdesk.mjs
CHANGED
|
@@ -60,13 +60,14 @@ if (!command || command === "help" || command === "--help") {
|
|
|
60
60
|
|
|
61
61
|
On a freshly cloned repo (already set up on another machine):
|
|
62
62
|
1. agentdesk login Sign in
|
|
63
|
-
2. agentdesk
|
|
63
|
+
2. agentdesk init Auto-detects the project and asks only for tokens
|
|
64
|
+
(or run 'agentdesk bootstrap' — same flow)
|
|
64
65
|
3. agentdesk team TASK-123 Run a team session
|
|
65
66
|
|
|
66
67
|
Commands:
|
|
67
68
|
agentdesk login Sign in to AgentDesk
|
|
68
69
|
agentdesk logout Sign out and remove credentials
|
|
69
|
-
agentdesk init [--quick]
|
|
70
|
+
agentdesk init [--quick | --force-full] Set up project (auto-detects cloned repos & offers token refresh)
|
|
70
71
|
agentdesk bootstrap Fill in per-machine tokens on a cloned repo
|
|
71
72
|
agentdesk team <TASK-ID> Run a team session on an existing task
|
|
72
73
|
agentdesk team -d "..." Create a task and run a session
|
|
@@ -148,8 +149,10 @@ else if (command === "logout") {
|
|
|
148
149
|
|
|
149
150
|
else if (command === "init") {
|
|
150
151
|
const { runInit } = await import("../cli/init.mjs");
|
|
151
|
-
const
|
|
152
|
-
|
|
152
|
+
const rest = args.slice(1);
|
|
153
|
+
const quick = rest.includes("--quick");
|
|
154
|
+
const forceFull = rest.includes("--force-full") || rest.includes("--full");
|
|
155
|
+
await runInit(process.cwd(), { quick, forceFull });
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
else if (command === "bootstrap") {
|
package/cli/bootstrap.mjs
CHANGED
|
@@ -7,22 +7,17 @@
|
|
|
7
7
|
// JIRA_EMAIL/JIRA_API_TOKEN, GITHUB_TOKEN). Bootstrap detects which are
|
|
8
8
|
// missing or invalid and prompts only for those.
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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.
|
|
10
|
+
// This module also exports reusable building blocks so `agentdesk init` can
|
|
11
|
+
// short-circuit to the bootstrap flow when it detects a fresh clone.
|
|
17
12
|
|
|
18
13
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
19
14
|
import { join } from "path";
|
|
20
|
-
import { createInterface } from "readline";
|
|
21
15
|
import { execSync } from "child_process";
|
|
22
16
|
import { loadConfig } from "./config.mjs";
|
|
23
17
|
import { getStoredApiKey } from "./login.mjs";
|
|
24
18
|
import { checkTrackerPermissions, resolveCredentialsFromEnv } from "./tracker-check.mjs";
|
|
25
19
|
import { assertPushable, PreflightError } from "./session-preflight.mjs";
|
|
20
|
+
import { select, promptRequired } from "./prompts.mjs";
|
|
26
21
|
|
|
27
22
|
const SERVER = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
|
|
28
23
|
|
|
@@ -65,14 +60,41 @@ function normalizeName(s) {
|
|
|
65
60
|
return tail.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
66
61
|
}
|
|
67
62
|
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
63
|
+
// Silent auto-match: returns { projectKey, name, reason } when the caller can
|
|
64
|
+
// pick a server-side project with high confidence without asking the user,
|
|
65
|
+
// or null otherwise. Used by both `agentdesk init` (to decide whether to
|
|
66
|
+
// skip the wizard) and `agentdesk bootstrap` (as step 1 of discovery).
|
|
67
|
+
export async function autoMatchProject(cwd, apiKey) {
|
|
68
|
+
const remote = detectGitRemote(cwd);
|
|
69
|
+
if (!remote) return null;
|
|
70
|
+
const projects = await fetchProjects(apiKey);
|
|
71
|
+
if (!projects || projects.length === 0) return null;
|
|
72
|
+
|
|
73
|
+
const remoteRepoName = remote.split("/").pop();
|
|
74
|
+
const remoteKey = normalizeName(remoteRepoName);
|
|
75
|
+
|
|
76
|
+
// Tier 1: exact github.repo match via settings.
|
|
77
|
+
const settings = await Promise.all(projects.map(p => fetchProjectSettings(apiKey, p.id)));
|
|
78
|
+
const tier1 = projects.filter((_, i) => {
|
|
79
|
+
const repo = settings[i]?.github?.repo;
|
|
80
|
+
return repo && repo.toLowerCase() === remote.toLowerCase();
|
|
81
|
+
});
|
|
82
|
+
if (tier1.length === 1) {
|
|
83
|
+
return { projectKey: tier1[0].id, name: tier1[0].name, reason: `github.repo matches ${remote}` };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Tier 2: id or name matches the repo name.
|
|
87
|
+
const tier2 = projects.filter(p => normalizeName(p.id) === remoteKey || normalizeName(p.name) === remoteKey);
|
|
88
|
+
if (tier2.length === 1) {
|
|
89
|
+
return { projectKey: tier2[0].id, name: tier2[0].name, reason: `project name matches repo "${remoteRepoName}"` };
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Full discovery with user fallback: auto-match first, then show a numbered
|
|
95
|
+
// picker. Writes `.agentdesk.json` with the chosen projectKey so subsequent
|
|
96
|
+
// loadConfig calls pick it up. Returns the chosen projectKey or null.
|
|
97
|
+
export async function discoverProject(cwd, apiKey) {
|
|
76
98
|
const remote = detectGitRemote(cwd);
|
|
77
99
|
if (remote) console.log(` Detected git remote: ${remote}`);
|
|
78
100
|
console.log(" Fetching your projects from the server...");
|
|
@@ -86,7 +108,6 @@ async function discoverProject(rl, cwd, apiKey) {
|
|
|
86
108
|
return null;
|
|
87
109
|
}
|
|
88
110
|
|
|
89
|
-
// Fetch settings in parallel so we can display/match the configured repo.
|
|
90
111
|
const settings = await Promise.all(projects.map(p => fetchProjectSettings(apiKey, p.id)));
|
|
91
112
|
const remoteRepoName = remote ? remote.split("/").pop() : null;
|
|
92
113
|
const remoteKey = normalizeName(remoteRepoName);
|
|
@@ -116,47 +137,29 @@ async function discoverProject(rl, cwd, apiKey) {
|
|
|
116
137
|
console.log(` ✓ Auto-matched: ${chosen.project.name} (${chosen.project.id}) — ${matchReason}`);
|
|
117
138
|
} else {
|
|
118
139
|
console.log("");
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
140
|
+
const picked = await select({
|
|
141
|
+
message: "Couldn't auto-match this clone — pick the project it maps to",
|
|
142
|
+
choices: [
|
|
143
|
+
...rows.map(r => {
|
|
144
|
+
const tag = r.tier1 ? " (repo match)" : r.tier2 ? " (name match)" : "";
|
|
145
|
+
const repoStr = r.repo ? ` [${r.repo}]` : "";
|
|
146
|
+
return { name: `${r.project.name} (${r.project.id})${repoStr}${tag}`, value: r.project.id };
|
|
147
|
+
}),
|
|
148
|
+
{ name: "Cancel", value: "__cancel__" },
|
|
149
|
+
],
|
|
125
150
|
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const answer = await askInline(rl, ` Pick one (1-${rows.length}, or 'c' to cancel): `);
|
|
129
|
-
const trimmed = answer.trim().toLowerCase();
|
|
130
|
-
if (trimmed === "c" || trimmed === "cancel") return null;
|
|
131
|
-
const num = parseInt(trimmed, 10);
|
|
132
|
-
if (num >= 1 && num <= rows.length) { chosen = rows[num - 1]; break; }
|
|
133
|
-
console.log(` Please enter a number between 1 and ${rows.length}`);
|
|
134
|
-
}
|
|
151
|
+
if (picked === "__cancel__") return null;
|
|
152
|
+
chosen = rows.find(r => r.project.id === picked);
|
|
135
153
|
}
|
|
136
154
|
|
|
137
|
-
|
|
138
|
-
// into it on the next call, producing the full cached view.
|
|
139
|
-
const minimal = { projectKey: chosen.project.id };
|
|
140
|
-
writeFileSync(join(cwd, ".agentdesk.json"), JSON.stringify(minimal, null, 2) + "\n");
|
|
155
|
+
writeProjectConfig(cwd, chosen.project.id);
|
|
141
156
|
console.log(` ✓ Wrote .agentdesk.json (projectKey: ${chosen.project.id})`);
|
|
142
157
|
console.log(" (commit this file so future clones skip discovery)");
|
|
143
158
|
return chosen.project.id;
|
|
144
159
|
}
|
|
145
160
|
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function ask(rl, question) {
|
|
151
|
-
return new Promise(resolve => rl.question(question, resolve));
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async function promptRequired(rl, label) {
|
|
155
|
-
while (true) {
|
|
156
|
-
const v = (await ask(rl, ` ${label}: `)).trim();
|
|
157
|
-
if (v) return v;
|
|
158
|
-
console.log(` ${label} is required. Ctrl+C to abort.`);
|
|
159
|
-
}
|
|
161
|
+
export function writeProjectConfig(cwd, projectKey) {
|
|
162
|
+
writeFileSync(join(cwd, ".agentdesk.json"), JSON.stringify({ projectKey }, null, 2) + "\n");
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
function saveEnvVar(dir, key, value) {
|
|
@@ -202,15 +205,13 @@ function printTrackerHint(tracker) {
|
|
|
202
205
|
}
|
|
203
206
|
}
|
|
204
207
|
|
|
205
|
-
|
|
206
|
-
// Returns the merged credentials map so the caller can verify.
|
|
207
|
-
async function promptTrackerCreds(rl, cwd, tracker, existing) {
|
|
208
|
+
async function promptTrackerCreds(cwd, tracker, existing) {
|
|
208
209
|
const creds = { ...existing };
|
|
209
210
|
if (tracker === "linear") {
|
|
210
211
|
if (!creds.LINEAR_API_KEY) {
|
|
211
212
|
console.log("");
|
|
212
213
|
printTrackerHint("linear");
|
|
213
|
-
const v = await promptRequired(
|
|
214
|
+
const v = await promptRequired("Linear API key");
|
|
214
215
|
saveEnvVar(cwd, "LINEAR_API_KEY", v);
|
|
215
216
|
creds.LINEAR_API_KEY = v;
|
|
216
217
|
console.log(" ✓ Saved LINEAR_API_KEY to .env");
|
|
@@ -219,14 +220,14 @@ async function promptTrackerCreds(rl, cwd, tracker, existing) {
|
|
|
219
220
|
if (!creds.JIRA_EMAIL) {
|
|
220
221
|
console.log("");
|
|
221
222
|
printTrackerHint("jira");
|
|
222
|
-
const v = await promptRequired(
|
|
223
|
+
const v = await promptRequired("Your Atlassian login email");
|
|
223
224
|
saveEnvVar(cwd, "JIRA_EMAIL", v);
|
|
224
225
|
creds.JIRA_EMAIL = v;
|
|
225
226
|
console.log(" ✓ Saved JIRA_EMAIL to .env");
|
|
226
227
|
}
|
|
227
228
|
if (!creds.JIRA_API_TOKEN) {
|
|
228
229
|
if (creds.JIRA_EMAIL) console.log("");
|
|
229
|
-
const v = await promptRequired(
|
|
230
|
+
const v = await promptRequired("Jira API token");
|
|
230
231
|
saveEnvVar(cwd, "JIRA_API_TOKEN", v);
|
|
231
232
|
creds.JIRA_API_TOKEN = v;
|
|
232
233
|
console.log(" ✓ Saved JIRA_API_TOKEN to .env");
|
|
@@ -235,67 +236,21 @@ async function promptTrackerCreds(rl, cwd, tracker, existing) {
|
|
|
235
236
|
return creds;
|
|
236
237
|
}
|
|
237
238
|
|
|
238
|
-
async function promptGitHubToken(
|
|
239
|
+
async function promptGitHubToken(cwd) {
|
|
239
240
|
console.log("");
|
|
240
241
|
printTrackerHint("github");
|
|
241
|
-
const v = await promptRequired(
|
|
242
|
+
const v = await promptRequired("GitHub token");
|
|
242
243
|
saveEnvVar(cwd, "GITHUB_TOKEN", v);
|
|
243
244
|
console.log(" ✓ Saved GITHUB_TOKEN to .env");
|
|
244
245
|
return v;
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
while (true) {
|
|
253
|
-
const answer = await ask(rl, ` Choose (1-${options.length}): `);
|
|
254
|
-
const num = parseInt(answer.trim(), 10);
|
|
255
|
-
if (num >= 1 && num <= options.length) return options[num - 1];
|
|
256
|
-
console.log(` Please enter a number between 1 and ${options.length}`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export async function runBootstrap(cwd = process.cwd()) {
|
|
261
|
-
console.log("");
|
|
262
|
-
console.log(" AgentDesk — Bootstrap this machine");
|
|
263
|
-
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
264
|
-
console.log("");
|
|
265
|
-
|
|
266
|
-
// 1. Must be logged in.
|
|
267
|
-
const apiKey = getStoredApiKey();
|
|
268
|
-
if (!apiKey) {
|
|
269
|
-
console.log(" Not logged in. Run `agentdesk login` first.");
|
|
270
|
-
console.log("");
|
|
271
|
-
process.exit(1);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
275
|
-
|
|
276
|
-
// 2. Find the server-side project for this clone. Prefer a committed
|
|
277
|
-
// .agentdesk.json (clean path); fall back to discovering from the
|
|
278
|
-
// user's server-side project list when the file is missing.
|
|
279
|
-
let localConfig = readLocalConfig(cwd);
|
|
280
|
-
let projectKey = localConfig?.projectKey || null;
|
|
281
|
-
|
|
282
|
-
if (!projectKey) {
|
|
283
|
-
if (localConfig) {
|
|
284
|
-
console.log(" .agentdesk.json has no projectKey — discovering project from server.");
|
|
285
|
-
} else {
|
|
286
|
-
console.log(" No .agentdesk.json in this directory — discovering project from server.");
|
|
287
|
-
}
|
|
288
|
-
console.log("");
|
|
289
|
-
projectKey = await discoverProject(rl, cwd, apiKey);
|
|
290
|
-
if (!projectKey) { rl.close(); process.exit(1); }
|
|
291
|
-
localConfig = readLocalConfig(cwd);
|
|
292
|
-
console.log("");
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// 3. Load merged config (server-authoritative when reachable). This is the
|
|
296
|
-
// same call `agentdesk team` makes, so we see the same view.
|
|
248
|
+
// Shared gap-scan: given a cwd with a valid `.agentdesk.json` (or at least
|
|
249
|
+
// a projectKey), load server-authoritative config, figure out which secrets
|
|
250
|
+
// are missing or broken, prompt for them, validate. Writes to `.env` only.
|
|
251
|
+
// Returns { trackerOk, githubOk } for the caller to surface.
|
|
252
|
+
export async function gapScan({ cwd, apiKey, projectKey }) {
|
|
297
253
|
const config = await loadConfig(cwd, { apiKey, serverUrl: SERVER, projectName: projectKey, silent: true });
|
|
298
|
-
|
|
299
254
|
const tracker = config.tracker || null;
|
|
300
255
|
const repo = config.github?.repo || null;
|
|
301
256
|
const login = config.github?.login || null;
|
|
@@ -307,24 +262,19 @@ export async function runBootstrap(cwd = process.cwd()) {
|
|
|
307
262
|
if (repo) console.log(` Repo: ${repo}${login ? ` (@${login})` : ""}`);
|
|
308
263
|
console.log("");
|
|
309
264
|
|
|
310
|
-
// 4. Gap scan + prompt loop. We retry tracker + github verification up to
|
|
311
|
-
// three times, matching init's retry/skip/cancel UX.
|
|
312
265
|
let creds = resolveCredentialsFromEnv(loadDotEnvLocal(cwd));
|
|
313
|
-
let trackerOk = !tracker;
|
|
266
|
+
let trackerOk = !tracker;
|
|
314
267
|
let githubOk = false;
|
|
315
268
|
let attempts = 0;
|
|
316
269
|
|
|
317
270
|
while (attempts < 5) {
|
|
318
271
|
attempts += 1;
|
|
319
272
|
|
|
320
|
-
// Tracker
|
|
321
273
|
if (tracker && !trackerOk) {
|
|
322
274
|
const missing =
|
|
323
275
|
(tracker === "linear" && !creds.LINEAR_API_KEY) ||
|
|
324
276
|
(tracker === "jira" && (!creds.JIRA_EMAIL || !creds.JIRA_API_TOKEN));
|
|
325
|
-
if (missing)
|
|
326
|
-
creds = await promptTrackerCreds(rl, cwd, tracker, creds);
|
|
327
|
-
}
|
|
277
|
+
if (missing) creds = await promptTrackerCreds(cwd, tracker, creds);
|
|
328
278
|
process.stdout.write(" Verifying tracker access... ");
|
|
329
279
|
const check = await checkTrackerPermissions({ tracker, config, credentials: creds });
|
|
330
280
|
if (check.ok) {
|
|
@@ -338,32 +288,28 @@ export async function runBootstrap(cwd = process.cwd()) {
|
|
|
338
288
|
console.log("failed");
|
|
339
289
|
for (const e of check.errors || []) console.log(` • ${e}`);
|
|
340
290
|
console.log("");
|
|
341
|
-
const next = await
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
291
|
+
const next = await select({
|
|
292
|
+
message: "What now?",
|
|
293
|
+
choices: [
|
|
294
|
+
{ name: "Re-enter credentials", value: "retry" },
|
|
295
|
+
{ name: "Skip tracker (fix later in .env)", value: "skip" },
|
|
296
|
+
{ name: "Cancel", value: "cancel" },
|
|
297
|
+
],
|
|
298
|
+
});
|
|
346
299
|
console.log("");
|
|
347
|
-
if (next
|
|
348
|
-
if (next
|
|
349
|
-
// retry: clear the tracker-specific creds so the next loop re-prompts
|
|
300
|
+
if (next === "cancel") process.exit(1);
|
|
301
|
+
if (next === "skip") { trackerOk = true; break; }
|
|
350
302
|
if (tracker === "linear") delete creds.LINEAR_API_KEY;
|
|
351
303
|
if (tracker === "jira") { delete creds.JIRA_EMAIL; delete creds.JIRA_API_TOKEN; }
|
|
352
304
|
continue;
|
|
353
305
|
}
|
|
354
306
|
}
|
|
355
307
|
|
|
356
|
-
// GitHub — always required (agents push code regardless of tracker)
|
|
357
308
|
if (!githubOk) {
|
|
358
|
-
if (!creds.GITHUB_TOKEN)
|
|
359
|
-
creds.GITHUB_TOKEN = await promptGitHubToken(rl, cwd);
|
|
360
|
-
}
|
|
309
|
+
if (!creds.GITHUB_TOKEN) creds.GITHUB_TOKEN = await promptGitHubToken(cwd);
|
|
361
310
|
try {
|
|
362
311
|
assertPushable({ cwd, creds: { GITHUB_TOKEN: creds.GITHUB_TOKEN }, projectName: projectKey });
|
|
363
|
-
// Best-effort gh API check to confirm the token is valid and matches
|
|
364
|
-
// the configured login. Mirrors init.mjs:495-506.
|
|
365
312
|
try {
|
|
366
|
-
const { execSync } = await import("child_process");
|
|
367
313
|
const env = { ...process.env, GH_TOKEN: creds.GITHUB_TOKEN };
|
|
368
314
|
const who = execSync("gh api user --jq .login", { env, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
|
|
369
315
|
if (who && login && who.toLowerCase() !== login.toLowerCase()) {
|
|
@@ -379,14 +325,17 @@ export async function runBootstrap(cwd = process.cwd()) {
|
|
|
379
325
|
if (err instanceof PreflightError) {
|
|
380
326
|
console.log(` ✗ ${err.message}`);
|
|
381
327
|
console.log("");
|
|
382
|
-
const next = await
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
328
|
+
const next = await select({
|
|
329
|
+
message: "What now?",
|
|
330
|
+
choices: [
|
|
331
|
+
{ name: "Re-enter GITHUB_TOKEN", value: "retry" },
|
|
332
|
+
{ name: "Skip (sessions will fail until fixed)", value: "skip" },
|
|
333
|
+
{ name: "Cancel", value: "cancel" },
|
|
334
|
+
],
|
|
335
|
+
});
|
|
387
336
|
console.log("");
|
|
388
|
-
if (next
|
|
389
|
-
if (next
|
|
337
|
+
if (next === "cancel") process.exit(1);
|
|
338
|
+
if (next === "skip") { githubOk = true; break; }
|
|
390
339
|
delete creds.GITHUB_TOKEN;
|
|
391
340
|
continue;
|
|
392
341
|
}
|
|
@@ -397,8 +346,6 @@ export async function runBootstrap(cwd = process.cwd()) {
|
|
|
397
346
|
if (trackerOk && githubOk) break;
|
|
398
347
|
}
|
|
399
348
|
|
|
400
|
-
rl.close();
|
|
401
|
-
|
|
402
349
|
console.log("");
|
|
403
350
|
if (trackerOk && githubOk) {
|
|
404
351
|
console.log(" Ready. Try:");
|
|
@@ -406,7 +353,39 @@ export async function runBootstrap(cwd = process.cwd()) {
|
|
|
406
353
|
console.log(` agentdesk team ${prefix}-123`);
|
|
407
354
|
console.log("");
|
|
408
355
|
} else {
|
|
409
|
-
console.log("
|
|
356
|
+
console.log(" Finished with missing pieces — edit .env directly or re-run.");
|
|
357
|
+
console.log("");
|
|
358
|
+
}
|
|
359
|
+
return { trackerOk, githubOk, config };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function runBootstrap(cwd = process.cwd()) {
|
|
363
|
+
console.log("");
|
|
364
|
+
console.log(" AgentDesk — Bootstrap this machine");
|
|
365
|
+
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
366
|
+
console.log("");
|
|
367
|
+
|
|
368
|
+
const apiKey = getStoredApiKey();
|
|
369
|
+
if (!apiKey) {
|
|
370
|
+
console.log(" Not logged in. Run `agentdesk login` first.");
|
|
410
371
|
console.log("");
|
|
372
|
+
process.exit(1);
|
|
411
373
|
}
|
|
374
|
+
|
|
375
|
+
let localConfig = readLocalConfig(cwd);
|
|
376
|
+
let projectKey = localConfig?.projectKey || null;
|
|
377
|
+
|
|
378
|
+
if (!projectKey) {
|
|
379
|
+
if (localConfig) {
|
|
380
|
+
console.log(" .agentdesk.json has no projectKey — discovering project from server.");
|
|
381
|
+
} else {
|
|
382
|
+
console.log(" No .agentdesk.json in this directory — discovering project from server.");
|
|
383
|
+
}
|
|
384
|
+
console.log("");
|
|
385
|
+
projectKey = await discoverProject(cwd, apiKey);
|
|
386
|
+
if (!projectKey) process.exit(1);
|
|
387
|
+
console.log("");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
await gapScan({ cwd, apiKey, projectKey });
|
|
412
391
|
}
|
package/cli/init.mjs
CHANGED
|
@@ -15,6 +15,8 @@ import { loadConfig, pushConfig } from "./config.mjs";
|
|
|
15
15
|
import { getStoredApiKey } from "./login.mjs";
|
|
16
16
|
import { registerLocalProject } from "./projects.mjs";
|
|
17
17
|
import { checkTrackerPermissions, resolveCredentialsFromEnv } from "./tracker-check.mjs";
|
|
18
|
+
import { autoMatchProject, gapScan, writeProjectConfig } from "./bootstrap.mjs";
|
|
19
|
+
import { select as promptSelect } from "./prompts.mjs";
|
|
18
20
|
|
|
19
21
|
const SERVER = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
|
|
20
22
|
|
|
@@ -101,22 +103,6 @@ function ask(rl, question) {
|
|
|
101
103
|
return new Promise(resolve => rl.question(question, resolve));
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
async function selectOption(rl, prompt, options) {
|
|
105
|
-
console.log(` ${prompt}`);
|
|
106
|
-
console.log("");
|
|
107
|
-
options.forEach((opt, i) => {
|
|
108
|
-
console.log(` ${i + 1}) ${opt.label}`);
|
|
109
|
-
});
|
|
110
|
-
console.log("");
|
|
111
|
-
|
|
112
|
-
while (true) {
|
|
113
|
-
const answer = await ask(rl, ` Choose (1-${options.length}): `);
|
|
114
|
-
const num = parseInt(answer.trim(), 10);
|
|
115
|
-
if (num >= 1 && num <= options.length) return options[num - 1];
|
|
116
|
-
console.log(` Please enter a number between 1 and ${options.length}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
106
|
function saveEnvVar(dir, key, value) {
|
|
121
107
|
const envPath = join(dir, ".env");
|
|
122
108
|
let content = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
|
|
@@ -221,12 +207,12 @@ async function listTrackerProjects({ tracker, creds, location }) {
|
|
|
221
207
|
return null;
|
|
222
208
|
}
|
|
223
209
|
|
|
224
|
-
async function pickFromList(
|
|
225
|
-
const
|
|
226
|
-
if (allowManual)
|
|
227
|
-
const
|
|
228
|
-
if (
|
|
229
|
-
return
|
|
210
|
+
async function pickFromList(_rl, prompt, items, { allowManual = true } = {}) {
|
|
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;
|
|
230
216
|
}
|
|
231
217
|
|
|
232
218
|
// Prompt for a value, showing instructions first, looping until non-empty.
|
|
@@ -283,34 +269,67 @@ async function verifyAndPickTrackerProject({ rl, cwd, finalProjectKey, tracker,
|
|
|
283
269
|
|
|
284
270
|
export async function runInit(cwd, opts = {}) {
|
|
285
271
|
const quick = !!opts.quick;
|
|
272
|
+
const forceFull = !!opts.forceFull;
|
|
286
273
|
const project = detectProject(cwd);
|
|
287
274
|
const existingConfig = loadConfig(cwd);
|
|
288
275
|
const projectId = project.name || cwd.split("/").pop();
|
|
289
276
|
const configPath = join(cwd, ".agentdesk.json");
|
|
290
277
|
const hasConfig = existsSync(configPath);
|
|
278
|
+
let editScope = "full";
|
|
291
279
|
|
|
292
|
-
|
|
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
|
+
}
|
|
310
|
+
}
|
|
293
311
|
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
if (hasConfig && !
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
312
|
+
// Fast path B: no local config, but the user's account has a project that
|
|
313
|
+
// silently matches this clone (git remote → project id/name). Switch to
|
|
314
|
+
// bootstrap mode without opening the wizard.
|
|
315
|
+
if (!hasConfig && !forceFull) {
|
|
316
|
+
const apiKey = getStoredApiKey();
|
|
317
|
+
if (apiKey) {
|
|
318
|
+
const match = await autoMatchProject(cwd, apiKey);
|
|
319
|
+
if (match) {
|
|
320
|
+
console.log("");
|
|
321
|
+
console.log(` ✓ Detected project "${match.name}" on your account — ${match.reason}`);
|
|
322
|
+
console.log(" Switching to token-refresh mode. Use `agentdesk init --force-full` to walk the wizard instead.");
|
|
323
|
+
console.log("");
|
|
324
|
+
writeProjectConfig(cwd, match.projectKey);
|
|
325
|
+
await gapScan({ cwd, apiKey, projectKey: match.projectKey });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
310
328
|
}
|
|
311
|
-
editScope = choice.value;
|
|
312
329
|
}
|
|
313
330
|
|
|
331
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
332
|
+
|
|
314
333
|
console.log("");
|
|
315
334
|
console.log(" AgentDesk — Project Setup");
|
|
316
335
|
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
@@ -336,13 +355,15 @@ export async function runInit(cwd, opts = {}) {
|
|
|
336
355
|
|
|
337
356
|
if (!quick) console.log(editScope === "tracker" ? " Tracker" : " Step 1 of 3 — Task tracker");
|
|
338
357
|
console.log("");
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
358
|
+
const tracker = await promptSelect({
|
|
359
|
+
message: "Task tracker:",
|
|
360
|
+
choices: [
|
|
361
|
+
{ name: "Linear", value: "linear" },
|
|
362
|
+
{ name: "Jira", value: "jira" },
|
|
363
|
+
{ name: "GitHub Issues", value: "github" },
|
|
364
|
+
{ name: "None (use descriptions only)", value: null },
|
|
365
|
+
],
|
|
366
|
+
});
|
|
346
367
|
console.log("");
|
|
347
368
|
if (tracker) config.tracker = tracker;
|
|
348
369
|
|
|
@@ -417,17 +438,20 @@ export async function runInit(cwd, opts = {}) {
|
|
|
417
438
|
verified = await verifyAndPickTrackerProject({ rl, cwd, finalProjectKey, tracker, config, location: trackerLocation });
|
|
418
439
|
if (verified.ok) break;
|
|
419
440
|
console.log("");
|
|
420
|
-
const next = await
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
441
|
+
const next = await promptSelect({
|
|
442
|
+
message: "What now?",
|
|
443
|
+
choices: [
|
|
444
|
+
{ name: "Re-enter credentials and try again", value: "retry" },
|
|
445
|
+
{ name: "Save config anyway (will need to fix before `agentdesk team`)", value: "skip" },
|
|
446
|
+
{ name: "Cancel init", value: "cancel" },
|
|
447
|
+
],
|
|
448
|
+
});
|
|
425
449
|
console.log("");
|
|
426
|
-
if (next
|
|
450
|
+
if (next === "cancel") {
|
|
427
451
|
rl.close();
|
|
428
452
|
return;
|
|
429
453
|
}
|
|
430
|
-
if (next
|
|
454
|
+
if (next === "skip") break;
|
|
431
455
|
// Retry: re-prompt only the tracker auth fields, not the whole wizard.
|
|
432
456
|
if (tracker === "linear") {
|
|
433
457
|
const key = await promptRequired(rl, "Linear API key");
|
package/cli/prompts.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Terminal prompt helpers.
|
|
2
|
+
//
|
|
3
|
+
// `select` — arrow-key menu via @inquirer/prompts (colored, one-line render).
|
|
4
|
+
// `ask` / `promptRequired` — free-text prompts via a short-lived readline.
|
|
5
|
+
//
|
|
6
|
+
// We deliberately do NOT keep a long-lived readline interface around; each
|
|
7
|
+
// text prompt creates its own so inquirer (which opens its own stdin reader
|
|
8
|
+
// during a select) never races against a parallel `rl.question` handler.
|
|
9
|
+
|
|
10
|
+
import { createInterface } from "readline";
|
|
11
|
+
import { select as inquirerSelect, input as inquirerInput } from "@inquirer/prompts";
|
|
12
|
+
|
|
13
|
+
// Ctrl-C inside an inquirer prompt throws `ExitPromptError`. Convert it to
|
|
14
|
+
// a clean exit(130) so the terminal doesn't see a node stack trace.
|
|
15
|
+
function handleExit(err) {
|
|
16
|
+
if (err?.name === "ExitPromptError") {
|
|
17
|
+
console.log("");
|
|
18
|
+
process.exit(130);
|
|
19
|
+
}
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Arrow-key select menu. `choices` is an array of { name, value, description? }.
|
|
24
|
+
// Returns the selected value.
|
|
25
|
+
export async function select({ message, choices, default: def }) {
|
|
26
|
+
try {
|
|
27
|
+
return await inquirerSelect({
|
|
28
|
+
message,
|
|
29
|
+
choices,
|
|
30
|
+
default: def,
|
|
31
|
+
loop: false,
|
|
32
|
+
});
|
|
33
|
+
} catch (err) { handleExit(err); }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Free-text prompt. Returns the raw answer (not trimmed).
|
|
37
|
+
export function ask(question) {
|
|
38
|
+
return new Promise(resolve => {
|
|
39
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
40
|
+
rl.question(question, answer => {
|
|
41
|
+
rl.close();
|
|
42
|
+
resolve(answer);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Loop until a non-empty trimmed value is entered. `label` is the visible
|
|
48
|
+
// prompt text; we wrap it with the same two-space indent init uses elsewhere.
|
|
49
|
+
export async function promptRequired(label) {
|
|
50
|
+
while (true) {
|
|
51
|
+
const v = (await ask(` ${label}: `)).trim();
|
|
52
|
+
if (v) return v;
|
|
53
|
+
console.log(` ${label} is required. Ctrl+C to abort.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Inquirer-based free-text prompt — used only where arrow-key UX is nice to
|
|
58
|
+
// have alongside the select menus (e.g. the project-picker fallback in
|
|
59
|
+
// bootstrap). Most text prompts in init still go through `ask`.
|
|
60
|
+
export async function promptInput({ message, default: def }) {
|
|
61
|
+
try {
|
|
62
|
+
return await inquirerInput({ message, default: def });
|
|
63
|
+
} catch (err) { handleExit(err); }
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kendoo.agentdesk/agentdesk",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.7",
|
|
4
4
|
"description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"prepublishOnly": "node scripts/lint-changelog.mjs"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@inquirer/prompts": "^7.10.1",
|
|
30
31
|
"@radix-ui/react-avatar": "^1.1.11",
|
|
31
32
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
32
33
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|