copillm 0.2.8 → 0.3.0-beta.1
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/README.md +70 -2
- package/dist/agentconfig/load.js +9 -1
- package/dist/agentconfig/render.js +8 -5
- package/dist/agentconfig/schema.js +6 -0
- package/dist/auth/accountManager.js +118 -0
- package/dist/auth/accounts.js +161 -0
- package/dist/auth/copilotToken.js +92 -23
- package/dist/auth/credentials.js +216 -40
- package/dist/auth/deviceFlow.js +110 -23
- package/dist/auth/githubIdentity.js +14 -10
- package/dist/cli/agentEnv.js +15 -9
- package/dist/cli/auth/runAuth.js +206 -9
- package/dist/cli/commands/agents/claude.js +22 -2
- package/dist/cli/commands/agents/codex.js +22 -2
- package/dist/cli/commands/agents/copilot.js +25 -4
- package/dist/cli/commands/agents/pi.js +22 -2
- package/dist/cli/commands/agents/shared.js +57 -0
- package/dist/cli/commands/auth.js +58 -7
- package/dist/cli/commands/daemon.js +79 -17
- package/dist/cli/commands/models.js +0 -5
- package/dist/cli/copillmFlags.js +8 -0
- package/dist/cli/daemon/lifecycle.js +26 -0
- package/dist/cli/daemon/probes.js +99 -33
- package/dist/cli/daemon/runDaemon.js +21 -2
- package/dist/cli/index.js +12 -0
- package/dist/cli/integrations/claudeExport.js +6 -4
- package/dist/cli/integrations/refreshCodex.js +5 -2
- package/dist/cli/integrations/refreshPi.js +5 -2
- package/dist/cli/packageInfo.js +1 -1
- package/dist/cli/shared/devMode.js +98 -0
- package/dist/config/accountId.js +44 -0
- package/dist/config/config.js +13 -2
- package/dist/config/home.js +69 -0
- package/dist/integrations/claude/cache.js +5 -2
- package/dist/integrations/claude/settingsConflict.js +5 -2
- package/dist/integrations/codex/init.js +31 -10
- package/dist/integrations/pi/init.js +8 -17
- package/dist/models/anthropicDefaults.js +13 -4
- package/dist/models/discovery.js +141 -15
- package/dist/server/accountResolver.js +85 -0
- package/dist/server/debugInfo.js +69 -24
- package/dist/server/errors.js +18 -0
- package/dist/server/proxy.js +40 -8
- package/dist/server/routes/debug.js +11 -1
- package/dist/server/routes/models.js +12 -6
- package/dist/server/routes/proxyForward.js +3 -3
- package/dist/server/routes/shared.js +66 -21
- package/dist/server/upstream/copilotClient.js +1 -30
- package/dist/server/upstream/retryPolicy.js +99 -0
- package/package.json +4 -1
package/dist/cli/auth/runAuth.js
CHANGED
|
@@ -1,26 +1,93 @@
|
|
|
1
|
-
import { clearStoredCredential, saveStoredCredential } from "../../auth/credentials.js";
|
|
1
|
+
import { clearStoredCredential, loadStoredCredential, loadStoredCredentialForAccount, registerExistingCredentialAsDefault, saveStoredCredential } from "../../auth/credentials.js";
|
|
2
|
+
import { readAccountsIndex, assertValidAccountId, InvalidAccountIdError, UnknownAccountError } from "../../auth/accounts.js";
|
|
3
|
+
import { addAccount, listAccountsDetailed, removeAccountAndCredential, removeAllAccounts, switchDefaultAccount } from "../../auth/accountManager.js";
|
|
2
4
|
import { loginViaDeviceFlow } from "../../auth/deviceFlow.js";
|
|
5
|
+
import { inspectGithubIdentity } from "../../auth/githubIdentity.js";
|
|
3
6
|
import { loadConfig } from "../../config/config.js";
|
|
4
7
|
import { inspectLock, releaseLock } from "../../server/lock.js";
|
|
5
8
|
import { stopByPid } from "../daemon/lifecycle.js";
|
|
6
|
-
import { describeBackend } from "../shared/backends.js";
|
|
9
|
+
import { describeBackend, formatHumanAuthStatusLine } from "../shared/backends.js";
|
|
7
10
|
import { writeCommandOutput } from "../shared/output.js";
|
|
11
|
+
/**
|
|
12
|
+
* Derive a friendly, path-safe account id from the GitHub login behind a token.
|
|
13
|
+
* Returns null when the lookup fails or the login isn't a valid id.
|
|
14
|
+
*/
|
|
15
|
+
async function deriveAccountId(token) {
|
|
16
|
+
let identity;
|
|
17
|
+
try {
|
|
18
|
+
identity = await inspectGithubIdentity({ token });
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const login = identity?.login;
|
|
24
|
+
if (!login) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
assertValidAccountId(login);
|
|
29
|
+
return login;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
8
35
|
export async function runAuthLogin(opts, options) {
|
|
9
36
|
if (options.forceSession) {
|
|
10
37
|
process.env.COPILLM_FORCE_SESSION_BACKEND = "1";
|
|
11
38
|
}
|
|
12
39
|
const config = loadConfig();
|
|
40
|
+
const accountType = opts.accountType ?? config.accountType;
|
|
41
|
+
const namedRequested = typeof opts.as === "string" && opts.as.trim().length > 0;
|
|
42
|
+
if (namedRequested) {
|
|
43
|
+
try {
|
|
44
|
+
assertValidAccountId(opts.as.trim());
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const message = error instanceof InvalidAccountIdError ? error.message : "invalid account id";
|
|
48
|
+
writeCommandOutput(opts, `Login failed: ${message}`, { status: "error", action: "login", error: message });
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
13
53
|
const token = await loginViaDeviceFlow();
|
|
14
54
|
const saveMode = options.forceSession ? "session" : "auto";
|
|
15
|
-
const
|
|
16
|
-
|
|
55
|
+
const index = readAccountsIndex();
|
|
56
|
+
// Pure single-account login: no index and no explicit name. Preserve the
|
|
57
|
+
// historical behaviour exactly — legacy storage, no accounts index created.
|
|
58
|
+
if (!index && !namedRequested) {
|
|
59
|
+
const backend = await saveStoredCredential(token, accountType, { mode: saveMode });
|
|
60
|
+
writeCommandOutput(opts, `Login succeeded. Credentials stored via ${describeBackend(backend)}.`, {
|
|
61
|
+
status: "ok",
|
|
62
|
+
action: "login",
|
|
63
|
+
credential_backend: backend
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const accountId = namedRequested ? opts.as.trim() : index.defaultAccount;
|
|
68
|
+
// First time we materialize the index via an explicit name: preserve any
|
|
69
|
+
// pre-existing single account as the default so its token isn't clobbered.
|
|
70
|
+
if (!index && namedRequested) {
|
|
71
|
+
const existing = await loadStoredCredential();
|
|
72
|
+
if (existing) {
|
|
73
|
+
const existingId = (await deriveAccountId(existing.token)) ?? "default";
|
|
74
|
+
if (existingId !== accountId) {
|
|
75
|
+
registerExistingCredentialAsDefault(existingId, existing.accountType);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const result = await addAccount({ id: accountId, accountType, token, mode: saveMode });
|
|
80
|
+
const defaultSuffix = result.isDefault ? " (default)" : "";
|
|
81
|
+
writeCommandOutput(opts, `Login succeeded for account "${result.id}"${defaultSuffix}. Credentials stored via ${describeBackend(result.backend)}.`, {
|
|
17
82
|
status: "ok",
|
|
18
83
|
action: "login",
|
|
19
|
-
|
|
84
|
+
account: result.id,
|
|
85
|
+
account_type: result.accountType,
|
|
86
|
+
is_default: result.isDefault,
|
|
87
|
+
credential_backend: result.backend
|
|
20
88
|
});
|
|
21
89
|
}
|
|
22
|
-
|
|
23
|
-
const result = await clearStoredCredential();
|
|
90
|
+
async function stopRunningDaemon() {
|
|
24
91
|
const lockState = inspectLock();
|
|
25
92
|
if (lockState.state === "running") {
|
|
26
93
|
await stopByPid(lockState.lock.pid);
|
|
@@ -28,11 +95,141 @@ export async function runAuthLogout(opts) {
|
|
|
28
95
|
else if (lockState.state === "stale") {
|
|
29
96
|
releaseLock();
|
|
30
97
|
}
|
|
98
|
+
}
|
|
99
|
+
export async function runAuthLogout(opts) {
|
|
100
|
+
// Stopping the daemon is always part of logout — its in-memory bearers are
|
|
101
|
+
// derived from the credentials we're clearing.
|
|
102
|
+
if (opts.all) {
|
|
103
|
+
const result = await removeAllAccounts();
|
|
104
|
+
await stopRunningDaemon();
|
|
105
|
+
writeCommandOutput(opts, `Logged out of all accounts (${result.clearedCount} credential(s) cleared).`, {
|
|
106
|
+
status: "ok",
|
|
107
|
+
action: "logout",
|
|
108
|
+
scope: "all",
|
|
109
|
+
cleared_count: result.clearedCount,
|
|
110
|
+
removed_accounts: result.removedAccountIds
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const index = readAccountsIndex();
|
|
115
|
+
// Single-account install (no index) and no explicit target: preserve the
|
|
116
|
+
// historical single-account logout behaviour.
|
|
117
|
+
if (!index && !opts.account) {
|
|
118
|
+
const result = await clearStoredCredential();
|
|
119
|
+
await stopRunningDaemon();
|
|
120
|
+
const credentialStatus = result.removed ? "removed" : "not present";
|
|
121
|
+
writeCommandOutput(opts, `Logged out. Credentials ${credentialStatus} from ${describeBackend(result.backend)}.`, {
|
|
122
|
+
status: "ok",
|
|
123
|
+
action: "logout",
|
|
124
|
+
credential_backend: result.backend,
|
|
125
|
+
credential_removed: result.removed
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (opts.account) {
|
|
130
|
+
try {
|
|
131
|
+
assertValidAccountId(opts.account);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
const message = error instanceof InvalidAccountIdError ? error.message : "invalid account id";
|
|
135
|
+
writeCommandOutput(opts, `Logout failed: ${message}`, { status: "error", action: "logout", error: message });
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const targetId = opts.account ?? index.defaultAccount;
|
|
141
|
+
const result = await removeAccountAndCredential(targetId);
|
|
142
|
+
await stopRunningDaemon();
|
|
31
143
|
const credentialStatus = result.removed ? "removed" : "not present";
|
|
32
|
-
|
|
144
|
+
const tail = result.indexDeleted
|
|
145
|
+
? " No accounts remain."
|
|
146
|
+
: result.newDefault
|
|
147
|
+
? ` Default is now "${result.newDefault}".`
|
|
148
|
+
: "";
|
|
149
|
+
writeCommandOutput(opts, `Logged out of account "${result.id}". Credentials ${credentialStatus} from ${describeBackend(result.backend)}.${tail}`, {
|
|
33
150
|
status: "ok",
|
|
34
151
|
action: "logout",
|
|
152
|
+
account: result.id,
|
|
35
153
|
credential_backend: result.backend,
|
|
36
|
-
credential_removed: result.removed
|
|
154
|
+
credential_removed: result.removed,
|
|
155
|
+
new_default: result.newDefault,
|
|
156
|
+
index_deleted: result.indexDeleted
|
|
37
157
|
});
|
|
38
158
|
}
|
|
159
|
+
export async function runAuthSwitch(opts, accountId) {
|
|
160
|
+
try {
|
|
161
|
+
assertValidAccountId(accountId);
|
|
162
|
+
const index = switchDefaultAccount(accountId);
|
|
163
|
+
writeCommandOutput(opts, `Default account is now "${index.defaultAccount}".`, {
|
|
164
|
+
status: "ok",
|
|
165
|
+
action: "switch",
|
|
166
|
+
default_account: index.defaultAccount
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
const message = error instanceof UnknownAccountError
|
|
171
|
+
? `Unknown account "${accountId}". Run \`copillm auth status\` to list accounts.`
|
|
172
|
+
: error instanceof InvalidAccountIdError
|
|
173
|
+
? error.message
|
|
174
|
+
: error instanceof Error
|
|
175
|
+
? error.message
|
|
176
|
+
: "switch failed";
|
|
177
|
+
writeCommandOutput(opts, `Switch failed: ${message}`, { status: "error", action: "switch", error: message });
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Multi-account `auth status` listing (used when an accounts index exists).
|
|
183
|
+
* Returns whether any account has a stored credential so the caller can pick
|
|
184
|
+
* the process exit code. Never prints a token.
|
|
185
|
+
*/
|
|
186
|
+
export async function runAuthStatusList(opts) {
|
|
187
|
+
const wantUser = opts.user !== false;
|
|
188
|
+
const listing = await listAccountsDetailed();
|
|
189
|
+
const anyStored = listing.accounts.some((account) => account.stored);
|
|
190
|
+
const enriched = await Promise.all(listing.accounts.map(async (account) => {
|
|
191
|
+
let login = null;
|
|
192
|
+
let name = null;
|
|
193
|
+
if (wantUser && account.stored) {
|
|
194
|
+
try {
|
|
195
|
+
const credential = await loadStoredCredentialForAccount(account.id);
|
|
196
|
+
if (credential) {
|
|
197
|
+
const identity = await inspectGithubIdentity({ token: credential.token });
|
|
198
|
+
login = identity?.login ?? null;
|
|
199
|
+
name = identity?.name ?? null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
login = null;
|
|
204
|
+
name = null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { ...account, login, name };
|
|
208
|
+
}));
|
|
209
|
+
if (opts.json) {
|
|
210
|
+
process.stdout.write(JSON.stringify({
|
|
211
|
+
status: anyStored ? "logged_in" : "logged_out",
|
|
212
|
+
default: listing.defaultAccount,
|
|
213
|
+
accounts: enriched.map((account) => ({
|
|
214
|
+
id: account.id,
|
|
215
|
+
account_type: account.accountType,
|
|
216
|
+
storage: account.storage,
|
|
217
|
+
default: account.isDefault,
|
|
218
|
+
stored: account.stored,
|
|
219
|
+
backend: account.backend,
|
|
220
|
+
user: account.login ? { login: account.login, name: account.name } : null
|
|
221
|
+
}))
|
|
222
|
+
}, null, 2) + "\n");
|
|
223
|
+
return { anyStored };
|
|
224
|
+
}
|
|
225
|
+
process.stdout.write(`copillm — ${enriched.length} account(s)\n`);
|
|
226
|
+
for (const account of enriched) {
|
|
227
|
+
const marker = account.isDefault ? "*" : " ";
|
|
228
|
+
const who = account.login ? ` @${account.login}` : "";
|
|
229
|
+
const state = account.stored
|
|
230
|
+
? formatHumanAuthStatusLine(account.backend, account.login ? { login: account.login, name: account.name } : null)
|
|
231
|
+
: "no credential";
|
|
232
|
+
process.stdout.write(`${marker} ${account.id} [${account.accountType}]${who} — ${state}\n`);
|
|
233
|
+
}
|
|
234
|
+
return { anyStored };
|
|
235
|
+
}
|
|
@@ -5,7 +5,7 @@ import { ensureDaemonRunningForLauncher } from "../../daemon/ensureRunning.js";
|
|
|
5
5
|
import { launchAgent } from "../../launchAgent.js";
|
|
6
6
|
import { buildClaudeExportCommand } from "../../integrations/claudeExport.js";
|
|
7
7
|
import { enableRuntimeDebug, resolveCopillmDebug } from "../../shared/debug.js";
|
|
8
|
-
import { applyYoloForLaunch } from "./shared.js";
|
|
8
|
+
import { applyYoloForLaunch, formatLaunchAccountNotice, resolveLaunchAccount } from "./shared.js";
|
|
9
9
|
export function register(program) {
|
|
10
10
|
program
|
|
11
11
|
.command("claude")
|
|
@@ -16,9 +16,29 @@ export function register(program) {
|
|
|
16
16
|
.action(async (forwardedArgs) => {
|
|
17
17
|
const { opts, forwarded } = processCopillmArgs(forwardedArgs ?? []);
|
|
18
18
|
const debug = resolveCopillmDebug(opts.copillmDebug);
|
|
19
|
+
let launchAccount;
|
|
20
|
+
try {
|
|
21
|
+
launchAccount = await resolveLaunchAccount({
|
|
22
|
+
flag: opts.copillmAccount,
|
|
23
|
+
envValue: process.env.COPILLM_ACCOUNT,
|
|
24
|
+
cwd: process.cwd(),
|
|
25
|
+
profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
process.stderr.write(`copillm: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (launchAccount) {
|
|
34
|
+
process.stderr.write(`${formatLaunchAccountNotice(launchAccount)}\n`);
|
|
35
|
+
}
|
|
19
36
|
enableRuntimeDebug(debug);
|
|
20
37
|
const lock = await ensureDaemonRunningForLauncher({ debug });
|
|
21
|
-
const claude = buildClaudeExportCommand(lock.port, null
|
|
38
|
+
const claude = buildClaudeExportCommand(lock.port, null, {
|
|
39
|
+
pathPrefix: launchAccount?.pathPrefix,
|
|
40
|
+
cacheId: launchAccount?.cacheId
|
|
41
|
+
});
|
|
22
42
|
const pinnedSpec = opts.copillmUse ?? process.env.COPILLM_CLAUDE_VERSION ?? undefined;
|
|
23
43
|
const conflicts = detectClaudeSettingsConflicts(claude.bundle.env);
|
|
24
44
|
for (const line of formatSettingsConflictWarning(conflicts)) {
|
|
@@ -5,7 +5,7 @@ import { ensureDaemonRunningForLauncher } from "../../daemon/ensureRunning.js";
|
|
|
5
5
|
import { launchAgent } from "../../launchAgent.js";
|
|
6
6
|
import { refreshCodexHome } from "../../integrations/refreshCodex.js";
|
|
7
7
|
import { enableRuntimeDebug, resolveCopillmDebug } from "../../shared/debug.js";
|
|
8
|
-
import { applyYoloForLaunch } from "./shared.js";
|
|
8
|
+
import { applyYoloForLaunch, formatLaunchAccountNotice, resolveLaunchAccount } from "./shared.js";
|
|
9
9
|
export function register(program) {
|
|
10
10
|
program
|
|
11
11
|
.command("codex")
|
|
@@ -16,9 +16,29 @@ export function register(program) {
|
|
|
16
16
|
.action(async (forwardedArgs) => {
|
|
17
17
|
const { opts, forwarded } = processCopillmArgs(forwardedArgs ?? []);
|
|
18
18
|
const debug = resolveCopillmDebug(opts.copillmDebug);
|
|
19
|
+
let launchAccount;
|
|
20
|
+
try {
|
|
21
|
+
launchAccount = await resolveLaunchAccount({
|
|
22
|
+
flag: opts.copillmAccount,
|
|
23
|
+
envValue: process.env.COPILLM_ACCOUNT,
|
|
24
|
+
cwd: process.cwd(),
|
|
25
|
+
profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
process.stderr.write(`copillm: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (launchAccount) {
|
|
34
|
+
process.stderr.write(`${formatLaunchAccountNotice(launchAccount)}\n`);
|
|
35
|
+
}
|
|
19
36
|
enableRuntimeDebug(debug);
|
|
20
37
|
const lock = await ensureDaemonRunningForLauncher({ debug });
|
|
21
|
-
const codex = await refreshCodexHome(lock.port, null
|
|
38
|
+
const codex = await refreshCodexHome(lock.port, null, undefined, {
|
|
39
|
+
pathPrefix: launchAccount?.pathPrefix,
|
|
40
|
+
account: launchAccount?.account
|
|
41
|
+
});
|
|
22
42
|
if (!codex) {
|
|
23
43
|
throw new Error("Failed to prepare Codex home (see warning above).");
|
|
24
44
|
}
|
|
@@ -2,7 +2,7 @@ import { applyAgentConfig, formatApplyNotes } from "../../../agentconfig/apply.j
|
|
|
2
2
|
import { loadStoredCredential } from "../../../auth/credentials.js";
|
|
3
3
|
import { processCopillmArgs } from "../../copillmFlags.js";
|
|
4
4
|
import { launchAgent } from "../../launchAgent.js";
|
|
5
|
-
import { applyYoloForLaunch } from "./shared.js";
|
|
5
|
+
import { applyYoloForLaunch, formatLaunchAccountNotice, resolveLaunchAccount } from "./shared.js";
|
|
6
6
|
export function register(program) {
|
|
7
7
|
program
|
|
8
8
|
.command("copilot")
|
|
@@ -12,8 +12,29 @@ export function register(program) {
|
|
|
12
12
|
.argument("[args...]", "Args forwarded to copilot")
|
|
13
13
|
.action(async (forwardedArgs) => {
|
|
14
14
|
const { opts, forwarded } = processCopillmArgs(forwardedArgs ?? []);
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
let launchAccount;
|
|
16
|
+
try {
|
|
17
|
+
launchAccount = await resolveLaunchAccount({
|
|
18
|
+
flag: opts.copillmAccount,
|
|
19
|
+
envValue: process.env.COPILLM_ACCOUNT,
|
|
20
|
+
cwd: process.cwd(),
|
|
21
|
+
profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
process.stderr.write(`copillm: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (launchAccount) {
|
|
30
|
+
process.stderr.write(`${formatLaunchAccountNotice(launchAccount)}\n`);
|
|
31
|
+
}
|
|
32
|
+
// Copilot CLI talks to GitHub directly with the account's OAuth token,
|
|
33
|
+
// so account selection picks which token to inject (not a URL prefix).
|
|
34
|
+
const githubToken = launchAccount
|
|
35
|
+
? launchAccount.account.githubToken
|
|
36
|
+
: (await loadStoredCredential())?.token ?? null;
|
|
37
|
+
if (!githubToken) {
|
|
17
38
|
process.stderr.write("copillm: no stored GitHub credential — run `copillm auth login` first.\n");
|
|
18
39
|
process.exit(1);
|
|
19
40
|
return;
|
|
@@ -34,7 +55,7 @@ export function register(program) {
|
|
|
34
55
|
// short-circuits its device-flow login when copillm already has a token.
|
|
35
56
|
const env = {
|
|
36
57
|
...applyResult.envOverlay,
|
|
37
|
-
COPILOT_GITHUB_TOKEN:
|
|
58
|
+
COPILOT_GITHUB_TOKEN: githubToken
|
|
38
59
|
};
|
|
39
60
|
const baseArgs = [...forwarded, ...applyResult.cliArgs];
|
|
40
61
|
const args = applyYoloForLaunch({ agent: "copilot", flag: opts.yolo, applyResult, baseArgs });
|
|
@@ -5,7 +5,7 @@ import { ensureDaemonRunningForLauncher } from "../../daemon/ensureRunning.js";
|
|
|
5
5
|
import { launchAgent } from "../../launchAgent.js";
|
|
6
6
|
import { refreshPiHome } from "../../integrations/refreshPi.js";
|
|
7
7
|
import { enableRuntimeDebug, resolveCopillmDebug } from "../../shared/debug.js";
|
|
8
|
-
import { applyYoloForLaunch } from "./shared.js";
|
|
8
|
+
import { applyYoloForLaunch, formatLaunchAccountNotice, resolveLaunchAccount } from "./shared.js";
|
|
9
9
|
export function register(program) {
|
|
10
10
|
program
|
|
11
11
|
.command("pi")
|
|
@@ -16,9 +16,29 @@ export function register(program) {
|
|
|
16
16
|
.action(async (forwardedArgs) => {
|
|
17
17
|
const { opts, forwarded } = processCopillmArgs(forwardedArgs ?? []);
|
|
18
18
|
const debug = resolveCopillmDebug(opts.copillmDebug);
|
|
19
|
+
let launchAccount;
|
|
20
|
+
try {
|
|
21
|
+
launchAccount = await resolveLaunchAccount({
|
|
22
|
+
flag: opts.copillmAccount,
|
|
23
|
+
envValue: process.env.COPILLM_ACCOUNT,
|
|
24
|
+
cwd: process.cwd(),
|
|
25
|
+
profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
process.stderr.write(`copillm: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (launchAccount) {
|
|
34
|
+
process.stderr.write(`${formatLaunchAccountNotice(launchAccount)}\n`);
|
|
35
|
+
}
|
|
19
36
|
enableRuntimeDebug(debug);
|
|
20
37
|
const lock = await ensureDaemonRunningForLauncher({ debug });
|
|
21
|
-
const pi = await refreshPiHome(lock.port
|
|
38
|
+
const pi = await refreshPiHome(lock.port, undefined, {
|
|
39
|
+
pathPrefix: launchAccount?.pathPrefix,
|
|
40
|
+
account: launchAccount?.account
|
|
41
|
+
});
|
|
22
42
|
if (!pi) {
|
|
23
43
|
throw new Error("Failed to prepare pi models.json (see warning above).");
|
|
24
44
|
}
|
|
@@ -1,4 +1,61 @@
|
|
|
1
1
|
import { applyYolo, resolveYoloWithSource } from "../../../agents/registry.js";
|
|
2
|
+
import { loadAgentConfig } from "../../../agentconfig/load.js";
|
|
3
|
+
import { findAccount } from "../../../auth/accounts.js";
|
|
4
|
+
import { assertValidAccountId } from "../../../config/accountId.js";
|
|
5
|
+
import { loadStoredCredentialForAccount } from "../../../auth/credentials.js";
|
|
6
|
+
/**
|
|
7
|
+
* Resolve which account a launch targets, applying precedence
|
|
8
|
+
* `--account` > `COPILLM_ACCOUNT` > the active profile's `account` > default.
|
|
9
|
+
* Returns null for the default account (no prefix — today's behaviour).
|
|
10
|
+
*
|
|
11
|
+
* Throws a user-facing Error when a requested account is malformed, not
|
|
12
|
+
* registered, or has no stored credential, so the launcher can fail fast
|
|
13
|
+
* before starting the daemon or the agent.
|
|
14
|
+
*/
|
|
15
|
+
export async function resolveLaunchAccount(input) {
|
|
16
|
+
let requested;
|
|
17
|
+
let source;
|
|
18
|
+
if (input.flag && input.flag.trim().length > 0) {
|
|
19
|
+
requested = input.flag.trim();
|
|
20
|
+
source = "flag";
|
|
21
|
+
}
|
|
22
|
+
else if (input.envValue && input.envValue.trim().length > 0) {
|
|
23
|
+
requested = input.envValue.trim();
|
|
24
|
+
source = "env";
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const config = loadAgentConfig({ cwd: input.cwd, profileOverride: input.profileOverride });
|
|
28
|
+
const pinned = config?.resolved.account ?? null;
|
|
29
|
+
if (pinned) {
|
|
30
|
+
requested = pinned;
|
|
31
|
+
source = "profile";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!requested) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
assertValidAccountId(requested);
|
|
38
|
+
const record = findAccount(requested);
|
|
39
|
+
if (!record) {
|
|
40
|
+
throw new Error(`Unknown account "${requested}". Run \`copillm auth status\` to list accounts.`);
|
|
41
|
+
}
|
|
42
|
+
const credential = await loadStoredCredentialForAccount(requested);
|
|
43
|
+
if (!credential) {
|
|
44
|
+
throw new Error(`No stored credential for account "${requested}". Run \`copillm auth login --as ${requested}\`.`);
|
|
45
|
+
}
|
|
46
|
+
const cacheId = record.storage === "legacy" ? undefined : requested;
|
|
47
|
+
return {
|
|
48
|
+
accountId: requested,
|
|
49
|
+
pathPrefix: `/${requested}`,
|
|
50
|
+
cacheId,
|
|
51
|
+
account: { accountType: credential.accountType, githubToken: credential.token, cacheId },
|
|
52
|
+
source: source
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function formatLaunchAccountNotice(resolved) {
|
|
56
|
+
const from = resolved.source === "flag" ? "--account" : resolved.source === "env" ? "COPILLM_ACCOUNT" : "profile";
|
|
57
|
+
return `copillm: using account "${resolved.accountId}" (from ${from})`;
|
|
58
|
+
}
|
|
2
59
|
/**
|
|
3
60
|
* Shared yolo wiring for the four agent subcommands. Resolves precedence
|
|
4
61
|
* (flag > env > profile > defaults > off), runs `applyYolo` with source
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import { inspectStoredCredential } from "../../auth/credentials.js";
|
|
1
|
+
import { inspectStoredCredential, loadStoredCredentialForStatus } from "../../auth/credentials.js";
|
|
2
|
+
import { readAccountsIndex } from "../../auth/accounts.js";
|
|
2
3
|
import { inspectGithubIdentity } from "../../auth/githubIdentity.js";
|
|
3
4
|
import { ensureAuthenticatedInteractive } from "../auth/ensure.js";
|
|
4
|
-
import { runAuthLogin, runAuthLogout } from "../auth/runAuth.js";
|
|
5
|
+
import { runAuthLogin, runAuthLogout, runAuthStatusList, runAuthSwitch } from "../auth/runAuth.js";
|
|
5
6
|
import { formatHumanAuthStatusLine } from "../shared/backends.js";
|
|
6
7
|
import { emitDeprecation } from "../shared/deprecation.js";
|
|
8
|
+
const ACCOUNT_TYPES = ["individual", "business", "enterprise"];
|
|
9
|
+
function parseAccountType(value) {
|
|
10
|
+
if (ACCOUNT_TYPES.includes(value)) {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`Invalid --account-type "${value}". Expected one of: ${ACCOUNT_TYPES.join(", ")}.`);
|
|
14
|
+
}
|
|
7
15
|
// Re-export for callers (e.g. start command) that need the interactive prompt.
|
|
8
16
|
export { ensureAuthenticatedInteractive };
|
|
9
17
|
export function register(program) {
|
|
@@ -28,6 +36,8 @@ export function register(program) {
|
|
|
28
36
|
.command("login")
|
|
29
37
|
.description("Authenticate with GitHub")
|
|
30
38
|
.option("--json", "JSON output")
|
|
39
|
+
.option("--as <account>", "Name this account (enables multiple accounts)")
|
|
40
|
+
.option("--account-type <type>", "Account plan type: individual | business | enterprise", parseAccountType)
|
|
31
41
|
// Undocumented test seam: force the session-only backend regardless of
|
|
32
42
|
// whether the OS keychain is available. Equivalent to setting
|
|
33
43
|
// COPILLM_FORCE_SESSION_BACKEND=1 for the duration of this command.
|
|
@@ -39,18 +49,61 @@ export function register(program) {
|
|
|
39
49
|
.command("logout")
|
|
40
50
|
.description("Clear credentials and stop running daemon")
|
|
41
51
|
.option("--json", "JSON output")
|
|
52
|
+
.option("--account <account>", "Log out a specific account (default: the default account)")
|
|
53
|
+
.option("--all", "Log out of every account")
|
|
42
54
|
.action(async (opts) => {
|
|
43
55
|
await runAuthLogout(opts);
|
|
44
56
|
});
|
|
57
|
+
auth
|
|
58
|
+
.command("switch")
|
|
59
|
+
.argument("<account>", "Account id to make the default")
|
|
60
|
+
.description("Set the default account")
|
|
61
|
+
.option("--json", "JSON output")
|
|
62
|
+
.action(async (account, opts) => {
|
|
63
|
+
await runAuthSwitch(opts, account);
|
|
64
|
+
});
|
|
45
65
|
auth
|
|
46
66
|
.command("status")
|
|
47
67
|
.description("Report whether a credential is stored (token is never printed)")
|
|
48
68
|
.option("--json", "JSON output")
|
|
49
69
|
.option("--no-user", "Skip the GitHub /user lookup that fetches the login name")
|
|
50
70
|
.action(async (opts) => {
|
|
71
|
+
// commander's --no-user toggles opts.user to false; when the flag is
|
|
72
|
+
// omitted opts.user is undefined and we treat that as "fetch by default".
|
|
73
|
+
const wantUserLookup = opts.user !== false;
|
|
74
|
+
// Multi-account installs (an accounts index exists) get the per-account
|
|
75
|
+
// listing. Single-account installs keep the exact original output below.
|
|
76
|
+
if (readAccountsIndex()) {
|
|
77
|
+
const { anyStored } = await runAuthStatusList(opts);
|
|
78
|
+
process.exit(anyStored ? 0 : 2);
|
|
79
|
+
}
|
|
80
|
+
// Two paths to minimize keychain probes:
|
|
81
|
+
// - With user lookup (default): `loadStoredCredentialForStatus()`
|
|
82
|
+
// does ONE keychain read that yields backend + token. Pass the
|
|
83
|
+
// token into `inspectGithubIdentity({ token })` so it doesn't
|
|
84
|
+
// re-read the keychain.
|
|
85
|
+
// - Without user lookup (--no-user): `inspectStoredCredential()`
|
|
86
|
+
// does ONE keychain probe and never sees the token. Preserves
|
|
87
|
+
// the no-token invariant for the surface where it matters most.
|
|
88
|
+
//
|
|
89
|
+
// Previously, the user-lookup path made TWO keychain reads — one in
|
|
90
|
+
// `inspectStoredCredential` then another in `inspectGithubIdentity` →
|
|
91
|
+
// `loadStoredCredential`. That doubled macOS keychain audit-log
|
|
92
|
+
// entries and doubled permission-prompt exposure on misconfigured
|
|
93
|
+
// systems.
|
|
51
94
|
let info;
|
|
95
|
+
let token;
|
|
52
96
|
try {
|
|
53
|
-
|
|
97
|
+
if (wantUserLookup) {
|
|
98
|
+
const loaded = await loadStoredCredentialForStatus();
|
|
99
|
+
info = { stored: loaded.stored, backend: loaded.backend };
|
|
100
|
+
if (loaded.stored) {
|
|
101
|
+
token = loaded.token;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
info = await inspectStoredCredential();
|
|
106
|
+
}
|
|
54
107
|
}
|
|
55
108
|
catch (error) {
|
|
56
109
|
const message = error instanceof Error ? error.message : "unknown_error";
|
|
@@ -62,9 +115,7 @@ export function register(program) {
|
|
|
62
115
|
}
|
|
63
116
|
process.exit(1);
|
|
64
117
|
}
|
|
65
|
-
|
|
66
|
-
// omitted opts.user is undefined and we treat that as "fetch by default".
|
|
67
|
-
const userLookupEnabled = info.stored && opts.user !== false;
|
|
118
|
+
const userLookupEnabled = info.stored && wantUserLookup;
|
|
68
119
|
let identity = null;
|
|
69
120
|
if (userLookupEnabled) {
|
|
70
121
|
// inspectGithubIdentity is designed to return null on any failure, but
|
|
@@ -74,7 +125,7 @@ export function register(program) {
|
|
|
74
125
|
// must never break the auth-status command. Status output should always
|
|
75
126
|
// succeed even when the network is broken.
|
|
76
127
|
try {
|
|
77
|
-
identity = await inspectGithubIdentity();
|
|
128
|
+
identity = await inspectGithubIdentity({ token });
|
|
78
129
|
}
|
|
79
130
|
catch {
|
|
80
131
|
identity = null;
|