@kendoo.agentdesk/agentdesk 0.19.4 → 0.19.6
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/cli/bootstrap.mjs +138 -13
- 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.6] — 2026-04-19
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `[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.
|
|
20
|
+
|
|
21
|
+
## [0.19.5] — 2026-04-19
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- `[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.
|
|
25
|
+
|
|
16
26
|
## [0.19.4] — 2026-04-19
|
|
17
27
|
|
|
18
28
|
### Added
|
package/cli/bootstrap.mjs
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
19
19
|
import { join } from "path";
|
|
20
20
|
import { createInterface } from "readline";
|
|
21
|
+
import { execSync } from "child_process";
|
|
21
22
|
import { loadConfig } from "./config.mjs";
|
|
22
23
|
import { getStoredApiKey } from "./login.mjs";
|
|
23
24
|
import { checkTrackerPermissions, resolveCredentialsFromEnv } from "./tracker-check.mjs";
|
|
@@ -25,6 +26,127 @@ import { assertPushable, PreflightError } from "./session-preflight.mjs";
|
|
|
25
26
|
|
|
26
27
|
const SERVER = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
|
|
27
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
|
+
// Normalize a name for fuzzy comparison: lowercase, strip npm scope (@scope/)
|
|
61
|
+
// and everything up to the last `/`, then drop non-alphanumerics.
|
|
62
|
+
function normalizeName(s) {
|
|
63
|
+
if (!s) return "";
|
|
64
|
+
const tail = s.includes("/") ? s.slice(s.lastIndexOf("/") + 1) : s;
|
|
65
|
+
return tail.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Discover the server-side project for this clone when .agentdesk.json is
|
|
69
|
+
// absent. Strategy (each tier that yields exactly one match auto-selects):
|
|
70
|
+
// Tier 1 — server-side github.repo equals the local git remote (owner/repo).
|
|
71
|
+
// Tier 2 — the repo name from the remote matches the project id or name
|
|
72
|
+
// (case-insensitive, scope-stripped).
|
|
73
|
+
// When 0 or 2+ projects match, fall back to a numbered picker.
|
|
74
|
+
// Returns the chosen projectKey, or null if the user aborts.
|
|
75
|
+
async function discoverProject(rl, cwd, apiKey) {
|
|
76
|
+
const remote = detectGitRemote(cwd);
|
|
77
|
+
if (remote) console.log(` Detected git remote: ${remote}`);
|
|
78
|
+
console.log(" Fetching your projects from the server...");
|
|
79
|
+
|
|
80
|
+
const projects = await fetchProjects(apiKey);
|
|
81
|
+
if (!projects || projects.length === 0) {
|
|
82
|
+
console.log("");
|
|
83
|
+
console.log(" No projects found on your account.");
|
|
84
|
+
console.log(" Run `agentdesk init` on the primary machine first, then retry here.");
|
|
85
|
+
console.log("");
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Fetch settings in parallel so we can display/match the configured repo.
|
|
90
|
+
const settings = await Promise.all(projects.map(p => fetchProjectSettings(apiKey, p.id)));
|
|
91
|
+
const remoteRepoName = remote ? remote.split("/").pop() : null;
|
|
92
|
+
const remoteKey = normalizeName(remoteRepoName);
|
|
93
|
+
const rows = projects.map((p, i) => {
|
|
94
|
+
const repo = settings[i]?.github?.repo || null;
|
|
95
|
+
const tier1 = remote && repo && repo.toLowerCase() === remote.toLowerCase();
|
|
96
|
+
const tier2 = !tier1 && remoteKey && (
|
|
97
|
+
normalizeName(p.id) === remoteKey || normalizeName(p.name) === remoteKey
|
|
98
|
+
);
|
|
99
|
+
return { project: p, settings: settings[i] || {}, repo, tier1, tier2 };
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const tier1Matches = rows.filter(r => r.tier1);
|
|
103
|
+
const tier2Matches = rows.filter(r => r.tier2);
|
|
104
|
+
let chosen = null;
|
|
105
|
+
let matchReason = null;
|
|
106
|
+
|
|
107
|
+
if (tier1Matches.length === 1) {
|
|
108
|
+
chosen = tier1Matches[0];
|
|
109
|
+
matchReason = `github.repo matches ${chosen.repo}`;
|
|
110
|
+
} else if (tier1Matches.length === 0 && tier2Matches.length === 1) {
|
|
111
|
+
chosen = tier2Matches[0];
|
|
112
|
+
matchReason = `project name matches repo "${remoteRepoName}"`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (chosen) {
|
|
116
|
+
console.log(` ✓ Auto-matched: ${chosen.project.name} (${chosen.project.id}) — ${matchReason}`);
|
|
117
|
+
} else {
|
|
118
|
+
console.log("");
|
|
119
|
+
console.log(" Couldn't auto-match this clone to one of your projects.");
|
|
120
|
+
console.log("");
|
|
121
|
+
rows.forEach((r, i) => {
|
|
122
|
+
const tag = r.tier1 ? " ← repo match" : r.tier2 ? " ← name match" : "";
|
|
123
|
+
const repoStr = r.repo ? ` [${r.repo}]` : "";
|
|
124
|
+
console.log(` ${i + 1}) ${r.project.name} (${r.project.id})${repoStr}${tag}`);
|
|
125
|
+
});
|
|
126
|
+
console.log("");
|
|
127
|
+
while (true) {
|
|
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
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Write a minimal .agentdesk.json — loadConfig will merge server state
|
|
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");
|
|
141
|
+
console.log(` ✓ Wrote .agentdesk.json (projectKey: ${chosen.project.id})`);
|
|
142
|
+
console.log(" (commit this file so future clones skip discovery)");
|
|
143
|
+
return chosen.project.id;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function askInline(rl, question) {
|
|
147
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
148
|
+
}
|
|
149
|
+
|
|
28
150
|
function ask(rl, question) {
|
|
29
151
|
return new Promise(resolve => rl.question(question, resolve));
|
|
30
152
|
}
|
|
@@ -149,26 +271,29 @@ export async function runBootstrap(cwd = process.cwd()) {
|
|
|
149
271
|
process.exit(1);
|
|
150
272
|
}
|
|
151
273
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
process.exit(1);
|
|
160
|
-
}
|
|
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;
|
|
161
281
|
|
|
162
|
-
const projectKey = localConfig.projectKey;
|
|
163
282
|
if (!projectKey) {
|
|
164
|
-
|
|
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);
|
|
165
292
|
console.log("");
|
|
166
|
-
process.exit(1);
|
|
167
293
|
}
|
|
168
294
|
|
|
169
295
|
// 3. Load merged config (server-authoritative when reachable). This is the
|
|
170
296
|
// same call `agentdesk team` makes, so we see the same view.
|
|
171
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
172
297
|
const config = await loadConfig(cwd, { apiKey, serverUrl: SERVER, projectName: projectKey, silent: true });
|
|
173
298
|
|
|
174
299
|
const tracker = config.tracker || null;
|