@kendoo.agentdesk/agentdesk 0.19.5 → 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 +11 -0
- package/README.md +4 -2
- package/bin/agentdesk.mjs +7 -4
- package/cli/bootstrap.mjs +149 -157
- package/cli/init.mjs +78 -54
- package/cli/prompts.mjs +64 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,17 @@ 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
|
+
|
|
22
|
+
## [0.19.6] — 2026-04-19
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `[CLI]` `agentdesk bootstrap` now auto-selects the project silently when the git remote's repo name matches the project's id or name (case-insensitive, scope-stripped), in addition to the existing exact `github.repo` match. No confirmation prompt is shown for a confident single match — the clone just picks up the right project and continues. The numbered picker is only shown when auto-match finds zero or multiple candidates.
|
|
26
|
+
|
|
16
27
|
## [0.19.5] — 2026-04-19
|
|
17
28
|
|
|
18
29
|
### 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
|
|
|
@@ -57,17 +52,49 @@ async function fetchProjectSettings(apiKey, projectId) {
|
|
|
57
52
|
} catch { return {}; }
|
|
58
53
|
}
|
|
59
54
|
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
55
|
+
// Normalize a name for fuzzy comparison: lowercase, strip npm scope (@scope/)
|
|
56
|
+
// and everything up to the last `/`, then drop non-alphanumerics.
|
|
57
|
+
function normalizeName(s) {
|
|
58
|
+
if (!s) return "";
|
|
59
|
+
const tail = s.includes("/") ? s.slice(s.lastIndexOf("/") + 1) : s;
|
|
60
|
+
return tail.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
61
|
+
}
|
|
62
|
+
|
|
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) {
|
|
71
98
|
const remote = detectGitRemote(cwd);
|
|
72
99
|
if (remote) console.log(` Detected git remote: ${remote}`);
|
|
73
100
|
console.log(" Fetching your projects from the server...");
|
|
@@ -81,69 +108,58 @@ async function discoverProject(rl, cwd, apiKey) {
|
|
|
81
108
|
return null;
|
|
82
109
|
}
|
|
83
110
|
|
|
84
|
-
// Fetch settings in parallel so we can display/match the configured repo.
|
|
85
111
|
const settings = await Promise.all(projects.map(p => fetchProjectSettings(apiKey, p.id)));
|
|
112
|
+
const remoteRepoName = remote ? remote.split("/").pop() : null;
|
|
113
|
+
const remoteKey = normalizeName(remoteRepoName);
|
|
86
114
|
const rows = projects.map((p, i) => {
|
|
87
115
|
const repo = settings[i]?.github?.repo || null;
|
|
88
|
-
const
|
|
89
|
-
|
|
116
|
+
const tier1 = remote && repo && repo.toLowerCase() === remote.toLowerCase();
|
|
117
|
+
const tier2 = !tier1 && remoteKey && (
|
|
118
|
+
normalizeName(p.id) === remoteKey || normalizeName(p.name) === remoteKey
|
|
119
|
+
);
|
|
120
|
+
return { project: p, settings: settings[i] || {}, repo, tier1, tier2 };
|
|
90
121
|
});
|
|
91
122
|
|
|
92
|
-
const
|
|
123
|
+
const tier1Matches = rows.filter(r => r.tier1);
|
|
124
|
+
const tier2Matches = rows.filter(r => r.tier2);
|
|
93
125
|
let chosen = null;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
126
|
+
let matchReason = null;
|
|
127
|
+
|
|
128
|
+
if (tier1Matches.length === 1) {
|
|
129
|
+
chosen = tier1Matches[0];
|
|
130
|
+
matchReason = `github.repo matches ${chosen.repo}`;
|
|
131
|
+
} else if (tier1Matches.length === 0 && tier2Matches.length === 1) {
|
|
132
|
+
chosen = tier2Matches[0];
|
|
133
|
+
matchReason = `project name matches repo "${remoteRepoName}"`;
|
|
101
134
|
}
|
|
102
135
|
|
|
103
|
-
if (
|
|
104
|
-
console.log(
|
|
105
|
-
|
|
136
|
+
if (chosen) {
|
|
137
|
+
console.log(` ✓ Auto-matched: ${chosen.project.name} (${chosen.project.id}) — ${matchReason}`);
|
|
138
|
+
} else {
|
|
106
139
|
console.log("");
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
],
|
|
111
150
|
});
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
}
|
|
151
|
+
if (picked === "__cancel__") return null;
|
|
152
|
+
chosen = rows.find(r => r.project.id === picked);
|
|
121
153
|
}
|
|
122
154
|
|
|
123
|
-
|
|
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("");
|
|
155
|
+
writeProjectConfig(cwd, chosen.project.id);
|
|
128
156
|
console.log(` ✓ Wrote .agentdesk.json (projectKey: ${chosen.project.id})`);
|
|
129
|
-
console.log(" (commit this file so future clones skip
|
|
157
|
+
console.log(" (commit this file so future clones skip discovery)");
|
|
130
158
|
return chosen.project.id;
|
|
131
159
|
}
|
|
132
160
|
|
|
133
|
-
function
|
|
134
|
-
|
|
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
|
-
}
|
|
161
|
+
export function writeProjectConfig(cwd, projectKey) {
|
|
162
|
+
writeFileSync(join(cwd, ".agentdesk.json"), JSON.stringify({ projectKey }, null, 2) + "\n");
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
function saveEnvVar(dir, key, value) {
|
|
@@ -189,15 +205,13 @@ function printTrackerHint(tracker) {
|
|
|
189
205
|
}
|
|
190
206
|
}
|
|
191
207
|
|
|
192
|
-
|
|
193
|
-
// Returns the merged credentials map so the caller can verify.
|
|
194
|
-
async function promptTrackerCreds(rl, cwd, tracker, existing) {
|
|
208
|
+
async function promptTrackerCreds(cwd, tracker, existing) {
|
|
195
209
|
const creds = { ...existing };
|
|
196
210
|
if (tracker === "linear") {
|
|
197
211
|
if (!creds.LINEAR_API_KEY) {
|
|
198
212
|
console.log("");
|
|
199
213
|
printTrackerHint("linear");
|
|
200
|
-
const v = await promptRequired(
|
|
214
|
+
const v = await promptRequired("Linear API key");
|
|
201
215
|
saveEnvVar(cwd, "LINEAR_API_KEY", v);
|
|
202
216
|
creds.LINEAR_API_KEY = v;
|
|
203
217
|
console.log(" ✓ Saved LINEAR_API_KEY to .env");
|
|
@@ -206,14 +220,14 @@ async function promptTrackerCreds(rl, cwd, tracker, existing) {
|
|
|
206
220
|
if (!creds.JIRA_EMAIL) {
|
|
207
221
|
console.log("");
|
|
208
222
|
printTrackerHint("jira");
|
|
209
|
-
const v = await promptRequired(
|
|
223
|
+
const v = await promptRequired("Your Atlassian login email");
|
|
210
224
|
saveEnvVar(cwd, "JIRA_EMAIL", v);
|
|
211
225
|
creds.JIRA_EMAIL = v;
|
|
212
226
|
console.log(" ✓ Saved JIRA_EMAIL to .env");
|
|
213
227
|
}
|
|
214
228
|
if (!creds.JIRA_API_TOKEN) {
|
|
215
229
|
if (creds.JIRA_EMAIL) console.log("");
|
|
216
|
-
const v = await promptRequired(
|
|
230
|
+
const v = await promptRequired("Jira API token");
|
|
217
231
|
saveEnvVar(cwd, "JIRA_API_TOKEN", v);
|
|
218
232
|
creds.JIRA_API_TOKEN = v;
|
|
219
233
|
console.log(" ✓ Saved JIRA_API_TOKEN to .env");
|
|
@@ -222,67 +236,21 @@ async function promptTrackerCreds(rl, cwd, tracker, existing) {
|
|
|
222
236
|
return creds;
|
|
223
237
|
}
|
|
224
238
|
|
|
225
|
-
async function promptGitHubToken(
|
|
239
|
+
async function promptGitHubToken(cwd) {
|
|
226
240
|
console.log("");
|
|
227
241
|
printTrackerHint("github");
|
|
228
|
-
const v = await promptRequired(
|
|
242
|
+
const v = await promptRequired("GitHub token");
|
|
229
243
|
saveEnvVar(cwd, "GITHUB_TOKEN", v);
|
|
230
244
|
console.log(" ✓ Saved GITHUB_TOKEN to .env");
|
|
231
245
|
return v;
|
|
232
246
|
}
|
|
233
247
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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.
|
|
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 }) {
|
|
284
253
|
const config = await loadConfig(cwd, { apiKey, serverUrl: SERVER, projectName: projectKey, silent: true });
|
|
285
|
-
|
|
286
254
|
const tracker = config.tracker || null;
|
|
287
255
|
const repo = config.github?.repo || null;
|
|
288
256
|
const login = config.github?.login || null;
|
|
@@ -294,24 +262,19 @@ export async function runBootstrap(cwd = process.cwd()) {
|
|
|
294
262
|
if (repo) console.log(` Repo: ${repo}${login ? ` (@${login})` : ""}`);
|
|
295
263
|
console.log("");
|
|
296
264
|
|
|
297
|
-
// 4. Gap scan + prompt loop. We retry tracker + github verification up to
|
|
298
|
-
// three times, matching init's retry/skip/cancel UX.
|
|
299
265
|
let creds = resolveCredentialsFromEnv(loadDotEnvLocal(cwd));
|
|
300
|
-
let trackerOk = !tracker;
|
|
266
|
+
let trackerOk = !tracker;
|
|
301
267
|
let githubOk = false;
|
|
302
268
|
let attempts = 0;
|
|
303
269
|
|
|
304
270
|
while (attempts < 5) {
|
|
305
271
|
attempts += 1;
|
|
306
272
|
|
|
307
|
-
// Tracker
|
|
308
273
|
if (tracker && !trackerOk) {
|
|
309
274
|
const missing =
|
|
310
275
|
(tracker === "linear" && !creds.LINEAR_API_KEY) ||
|
|
311
276
|
(tracker === "jira" && (!creds.JIRA_EMAIL || !creds.JIRA_API_TOKEN));
|
|
312
|
-
if (missing)
|
|
313
|
-
creds = await promptTrackerCreds(rl, cwd, tracker, creds);
|
|
314
|
-
}
|
|
277
|
+
if (missing) creds = await promptTrackerCreds(cwd, tracker, creds);
|
|
315
278
|
process.stdout.write(" Verifying tracker access... ");
|
|
316
279
|
const check = await checkTrackerPermissions({ tracker, config, credentials: creds });
|
|
317
280
|
if (check.ok) {
|
|
@@ -325,32 +288,28 @@ export async function runBootstrap(cwd = process.cwd()) {
|
|
|
325
288
|
console.log("failed");
|
|
326
289
|
for (const e of check.errors || []) console.log(` • ${e}`);
|
|
327
290
|
console.log("");
|
|
328
|
-
const next = await
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
+
});
|
|
333
299
|
console.log("");
|
|
334
|
-
if (next
|
|
335
|
-
if (next
|
|
336
|
-
// 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; }
|
|
337
302
|
if (tracker === "linear") delete creds.LINEAR_API_KEY;
|
|
338
303
|
if (tracker === "jira") { delete creds.JIRA_EMAIL; delete creds.JIRA_API_TOKEN; }
|
|
339
304
|
continue;
|
|
340
305
|
}
|
|
341
306
|
}
|
|
342
307
|
|
|
343
|
-
// GitHub — always required (agents push code regardless of tracker)
|
|
344
308
|
if (!githubOk) {
|
|
345
|
-
if (!creds.GITHUB_TOKEN)
|
|
346
|
-
creds.GITHUB_TOKEN = await promptGitHubToken(rl, cwd);
|
|
347
|
-
}
|
|
309
|
+
if (!creds.GITHUB_TOKEN) creds.GITHUB_TOKEN = await promptGitHubToken(cwd);
|
|
348
310
|
try {
|
|
349
311
|
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
312
|
try {
|
|
353
|
-
const { execSync } = await import("child_process");
|
|
354
313
|
const env = { ...process.env, GH_TOKEN: creds.GITHUB_TOKEN };
|
|
355
314
|
const who = execSync("gh api user --jq .login", { env, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
|
|
356
315
|
if (who && login && who.toLowerCase() !== login.toLowerCase()) {
|
|
@@ -366,14 +325,17 @@ export async function runBootstrap(cwd = process.cwd()) {
|
|
|
366
325
|
if (err instanceof PreflightError) {
|
|
367
326
|
console.log(` ✗ ${err.message}`);
|
|
368
327
|
console.log("");
|
|
369
|
-
const next = await
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
+
});
|
|
374
336
|
console.log("");
|
|
375
|
-
if (next
|
|
376
|
-
if (next
|
|
337
|
+
if (next === "cancel") process.exit(1);
|
|
338
|
+
if (next === "skip") { githubOk = true; break; }
|
|
377
339
|
delete creds.GITHUB_TOKEN;
|
|
378
340
|
continue;
|
|
379
341
|
}
|
|
@@ -384,8 +346,6 @@ export async function runBootstrap(cwd = process.cwd()) {
|
|
|
384
346
|
if (trackerOk && githubOk) break;
|
|
385
347
|
}
|
|
386
348
|
|
|
387
|
-
rl.close();
|
|
388
|
-
|
|
389
349
|
console.log("");
|
|
390
350
|
if (trackerOk && githubOk) {
|
|
391
351
|
console.log(" Ready. Try:");
|
|
@@ -393,7 +353,39 @@ export async function runBootstrap(cwd = process.cwd()) {
|
|
|
393
353
|
console.log(` agentdesk team ${prefix}-123`);
|
|
394
354
|
console.log("");
|
|
395
355
|
} else {
|
|
396
|
-
console.log("
|
|
356
|
+
console.log(" Finished with missing pieces — edit .env directly or re-run.");
|
|
397
357
|
console.log("");
|
|
398
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.");
|
|
371
|
+
console.log("");
|
|
372
|
+
process.exit(1);
|
|
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 });
|
|
399
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",
|