@agentteams/cli 0.1.92 → 0.1.94
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/.eslintcache +1 -1
- package/README.md +2 -0
- package/dist/api/linear.d.ts +1 -1
- package/dist/api/linear.d.ts.map +1 -1
- package/dist/api/linear.js +8 -2
- package/dist/api/linear.js.map +1 -1
- package/dist/api/report.d.ts +1 -0
- package/dist/api/report.d.ts.map +1 -1
- package/dist/api/report.js +5 -0
- package/dist/api/report.js.map +1 -1
- package/dist/commands/convention.d.ts +3 -0
- package/dist/commands/convention.d.ts.map +1 -1
- package/dist/commands/convention.js +35 -5
- package/dist/commands/convention.js.map +1 -1
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +93 -45
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +110 -27
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +659 -462
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/initWorktree.d.ts +32 -0
- package/dist/commands/initWorktree.d.ts.map +1 -0
- package/dist/commands/initWorktree.js +348 -0
- package/dist/commands/initWorktree.js.map +1 -0
- package/dist/commands/report.d.ts.map +1 -1
- package/dist/commands/report.js +40 -7
- package/dist/commands/report.js.map +1 -1
- package/dist/commands/resolve.js +1 -1
- package/dist/commands/resolve.js.map +1 -1
- package/dist/index.js +31 -8
- package/dist/index.js.map +1 -1
- package/dist/mcp/catalog.d.ts +3 -0
- package/dist/mcp/catalog.d.ts.map +1 -1
- package/dist/mcp/catalog.js +4 -0
- package/dist/mcp/catalog.js.map +1 -1
- package/dist/mcp/localTools.d.ts +65 -0
- package/dist/mcp/localTools.d.ts.map +1 -0
- package/dist/mcp/localTools.js +210 -0
- package/dist/mcp/localTools.js.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +6 -5
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/writeTools.d.ts +3 -12
- package/dist/mcp/writeTools.d.ts.map +1 -1
- package/dist/mcp/writeTools.js +2 -41
- package/dist/mcp/writeTools.js.map +1 -1
- package/dist/utils/agentEntryPoints.d.ts +68 -0
- package/dist/utils/agentEntryPoints.d.ts.map +1 -0
- package/dist/utils/agentEntryPoints.js +112 -0
- package/dist/utils/agentEntryPoints.js.map +1 -0
- package/dist/utils/authServer.d.ts +51 -1
- package/dist/utils/authServer.d.ts.map +1 -1
- package/dist/utils/authServer.js +77 -0
- package/dist/utils/authServer.js.map +1 -1
- package/dist/utils/doctorOutput.d.ts.map +1 -1
- package/dist/utils/doctorOutput.js +7 -3
- package/dist/utils/doctorOutput.js.map +1 -1
- package/dist/utils/git.d.ts +28 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +43 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/initOutput.d.ts.map +1 -1
- package/dist/utils/initOutput.js +97 -15
- package/dist/utils/initOutput.js.map +1 -1
- package/dist/utils/report.d.ts +1 -1
- package/dist/utils/report.js +1 -1
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -1,31 +1,29 @@
|
|
|
1
|
-
import { appendFileSync,
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
3
4
|
import { constants, createCipheriv, publicEncrypt, randomBytes } from 'node:crypto';
|
|
4
5
|
import { multiselect, isCancel, cancel } from '@clack/prompts';
|
|
5
6
|
import httpClient from '../utils/httpClient.js';
|
|
6
7
|
import open from 'open';
|
|
7
|
-
import { createAuthState, startLocalAuthServer } from '../utils/authServer.js';
|
|
8
|
-
import { DEFAULT_API_URL, saveConfig, saveLegacyApiKeyConfig, } from '../utils/config.js';
|
|
9
|
-
import {
|
|
8
|
+
import { createAuthState, createPkcePair, isUnifiedSetupMetadataMissing, SETUP_LEGACY_AGENT_KEY_HINT, SETUP_METADATA_MISSING_HINT, startLocalAuthServer, startUnifiedSetupServer, } from '../utils/authServer.js';
|
|
9
|
+
import { DEFAULT_API_URL, findProjectConfig, loadProjectConfig, loadConfigWithCredential, saveConfig, saveLegacyApiKeyConfig, } from '../utils/config.js';
|
|
10
|
+
import { getPersonalTokenClient } from '../auth/personalTokenClient.js';
|
|
10
11
|
import { createSpinner, withSpinner } from '../utils/spinner.js';
|
|
11
12
|
import { withCommandContext } from '../utils/commandContext.js';
|
|
12
|
-
import { conventionDownload } from './convention.js';
|
|
13
|
-
import {
|
|
13
|
+
import { conventionDownload, conventionStatus, CONVENTION_MANIFEST_FILE, } from './convention.js';
|
|
14
|
+
import { executeDoctorCommand } from './doctor.js';
|
|
15
|
+
import { resolveGitTopLevel, shouldInstallWorktreeHook } from '../utils/git.js';
|
|
14
16
|
import { canonicalizePath } from '../utils/path.js';
|
|
15
17
|
import { readOrCreateMachineId } from '../utils/machineId.js';
|
|
16
18
|
import { buildAuthHeaders } from '../utils/apiContext.js';
|
|
17
|
-
import {
|
|
19
|
+
import { AGENT_ENTRY_POINT_FILES, detectAgentEntryPointFiles, parseAgentFilesOption, } from '../utils/agentEntryPoints.js';
|
|
20
|
+
import { bootstrapLinkedWorktree, resolveLinkedWorktreeSource } from './initWorktree.js';
|
|
21
|
+
import { DEFAULT_CONVENTION_REFERENCE, ensurePostCheckoutHook, } from '../utils/conventionLink.js';
|
|
22
|
+
export { bootstrapLinkedWorktree } from './initWorktree.js';
|
|
18
23
|
const AUTH_BASE_URL = process.env.AGENTTEAMS_WEB_URL || 'https://agentteams.run';
|
|
19
|
-
const AGENT_ENTRY_POINT_FILES = [
|
|
20
|
-
{ value: 'CLAUDE.md', label: 'CLAUDE.md', hint: 'Claude Code' },
|
|
21
|
-
{ value: 'AGENTS.md', label: 'AGENTS.md', hint: 'OpenCode / Codex' },
|
|
22
|
-
{ value: 'GEMINI.md', label: 'GEMINI.md', hint: 'Antigravity' },
|
|
23
|
-
{ value: '.cursor/rules/agentteams.mdc', label: '.cursor/rules/agentteams.mdc', hint: 'Cursor' },
|
|
24
|
-
];
|
|
25
24
|
const CONFIG_DIR = '.agentteams';
|
|
26
25
|
const CONFIG_FILE = 'config.json';
|
|
27
26
|
const CONVENTION_FILE = 'convention.md';
|
|
28
|
-
const RELINK_BACKUP_NAME = 'agentteams-relink';
|
|
29
27
|
const AUTH_PATH_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
30
28
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs9s9+n0C8Z099LrOTlKB
|
|
31
29
|
83c2WluO/TxZFxJQ07XgfKJ2RG/8K2kvCwVKeSgzzBP/hmY2qWAgAOrXIoSHNYGt
|
|
@@ -72,26 +70,30 @@ export function detectOsType() {
|
|
|
72
70
|
}
|
|
73
71
|
return undefined;
|
|
74
72
|
}
|
|
75
|
-
export function buildAuthorizeUrl(
|
|
73
|
+
export function buildAuthorizeUrl(input) {
|
|
76
74
|
const params = new URLSearchParams({
|
|
77
|
-
port: String(port),
|
|
78
|
-
projectName,
|
|
75
|
+
port: String(input.port),
|
|
76
|
+
projectName: input.projectName,
|
|
79
77
|
});
|
|
80
|
-
if (authPathEnc && authPathEnc.length > 0) {
|
|
81
|
-
params.set('ap', authPathEnc);
|
|
78
|
+
if (input.authPathEnc && input.authPathEnc.length > 0) {
|
|
79
|
+
params.set('ap', input.authPathEnc);
|
|
82
80
|
}
|
|
83
|
-
if (osType && osType.length > 0) {
|
|
84
|
-
params.set('ot', osType);
|
|
81
|
+
if (input.osType && input.osType.length > 0) {
|
|
82
|
+
params.set('ot', input.osType);
|
|
85
83
|
}
|
|
86
84
|
// Machine identity, shared with the runner installed on this machine. It is not a secret and is
|
|
87
85
|
// only used to bind the agent to the runner that can actually reach this workspace.
|
|
88
|
-
if (machineId && machineId.length > 0) {
|
|
89
|
-
params.set('mid', machineId);
|
|
86
|
+
if (input.machineId && input.machineId.length > 0) {
|
|
87
|
+
params.set('mid', input.machineId);
|
|
90
88
|
}
|
|
91
89
|
// The web page echoes this back through the callback; without it the local
|
|
92
90
|
// server cannot tell this login apart from one someone else started.
|
|
93
|
-
if (state && state.length > 0) {
|
|
94
|
-
params.set('state', state);
|
|
91
|
+
if (input.state && input.state.length > 0) {
|
|
92
|
+
params.set('state', input.state);
|
|
93
|
+
}
|
|
94
|
+
if (input.codeChallenge && input.codeChallenge.length > 0) {
|
|
95
|
+
params.set('code_challenge', input.codeChallenge);
|
|
96
|
+
params.set('flow', 'setup');
|
|
95
97
|
}
|
|
96
98
|
return `${AUTH_BASE_URL}/cli/authorize?${params.toString()}`;
|
|
97
99
|
}
|
|
@@ -183,29 +185,28 @@ async function fetchConventionTemplate(authResult, apiUrl) {
|
|
|
183
185
|
return content;
|
|
184
186
|
}
|
|
185
187
|
/**
|
|
186
|
-
*
|
|
188
|
+
* Which entry point files this run may write.
|
|
187
189
|
*
|
|
188
|
-
* The
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* credential is the liability.
|
|
190
|
+
* The order is deliberate: an explicit `--agent-files` wins over everything, a
|
|
191
|
+
* TTY still gets the multiselect (now seeded with what was detected instead of
|
|
192
|
+
* everything), and a non-TTY run gets the detection result alone. The old
|
|
193
|
+
* non-TTY branch returned the full catalog, so every automated re-run wrote all
|
|
194
|
+
* four files — and an `-example` sibling for each one that already existed.
|
|
194
195
|
*/
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
196
|
+
/**
|
|
197
|
+
* `allowPrompt` is false on the configured-project repair pass: that path exists
|
|
198
|
+
* to make a re-run of `agentteams init` actually re-apply the local adapters,
|
|
199
|
+
* and a fast path that stops to ask a question is no longer a fast path.
|
|
200
|
+
* Detection still runs there, so a missing entry point is created and an
|
|
201
|
+
* existing one is reported as untouched.
|
|
202
|
+
*/
|
|
203
|
+
async function resolveAgentFileSelection(cwd, explicitFiles, options) {
|
|
204
|
+
if (explicitFiles) {
|
|
205
|
+
return explicitFiles;
|
|
204
206
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return AGENT_ENTRY_POINT_FILES.map((f) => f.value);
|
|
207
|
+
const detected = detectAgentEntryPointFiles(cwd);
|
|
208
|
+
if (options?.allowPrompt === false || !process.stdin.isTTY) {
|
|
209
|
+
return detected;
|
|
209
210
|
}
|
|
210
211
|
const selected = await multiselect({
|
|
211
212
|
message: 'Select agent entry point files to create:',
|
|
@@ -214,7 +215,7 @@ async function promptAgentFileSelection() {
|
|
|
214
215
|
label: f.label,
|
|
215
216
|
hint: f.hint,
|
|
216
217
|
})),
|
|
217
|
-
initialValues:
|
|
218
|
+
initialValues: detected,
|
|
218
219
|
required: false,
|
|
219
220
|
});
|
|
220
221
|
if (isCancel(selected)) {
|
|
@@ -258,378 +259,657 @@ function ensureGeminiIgnore(cwd) {
|
|
|
258
259
|
writeFileSync(geminiIgnorePath, `# Allow AgentTeams convention files\n${entry}\n`, 'utf-8');
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Write the selected entry points, never over an existing file.
|
|
264
|
+
*
|
|
265
|
+
* An occupied path is reported as `skipped` rather than answered with a
|
|
266
|
+
* `<name>-example` sibling: the example write is unconditional, so a repeated
|
|
267
|
+
* init kept re-creating files nobody merged. `--agent-files-example` restores
|
|
268
|
+
* the old behavior for the case it was built for — a first-time setup on a repo
|
|
269
|
+
* that already has its own CLAUDE.md.
|
|
270
|
+
*
|
|
271
|
+
* Each file is written under its own try/catch and failures come back alongside
|
|
272
|
+
* the entries instead of as an exception. Letting one unwritable path throw out
|
|
273
|
+
* of the loop discarded the record of every file already on disk, so the caller
|
|
274
|
+
* reported `agentFiles: []` for a run that had in fact created some of them —
|
|
275
|
+
* the JSON contract and the human output both disagreeing with the filesystem.
|
|
276
|
+
*/
|
|
277
|
+
function generateAgentEntryPointFiles(cwd, selectedFiles, options) {
|
|
265
278
|
const entries = [];
|
|
279
|
+
const failures = [];
|
|
266
280
|
for (const relativePath of selectedFiles) {
|
|
267
281
|
const fullPath = join(cwd, relativePath);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
282
|
+
try {
|
|
283
|
+
if (existsSync(fullPath)) {
|
|
284
|
+
if (!options.createExample) {
|
|
285
|
+
entries.push({ relativePath, type: 'skipped' });
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const ext = relativePath.includes('.') ? `.${relativePath.split('.').pop()}` : '';
|
|
289
|
+
const base = ext ? relativePath.slice(0, -ext.length) : relativePath;
|
|
290
|
+
const exampleRelativePath = `${base}-example${ext}`;
|
|
291
|
+
const exampleFullPath = join(cwd, exampleRelativePath);
|
|
292
|
+
const exampleDir = dirname(exampleFullPath);
|
|
293
|
+
if (!existsSync(exampleDir)) {
|
|
294
|
+
mkdirSync(exampleDir, { recursive: true });
|
|
295
|
+
}
|
|
296
|
+
writeFileSync(exampleFullPath, DEFAULT_CONVENTION_REFERENCE, 'utf-8');
|
|
297
|
+
entries.push({ relativePath: exampleRelativePath, type: 'example' });
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const dir = dirname(fullPath);
|
|
301
|
+
if (!existsSync(dir)) {
|
|
302
|
+
mkdirSync(dir, { recursive: true });
|
|
303
|
+
}
|
|
273
304
|
writeFileSync(fullPath, DEFAULT_CONVENTION_REFERENCE, 'utf-8');
|
|
274
305
|
entries.push({ relativePath, type: 'created' });
|
|
275
306
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const base = ext ? relativePath.slice(0, -ext.length) : relativePath;
|
|
279
|
-
const exampleRelativePath = `${base}-example${ext}`;
|
|
280
|
-
const exampleFullPath = join(cwd, exampleRelativePath);
|
|
281
|
-
const exampleDir = dirname(exampleFullPath);
|
|
282
|
-
if (!existsSync(exampleDir)) {
|
|
283
|
-
mkdirSync(exampleDir, { recursive: true });
|
|
284
|
-
}
|
|
285
|
-
writeFileSync(exampleFullPath, DEFAULT_CONVENTION_REFERENCE, 'utf-8');
|
|
286
|
-
entries.push({ relativePath: exampleRelativePath, type: 'example' });
|
|
307
|
+
catch (error) {
|
|
308
|
+
failures.push({ relativePath, message: toErrorMessage(error) });
|
|
287
309
|
}
|
|
288
310
|
}
|
|
289
|
-
return entries;
|
|
311
|
+
return { entries, failures };
|
|
290
312
|
}
|
|
291
|
-
function
|
|
313
|
+
function toComparablePath(path) {
|
|
292
314
|
try {
|
|
293
|
-
|
|
294
|
-
return ['teamId', 'projectId'].every((field) => typeof config[field] === 'string' && config[field].length > 0);
|
|
315
|
+
return canonicalizePath(path);
|
|
295
316
|
}
|
|
296
317
|
catch {
|
|
297
|
-
return
|
|
318
|
+
return path;
|
|
298
319
|
}
|
|
299
320
|
}
|
|
300
321
|
/**
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* Windows directory symlinks require SeCreateSymbolicLinkPrivilege (Developer
|
|
304
|
-
* Mode or elevation), so an unprivileged win32 run fails with EPERM and used to
|
|
305
|
-
* fall through to a copy — silently breaking the guarantee that a worktree
|
|
306
|
-
* tracks the main checkout's conventions. Junctions need no privilege, which is
|
|
307
|
-
* why `utils/conventionLink.ts` and the daemon's worktree helper already use
|
|
308
|
-
* them; this is the same documented platform exception.
|
|
322
|
+
* Does the discovered config actually belong to the directory init was asked to
|
|
323
|
+
* initialize?
|
|
309
324
|
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
325
|
+
* `findProjectConfig` walks ancestors — outside a repository all the way to the
|
|
326
|
+
* filesystem root, which includes the global `~/.agentteams/config.json` that
|
|
327
|
+
* `mergeConfigSources` reads as a fallback. The new-project path, in contrast,
|
|
328
|
+
* always writes to `join(cwd, CONFIG_DIR, CONFIG_FILE)`. Reusing an ancestor's
|
|
329
|
+
* binding as a "configured project" would therefore make `init` in a fresh
|
|
330
|
+
* folder a silent no-op that reports someone else's project.
|
|
314
331
|
*/
|
|
315
|
-
function
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
catch (error) {
|
|
327
|
-
cpSync(sourcePath, targetPath, { recursive: true, dereference: true });
|
|
328
|
-
return {
|
|
329
|
-
materialization: 'copy',
|
|
330
|
-
warning: `Could not create the .agentteams symlink (${error instanceof Error ? error.message : String(error)}). Copied the directory instead.`,
|
|
331
|
-
};
|
|
332
|
-
}
|
|
332
|
+
function ownsDiscoveredConfig(resolvedCwd, configPath, userHomeDir) {
|
|
333
|
+
const configRoot = toComparablePath(dirname(dirname(configPath)));
|
|
334
|
+
// The global config is a fallback for commands, never a project binding.
|
|
335
|
+
if (configRoot === toComparablePath(userHomeDir))
|
|
336
|
+
return false;
|
|
337
|
+
const owners = new Set([toComparablePath(resolvedCwd)]);
|
|
338
|
+
const repositoryRoot = resolveGitTopLevel(resolvedCwd);
|
|
339
|
+
if (repositoryRoot)
|
|
340
|
+
owners.add(toComparablePath(repositoryRoot));
|
|
341
|
+
return owners.has(configRoot);
|
|
333
342
|
}
|
|
334
343
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
344
|
+
* Classify init before it opens a browser or materializes any local adapters.
|
|
345
|
+
*
|
|
346
|
+
* This resolver is intentionally read-only: the linked-worktree branch calls
|
|
347
|
+
* `bootstrapLinkedWorktree` and the configured-project branch resolves its
|
|
348
|
+
* credential only after classification, so a classification never touches a
|
|
349
|
+
* keychain, a token, or the filesystem.
|
|
350
|
+
*
|
|
351
|
+
* `userHomeDir` is injectable for the same reason as in
|
|
352
|
+
* `getConfigurationNotFoundMessage`: `os.homedir()` does not follow a test's
|
|
353
|
+
* `process.env.HOME`, so the global-config rule would be untestable otherwise.
|
|
341
354
|
*/
|
|
342
|
-
function
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return
|
|
348
|
-
}
|
|
355
|
+
export function detectInitExecutionContext(cwd, explicitAuthMode, userHomeDir = homedir()) {
|
|
356
|
+
const resolvedCwd = resolve(cwd);
|
|
357
|
+
const configPath = findProjectConfig(resolvedCwd);
|
|
358
|
+
const config = configPath ? loadProjectConfig(resolvedCwd) : null;
|
|
359
|
+
if (resolveLinkedWorktreeSource(resolvedCwd)) {
|
|
360
|
+
return { kind: 'linked-worktree', configPath, config };
|
|
361
|
+
}
|
|
362
|
+
const hasProjectBinding = typeof config?.teamId === 'string' &&
|
|
363
|
+
config.teamId.length > 0 &&
|
|
364
|
+
typeof config.projectId === 'string' &&
|
|
365
|
+
config.projectId.length > 0;
|
|
366
|
+
// An explicit --auth choice means the caller intends to reconnect instead of
|
|
367
|
+
// validating the existing binding through the configured-project fast path.
|
|
368
|
+
const kind = configPath && hasProjectBinding && ownsDiscoveredConfig(resolvedCwd, configPath, userHomeDir) && !explicitAuthMode
|
|
369
|
+
? 'configured-project'
|
|
370
|
+
: 'new-project';
|
|
371
|
+
return { kind, configPath, config };
|
|
349
372
|
}
|
|
350
373
|
function toErrorMessage(error) {
|
|
351
374
|
return error instanceof Error ? error.message : String(error);
|
|
352
375
|
}
|
|
353
|
-
function
|
|
354
|
-
|
|
355
|
-
const sourceStats = statSync(sourceFile);
|
|
356
|
-
if (!sourceStats.isFile() || sourceStats.size !== statSync(backupFile).size)
|
|
357
|
-
return false;
|
|
358
|
-
return readFileSync(sourceFile).equals(readFileSync(backupFile));
|
|
359
|
-
}
|
|
360
|
-
catch {
|
|
361
|
-
return false;
|
|
362
|
-
}
|
|
376
|
+
export async function executeInitCommand(options) {
|
|
377
|
+
return withCommandContext('init', () => executeInitCommandWithContext(options));
|
|
363
378
|
}
|
|
364
379
|
/**
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* safe to delete when it is a strict subset of the source. Anything that is not
|
|
370
|
-
* a plain matching file (a symlink, a special file, an unreadable directory)
|
|
371
|
-
* counts as copy-only: it cannot be proven redundant.
|
|
380
|
+
* Wait for the browser round trip without losing Ctrl+C.
|
|
381
|
+
*
|
|
382
|
+
* Shared by both auth paths so the cancel handling — SIGINT plus the raw-mode
|
|
383
|
+
* ^C read a spinner would otherwise swallow — exists exactly once.
|
|
372
384
|
*/
|
|
373
|
-
function
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
catch {
|
|
381
|
-
copyOnlyFiles.push(relativeDir || '.');
|
|
385
|
+
async function waitForBrowserCallback(authContext, spinner) {
|
|
386
|
+
let restored = false;
|
|
387
|
+
// Runs on every exit — success, timeout, or server error. Leaving it on the
|
|
388
|
+
// success path only meant a failed init handed the shell back in raw mode,
|
|
389
|
+
// with input no longer echoing until the user typed `reset`.
|
|
390
|
+
const restoreTerminal = () => {
|
|
391
|
+
if (restored) {
|
|
382
392
|
return;
|
|
383
393
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
copyOnlyFiles.push(relativePath);
|
|
391
|
-
}
|
|
394
|
+
restored = true;
|
|
395
|
+
process.removeListener('SIGINT', onSigint);
|
|
396
|
+
if (process.stdin.isTTY) {
|
|
397
|
+
process.stdin.setRawMode(false);
|
|
398
|
+
process.stdin.pause();
|
|
399
|
+
process.stdin.removeListener('data', onKeypress);
|
|
392
400
|
}
|
|
393
401
|
};
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
* backup next to `.agentteams` would show up in `git status` — the anchored
|
|
400
|
-
* `/.agentteams` exclude is an exact match and does not cover a sibling name —
|
|
401
|
-
* and a copied legacy `config.json` may carry the project apiKey.
|
|
402
|
-
* `git-common-dir` is part of no working tree, so nothing can surface from
|
|
403
|
-
* there. A worktree on a different volume cannot be renamed into it, and only
|
|
404
|
-
* then does the in-worktree fallback apply — after registering its own exclude
|
|
405
|
-
* pattern.
|
|
406
|
-
*/
|
|
407
|
-
function moveConventionCopyAside(worktreePath, targetPath) {
|
|
408
|
-
const failures = [];
|
|
409
|
-
const commonDir = resolveGitCommonDir(worktreePath);
|
|
410
|
-
if (commonDir) {
|
|
411
|
-
const backupPath = join(commonDir, `${RELINK_BACKUP_NAME}-${process.pid}`);
|
|
412
|
-
try {
|
|
413
|
-
renameSync(targetPath, backupPath);
|
|
414
|
-
return { backupPath };
|
|
402
|
+
function onSigint() {
|
|
403
|
+
restoreTerminal();
|
|
404
|
+
spinner?.fail('Init cancelled.');
|
|
405
|
+
if (authContext.server.listening) {
|
|
406
|
+
authContext.server.close();
|
|
415
407
|
}
|
|
416
|
-
|
|
417
|
-
|
|
408
|
+
process.exit(0);
|
|
409
|
+
}
|
|
410
|
+
function onKeypress(key) {
|
|
411
|
+
if (key[0] === 0x03) {
|
|
412
|
+
onSigint();
|
|
418
413
|
}
|
|
419
414
|
}
|
|
420
|
-
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
|
|
415
|
+
process.on('SIGINT', onSigint);
|
|
416
|
+
if (process.stdin.isTTY) {
|
|
417
|
+
process.stdin.setRawMode(true);
|
|
418
|
+
process.stdin.resume();
|
|
419
|
+
process.stdin.on('data', onKeypress);
|
|
424
420
|
}
|
|
425
421
|
try {
|
|
426
|
-
|
|
427
|
-
renameSync(targetPath, backupPath);
|
|
428
|
-
return { backupPath };
|
|
422
|
+
return await authContext.waitForCallback();
|
|
429
423
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return { error: failures.join('; ') };
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
/** Undo the move-aside. The target may hold a fresh link or a partial copy, and
|
|
436
|
-
* `renameSync` onto a non-empty directory fails with ENOTEMPTY, so clear it
|
|
437
|
-
* first and never let the restore itself escape as an exception. */
|
|
438
|
-
function restoreConventionCopy(backupPath, targetPath) {
|
|
439
|
-
try {
|
|
440
|
-
rmSync(targetPath, { recursive: true, force: true });
|
|
441
|
-
renameSync(backupPath, targetPath);
|
|
442
|
-
return true;
|
|
443
|
-
}
|
|
444
|
-
catch {
|
|
445
|
-
return false;
|
|
424
|
+
finally {
|
|
425
|
+
restoreTerminal();
|
|
446
426
|
}
|
|
447
427
|
}
|
|
448
428
|
/**
|
|
449
|
-
*
|
|
450
|
-
* predates the link fix starts tracking the main checkout again. Without this
|
|
451
|
-
* the `existing` branch treats the copy as settled and the worktree keeps
|
|
452
|
-
* serving stale conventions forever.
|
|
429
|
+
* The default path: one browser screen, no agent API key, one convention write.
|
|
453
430
|
*
|
|
454
|
-
* The
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
431
|
+
* The browser returns an authorization code plus the connection identifiers, so
|
|
432
|
+
* the second round trip that used to run here (`performPersonalTokenLogin`) is
|
|
433
|
+
* gone, and with it the `key_` that only ever existed to read the convention
|
|
434
|
+
* template once. `agentteams auth login` still owns that helper for the
|
|
435
|
+
* project-less login.
|
|
458
436
|
*/
|
|
459
|
-
function
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
code: 'link-occupied',
|
|
465
|
-
path: targetPath,
|
|
466
|
-
message: `The directory at ${targetPath} does not match the main checkout's ${CONFIG_DIR}; leaving it untouched. Remove it manually to restore the convention link.`,
|
|
467
|
-
},
|
|
468
|
-
};
|
|
437
|
+
async function runUnifiedSetup(context) {
|
|
438
|
+
const pkce = createPkcePair();
|
|
439
|
+
let server;
|
|
440
|
+
try {
|
|
441
|
+
server = await startUnifiedSetupServer({ state: createAuthState() });
|
|
469
442
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
return {
|
|
473
|
-
materialization: 'existing',
|
|
474
|
-
issue: {
|
|
475
|
-
code: 'link-create-failed',
|
|
476
|
-
path: targetPath,
|
|
477
|
-
message: `Could not move the copied ${CONFIG_DIR} aside at ${targetPath}: ${moved.error}`,
|
|
478
|
-
},
|
|
479
|
-
};
|
|
443
|
+
catch (error) {
|
|
444
|
+
throw new Error(`Failed to start local OAuth server: ${error instanceof Error ? error.message : String(error)}`);
|
|
480
445
|
}
|
|
481
|
-
const
|
|
446
|
+
const authUrl = buildAuthorizeUrl({
|
|
447
|
+
port: server.port,
|
|
448
|
+
projectName: context.projectName,
|
|
449
|
+
authPathEnc: context.authPathEnc,
|
|
450
|
+
osType: detectOsType(),
|
|
451
|
+
state: server.state,
|
|
452
|
+
machineId: readOrCreateMachineId() ?? undefined,
|
|
453
|
+
codeChallenge: pkce.challenge,
|
|
454
|
+
});
|
|
455
|
+
await tryOpenBrowser(authUrl);
|
|
456
|
+
const authSpinner = createSpinner('Waiting for authentication... (Ctrl+C to cancel)');
|
|
482
457
|
try {
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
if (
|
|
486
|
-
throw new Error(
|
|
458
|
+
const callback = await waitForBrowserCallback(server, authSpinner);
|
|
459
|
+
authSpinner?.succeed();
|
|
460
|
+
if (isUnifiedSetupMetadataMissing(callback)) {
|
|
461
|
+
throw new Error(callback.legacyAgentKeyIssued ? SETUP_LEGACY_AGENT_KEY_HINT : SETUP_METADATA_MISSING_HINT);
|
|
487
462
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
463
|
+
const setup = callback;
|
|
464
|
+
// Storing the credential comes before `saveConfig` on purpose: a login that
|
|
465
|
+
// cannot be kept throws here, so init stops rather than leaving a project
|
|
466
|
+
// configured for a credential nothing can find.
|
|
467
|
+
const client = getPersonalTokenClient(context.apiUrl);
|
|
468
|
+
const session = await client.exchangeAuthorizationCode({
|
|
469
|
+
code: setup.code,
|
|
470
|
+
codeVerifier: pkce.verifier,
|
|
471
|
+
// Must match the redirect the web registered the code against.
|
|
472
|
+
redirectUri: `http://localhost:${server.port}/callback`,
|
|
473
|
+
});
|
|
474
|
+
saveConfig(context.configPath, toConfig(setup, context.apiUrl));
|
|
475
|
+
// The identifiers are passed explicitly rather than re-read from disk: this
|
|
476
|
+
// command may run against a `cwd` that is not `process.cwd()`, and the
|
|
477
|
+
// config resolver only ever looks at the latter.
|
|
478
|
+
const runtimeConfig = await loadConfigWithCredential({
|
|
479
|
+
teamId: setup.teamId,
|
|
480
|
+
projectId: setup.projectId,
|
|
481
|
+
apiUrl: context.apiUrl,
|
|
482
|
+
authMode: 'personal-token',
|
|
483
|
+
});
|
|
484
|
+
// Falling back to a bare `conventionDownload()` here would undo the sentence
|
|
485
|
+
// above: with no config passed it re-resolves the project from
|
|
486
|
+
// `process.cwd()`, so an `init --cwd` (a worktree bootstrap included) would
|
|
487
|
+
// quietly pull conventions for whichever project the shell happens to sit
|
|
488
|
+
// in. Fail instead — this only happens if credential resolution regressed.
|
|
489
|
+
if (!runtimeConfig) {
|
|
490
|
+
throw new Error('Signed in, but the stored credential could not be read back, so the convention template was not downloaded. ' +
|
|
491
|
+
'Check `agentteams auth status`, then run `agentteams convention download` in this folder.');
|
|
492
|
+
}
|
|
493
|
+
// The convention template is now written by `conventionDownload` alone —
|
|
494
|
+
// exactly one write of `.agentteams/convention.md` per init — using the
|
|
495
|
+
// agent this setup actually created instead of "whichever config is first".
|
|
496
|
+
await conventionDownload({
|
|
497
|
+
cwd: context.cwd,
|
|
498
|
+
agentConfigId: setup.configId,
|
|
499
|
+
config: runtimeConfig,
|
|
500
|
+
});
|
|
491
501
|
return {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
502
|
+
authUrl,
|
|
503
|
+
teamId: setup.teamId,
|
|
504
|
+
projectId: setup.projectId,
|
|
505
|
+
agentName: setup.agentName,
|
|
506
|
+
seedPlanId: setup.seedPlanId ?? null,
|
|
507
|
+
personalLogin: {
|
|
508
|
+
email: session.identity.email,
|
|
509
|
+
nickname: session.identity.nickname,
|
|
510
|
+
persisted: client.state().persisted,
|
|
499
511
|
},
|
|
500
512
|
};
|
|
501
513
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
path: backupPath,
|
|
509
|
-
message: `Kept ${copyOnlyFiles.length} file(s) that exist only in the replaced ${CONFIG_DIR} copy (${copyOnlyFiles.slice(0, 3).join(', ')}${copyOnlyFiles.length > 3 ? ', …' : ''}) at ${backupPath}; move what you still need out of it and delete it.`,
|
|
510
|
-
},
|
|
511
|
-
};
|
|
514
|
+
catch (error) {
|
|
515
|
+
authSpinner?.fail();
|
|
516
|
+
if (server.server.listening) {
|
|
517
|
+
server.server.close();
|
|
518
|
+
}
|
|
519
|
+
throw error;
|
|
512
520
|
}
|
|
513
|
-
rmSync(backupPath, { recursive: true, force: true });
|
|
514
|
-
return { materialization: 'relinked' };
|
|
515
521
|
}
|
|
516
|
-
|
|
522
|
+
/**
|
|
523
|
+
* The explicit `--auth api-key` compatibility path, unchanged.
|
|
524
|
+
*
|
|
525
|
+
* It keeps the legacy authorize URL (no `flow=setup`, no `code_challenge`), the
|
|
526
|
+
* legacy callback payload with its `key_`, and the direct convention write. CI
|
|
527
|
+
* users depend on this shape, so it is isolated from the default path rather
|
|
528
|
+
* than sharing a branch with it.
|
|
529
|
+
*/
|
|
530
|
+
async function runLegacyApiKeySetup(context) {
|
|
531
|
+
let authContext;
|
|
517
532
|
try {
|
|
518
|
-
|
|
519
|
-
}
|
|
520
|
-
catch {
|
|
521
|
-
return false;
|
|
533
|
+
authContext = await startLocalAuthServer({ state: createAuthState() });
|
|
522
534
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
let worktreePath;
|
|
526
|
-
try {
|
|
527
|
-
worktreePath = canonicalizePath(resolve(cwd));
|
|
535
|
+
catch (error) {
|
|
536
|
+
throw new Error(`Failed to start local OAuth server: ${error instanceof Error ? error.message : String(error)}`);
|
|
528
537
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
return null;
|
|
540
|
-
// The convention root is the parent of the canonical .agentteams directory.
|
|
541
|
-
// Following the canonical path resolves double links as well
|
|
542
|
-
// (worktree/.agentteams → member/.agentteams → non-git-root/.agentteams),
|
|
543
|
-
// so the entry point set is read from the actual root — not the member repo.
|
|
544
|
-
let conventionRoot = null;
|
|
538
|
+
const authUrl = buildAuthorizeUrl({
|
|
539
|
+
port: authContext.port,
|
|
540
|
+
projectName: context.projectName,
|
|
541
|
+
authPathEnc: context.authPathEnc,
|
|
542
|
+
osType: detectOsType(),
|
|
543
|
+
state: authContext.state,
|
|
544
|
+
machineId: readOrCreateMachineId() ?? undefined,
|
|
545
|
+
});
|
|
546
|
+
await tryOpenBrowser(authUrl);
|
|
547
|
+
const authSpinner = createSpinner('Waiting for authentication... (Ctrl+C to cancel)');
|
|
545
548
|
try {
|
|
546
|
-
|
|
549
|
+
const authResult = await waitForBrowserCallback(authContext, authSpinner);
|
|
550
|
+
authSpinner?.succeed();
|
|
551
|
+
const runtimeConfig = {
|
|
552
|
+
teamId: authResult.teamId,
|
|
553
|
+
projectId: authResult.projectId,
|
|
554
|
+
apiKey: authResult.apiKey,
|
|
555
|
+
apiUrl: context.apiUrl,
|
|
556
|
+
};
|
|
557
|
+
const conventionContent = await withSpinner('Fetching convention template...', () => fetchConventionTemplate(authResult, context.apiUrl));
|
|
558
|
+
saveLegacyApiKeyConfig(context.configPath, toLegacyApiKeyConfig(authResult, context.apiUrl));
|
|
559
|
+
writeFileSync(context.conventionPath, conventionContent, 'utf-8');
|
|
560
|
+
await conventionDownload({ cwd: context.cwd, config: runtimeConfig });
|
|
561
|
+
return {
|
|
562
|
+
authUrl,
|
|
563
|
+
teamId: authResult.teamId,
|
|
564
|
+
projectId: authResult.projectId,
|
|
565
|
+
agentName: authResult.agentName,
|
|
566
|
+
seedPlanId: authResult.seedPlanId ?? null,
|
|
567
|
+
};
|
|
547
568
|
}
|
|
548
|
-
catch {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
? AGENT_ENTRY_POINT_FILES.map((f) => f.value).filter((relativePath) => isReadableRegularFile(join(conventionRoot, relativePath)))
|
|
553
|
-
: [];
|
|
554
|
-
// Local exclude registration comes before creating any managed path so a
|
|
555
|
-
// bootstrap never dirties the shared repository state.
|
|
556
|
-
const issues = [];
|
|
557
|
-
const excludeResult = ensureLocalExclude(worktreePath, [
|
|
558
|
-
toAnchoredExcludePattern(CONFIG_DIR),
|
|
559
|
-
...selectedEntryPoints.map(toAnchoredExcludePattern),
|
|
560
|
-
]);
|
|
561
|
-
if (excludeResult.status === 'blocked' && excludeResult.issue) {
|
|
562
|
-
issues.push(excludeResult.issue);
|
|
563
|
-
}
|
|
564
|
-
const excludeReady = excludeResult.status === 'ready';
|
|
565
|
-
const targetPath = join(worktreePath, CONFIG_DIR);
|
|
566
|
-
let materialization;
|
|
567
|
-
let warning;
|
|
568
|
-
if (!excludeReady) {
|
|
569
|
-
materialization = existsSync(targetPath) ? 'existing' : 'blocked';
|
|
570
|
-
}
|
|
571
|
-
else if (isBrokenSymbolicLink(targetPath)) {
|
|
572
|
-
unlinkSync(targetPath);
|
|
573
|
-
({ materialization, warning } = linkConventionDir(sourcePath, targetPath));
|
|
574
|
-
}
|
|
575
|
-
else if (existsSync(targetPath)) {
|
|
576
|
-
// An entry that is already a link is settled; a plain directory is either a
|
|
577
|
-
// copy left by a failed link attempt or something the user put there.
|
|
578
|
-
if (lstatSync(targetPath).isSymbolicLink()) {
|
|
579
|
-
materialization = 'existing';
|
|
580
|
-
}
|
|
581
|
-
else {
|
|
582
|
-
const promotion = promoteCopiedConventionDir(worktreePath, sourcePath, targetPath);
|
|
583
|
-
materialization = promotion.materialization;
|
|
584
|
-
if (promotion.issue) {
|
|
585
|
-
issues.push(promotion.issue);
|
|
586
|
-
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
authSpinner?.fail();
|
|
571
|
+
if (authContext.server.listening) {
|
|
572
|
+
authContext.server.close();
|
|
587
573
|
}
|
|
574
|
+
throw error;
|
|
588
575
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
576
|
+
}
|
|
577
|
+
async function runConfiguredProjectInit(cwd, executionContext, adapterOptions) {
|
|
578
|
+
const configPath = executionContext.configPath;
|
|
579
|
+
const projectConfig = executionContext.config;
|
|
580
|
+
if (!configPath || !projectConfig) {
|
|
581
|
+
throw new Error('The configured project could not be read. Run `agentteams init --auth personal-token` to relink it.');
|
|
582
|
+
}
|
|
583
|
+
// Only the identifiers are forwarded as overrides. Handing the whole config
|
|
584
|
+
// document to `loadConfigWithCredential` would put it above the environment in
|
|
585
|
+
// `mergeConfigSources` (global → project → env → options), so AGENTTEAMS_*
|
|
586
|
+
// would be ignored on this path alone, and a leftover legacy `apiKey` would be
|
|
587
|
+
// read as an *explicit* key — skipping the migration warning and the refresh
|
|
588
|
+
// slot. `apiUrl` follows the same env-first rule as `resolveApiUrl()`.
|
|
589
|
+
const runtimeConfig = await loadConfigWithCredential({
|
|
590
|
+
teamId: projectConfig.teamId,
|
|
591
|
+
projectId: projectConfig.projectId,
|
|
592
|
+
apiUrl: normalizeApiUrl(process.env.AGENTTEAMS_API_URL || projectConfig.apiUrl || DEFAULT_API_URL),
|
|
593
|
+
// `loadConfigWithCredential` normally discovers the project from
|
|
594
|
+
// `process.cwd()`, but init may repair an explicit `options.cwd`. An empty
|
|
595
|
+
// override is intentional: it prevents a legacy key from an unrelated
|
|
596
|
+
// ambient project from becoming this binding's fallback credential.
|
|
597
|
+
apiKey: process.env.AGENTTEAMS_API_KEY || projectConfig.apiKey || '',
|
|
598
|
+
...(projectConfig.authMode ? { authMode: projectConfig.authMode } : {}),
|
|
595
599
|
});
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
600
|
+
if (!runtimeConfig) {
|
|
601
|
+
throw new Error("The existing project binding has no usable credential. Run 'agentteams auth login'.");
|
|
602
|
+
}
|
|
603
|
+
// `checkConventionFreshness` reports "no changes" when the download manifest is
|
|
604
|
+
// absent, which is exactly the state of a project that has never synced. Left
|
|
605
|
+
// alone it would report READY while `.agentteams/convention.md` and
|
|
606
|
+
// `.agentteams/platform/*` — the rules every runner loads as always_on — are
|
|
607
|
+
// missing, so init treats that state as "sync required" instead.
|
|
608
|
+
const conventionDir = dirname(configPath);
|
|
609
|
+
const conventionsNeverSynced = !existsSync(join(conventionDir, CONVENTION_MANIFEST_FILE)) || !existsSync(join(conventionDir, CONVENTION_FILE));
|
|
610
|
+
let freshness;
|
|
611
|
+
let conventionError;
|
|
612
|
+
let conventionsUpdated = false;
|
|
613
|
+
try {
|
|
614
|
+
freshness = await conventionStatus({ cwd, config: runtimeConfig });
|
|
615
|
+
if (freshness.conventionUpdateAvailable || conventionsNeverSynced) {
|
|
616
|
+
await conventionDownload({ cwd, config: runtimeConfig });
|
|
617
|
+
conventionsUpdated = true;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
conventionError = toErrorMessage(error);
|
|
622
|
+
}
|
|
623
|
+
// The same adapters the new-project path runs. Without them a re-run of
|
|
624
|
+
// `agentteams init` — which is exactly what every adapter retry command tells
|
|
625
|
+
// the user to do — verified the binding and changed nothing else.
|
|
626
|
+
const adapterPass = await runLocalAdapterPass(cwd, adapterOptions);
|
|
627
|
+
const doctor = await executeDoctorCommand({ cwd, installWorktreeHook: adapterOptions.installWorktreeHook });
|
|
628
|
+
const configuredAuthMode = runtimeConfig.credentialSource === 'personal-token' ? 'personal-token' : 'api-key';
|
|
629
|
+
const readiness = buildConfiguredProjectReadiness(conventionError, doctor, adapterPass.adapters);
|
|
630
|
+
return {
|
|
599
631
|
success: true,
|
|
600
|
-
mode: '
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
632
|
+
mode: 'configured-project',
|
|
633
|
+
configPath,
|
|
634
|
+
conventionPath: join(dirname(configPath), CONVENTION_FILE),
|
|
635
|
+
teamId: runtimeConfig.teamId,
|
|
636
|
+
projectId: runtimeConfig.projectId,
|
|
637
|
+
authMode: configuredAuthMode,
|
|
638
|
+
credentialSource: runtimeConfig.credentialSource,
|
|
639
|
+
conventionsUpdated,
|
|
640
|
+
...(freshness ? { conventionStatus: freshness } : {}),
|
|
641
|
+
...(conventionError ? { conventionError } : {}),
|
|
642
|
+
doctor,
|
|
643
|
+
readiness,
|
|
644
|
+
agentFiles: adapterPass.agentFiles,
|
|
645
|
+
localAdapters: adapterPass.adapters,
|
|
646
|
+
...(adapterPass.postCheckoutHook ? { postCheckoutHook: adapterPass.postCheckoutHook } : {}),
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Roll the doctor verdict *and* the local adapter outcomes into one
|
|
651
|
+
* `local-adapters` step.
|
|
652
|
+
*
|
|
653
|
+
* Both sources matter: the doctor owns the layout-level diagnosis (member repo
|
|
654
|
+
* links, config permissions) while the adapters own the files this command
|
|
655
|
+
* writes, and a step that reported only one of them would call the stage READY
|
|
656
|
+
* while the other was degraded.
|
|
657
|
+
*/
|
|
658
|
+
function buildConfiguredProjectReadiness(conventionError, doctor, adapters) {
|
|
659
|
+
const conventionStep = conventionError
|
|
660
|
+
? {
|
|
661
|
+
stage: 'convention-sync',
|
|
662
|
+
status: 'DEGRADED',
|
|
663
|
+
issues: [{ code: 'convention-sync-failed', message: conventionError }],
|
|
664
|
+
retryCommand: 'agentteams convention download',
|
|
665
|
+
}
|
|
666
|
+
: { stage: 'convention-sync', status: 'READY', issues: [] };
|
|
667
|
+
const degradedAdapters = adapters.filter((adapter) => adapter.status === 'DEGRADED');
|
|
668
|
+
const doctorDegraded = doctor.status === 'DEGRADED';
|
|
669
|
+
// A NOT_APPLICABLE doctor still contributes its reason: `printReadiness` shows
|
|
670
|
+
// issues at every status, so the note survives even when an adapter succeeded
|
|
671
|
+
// and the step as a whole is READY.
|
|
672
|
+
const issues = [
|
|
673
|
+
...(doctor.status === 'READY' ? [] : doctor.issues.map(({ code, message }) => ({ code, message }))),
|
|
674
|
+
...adapters.filter((adapter) => adapter.status !== 'READY').flatMap((adapter) => adapter.issues),
|
|
675
|
+
];
|
|
676
|
+
const status = doctorDegraded || degradedAdapters.length > 0
|
|
677
|
+
? 'DEGRADED'
|
|
678
|
+
: adapters.some((adapter) => adapter.status === 'READY')
|
|
679
|
+
? 'READY'
|
|
680
|
+
: 'SKIPPED';
|
|
681
|
+
const localAdaptersStep = {
|
|
682
|
+
stage: 'local-adapters',
|
|
683
|
+
status,
|
|
684
|
+
issues: status === 'DEGRADED' && issues.length === 0
|
|
685
|
+
? [{ code: 'doctor-degraded', message: 'Local adapters still need attention.' }]
|
|
686
|
+
: issues,
|
|
687
|
+
...(status === 'DEGRADED' ? { retryCommand: degradedAdapters[0]?.retryCommand ?? 'agentteams doctor' } : {}),
|
|
607
688
|
};
|
|
608
|
-
|
|
609
|
-
|
|
689
|
+
return [
|
|
690
|
+
{ stage: 'project-binding', status: 'READY', issues: [] },
|
|
691
|
+
{ stage: 'credential', status: 'READY', issues: [] },
|
|
692
|
+
conventionStep,
|
|
693
|
+
localAdaptersStep,
|
|
694
|
+
];
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Roll the per-adapter outcomes into the single `local-adapters` readiness step.
|
|
698
|
+
*
|
|
699
|
+
* A degraded adapter dominates, and `SKIPPED` requires *every* adapter to have
|
|
700
|
+
* skipped — which the always-on `.gitignore` adapter makes unreachable today.
|
|
701
|
+
* The rule is kept honest for the day that adapter becomes conditional, but it
|
|
702
|
+
* is deliberately not what makes a partial skip visible: one green
|
|
703
|
+
* `[READY] local-adapters` line above a hook that was never installed is exactly
|
|
704
|
+
* the mismatch this list exists to prevent, so `printReadiness` prints the
|
|
705
|
+
* issues of every step regardless of its status. Every non-READY adapter
|
|
706
|
+
* contributes its issues, so the reason travels with the line.
|
|
707
|
+
*/
|
|
708
|
+
function buildNewProjectReadiness(adapters) {
|
|
709
|
+
const degraded = adapters.filter((adapter) => adapter.status === 'DEGRADED');
|
|
710
|
+
const status = degraded.length > 0 ? 'DEGRADED' : adapters.some((adapter) => adapter.status === 'READY') ? 'READY' : 'SKIPPED';
|
|
711
|
+
const localAdaptersStep = {
|
|
712
|
+
stage: 'local-adapters',
|
|
713
|
+
status,
|
|
714
|
+
issues: adapters.filter((adapter) => adapter.status !== 'READY').flatMap((adapter) => adapter.issues),
|
|
715
|
+
...(degraded.length > 0 ? { retryCommand: degraded[0].retryCommand ?? 'agentteams doctor' } : {}),
|
|
716
|
+
};
|
|
717
|
+
return [
|
|
718
|
+
{ stage: 'project-binding', status: 'READY', issues: [] },
|
|
719
|
+
{ stage: 'credential', status: 'READY', issues: [] },
|
|
720
|
+
{ stage: 'convention-sync', status: 'READY', issues: [] },
|
|
721
|
+
localAdaptersStep,
|
|
722
|
+
];
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Run one local adapter without letting it fail the init that already
|
|
726
|
+
* succeeded.
|
|
727
|
+
*
|
|
728
|
+
* By the time these run, the credential is in the OS store and the config is on
|
|
729
|
+
* disk. An exception escaping from here used to surface as
|
|
730
|
+
* `Initialization failed: EACCES ...` for a project that was, in fact, fully
|
|
731
|
+
* connected — so every adapter is isolated and reports `DEGRADED` with its own
|
|
732
|
+
* retry command instead. Failures are never swallowed: an adapter that throws
|
|
733
|
+
* always produces an issue.
|
|
734
|
+
*/
|
|
735
|
+
async function runLocalAdapter(adapter, retryCommand, run) {
|
|
736
|
+
try {
|
|
737
|
+
const outcome = await run();
|
|
738
|
+
return {
|
|
739
|
+
adapter,
|
|
740
|
+
status: outcome.status,
|
|
741
|
+
issues: outcome.issues,
|
|
742
|
+
...(outcome.status === 'DEGRADED' ? { retryCommand } : {}),
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
return {
|
|
747
|
+
adapter,
|
|
748
|
+
status: 'DEGRADED',
|
|
749
|
+
issues: [{ code: `${adapter}-failed`, message: toErrorMessage(error) }],
|
|
750
|
+
retryCommand,
|
|
751
|
+
};
|
|
610
752
|
}
|
|
611
|
-
return result;
|
|
612
753
|
}
|
|
613
|
-
|
|
614
|
-
|
|
754
|
+
/**
|
|
755
|
+
* The four local adapters, run identically by both init paths.
|
|
756
|
+
*
|
|
757
|
+
* This is shared rather than duplicated because the adapters advertise retry
|
|
758
|
+
* commands (`agentteams init`, `agentteams init --agent-files <list>`) and those
|
|
759
|
+
* are all `agentteams init`. A project that has just been configured takes the
|
|
760
|
+
* configured-project fast path on the *next* run, so while this pass lived only
|
|
761
|
+
* in the new-project branch every retry command it printed was a no-op: the
|
|
762
|
+
* user was told how to fix a degraded `.gitignore` — the single thing keeping a
|
|
763
|
+
* legacy `--auth api-key` config's agent key out of the repository — by running
|
|
764
|
+
* a command that would not touch `.gitignore` at all.
|
|
765
|
+
*/
|
|
766
|
+
async function runLocalAdapterPass(cwd, options) {
|
|
767
|
+
const adapters = [];
|
|
768
|
+
adapters.push(
|
|
769
|
+
// `.gitignore` is not conditional and never will be: it is what keeps
|
|
770
|
+
// `.agentteams` — including a legacy config carrying an apiKey — out of the
|
|
771
|
+
// repository.
|
|
772
|
+
await runLocalAdapter('gitignore', 'agentteams init', () => {
|
|
773
|
+
ensureGitignore(cwd);
|
|
774
|
+
return { status: 'READY', issues: [] };
|
|
775
|
+
}));
|
|
776
|
+
let selectedFiles = [];
|
|
777
|
+
let agentFiles = [];
|
|
778
|
+
adapters.push(await runLocalAdapter('agent-entry-points', 'agentteams init --agent-files <list>', async () => {
|
|
779
|
+
selectedFiles = await resolveAgentFileSelection(cwd, options.explicitAgentFiles, {
|
|
780
|
+
allowPrompt: options.allowPrompt,
|
|
781
|
+
});
|
|
782
|
+
if (selectedFiles.length === 0) {
|
|
783
|
+
return {
|
|
784
|
+
status: 'SKIPPED',
|
|
785
|
+
issues: [
|
|
786
|
+
{
|
|
787
|
+
code: 'agent-entry-points-not-selected',
|
|
788
|
+
message: 'No agent entry point file was created. Pass --agent-files (CLAUDE.md, AGENTS.md, GEMINI.md, .cursor/rules/agentteams.mdc) to create them explicitly.',
|
|
789
|
+
},
|
|
790
|
+
],
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
// Assigned before the failure check so a partially failed write still
|
|
794
|
+
// reports the files that actually reached disk.
|
|
795
|
+
const written = generateAgentEntryPointFiles(cwd, selectedFiles, {
|
|
796
|
+
createExample: options.agentFilesExample,
|
|
797
|
+
});
|
|
798
|
+
agentFiles = written.entries;
|
|
799
|
+
if (written.failures.length > 0) {
|
|
800
|
+
return {
|
|
801
|
+
status: 'DEGRADED',
|
|
802
|
+
issues: written.failures.map((failure) => ({
|
|
803
|
+
code: 'agent-entry-point-write-failed',
|
|
804
|
+
message: `${failure.relativePath} could not be written: ${failure.message}`,
|
|
805
|
+
})),
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
const skipped = agentFiles.filter((file) => file.type === 'skipped');
|
|
809
|
+
if (skipped.length === agentFiles.length) {
|
|
810
|
+
return {
|
|
811
|
+
status: 'SKIPPED',
|
|
812
|
+
issues: [
|
|
813
|
+
{
|
|
814
|
+
code: 'agent-entry-points-exist',
|
|
815
|
+
message: `Left ${skipped.map((file) => file.relativePath).join(', ')} untouched because the file already exists.`,
|
|
816
|
+
},
|
|
817
|
+
],
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
return { status: 'READY', issues: [] };
|
|
821
|
+
}));
|
|
822
|
+
adapters.push(await runLocalAdapter('gemini-ignore', 'agentteams init --agent-files GEMINI.md', () => {
|
|
823
|
+
if (!selectedFiles.includes('GEMINI.md')) {
|
|
824
|
+
return {
|
|
825
|
+
status: 'SKIPPED',
|
|
826
|
+
issues: [{ code: 'gemini-ignore-not-selected', message: 'GEMINI.md was not selected.' }],
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
ensureGeminiIgnore(cwd);
|
|
830
|
+
return { status: 'READY', issues: [] };
|
|
831
|
+
}));
|
|
832
|
+
// A git-root project shares one hooks directory with all its worktrees, so the
|
|
833
|
+
// managed post-checkout hook lets future `git worktree add` runs bootstrap
|
|
834
|
+
// conventions automatically (via `agentteams init`). It is no longer installed
|
|
835
|
+
// unconditionally: a repository that never uses linked worktrees got its
|
|
836
|
+
// shared `.git/hooks` written for a hook it would never fire. The gate itself
|
|
837
|
+
// lives in `shouldInstallWorktreeHook` so `agentteams doctor` — which this
|
|
838
|
+
// command's own fast path invokes — applies the same rule.
|
|
839
|
+
let postCheckoutHook;
|
|
840
|
+
adapters.push(await runLocalAdapter('post-checkout-hook', 'agentteams doctor', () => {
|
|
841
|
+
if (resolveGitTopLevel(cwd) === null) {
|
|
842
|
+
// Non-git roots (a parent folder grouping member repos) have no
|
|
843
|
+
// git-common dir; doctor owns their per-member hooks instead.
|
|
844
|
+
return {
|
|
845
|
+
status: 'SKIPPED',
|
|
846
|
+
issues: [
|
|
847
|
+
{
|
|
848
|
+
code: 'post-checkout-hook-not-a-git-root',
|
|
849
|
+
message: "Not a git repository root, so no worktree bootstrap hook was installed. Run 'agentteams doctor' to set up member repository hooks.",
|
|
850
|
+
},
|
|
851
|
+
],
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
if (!shouldInstallWorktreeHook(cwd, { force: options.installWorktreeHook })) {
|
|
855
|
+
return {
|
|
856
|
+
status: 'SKIPPED',
|
|
857
|
+
issues: [
|
|
858
|
+
{
|
|
859
|
+
code: 'post-checkout-hook-no-worktrees',
|
|
860
|
+
message: "This repository has no linked git worktrees, so the worktree bootstrap hook was not installed. Run 'agentteams init --install-worktree-hook' (or 'agentteams doctor --install-worktree-hook') to install it anyway.",
|
|
861
|
+
},
|
|
862
|
+
],
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
postCheckoutHook = ensurePostCheckoutHook(cwd);
|
|
866
|
+
if (postCheckoutHook.status === 'blocked') {
|
|
867
|
+
// The existing issue code and message, verbatim — the user hook
|
|
868
|
+
// protection in `ensurePostCheckoutHook` is what produced them.
|
|
869
|
+
return {
|
|
870
|
+
status: 'DEGRADED',
|
|
871
|
+
issues: [
|
|
872
|
+
{
|
|
873
|
+
code: postCheckoutHook.issue?.code ?? 'post-checkout-hook-blocked',
|
|
874
|
+
message: postCheckoutHook.issue?.message ?? 'The worktree bootstrap hook could not be installed.',
|
|
875
|
+
},
|
|
876
|
+
],
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
return { status: 'READY', issues: [] };
|
|
880
|
+
}));
|
|
881
|
+
return { adapters, agentFiles, ...(postCheckoutHook ? { postCheckoutHook } : {}) };
|
|
615
882
|
}
|
|
616
883
|
async function executeInitCommandWithContext(options) {
|
|
617
884
|
const cwd = resolve(options?.cwd ?? process.cwd());
|
|
618
|
-
// Only an explicit compatibility request
|
|
885
|
+
// Only an explicit compatibility request goes through the agent-key round trip.
|
|
619
886
|
const authMode = options?.authMode === 'api-key' ? 'api-key' : 'personal-token';
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
887
|
+
// Parsed before anything opens a browser: a typo in --agent-files must not be
|
|
888
|
+
// discovered after the user has already signed in.
|
|
889
|
+
const explicitAgentFiles = parseAgentFilesOption(options?.agentFiles);
|
|
890
|
+
const adapterOptions = {
|
|
891
|
+
explicitAgentFiles,
|
|
892
|
+
agentFilesExample: options?.agentFilesExample === true,
|
|
893
|
+
installWorktreeHook: options?.installWorktreeHook === true,
|
|
894
|
+
// Overridden per path below: only a first-time setup may prompt.
|
|
895
|
+
allowPrompt: true,
|
|
896
|
+
};
|
|
897
|
+
const executionContext = detectInitExecutionContext(cwd, options?.authMode);
|
|
898
|
+
if (executionContext.kind === 'linked-worktree') {
|
|
899
|
+
const worktreeResult = bootstrapLinkedWorktree(cwd);
|
|
900
|
+
if (worktreeResult)
|
|
901
|
+
return worktreeResult;
|
|
629
902
|
}
|
|
630
|
-
|
|
631
|
-
|
|
903
|
+
if (executionContext.kind === 'configured-project') {
|
|
904
|
+
try {
|
|
905
|
+
return await runConfiguredProjectInit(cwd, executionContext, { ...adapterOptions, allowPrompt: false });
|
|
906
|
+
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
throw new Error(`Initialization failed: ${toErrorMessage(error)}`);
|
|
909
|
+
}
|
|
632
910
|
}
|
|
911
|
+
const configPath = join(cwd, CONFIG_DIR, CONFIG_FILE);
|
|
912
|
+
const conventionPath = join(cwd, CONFIG_DIR, CONVENTION_FILE);
|
|
633
913
|
let authPathEnc;
|
|
634
914
|
try {
|
|
635
915
|
authPathEnc = encryptAuthPath(cwd);
|
|
@@ -637,125 +917,42 @@ async function executeInitCommandWithContext(options) {
|
|
|
637
917
|
catch {
|
|
638
918
|
authPathEnc = undefined;
|
|
639
919
|
}
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
920
|
+
const context = {
|
|
921
|
+
cwd,
|
|
922
|
+
projectName: basename(cwd),
|
|
923
|
+
apiUrl: resolveApiUrl(),
|
|
924
|
+
configPath,
|
|
925
|
+
conventionPath,
|
|
926
|
+
authPathEnc,
|
|
647
927
|
};
|
|
928
|
+
let setup;
|
|
648
929
|
try {
|
|
649
|
-
|
|
650
|
-
const onSigint = () => {
|
|
651
|
-
authSpinner?.fail('Init cancelled.');
|
|
652
|
-
cleanup();
|
|
653
|
-
process.exit(0);
|
|
654
|
-
};
|
|
655
|
-
process.on('SIGINT', onSigint);
|
|
656
|
-
if (process.stdin.isTTY) {
|
|
657
|
-
process.stdin.setRawMode(true);
|
|
658
|
-
process.stdin.resume();
|
|
659
|
-
process.stdin.on('data', (key) => {
|
|
660
|
-
if (key[0] === 0x03) {
|
|
661
|
-
process.stdin.setRawMode(false);
|
|
662
|
-
process.stdin.pause();
|
|
663
|
-
onSigint();
|
|
664
|
-
}
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
authContext
|
|
668
|
-
.waitForCallback()
|
|
669
|
-
.then((result) => {
|
|
670
|
-
process.removeListener('SIGINT', onSigint);
|
|
671
|
-
if (process.stdin.isTTY) {
|
|
672
|
-
process.stdin.setRawMode(false);
|
|
673
|
-
process.stdin.pause();
|
|
674
|
-
process.stdin.removeAllListeners('data');
|
|
675
|
-
}
|
|
676
|
-
resolve(result);
|
|
677
|
-
})
|
|
678
|
-
.catch(reject);
|
|
679
|
-
});
|
|
680
|
-
authSpinner?.succeed();
|
|
681
|
-
const apiUrl = resolveApiUrl();
|
|
682
|
-
const runtimeConfig = {
|
|
683
|
-
teamId: authResult.teamId,
|
|
684
|
-
projectId: authResult.projectId,
|
|
685
|
-
apiKey: authResult.apiKey,
|
|
686
|
-
apiUrl,
|
|
687
|
-
};
|
|
688
|
-
const conventionContent = await withSpinner('Fetching convention template...', () => fetchConventionTemplate(authResult, apiUrl));
|
|
689
|
-
// The project is known now, so the personal login can run before anything is
|
|
690
|
-
// written: a failed login must not leave a config with no usable credential.
|
|
691
|
-
// A login that cannot be stored throws here, which is the point — init stops
|
|
692
|
-
// before `saveConfig` rather than leaving a half-configured project behind.
|
|
693
|
-
let personalLogin;
|
|
694
|
-
const warnings = [];
|
|
695
|
-
if (authMode === 'personal-token') {
|
|
696
|
-
const outcome = await performPersonalTokenLogin({ apiUrl, projectName });
|
|
697
|
-
personalLogin = {
|
|
698
|
-
email: outcome.identity.email,
|
|
699
|
-
nickname: outcome.identity.nickname,
|
|
700
|
-
persisted: outcome.persisted,
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
if (authMode === 'api-key') {
|
|
704
|
-
saveLegacyApiKeyConfig(configPath, toLegacyApiKeyConfig(authResult, apiUrl));
|
|
705
|
-
}
|
|
706
|
-
else {
|
|
707
|
-
saveConfig(configPath, toConfig(authResult, apiUrl));
|
|
708
|
-
}
|
|
709
|
-
writeFileSync(conventionPath, conventionContent, 'utf-8');
|
|
710
|
-
await conventionDownload({ cwd, config: runtimeConfig });
|
|
711
|
-
// Last use of the agent key is the convention download above.
|
|
712
|
-
let agentKeyRevoked;
|
|
713
|
-
if (authMode === 'personal-token') {
|
|
714
|
-
agentKeyRevoked = await revokeInitAgentKey(authResult, apiUrl);
|
|
715
|
-
if (!agentKeyRevoked) {
|
|
716
|
-
warnings.push(`The agent key created for "${authResult.agentName}" could not be revoked and is still valid. ` +
|
|
717
|
-
'Revoke it in the AgentTeams web app (project settings → agents) — it was never written to this repository.');
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
ensureGitignore(cwd);
|
|
721
|
-
const selectedFiles = await promptAgentFileSelection();
|
|
722
|
-
if (selectedFiles.includes('GEMINI.md')) {
|
|
723
|
-
ensureGeminiIgnore(cwd);
|
|
724
|
-
}
|
|
725
|
-
const agentFiles = generateAgentEntryPointFiles(cwd, selectedFiles);
|
|
726
|
-
const seedPlanId = authResult.seedPlanId ?? null;
|
|
727
|
-
const seedPlanWebUrl = seedPlanId ? `${AUTH_BASE_URL.replace(/\/+$/, '')}/go?type=plan&id=${seedPlanId}` : null;
|
|
728
|
-
// A git-root project shares one hooks directory with all its worktrees, so
|
|
729
|
-
// installing the managed post-checkout hook here lets future `git worktree
|
|
730
|
-
// add` runs bootstrap conventions automatically (via `agentteams init`).
|
|
731
|
-
// Non-git roots (a parent folder grouping member repos) have no git-common
|
|
732
|
-
// dir; `agentteams doctor` owns their per-member hooks instead, so skip.
|
|
733
|
-
let postCheckoutHook;
|
|
734
|
-
if (resolveGitTopLevel(cwd) !== null) {
|
|
735
|
-
postCheckoutHook = ensurePostCheckoutHook(cwd);
|
|
736
|
-
}
|
|
737
|
-
return {
|
|
738
|
-
success: true,
|
|
739
|
-
authUrl,
|
|
740
|
-
configPath,
|
|
741
|
-
conventionPath,
|
|
742
|
-
teamId: authResult.teamId,
|
|
743
|
-
projectId: authResult.projectId,
|
|
744
|
-
agentName: authResult.agentName,
|
|
745
|
-
agentFiles,
|
|
746
|
-
seedPlanId,
|
|
747
|
-
seedPlanWebUrl,
|
|
748
|
-
postCheckoutHook,
|
|
749
|
-
authMode,
|
|
750
|
-
...(personalLogin ? { personalLogin } : {}),
|
|
751
|
-
...(agentKeyRevoked === undefined ? {} : { agentKeyRevoked }),
|
|
752
|
-
...(warnings.length > 0 ? { warning: warnings.join(' ') } : {}),
|
|
753
|
-
};
|
|
930
|
+
setup = authMode === 'api-key' ? await runLegacyApiKeySetup(context) : await runUnifiedSetup(context);
|
|
754
931
|
}
|
|
755
932
|
catch (error) {
|
|
756
|
-
authSpinner?.fail();
|
|
757
|
-
cleanup();
|
|
758
933
|
throw new Error(`Initialization failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
759
934
|
}
|
|
935
|
+
// Everything below runs *after* the credential and the config are stored, so
|
|
936
|
+
// none of it may fail the init. Each adapter reports itself.
|
|
937
|
+
const { adapters: localAdapters, agentFiles, postCheckoutHook } = await runLocalAdapterPass(cwd, adapterOptions);
|
|
938
|
+
const seedPlanId = setup.seedPlanId;
|
|
939
|
+
const seedPlanWebUrl = seedPlanId ? `${AUTH_BASE_URL.replace(/\/+$/, '')}/go?type=plan&id=${seedPlanId}` : null;
|
|
940
|
+
return {
|
|
941
|
+
success: true,
|
|
942
|
+
authUrl: setup.authUrl,
|
|
943
|
+
configPath,
|
|
944
|
+
conventionPath,
|
|
945
|
+
teamId: setup.teamId,
|
|
946
|
+
projectId: setup.projectId,
|
|
947
|
+
agentName: setup.agentName,
|
|
948
|
+
agentFiles,
|
|
949
|
+
seedPlanId,
|
|
950
|
+
seedPlanWebUrl,
|
|
951
|
+
postCheckoutHook,
|
|
952
|
+
authMode,
|
|
953
|
+
readiness: buildNewProjectReadiness(localAdapters),
|
|
954
|
+
localAdapters,
|
|
955
|
+
...(setup.personalLogin ? { personalLogin: setup.personalLogin } : {}),
|
|
956
|
+
};
|
|
760
957
|
}
|
|
761
958
|
//# sourceMappingURL=init.js.map
|