@kendoo.agentdesk/agentdesk 0.20.1 → 0.20.3
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 +16 -0
- package/README.md +16 -15
- package/bin/agentdesk.mjs +6 -5
- package/cli/config.mjs +55 -10
- package/cli/init.mjs +393 -333
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,22 @@ 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.20.3] — 2026-04-19
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- `[CLI]` Critical: `loadConfig` no longer lets a server row with `null` or missing fields override a fully-configured local `.agentdesk.json`. The merge now treats `null`/`undefined` from the server as "no value known" — only explicit non-null server values win. Previously a partially-synced project row on agentdesk.live would silently strip local tracker/repo/badge fields during the merge, and the cached-back write then persisted the stripped state to disk. On the next `init` run, the config looked empty even though the user had typed everything in.
|
|
20
|
+
- `[CLI]` Existing-mode save in `init` preserves the full local `.agentdesk.json` — it only ensures `projectKey` is set, never replaces other fields. The previous behavior wrote `{ "projectKey": "..." }` as the entire file, which destroyed any data not also on the server.
|
|
21
|
+
- `[CLI]` Auto-heal sync: when `loadConfig` sees a server row missing fields the local file has, it now pushes the merged config back to the server (fire-and-forget) so both sides converge. Projects that were set up before server-settings-push landed will heal themselves on next init.
|
|
22
|
+
|
|
23
|
+
## [0.20.2] — 2026-04-19
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- `[CLI]` `agentdesk init` is now a single mode-aware wizard instead of branching into separate flows. Two modes, picked automatically: **new** (no matching project on the server) walks every step interactively; **existing** (local `.agentdesk.json` has `projectKey`, or the git remote auto-matches a project on your account) shows the same steps but locks the project-wide ones (repo, tracker type/location/team, signature) as read-only with a "change in agentdesk.live" hint. Only per-machine credential steps (GitHub token, tracker credentials) are editable in existing mode — the CLI never mutates project-wide settings for an already-configured project.
|
|
27
|
+
- `[CLI]` Removed the "Refresh tokens / Cancel" menu. With the mode-aware wizard, the existing flow always surfaces every setting so you can see what's there and update credentials cleanly; nothing is hidden behind a "refresh only" shortcut that silently skipped visibility.
|
|
28
|
+
- `[CLI]` When no local config and the git remote doesn't auto-match, `init` shows a picker of the user's account projects plus "Set up as a new project" plus "Cancel" — no ambiguity about whether you're about to create a duplicate.
|
|
29
|
+
- `[CLI]` Per-machine credential prompts (GitHub token, Linear API key, Jira email/token) now offer a "Keep current / Replace" choice when an existing value is present, instead of blindly re-prompting.
|
|
30
|
+
- `[CLI]` `--force-full` still bypasses detection and runs the full new-mode wizard — useful when the auto-match is wrong or you're intentionally re-pushing settings.
|
|
31
|
+
|
|
16
32
|
## [0.20.1] — 2026-04-19
|
|
17
33
|
|
|
18
34
|
### Fixed
|
package/README.md
CHANGED
|
@@ -43,28 +43,29 @@ Opens your browser to [agentdesk.live](https://agentdesk.live) where you sign up
|
|
|
43
43
|
agentdesk init
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
`agentdesk init` is a single
|
|
46
|
+
`agentdesk init` is a single mode-aware wizard. It detects whether this clone maps to an existing project on your account and picks a mode accordingly:
|
|
47
47
|
|
|
48
|
-
- **
|
|
49
|
-
- **
|
|
50
|
-
- **No local config** → auto-matches the git remote against your account's projects. On a single match, prints `✓ Recognized: <name>` and confirms; otherwise a top-level *Connect to existing / New project / Cancel* menu.
|
|
48
|
+
- **Existing mode** — local `.agentdesk.json` has a `projectKey`, or the git remote matches a project on your account. The wizard walks every step, but project-wide steps (repo, tracker type/location/team, signature) are shown as read-only with a "change in agentdesk.live" hint. Only per-machine credential steps (GitHub token, tracker credentials) are interactive. Rationale: tracker/repo/team are shared state — changing them in one CLI would desync teammates and the dashboard, so those changes belong in project settings on agentdesk.live.
|
|
49
|
+
- **New mode** — no match found. Every step is interactive. On save, the new project row is POSTed to agentdesk.live and its settings are pushed.
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
When there's no local config and no unique auto-match but the account has other projects, `init` shows a picker of your projects plus *"Set up as a new project"* and *"Cancel"* so you can't accidentally create a duplicate.
|
|
52
|
+
|
|
53
|
+
The wizard is ten tiny steps, each doing one thing:
|
|
53
54
|
|
|
54
55
|
1. Detect git repo in cwd.
|
|
55
|
-
2.
|
|
56
|
-
3.
|
|
57
|
-
4. `git clone` the repo into `./<repo>`
|
|
58
|
-
5. Tracker type
|
|
59
|
-
6. Tracker location — Linear workspace
|
|
60
|
-
7. Tracker credentials —
|
|
61
|
-
8.
|
|
62
|
-
9. Verify tracker + pick team/project from the fetched list.
|
|
56
|
+
2. GitHub repo (prompted in new mode; locked-display in existing mode).
|
|
57
|
+
3. GitHub token — per-machine, always editable. If present, offers Keep/Replace; if new, prompts. `@login` is fetched from the GitHub API and echoed.
|
|
58
|
+
4. `git clone` the repo into `./<repo>` when cwd isn't already that repo.
|
|
59
|
+
5. Tracker type (locked in existing mode).
|
|
60
|
+
6. Tracker location — Linear workspace / Jira tenant URL (locked in existing mode).
|
|
61
|
+
7. Tracker credentials — per-machine, always editable with Keep/Replace.
|
|
62
|
+
8. "How should the AI team sign its work?" (locked in existing mode).
|
|
63
|
+
9. Verify tracker + (new mode) pick team/project from the fetched list.
|
|
63
64
|
10. Review, access-check checklist (`agentdesk.live ✓`, `GitHub ✓`, tracker ✓), confirm save.
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
Legacy `.agentdesk.json` files (without `projectKey`) are auto-migrated: `init` matches your account's projects by `github.repo` / git remote / tracker team, writes the `projectKey` back, and enters existing mode.
|
|
66
67
|
|
|
67
|
-
Use `agentdesk init --force-full` to bypass
|
|
68
|
+
Use `agentdesk init --force-full` to bypass detection and always run the new-mode wizard (useful when the auto-match is wrong or you're intentionally re-pushing settings). `agentdesk bootstrap` is a compatibility alias.
|
|
68
69
|
|
|
69
70
|
All menus use arrow-key navigation with color highlighting; Ctrl-C exits cleanly.
|
|
70
71
|
|
package/bin/agentdesk.mjs
CHANGED
|
@@ -58,11 +58,12 @@ if (!command || command === "help" || command === "--help") {
|
|
|
58
58
|
2. agentdesk init Guided setup — auto-detects new vs existing projects
|
|
59
59
|
3. agentdesk team TASK-123 Run a team session
|
|
60
60
|
|
|
61
|
-
'agentdesk init'
|
|
62
|
-
•
|
|
63
|
-
|
|
64
|
-
•
|
|
65
|
-
|
|
61
|
+
'agentdesk init' is a mode-aware wizard:
|
|
62
|
+
• Existing mode (local projectKey / git-remote auto-match) — walks every step;
|
|
63
|
+
project-wide fields are locked (change them in agentdesk.live), credentials editable.
|
|
64
|
+
• New mode (no match) — every step interactive; project POSTed to the server.
|
|
65
|
+
• Ambiguous — picker of your projects + 'Set up as a new project'.
|
|
66
|
+
Use 'agentdesk init --force-full' to bypass detection and always run new mode.
|
|
66
67
|
|
|
67
68
|
Commands:
|
|
68
69
|
agentdesk login Sign in to AgentDesk
|
package/cli/config.mjs
CHANGED
|
@@ -87,20 +87,32 @@ export async function loadConfig(dir, opts = {}) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
// Merge: DEFAULTS ← local ← server
|
|
90
|
+
// Merge: DEFAULTS ← local ← server, where the "server overrides local"
|
|
91
|
+
// rule only kicks in when the server actually has a value. Null /
|
|
92
|
+
// undefined on the server means "the server doesn't know about this
|
|
93
|
+
// field yet" — local wins. This is what keeps a partially-configured
|
|
94
|
+
// server row from silently wiping a fully-configured local file on
|
|
95
|
+
// next read.
|
|
91
96
|
let config = { ...DEFAULTS };
|
|
92
97
|
if (localConfig) config = deepMerge(config, localConfig);
|
|
93
|
-
if (serverConfig) config =
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
if (serverConfig) config = mergeNonNull(config, serverConfig);
|
|
99
|
+
|
|
100
|
+
// Auto-heal sync: if local has fields the server is missing (either
|
|
101
|
+
// no row at all or a partial row), push the merged config up so the
|
|
102
|
+
// server catches up. Fire-and-forget — we already have the right
|
|
103
|
+
// answer in `config` for the current caller.
|
|
104
|
+
const shouldHeal = localConfig && apiKey && serverUrl && projectName && (
|
|
105
|
+
!serverConfig || hasFieldsServerLacks(localConfig, serverConfig)
|
|
106
|
+
);
|
|
107
|
+
if (shouldHeal) {
|
|
108
|
+
pushConfig(apiKey, serverUrl, projectName, stripCredentials(config)).catch(() => {});
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
// Cache the merged, credential-free config back to disk so the local
|
|
101
|
-
// stays a readable view of the authoritative state. Only when we
|
|
102
|
-
// fetched from the server — offline runs must not silently
|
|
103
|
-
// user's local file.
|
|
111
|
+
// Cache the merged, credential-free config back to disk so the local
|
|
112
|
+
// file stays a readable view of the authoritative state. Only when we
|
|
113
|
+
// actually fetched from the server — offline runs must not silently
|
|
114
|
+
// mutate the user's local file. With mergeNonNull above, we're
|
|
115
|
+
// guaranteed this write never strips existing local fields.
|
|
104
116
|
if (serverConfig) {
|
|
105
117
|
writeLocalConfig(configPath, config);
|
|
106
118
|
}
|
|
@@ -108,6 +120,23 @@ export async function loadConfig(dir, opts = {}) {
|
|
|
108
120
|
return config;
|
|
109
121
|
}
|
|
110
122
|
|
|
123
|
+
// True when `local` has any non-null leaf that's null/missing on `server`.
|
|
124
|
+
// Walked recursively for nested blocks (linear, jira, github). Used to
|
|
125
|
+
// decide whether it's worth pushing a heal sync.
|
|
126
|
+
function hasFieldsServerLacks(local, server) {
|
|
127
|
+
const isObj = v => v && typeof v === "object" && !Array.isArray(v);
|
|
128
|
+
for (const [key, v] of Object.entries(local || {})) {
|
|
129
|
+
if (v === null || v === undefined) continue;
|
|
130
|
+
const s = server?.[key];
|
|
131
|
+
if (isObj(v)) {
|
|
132
|
+
if (!isObj(s) || hasFieldsServerLacks(v, s)) return true;
|
|
133
|
+
} else {
|
|
134
|
+
if (s === null || s === undefined) return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
111
140
|
// Push a config object to the server. Caller is responsible for deciding when.
|
|
112
141
|
export async function pushConfig(apiKey, serverUrl, projectName, payload) {
|
|
113
142
|
if (!apiKey || !serverUrl || !projectName) return { ok: false, error: "missing_auth" };
|
|
@@ -185,3 +214,19 @@ function deepMerge(defaults, overrides) {
|
|
|
185
214
|
}
|
|
186
215
|
return result;
|
|
187
216
|
}
|
|
217
|
+
|
|
218
|
+
// Like deepMerge, but null/undefined on the overrides side never
|
|
219
|
+
// clobbers a non-null value already in base. Used for the local ← server
|
|
220
|
+
// step so an empty server row can't wipe a fully-populated local file.
|
|
221
|
+
function mergeNonNull(base, overrides) {
|
|
222
|
+
const result = { ...base };
|
|
223
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
224
|
+
if (value === null || value === undefined) continue;
|
|
225
|
+
if (value && typeof value === "object" && !Array.isArray(value) && base[key] && typeof base[key] === "object") {
|
|
226
|
+
result[key] = mergeNonNull(base[key], value);
|
|
227
|
+
} else {
|
|
228
|
+
result[key] = value;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
}
|
package/cli/init.mjs
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
// `agentdesk init` —
|
|
1
|
+
// `agentdesk init` — always the wizard, mode-aware.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
3
|
+
// Two modes, decided automatically at entry time:
|
|
4
|
+
// • new — server has no project for this clone. Every step is an
|
|
5
|
+
// interactive prompt; save writes a new project row to the
|
|
6
|
+
// server and pushes its settings.
|
|
7
|
+
// • existing — server already has a project for this clone (local
|
|
8
|
+
// .agentdesk.json has projectKey, or git remote auto-matches
|
|
9
|
+
// one). Project-wide steps (repo, tracker type/location,
|
|
10
|
+
// team, signature) become read-only display lines with a
|
|
11
|
+
// "configured on agentdesk.live — change there" hint;
|
|
12
|
+
// only the per-machine credential steps (GitHub token,
|
|
13
|
+
// tracker creds) are interactive.
|
|
13
14
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
15
|
+
// Rationale for the locks: tracker/repo/team are shared state. Changing
|
|
16
|
+
// them in one CLI would desync teammates and the dashboard. Those
|
|
17
|
+
// changes belong in project settings on agentdesk.live. CLI init is the
|
|
18
|
+
// single, consistent way to get a machine ready — nothing more.
|
|
17
19
|
|
|
18
20
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
19
21
|
import { join } from "path";
|
|
20
|
-
import { execSync } from "child_process";
|
|
21
22
|
import { loadConfig, pushConfig } from "./config.mjs";
|
|
22
23
|
import { getStoredApiKey } from "./login.mjs";
|
|
23
24
|
import { registerLocalProject } from "./projects.mjs";
|
|
24
25
|
import { checkTrackerPermissions } from "./tracker-check.mjs";
|
|
25
|
-
import { autoMatchProject
|
|
26
|
+
import { autoMatchProject } from "./bootstrap.mjs";
|
|
26
27
|
import { select as promptSelect, ask, promptRequired } from "./prompts.mjs";
|
|
27
28
|
import {
|
|
28
29
|
cloneRepoWithToken,
|
|
@@ -34,16 +35,12 @@ import {
|
|
|
34
35
|
readLocalConfig,
|
|
35
36
|
runAccessChecks,
|
|
36
37
|
saveEnvVar,
|
|
37
|
-
writeMinimalLocalConfig,
|
|
38
38
|
} from "./setup-helpers.mjs";
|
|
39
39
|
|
|
40
40
|
const SERVER = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
|
|
41
41
|
|
|
42
|
-
// ---------- Tracker
|
|
42
|
+
// ---------- Tracker list helpers (for step 9 in new mode) ----------
|
|
43
43
|
|
|
44
|
-
// Fetch the pickable list of teams/projects after tracker creds are known.
|
|
45
|
-
// Returns [{ id, name }] or null when the API couldn't answer (caller
|
|
46
|
-
// falls back to a manual key prompt).
|
|
47
44
|
async function listTrackerProjects({ tracker, creds, location }) {
|
|
48
45
|
if (tracker === "linear") {
|
|
49
46
|
if (!creds.LINEAR_API_KEY) return null;
|
|
@@ -75,19 +72,24 @@ async function listTrackerProjects({ tracker, creds, location }) {
|
|
|
75
72
|
return null;
|
|
76
73
|
}
|
|
77
74
|
|
|
78
|
-
// ----------
|
|
75
|
+
// ---------- Project picker (when auto-match fails and server has projects) ----------
|
|
79
76
|
|
|
80
|
-
async function
|
|
81
|
-
// Fetch the user's projects once; filter on server-known fields.
|
|
82
|
-
let projects;
|
|
77
|
+
async function fetchAccountProjects(apiKey) {
|
|
83
78
|
try {
|
|
84
79
|
const res = await fetch(`${SERVER}/api/projects`, {
|
|
85
|
-
headers: { "x-api-key": apiKey },
|
|
80
|
+
headers: { "x-api-key": apiKey },
|
|
81
|
+
signal: AbortSignal.timeout(8000),
|
|
86
82
|
});
|
|
87
|
-
if (!res.ok) return
|
|
88
|
-
|
|
89
|
-
} catch { return
|
|
90
|
-
|
|
83
|
+
if (!res.ok) return [];
|
|
84
|
+
return await res.json();
|
|
85
|
+
} catch { return []; }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------- Legacy migration ----------
|
|
89
|
+
|
|
90
|
+
async function autoMatchLegacy(cwd, apiKey, localConfig) {
|
|
91
|
+
const projects = await fetchAccountProjects(apiKey);
|
|
92
|
+
if (!projects.length) return null;
|
|
91
93
|
|
|
92
94
|
const settingsList = await Promise.all(projects.map(async p => {
|
|
93
95
|
try {
|
|
@@ -105,223 +107,258 @@ async function autoMatchLegacy(cwd, apiKey, localConfig) {
|
|
|
105
107
|
for (let i = 0; i < projects.length; i++) {
|
|
106
108
|
const p = projects[i];
|
|
107
109
|
const s = settingsList[i];
|
|
108
|
-
|
|
109
|
-
const serverTeam = (s?.linear?.teamKey || "").toLowerCase();
|
|
110
|
-
const serverJira = (s?.jira?.project || "").toLowerCase();
|
|
111
|
-
|
|
112
|
-
if (legacyRepo && serverRepo === legacyRepo) {
|
|
110
|
+
if (legacyRepo && (s?.github?.repo || "").toLowerCase() === legacyRepo) {
|
|
113
111
|
return { projectKey: p.id, name: p.name, reason: `github.repo matches ${s.github.repo}` };
|
|
114
112
|
}
|
|
115
|
-
if (legacyTeamKey &&
|
|
113
|
+
if (legacyTeamKey && (s?.linear?.teamKey || "").toLowerCase() === legacyTeamKey) {
|
|
116
114
|
return { projectKey: p.id, name: p.name, reason: `Linear team matches ${s.linear.teamKey}` };
|
|
117
115
|
}
|
|
118
|
-
if (legacyJiraProject &&
|
|
116
|
+
if (legacyJiraProject && (s?.jira?.project || "").toLowerCase() === legacyJiraProject) {
|
|
119
117
|
return { projectKey: p.id, name: p.name, reason: `Jira project matches ${s.jira.project}` };
|
|
120
118
|
}
|
|
121
119
|
}
|
|
122
120
|
return null;
|
|
123
121
|
}
|
|
124
122
|
|
|
125
|
-
// ----------
|
|
123
|
+
// ---------- The unified wizard ----------
|
|
126
124
|
|
|
127
|
-
async function
|
|
125
|
+
async function runWizard({ cwd, apiKey, mode, projectKey }) {
|
|
126
|
+
const isExisting = mode === "existing";
|
|
128
127
|
let currentCwd = cwd;
|
|
129
128
|
|
|
130
129
|
console.log("");
|
|
131
|
-
console.log(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// --- E3 (was E5): Fetch server config first, so every later prompt can
|
|
136
|
-
// reference the project's known tracker/repo instead of re-asking.
|
|
137
|
-
process.stdout.write(" Fetching project settings from agentdesk.live... ");
|
|
138
|
-
const config = await loadConfig(currentCwd, { apiKey, serverUrl: SERVER, projectName: projectKey, silent: true });
|
|
139
|
-
console.log("✓");
|
|
140
|
-
console.log(` Project: ${projectKey}`);
|
|
141
|
-
if (config.tracker) console.log(` Tracker: ${config.tracker}`);
|
|
142
|
-
if (config.github?.repo) console.log(` Repo: ${config.github.repo}`);
|
|
143
|
-
console.log("");
|
|
144
|
-
|
|
145
|
-
// --- E4 (was E3): GitHub token. Names the repo it's for so the user
|
|
146
|
-
// knows why we're asking.
|
|
147
|
-
let env = loadDotEnv(currentCwd);
|
|
148
|
-
let githubToken = env.GITHUB_TOKEN;
|
|
149
|
-
if (!githubToken) {
|
|
150
|
-
const repoHint = config.github?.repo ? ` (needed to push to ${config.github.repo})` : "";
|
|
151
|
-
console.log(` GitHub token${repoHint} — create one at https://github.com/settings/tokens, scope: repo`);
|
|
152
|
-
githubToken = await promptRequired("GitHub token");
|
|
153
|
-
saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
|
|
154
|
-
console.log(" ✓ Saved GITHUB_TOKEN to .env");
|
|
155
|
-
}
|
|
156
|
-
const githubLogin = await fetchGitHubLogin(githubToken);
|
|
157
|
-
if (githubLogin) console.log(` ✓ Token authenticates as @${githubLogin}`);
|
|
158
|
-
else console.log(" ⚠ Token couldn't be verified against the GitHub API (continuing anyway)");
|
|
130
|
+
console.log(isExisting
|
|
131
|
+
? ` AgentDesk — configuring existing project`
|
|
132
|
+
: ` AgentDesk — setting up a new project`);
|
|
133
|
+
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
159
134
|
console.log("");
|
|
160
135
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner: serverRepo.owner, repo: serverRepo.repo, token: githubToken });
|
|
175
|
-
} catch (err) {
|
|
176
|
-
console.log(` ✗ ${err.message}`);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
// Re-save the token into the cloned working tree's .env (previous
|
|
180
|
-
// save was into the parent dir).
|
|
181
|
-
saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
|
|
182
|
-
console.log("");
|
|
183
|
-
}
|
|
136
|
+
// Server config for existing mode. Fetched up front so every subsequent
|
|
137
|
+
// step knows what's already set on agentdesk.live.
|
|
138
|
+
let serverConfig = null;
|
|
139
|
+
if (isExisting) {
|
|
140
|
+
process.stdout.write(" Fetching project settings from agentdesk.live... ");
|
|
141
|
+
serverConfig = await loadConfig(currentCwd, { apiKey, serverUrl: SERVER, projectName: projectKey, silent: true });
|
|
142
|
+
console.log("✓");
|
|
143
|
+
console.log(` Project: ${projectKey}`);
|
|
144
|
+
console.log(` Tracker: ${serverConfig.tracker || "(not set — change in agentdesk.live)"}`);
|
|
145
|
+
console.log(` Repo: ${serverConfig.github?.repo || "(not set)"}`);
|
|
146
|
+
console.log(` Signature: ${serverConfig.identityBadge || "(not set)"}`);
|
|
147
|
+
console.log("");
|
|
184
148
|
}
|
|
185
149
|
|
|
186
|
-
//
|
|
187
|
-
// works on subsequent runs without needing discovery.
|
|
188
|
-
writeMinimalLocalConfig(currentCwd, projectKey);
|
|
189
|
-
|
|
190
|
-
// --- E6: Fill missing tracker creds + verify (gapScan handles both) ---
|
|
191
|
-
await gapScan({ cwd: currentCwd, apiKey, projectKey });
|
|
192
|
-
|
|
193
|
-
// --- E7: Final access-checks summary ---
|
|
194
|
-
env = loadDotEnv(currentCwd);
|
|
195
|
-
const results = await runAccessChecks({ apiKey, serverUrl: SERVER, cwd: currentCwd, config, creds: env });
|
|
196
|
-
printAccessChecks(results, { tracker: config.tracker });
|
|
197
|
-
console.log("");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ---------- New-project wizard (steps 1–10) ----------
|
|
201
|
-
|
|
202
|
-
async function runNewProjectWizard(cwd, apiKey) {
|
|
203
|
-
let currentCwd = cwd;
|
|
204
|
-
|
|
205
|
-
console.log("");
|
|
206
|
-
console.log(" AgentDesk — new project setup");
|
|
207
|
-
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
208
|
-
console.log("");
|
|
209
|
-
|
|
210
|
-
// --- Step 1: Detect git repo ---
|
|
150
|
+
// Step 1 — detect git repo (silent).
|
|
211
151
|
const git = detectGitRepo(currentCwd);
|
|
212
|
-
let ownerRepo = git.ownerRepo;
|
|
213
152
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
153
|
+
// Step 2 — GitHub repo.
|
|
154
|
+
let ownerRepo = git.ownerRepo;
|
|
155
|
+
if (isExisting) {
|
|
156
|
+
if (serverConfig.github?.repo) {
|
|
157
|
+
ownerRepo = serverConfig.github.repo;
|
|
158
|
+
console.log(` Repo: ${ownerRepo} (configured — change in agentdesk.live)`);
|
|
159
|
+
console.log("");
|
|
160
|
+
}
|
|
161
|
+
} else if (!git.inRepo) {
|
|
162
|
+
console.log(" No git repo here yet.");
|
|
163
|
+
console.log("");
|
|
217
164
|
while (!ownerRepo) {
|
|
218
165
|
const input = await promptRequired("GitHub repo (owner/repo or URL)");
|
|
219
166
|
const parsed = parseGitHubRef(input);
|
|
220
|
-
if (parsed)
|
|
167
|
+
if (parsed) ownerRepo = `${parsed.owner}/${parsed.repo}`;
|
|
221
168
|
else console.log(" Not a valid GitHub repo. Only github.com is supported right now.");
|
|
222
169
|
}
|
|
223
170
|
console.log("");
|
|
224
171
|
}
|
|
225
172
|
|
|
226
|
-
//
|
|
173
|
+
// Step 3 — GitHub token (always editable, per-machine).
|
|
227
174
|
let env = loadDotEnv(currentCwd);
|
|
228
175
|
let githubToken = env.GITHUB_TOKEN;
|
|
229
|
-
|
|
230
|
-
|
|
176
|
+
let githubLogin = null;
|
|
177
|
+
|
|
178
|
+
if (githubToken) {
|
|
179
|
+
githubLogin = await fetchGitHubLogin(githubToken);
|
|
180
|
+
if (githubLogin) {
|
|
181
|
+
const choice = await promptSelect({
|
|
182
|
+
message: `GitHub token — currently authenticates as @${githubLogin}`,
|
|
183
|
+
default: "keep",
|
|
184
|
+
choices: [
|
|
185
|
+
{ name: "Keep current token", value: "keep" },
|
|
186
|
+
{ name: "Replace with a new token", value: "new" },
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
if (choice === "new") {
|
|
190
|
+
githubToken = await promptRequired("GitHub token");
|
|
191
|
+
saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
|
|
192
|
+
githubLogin = await fetchGitHubLogin(githubToken);
|
|
193
|
+
if (githubLogin) console.log(` ✓ Token authenticates as @${githubLogin}`);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
console.log(" ⚠ GITHUB_TOKEN in .env doesn't authenticate with GitHub — enter a new one.");
|
|
197
|
+
githubToken = await promptRequired("GitHub token");
|
|
198
|
+
saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
|
|
199
|
+
githubLogin = await fetchGitHubLogin(githubToken);
|
|
200
|
+
if (githubLogin) console.log(` ✓ Token authenticates as @${githubLogin}`);
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
const repoHint = ownerRepo ? ` (needed to push to ${ownerRepo})` : "";
|
|
204
|
+
console.log(` GitHub token${repoHint} — create one at https://github.com/settings/tokens, scope: repo`);
|
|
231
205
|
githubToken = await promptRequired("GitHub token");
|
|
232
206
|
saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
|
|
233
|
-
|
|
207
|
+
githubLogin = await fetchGitHubLogin(githubToken);
|
|
208
|
+
if (githubLogin) console.log(` ✓ Token authenticates as @${githubLogin}`);
|
|
209
|
+
else console.log(" ⚠ Token couldn't be verified against the GitHub API (continuing anyway)");
|
|
234
210
|
}
|
|
235
|
-
const githubLogin = await fetchGitHubLogin(githubToken);
|
|
236
|
-
if (githubLogin) console.log(` ✓ Token authenticates as @${githubLogin}`);
|
|
237
|
-
else console.log(" ⚠ Token couldn't be verified against the GitHub API (continuing anyway)");
|
|
238
211
|
console.log("");
|
|
239
212
|
|
|
240
|
-
//
|
|
241
|
-
if (
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
213
|
+
// Step 4 — Clone if the correct repo isn't here locally.
|
|
214
|
+
if (ownerRepo) {
|
|
215
|
+
const parsed = parseGitHubRef(ownerRepo);
|
|
216
|
+
if (parsed) {
|
|
217
|
+
const expected = `${parsed.owner}/${parsed.repo}`.toLowerCase();
|
|
218
|
+
const haveHere = (git.ownerRepo || "").toLowerCase();
|
|
219
|
+
if (!git.inRepo || haveHere !== expected) {
|
|
220
|
+
console.log(` Cloning ${parsed.owner}/${parsed.repo} into ./${parsed.repo}...`);
|
|
221
|
+
try {
|
|
222
|
+
currentCwd = cloneRepoWithToken({ cwd: currentCwd, owner: parsed.owner, repo: parsed.repo, token: githubToken });
|
|
223
|
+
saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
console.log(` ✗ ${err.message}`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
console.log("");
|
|
229
|
+
}
|
|
249
230
|
}
|
|
250
|
-
saveEnvVar(currentCwd, "GITHUB_TOKEN", githubToken);
|
|
251
|
-
console.log("");
|
|
252
231
|
}
|
|
253
232
|
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
233
|
+
// Step 5 — Tracker type.
|
|
234
|
+
let tracker;
|
|
235
|
+
if (isExisting) {
|
|
236
|
+
tracker = serverConfig.tracker || null;
|
|
237
|
+
console.log(` Tracker: ${tracker || "none"} (configured — change in agentdesk.live)`);
|
|
238
|
+
console.log("");
|
|
239
|
+
} else {
|
|
240
|
+
tracker = await promptSelect({
|
|
241
|
+
message: "Task tracker",
|
|
242
|
+
choices: [
|
|
243
|
+
{ name: "Linear", value: "linear" },
|
|
244
|
+
{ name: "Jira", value: "jira" },
|
|
245
|
+
{ name: "GitHub Issues", value: "github" },
|
|
246
|
+
{ name: "None (use descriptions only)", value: null },
|
|
247
|
+
],
|
|
248
|
+
});
|
|
249
|
+
console.log("");
|
|
250
|
+
}
|
|
265
251
|
|
|
266
|
-
//
|
|
252
|
+
// Step 6 — Tracker location.
|
|
267
253
|
let linearWorkspace = null;
|
|
268
254
|
let jiraBaseUrl = null;
|
|
269
|
-
if (
|
|
270
|
-
linearWorkspace =
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
255
|
+
if (isExisting) {
|
|
256
|
+
linearWorkspace = serverConfig.linear?.workspace || null;
|
|
257
|
+
jiraBaseUrl = serverConfig.jira?.baseUrl || null;
|
|
258
|
+
if (tracker === "linear") {
|
|
259
|
+
console.log(` Linear workspace: ${linearWorkspace || "(not set)"} (configured — change in agentdesk.live)`);
|
|
260
|
+
console.log("");
|
|
261
|
+
} else if (tracker === "jira") {
|
|
262
|
+
console.log(` Jira URL: ${jiraBaseUrl || "(not set)"} (configured — change in agentdesk.live)`);
|
|
263
|
+
console.log("");
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
if (tracker === "linear") {
|
|
267
|
+
linearWorkspace = await promptRequired("Linear workspace slug (the 'kendoo' in linear.app/kendoo)");
|
|
268
|
+
console.log("");
|
|
269
|
+
} else if (tracker === "jira") {
|
|
270
|
+
const raw = await promptRequired("Jira tenant URL (e.g. https://yourco.atlassian.net)");
|
|
271
|
+
jiraBaseUrl = raw.replace(/\/+$/, "");
|
|
272
|
+
console.log("");
|
|
273
|
+
}
|
|
276
274
|
}
|
|
277
275
|
|
|
278
|
-
//
|
|
276
|
+
// Step 7 — Tracker credentials (always editable, per-machine).
|
|
279
277
|
env = loadDotEnv(currentCwd);
|
|
280
278
|
let linearApiKey = env.LINEAR_API_KEY;
|
|
281
279
|
let jiraEmail = env.JIRA_EMAIL;
|
|
282
280
|
let jiraApiToken = env.JIRA_API_TOKEN;
|
|
283
281
|
|
|
284
|
-
if (tracker === "linear"
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
282
|
+
if (tracker === "linear") {
|
|
283
|
+
if (linearApiKey) {
|
|
284
|
+
const choice = await promptSelect({
|
|
285
|
+
message: "Linear API key — already in .env",
|
|
286
|
+
default: "keep",
|
|
287
|
+
choices: [
|
|
288
|
+
{ name: "Keep current", value: "keep" },
|
|
289
|
+
{ name: "Replace with a new one", value: "new" },
|
|
290
|
+
],
|
|
291
|
+
});
|
|
292
|
+
if (choice === "new") {
|
|
293
|
+
linearApiKey = await promptRequired("Linear API key");
|
|
294
|
+
saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
console.log(" (Get a Linear API key: https://linear.app/settings/api)");
|
|
298
|
+
linearApiKey = await promptRequired("Linear API key");
|
|
299
|
+
saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
|
|
294
300
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
301
|
+
console.log("");
|
|
302
|
+
} else if (tracker === "jira") {
|
|
303
|
+
if (jiraEmail && jiraApiToken) {
|
|
304
|
+
const choice = await promptSelect({
|
|
305
|
+
message: `Jira credentials — ${jiraEmail} / token in .env`,
|
|
306
|
+
default: "keep",
|
|
307
|
+
choices: [
|
|
308
|
+
{ name: "Keep current", value: "keep" },
|
|
309
|
+
{ name: "Replace with new credentials", value: "new" },
|
|
310
|
+
],
|
|
311
|
+
});
|
|
312
|
+
if (choice === "new") {
|
|
313
|
+
jiraEmail = await promptRequired("Atlassian login email");
|
|
314
|
+
jiraApiToken = await promptRequired("Jira API token");
|
|
315
|
+
saveEnvVar(currentCwd, "JIRA_EMAIL", jiraEmail);
|
|
316
|
+
saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
if (!jiraEmail) {
|
|
320
|
+
console.log(" (Create a Jira API token: https://id.atlassian.com/manage-profile/security/api-tokens)");
|
|
321
|
+
jiraEmail = await promptRequired("Atlassian login email");
|
|
322
|
+
saveEnvVar(currentCwd, "JIRA_EMAIL", jiraEmail);
|
|
323
|
+
}
|
|
324
|
+
if (!jiraApiToken) {
|
|
325
|
+
jiraApiToken = await promptRequired("Jira API token");
|
|
326
|
+
saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
|
|
327
|
+
}
|
|
298
328
|
}
|
|
299
|
-
console.log("
|
|
329
|
+
console.log("");
|
|
300
330
|
}
|
|
301
331
|
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
332
|
+
// Step 8 — Signature.
|
|
333
|
+
let identityBadge;
|
|
334
|
+
if (isExisting) {
|
|
335
|
+
identityBadge = serverConfig.identityBadge || null;
|
|
336
|
+
console.log(` Signature: ${identityBadge || "(not set)"} (configured — change in agentdesk.live)`);
|
|
337
|
+
console.log("");
|
|
338
|
+
} else {
|
|
339
|
+
const badgeValue = await promptSelect({
|
|
340
|
+
message: "How should the AI team sign its work?",
|
|
341
|
+
default: "AgentDesk",
|
|
342
|
+
choices: [
|
|
343
|
+
{ name: "AgentDesk", value: "AgentDesk" },
|
|
344
|
+
{ name: "Claude Code", value: "Claude Code" },
|
|
345
|
+
{ name: "Custom…", value: "__custom__" },
|
|
346
|
+
{ name: "Skip (no signature)", value: null },
|
|
347
|
+
],
|
|
348
|
+
});
|
|
349
|
+
identityBadge = badgeValue;
|
|
350
|
+
if (badgeValue === "__custom__") {
|
|
351
|
+
const v = (await ask(" Custom signature: ")).trim();
|
|
352
|
+
identityBadge = v || null;
|
|
353
|
+
}
|
|
354
|
+
console.log("");
|
|
317
355
|
}
|
|
318
|
-
console.log("");
|
|
319
356
|
|
|
320
|
-
//
|
|
321
|
-
//
|
|
357
|
+
// Build the in-memory config (used for verification + summary; written
|
|
358
|
+
// to disk only in new mode).
|
|
322
359
|
const config = {
|
|
323
360
|
tracker,
|
|
324
|
-
github: { repo: ownerRepo, login: githubLogin || null },
|
|
361
|
+
github: { repo: ownerRepo || null, login: githubLogin || null },
|
|
325
362
|
};
|
|
326
363
|
if (tracker === "linear") config.linear = { workspace: linearWorkspace };
|
|
327
364
|
if (tracker === "jira") config.jira = { baseUrl: jiraBaseUrl };
|
|
@@ -330,10 +367,15 @@ async function runNewProjectWizard(cwd, apiKey) {
|
|
|
330
367
|
let trackerTeamOrProjectId = null;
|
|
331
368
|
let trackerTeamOrProjectName = null;
|
|
332
369
|
|
|
370
|
+
// Step 9 — Verify tracker + (new mode) pick team/project.
|
|
333
371
|
if (tracker && tracker !== "github") {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
372
|
+
if (isExisting) {
|
|
373
|
+
trackerTeamOrProjectId = serverConfig.linear?.teamKey || serverConfig.jira?.project || null;
|
|
374
|
+
trackerTeamOrProjectName = trackerTeamOrProjectId;
|
|
375
|
+
if (tracker === "linear" && trackerTeamOrProjectId) config.linear.teamKey = trackerTeamOrProjectId;
|
|
376
|
+
if (tracker === "jira" && trackerTeamOrProjectId) config.jira.project = trackerTeamOrProjectId;
|
|
377
|
+
|
|
378
|
+
process.stdout.write(" Verifying tracker access... ");
|
|
337
379
|
const creds = { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken };
|
|
338
380
|
const check = await checkTrackerPermissions({ tracker, config, credentials: creds });
|
|
339
381
|
if (check.ok) {
|
|
@@ -341,91 +383,128 @@ async function runNewProjectWizard(cwd, apiKey) {
|
|
|
341
383
|
if (check.identity?.name || check.identity?.email) {
|
|
342
384
|
console.log(` ✓ Posting as: ${check.identity.name || check.identity.email}`);
|
|
343
385
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
console.log("failed");
|
|
348
|
-
for (const e of check.errors || []) console.log(` • ${e}`);
|
|
349
|
-
const next = await promptSelect({
|
|
350
|
-
message: "What now?",
|
|
351
|
-
choices: [
|
|
352
|
-
{ name: "Re-enter credentials and try again", value: "retry" },
|
|
353
|
-
{ name: "Save anyway (fix later in .env)", value: "skip" },
|
|
354
|
-
{ name: "Cancel", value: "cancel" },
|
|
355
|
-
],
|
|
356
|
-
});
|
|
357
|
-
if (next === "cancel") return;
|
|
358
|
-
if (next === "skip") break;
|
|
359
|
-
if (tracker === "linear") {
|
|
360
|
-
linearApiKey = await promptRequired("Linear API key");
|
|
361
|
-
saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
|
|
362
|
-
} else if (tracker === "jira") {
|
|
363
|
-
jiraEmail = await promptRequired("Atlassian login email");
|
|
364
|
-
jiraApiToken = await promptRequired("Jira API token");
|
|
365
|
-
saveEnvVar(currentCwd, "JIRA_EMAIL", jiraEmail);
|
|
366
|
-
saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
|
|
386
|
+
} else {
|
|
387
|
+
console.log("failed");
|
|
388
|
+
for (const e of check.errors || []) console.log(` • ${e}`);
|
|
367
389
|
}
|
|
368
|
-
process.stdout.write(" Re-verifying tracker access... ");
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (verified) {
|
|
372
|
-
const creds = { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken };
|
|
373
|
-
const items = await listTrackerProjects({ tracker, creds, location: jiraBaseUrl });
|
|
374
390
|
console.log("");
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
391
|
+
} else {
|
|
392
|
+
process.stdout.write(" Verifying tracker access... ");
|
|
393
|
+
let verified = false;
|
|
394
|
+
while (!verified) {
|
|
395
|
+
const creds = { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken };
|
|
396
|
+
const check = await checkTrackerPermissions({ tracker, config, credentials: creds });
|
|
397
|
+
if (check.ok) {
|
|
398
|
+
console.log("✓");
|
|
399
|
+
if (check.identity?.name || check.identity?.email) {
|
|
400
|
+
console.log(` ✓ Posting as: ${check.identity.name || check.identity.email}`);
|
|
401
|
+
}
|
|
402
|
+
verified = true;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
console.log("failed");
|
|
406
|
+
for (const e of check.errors || []) console.log(` • ${e}`);
|
|
407
|
+
const next = await promptSelect({
|
|
408
|
+
message: "What now?",
|
|
409
|
+
choices: [
|
|
410
|
+
{ name: "Re-enter credentials and try again", value: "retry" },
|
|
411
|
+
{ name: "Save anyway (fix later in .env)", value: "skip" },
|
|
412
|
+
{ name: "Cancel", value: "cancel" },
|
|
413
|
+
],
|
|
379
414
|
});
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
415
|
+
if (next === "cancel") return;
|
|
416
|
+
if (next === "skip") break;
|
|
417
|
+
if (tracker === "linear") {
|
|
418
|
+
linearApiKey = await promptRequired("Linear API key");
|
|
419
|
+
saveEnvVar(currentCwd, "LINEAR_API_KEY", linearApiKey);
|
|
420
|
+
} else if (tracker === "jira") {
|
|
421
|
+
jiraEmail = await promptRequired("Atlassian login email");
|
|
422
|
+
jiraApiToken = await promptRequired("Jira API token");
|
|
423
|
+
saveEnvVar(currentCwd, "JIRA_EMAIL", jiraEmail);
|
|
424
|
+
saveEnvVar(currentCwd, "JIRA_API_TOKEN", jiraApiToken);
|
|
425
|
+
}
|
|
426
|
+
process.stdout.write(" Re-verifying tracker access... ");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (verified) {
|
|
430
|
+
const creds = { LINEAR_API_KEY: linearApiKey, JIRA_EMAIL: jiraEmail, JIRA_API_TOKEN: jiraApiToken };
|
|
431
|
+
const items = await listTrackerProjects({ tracker, creds, location: jiraBaseUrl });
|
|
432
|
+
if (items && items.length > 0) {
|
|
433
|
+
const pickedId = await promptSelect({
|
|
434
|
+
message: `Pick the ${tracker === "linear" ? "team" : "project"} this project maps to`,
|
|
435
|
+
choices: items.map(it => ({ name: it.name, value: it.id })),
|
|
436
|
+
});
|
|
437
|
+
trackerTeamOrProjectId = pickedId;
|
|
438
|
+
trackerTeamOrProjectName = items.find(it => it.id === pickedId)?.name || pickedId;
|
|
439
|
+
} else {
|
|
440
|
+
trackerTeamOrProjectId = await promptRequired(tracker === "linear" ? "Linear team key (e.g. KEN)" : "Jira project key (e.g. PROJ)");
|
|
441
|
+
trackerTeamOrProjectName = trackerTeamOrProjectId;
|
|
442
|
+
}
|
|
443
|
+
if (tracker === "linear") config.linear = { ...config.linear, teamKey: trackerTeamOrProjectId };
|
|
444
|
+
if (tracker === "jira") config.jira = { ...config.jira, project: trackerTeamOrProjectId };
|
|
445
|
+
console.log("");
|
|
386
446
|
}
|
|
387
|
-
if (tracker === "linear") config.linear = { ...config.linear, teamKey: trackerTeamOrProjectId };
|
|
388
|
-
if (tracker === "jira") config.jira = { ...config.jira, project: trackerTeamOrProjectId };
|
|
389
|
-
console.log("");
|
|
390
447
|
}
|
|
391
448
|
}
|
|
392
449
|
|
|
393
|
-
// Derive projectName + projectKey
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
450
|
+
// Derive projectName + projectKey (only used in new mode).
|
|
451
|
+
let projectName, computedKey;
|
|
452
|
+
if (isExisting) {
|
|
453
|
+
projectName = serverConfig.name || projectKey;
|
|
454
|
+
computedKey = projectKey;
|
|
455
|
+
} else {
|
|
456
|
+
const repoShort = ownerRepo ? ownerRepo.split("/").pop() : "project";
|
|
457
|
+
projectName = trackerTeamOrProjectName
|
|
458
|
+
? `${repoShort} — ${String(trackerTeamOrProjectName).split(" (")[0]}`
|
|
459
|
+
: repoShort;
|
|
460
|
+
const keySource = trackerTeamOrProjectId || repoShort;
|
|
461
|
+
computedKey = String(keySource).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
462
|
+
}
|
|
463
|
+
config.projectKey = computedKey;
|
|
401
464
|
|
|
402
|
-
//
|
|
465
|
+
// Step 10 — Review + access checks + save.
|
|
403
466
|
const credsAll = loadDotEnv(currentCwd);
|
|
404
467
|
const results = await runAccessChecks({ apiKey, serverUrl: SERVER, cwd: currentCwd, config, creds: credsAll });
|
|
405
468
|
printAccessChecks(results, { tracker });
|
|
406
469
|
console.log("");
|
|
407
470
|
console.log(" Summary");
|
|
408
471
|
console.log(" ───────");
|
|
409
|
-
console.log(` Project: ${projectName} (${
|
|
472
|
+
console.log(` Project: ${projectName} (${computedKey})`);
|
|
410
473
|
console.log(` Tracker: ${tracker || "none"}${trackerTeamOrProjectId ? ` (${trackerTeamOrProjectId})` : ""}`);
|
|
411
|
-
console.log(` GitHub: ${ownerRepo}${githubLogin ? ` (@${githubLogin})` : ""}`);
|
|
412
|
-
console.log(` Signature: ${identityBadge || "(
|
|
413
|
-
const envNames = Object.keys(credsAll).filter(k => ["LINEAR_API_KEY","JIRA_EMAIL","JIRA_API_TOKEN","GITHUB_TOKEN"].includes(k));
|
|
414
|
-
console.log(` .env
|
|
474
|
+
console.log(` GitHub: ${ownerRepo || "(not set)"}${githubLogin ? ` (@${githubLogin})` : ""}`);
|
|
475
|
+
console.log(` Signature: ${identityBadge || "(none)"}`);
|
|
476
|
+
const envNames = Object.keys(credsAll).filter(k => ["LINEAR_API_KEY", "JIRA_EMAIL", "JIRA_API_TOKEN", "GITHUB_TOKEN"].includes(k));
|
|
477
|
+
console.log(` .env: ${envNames.join(", ") || "(none)"}`);
|
|
415
478
|
console.log("");
|
|
416
479
|
|
|
417
480
|
const save = (await ask(" Save this config? [Y/n]: ")).trim().toLowerCase();
|
|
418
481
|
if (save && save !== "y" && save !== "yes") {
|
|
419
|
-
console.log(" Cancelled
|
|
482
|
+
console.log(" Cancelled.");
|
|
420
483
|
return;
|
|
421
484
|
}
|
|
422
485
|
console.log("");
|
|
423
486
|
|
|
424
|
-
// Write .agentdesk.json
|
|
425
|
-
|
|
487
|
+
// Write local .agentdesk.json.
|
|
488
|
+
// • Existing mode: preserve whatever's already on disk, ensuring
|
|
489
|
+
// only that `projectKey` is set. Never clobber tracker/github/
|
|
490
|
+
// badge fields the user may have locally even when the server
|
|
491
|
+
// hasn't caught up yet (auto-heal push in config.mjs will
|
|
492
|
+
// reconcile the server side).
|
|
493
|
+
// • New mode: write the full config we just built.
|
|
494
|
+
const configPath = join(currentCwd, ".agentdesk.json");
|
|
495
|
+
if (isExisting) {
|
|
496
|
+
let existingOnDisk = {};
|
|
497
|
+
if (existsSync(configPath)) {
|
|
498
|
+
try { existingOnDisk = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
|
|
499
|
+
}
|
|
500
|
+
const merged = { ...existingOnDisk, projectKey: computedKey };
|
|
501
|
+
writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n");
|
|
502
|
+
} else {
|
|
503
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
504
|
+
}
|
|
426
505
|
console.log(" ✓ Saved .agentdesk.json");
|
|
427
506
|
|
|
428
|
-
// .
|
|
507
|
+
// Ensure .agentdesk/ is gitignored (runtime state).
|
|
429
508
|
const gitignorePath = join(currentCwd, ".gitignore");
|
|
430
509
|
try {
|
|
431
510
|
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
|
|
@@ -436,124 +515,105 @@ async function runNewProjectWizard(cwd, apiKey) {
|
|
|
436
515
|
}
|
|
437
516
|
} catch {}
|
|
438
517
|
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
518
|
+
// Server sync — new mode only. Existing mode doesn't push anything
|
|
519
|
+
// because every project-wide field was locked and unchanged.
|
|
520
|
+
if (!isExisting) {
|
|
521
|
+
try {
|
|
522
|
+
await fetch(`${SERVER}/api/projects`, {
|
|
523
|
+
method: "POST",
|
|
524
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
|
525
|
+
body: JSON.stringify({ id: computedKey, name: projectName, path: currentCwd, tracker }),
|
|
526
|
+
});
|
|
527
|
+
const push = await pushConfig(apiKey, SERVER, computedKey, config);
|
|
528
|
+
if (!push.ok && push.error !== "missing_auth") {
|
|
529
|
+
console.log(` ⚠ Could not push settings to agentdesk.live: ${push.error}`);
|
|
530
|
+
} else {
|
|
531
|
+
console.log(" ✓ Registered with agentdesk.live");
|
|
532
|
+
}
|
|
533
|
+
} catch {
|
|
534
|
+
console.log(" ⚠ Couldn't reach agentdesk.live — local config saved, retry later.");
|
|
451
535
|
}
|
|
452
|
-
} catch {
|
|
453
|
-
console.log(" ⚠ Couldn't reach agentdesk.live — local config saved, retry from the UI later.");
|
|
454
536
|
}
|
|
455
537
|
|
|
456
|
-
registerLocalProject(
|
|
538
|
+
registerLocalProject(computedKey, projectName, currentCwd);
|
|
457
539
|
|
|
458
540
|
console.log("");
|
|
459
|
-
const cmdPrefix = trackerTeamOrProjectId ||
|
|
541
|
+
const cmdPrefix = trackerTeamOrProjectId || computedKey.toUpperCase();
|
|
460
542
|
console.log(" Ready. Run a team session:");
|
|
461
543
|
console.log(` agentdesk team ${cmdPrefix}-123`);
|
|
462
544
|
console.log("");
|
|
463
545
|
}
|
|
464
546
|
|
|
465
|
-
// ---------- Entry:
|
|
547
|
+
// ---------- Entry: mode detection ----------
|
|
466
548
|
|
|
467
549
|
export async function runInit(cwd, opts = {}) {
|
|
468
550
|
const forceFull = !!opts.forceFull;
|
|
551
|
+
const forceNew = !!opts.forceNew;
|
|
469
552
|
const apiKey = getStoredApiKey();
|
|
470
553
|
if (!apiKey) {
|
|
471
554
|
console.log("\n Not logged in. Run `agentdesk login` first.\n");
|
|
472
555
|
process.exit(1);
|
|
473
556
|
}
|
|
474
557
|
|
|
558
|
+
// Step 1 of mode detection: look at local config.
|
|
475
559
|
const local = readLocalConfig(cwd);
|
|
476
560
|
|
|
477
|
-
//
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
487
|
-
console.log("");
|
|
488
|
-
const choice = await promptSelect({
|
|
489
|
-
message: "What would you like to do?",
|
|
490
|
-
default: "refresh",
|
|
491
|
-
choices: [
|
|
492
|
-
{ name: "Refresh tokens (update .env only)", value: "refresh" },
|
|
493
|
-
{ name: "Cancel", value: "cancel" },
|
|
494
|
-
],
|
|
495
|
-
});
|
|
496
|
-
if (choice === "cancel") return;
|
|
497
|
-
await runExistingFlow(cwd, apiKey, local.projectKey);
|
|
561
|
+
// --force-new / --force-full → treat as new no matter what the local/remote state says.
|
|
562
|
+
if (forceFull || forceNew) {
|
|
563
|
+
await runWizard({ cwd, apiKey, mode: "new" });
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Modern local config with projectKey → existing mode.
|
|
568
|
+
if (local?.projectKey) {
|
|
569
|
+
await runWizard({ cwd, apiKey, mode: "existing", projectKey: local.projectKey });
|
|
498
570
|
return;
|
|
499
571
|
}
|
|
500
572
|
|
|
501
|
-
|
|
502
|
-
|
|
573
|
+
// Legacy local config — try to migrate.
|
|
574
|
+
if (local && !local.projectKey) {
|
|
503
575
|
const match = await autoMatchLegacy(cwd, apiKey, local);
|
|
504
576
|
if (match) {
|
|
505
|
-
console.log(`\n ✓ Legacy .agentdesk.json recognized — ${match.reason}. Migrating to project "${match.name}"
|
|
577
|
+
console.log(`\n ✓ Legacy .agentdesk.json recognized — ${match.reason}. Migrating to project "${match.name}".`);
|
|
506
578
|
const migrated = { ...local, projectKey: match.projectKey };
|
|
507
579
|
writeFileSync(join(cwd, ".agentdesk.json"), JSON.stringify(migrated, null, 2) + "\n");
|
|
508
|
-
await
|
|
580
|
+
await runWizard({ cwd, apiKey, mode: "existing", projectKey: match.projectKey });
|
|
509
581
|
return;
|
|
510
582
|
}
|
|
511
|
-
|
|
512
|
-
// Fall through to Connect/New menu.
|
|
583
|
+
// Fall through to git-remote auto-match / picker.
|
|
513
584
|
}
|
|
514
585
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
message: "Use this project?",
|
|
523
|
-
default: "yes",
|
|
524
|
-
choices: [
|
|
525
|
-
{ name: "Yes, continue with this project", value: "yes" },
|
|
526
|
-
{ name: "No, pick another from my account", value: "other" },
|
|
527
|
-
{ name: "Cancel", value: "cancel" },
|
|
528
|
-
],
|
|
529
|
-
});
|
|
530
|
-
if (confirm === "cancel") return;
|
|
531
|
-
if (confirm === "yes") {
|
|
532
|
-
await runExistingFlow(cwd, apiKey, match.projectKey);
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
// "other" — fall through to picker below
|
|
536
|
-
}
|
|
586
|
+
// No local config (or legacy-unmatched). Try auto-match by git remote.
|
|
587
|
+
const match = await autoMatchProject(cwd, apiKey);
|
|
588
|
+
if (match) {
|
|
589
|
+
console.log(`\n ✓ Recognized "${match.name}" (${match.projectKey}) — ${match.reason}.`);
|
|
590
|
+
await runWizard({ cwd, apiKey, mode: "existing", projectKey: match.projectKey });
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
537
593
|
|
|
594
|
+
// No unique auto-match. If the account has other projects, offer to
|
|
595
|
+
// pick one; otherwise go straight to new mode.
|
|
596
|
+
const projects = await fetchAccountProjects(apiKey);
|
|
597
|
+
if (projects.length > 0) {
|
|
538
598
|
console.log("");
|
|
539
|
-
const
|
|
540
|
-
message: "
|
|
599
|
+
const picked = await promptSelect({
|
|
600
|
+
message: "Which project is this clone?",
|
|
541
601
|
choices: [
|
|
542
|
-
{ name:
|
|
543
|
-
{ name: "Set up
|
|
544
|
-
{ name: "Cancel", value: "
|
|
602
|
+
...projects.map(p => ({ name: `${p.name} (${p.id})`, value: p.id })),
|
|
603
|
+
{ name: "Set up as a new project", value: "__new__" },
|
|
604
|
+
{ name: "Cancel", value: "__cancel__" },
|
|
545
605
|
],
|
|
546
606
|
});
|
|
547
|
-
|
|
548
|
-
if (
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
console.log("");
|
|
552
|
-
await runExistingFlow(cwd, apiKey, projectKey);
|
|
607
|
+
console.log("");
|
|
608
|
+
if (picked === "__cancel__") return;
|
|
609
|
+
if (picked === "__new__") {
|
|
610
|
+
await runWizard({ cwd, apiKey, mode: "new" });
|
|
553
611
|
return;
|
|
554
612
|
}
|
|
555
|
-
|
|
613
|
+
await runWizard({ cwd, apiKey, mode: "existing", projectKey: picked });
|
|
614
|
+
return;
|
|
556
615
|
}
|
|
557
616
|
|
|
558
|
-
|
|
617
|
+
// Account has no projects — must be a first-time user.
|
|
618
|
+
await runWizard({ cwd, apiKey, mode: "new" });
|
|
559
619
|
}
|