copillm 0.2.9 → 0.3.0-beta.2
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 +69 -1
- package/dist/agentconfig/load.js +9 -1
- package/dist/agentconfig/schema.js +6 -0
- package/dist/auth/accountManager.js +118 -0
- package/dist/auth/accounts.js +161 -0
- package/dist/auth/credentials.js +196 -67
- package/dist/cli/agentEnv.js +2 -1
- package/dist/cli/auth/runAuth.js +243 -7
- 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 +27 -1
- package/dist/cli/copillmFlags.js +8 -0
- package/dist/cli/daemon/runDaemon.js +21 -2
- package/dist/cli/integrations/claudeExport.js +6 -4
- package/dist/cli/integrations/refreshCodex.js +4 -2
- package/dist/cli/integrations/refreshPi.js +4 -2
- package/dist/cli/packageInfo.js +1 -1
- package/dist/config/accountId.js +44 -0
- package/dist/config/home.js +35 -0
- package/dist/integrations/codex/init.js +12 -3
- package/dist/integrations/pi/init.js +4 -3
- package/dist/models/anthropicDefaults.js +13 -4
- package/dist/models/discovery.js +32 -10
- package/dist/server/accountResolver.js +85 -0
- package/dist/server/proxy.js +40 -8
- package/dist/server/routes/debug.js +7 -0
- package/dist/server/routes/models.js +5 -5
- package/dist/server/routes/proxyForward.js +3 -3
- package/dist/server/routes/shared.js +66 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,18 @@ Alternatively, you can invoke it directly with `npx` without a global install:
|
|
|
28
28
|
npx copillm --help
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
### Preview (beta) releases
|
|
32
|
+
|
|
33
|
+
Experimental builds are published to the `beta` channel ahead of a stable
|
|
34
|
+
release. They let you try in-progress features early; expect rough edges. Stable
|
|
35
|
+
installs are never affected unless you explicitly opt in:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install -g copillm@beta
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
To return to the stable channel, reinstall without the tag: `npm install -g copillm@latest`.
|
|
42
|
+
|
|
31
43
|
## Quick start
|
|
32
44
|
|
|
33
45
|
```bash
|
|
@@ -48,6 +60,62 @@ copillm claude --model opus
|
|
|
48
60
|
copillm codex --help
|
|
49
61
|
```
|
|
50
62
|
|
|
63
|
+
## Multiple accounts
|
|
64
|
+
|
|
65
|
+
copillm can hold more than one GitHub account at once and serve them from the
|
|
66
|
+
same daemon. If you only ever use one account, nothing changes — you never see
|
|
67
|
+
any of this.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Your first login is the default account (no naming needed).
|
|
71
|
+
copillm auth login
|
|
72
|
+
|
|
73
|
+
# Add another account under a name of your choice.
|
|
74
|
+
copillm auth login --as work
|
|
75
|
+
copillm auth login --as work --account-type business # set its plan type
|
|
76
|
+
|
|
77
|
+
# See every account; the default is marked with *.
|
|
78
|
+
copillm auth status
|
|
79
|
+
|
|
80
|
+
# Change which account is the default.
|
|
81
|
+
copillm auth switch work
|
|
82
|
+
|
|
83
|
+
# Log out of one account, or all of them.
|
|
84
|
+
copillm auth logout --account work
|
|
85
|
+
copillm auth logout --all
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The **default account** is what every agent and the model endpoints use unless
|
|
89
|
+
told otherwise. `copillm auth status` lists each account with its plan type and
|
|
90
|
+
whether a credential is stored; tokens are never printed.
|
|
91
|
+
|
|
92
|
+
Different accounts can be entitled to different models, so each account keeps
|
|
93
|
+
its own model list.
|
|
94
|
+
|
|
95
|
+
### Launching an agent against a specific account
|
|
96
|
+
|
|
97
|
+
Point any agent at a non-default account for a single launch with `--account`,
|
|
98
|
+
or set `COPILLM_ACCOUNT` in the environment:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
copillm codex --account work
|
|
102
|
+
COPILLM_ACCOUNT=work copillm claude
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
To make it automatic, pin an account to a profile in `~/.copillm/agent.toml`
|
|
106
|
+
(or a project's `.copillm/agent.toml`):
|
|
107
|
+
|
|
108
|
+
```toml
|
|
109
|
+
[profiles.work]
|
|
110
|
+
account = "work"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Then `copillm codex --profile work` always uses the `work` account. Precedence
|
|
114
|
+
is `--account` > `COPILLM_ACCOUNT` > the profile's pinned account > the default
|
|
115
|
+
account. copillm prints a short notice such as `using account "work" (from
|
|
116
|
+
profile)` so you always know which account a launch is using, and refuses to
|
|
117
|
+
launch with a clear error if the account isn't one you've logged into.
|
|
118
|
+
|
|
51
119
|
## Documentation
|
|
52
120
|
|
|
53
121
|
Full documentation is published at **[jcjc-dev.github.io/copillm](https://jcjc-dev.github.io/copillm/)**.
|
|
@@ -63,7 +131,7 @@ Full documentation is published at **[jcjc-dev.github.io/copillm](https://jcjc-d
|
|
|
63
131
|
|
|
64
132
|
## Contributing
|
|
65
133
|
|
|
66
|
-
Bug reports and pull requests are welcome.
|
|
134
|
+
Bug reports and pull requests are welcome. Develop against an isolated dev daemon (`npm run dev:start`) so you don't disturb a running copillm, and run `npm run lint && npm test && npm run test:e2e:pr` before opening a pull request. See the [development guide](https://jcjc-dev.github.io/copillm/development/) for the full workflow.
|
|
67
135
|
|
|
68
136
|
## Disclaimer
|
|
69
137
|
|
package/dist/agentconfig/load.js
CHANGED
|
@@ -97,6 +97,14 @@ function mergeAndResolve(input) {
|
|
|
97
97
|
const instructions = instructionsBody !== null && instructionsBody.trim().length > 0
|
|
98
98
|
? { body: instructionsBody }
|
|
99
99
|
: null;
|
|
100
|
+
// Merge the pinned account: later layers (project over global, profile over
|
|
101
|
+
// defaults) win. Empty string is treated as unset.
|
|
102
|
+
let account = null;
|
|
103
|
+
for (const layer of layers) {
|
|
104
|
+
if (layer.account !== undefined && layer.account.trim().length > 0) {
|
|
105
|
+
account = layer.account.trim();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
100
108
|
// Merge mcp.servers map; later layers replace earlier same-named entries.
|
|
101
109
|
// Defaults are always-on: a profile may override a default by name but
|
|
102
110
|
// cannot remove it.
|
|
@@ -114,7 +122,7 @@ function mergeAndResolve(input) {
|
|
|
114
122
|
permissions: mergeRecord(layers, "permissions")
|
|
115
123
|
};
|
|
116
124
|
const yolo = mergeYolo(layers);
|
|
117
|
-
return { instructions, mcpServers: servers, yolo, reserved };
|
|
125
|
+
return { instructions, mcpServers: servers, account, yolo, reserved };
|
|
118
126
|
}
|
|
119
127
|
/**
|
|
120
128
|
* Layer yolo blocks across defaults + active profile. Later layers (project
|
|
@@ -69,6 +69,12 @@ const SectionSchema = z
|
|
|
69
69
|
instructions: InstructionsSchema.optional(),
|
|
70
70
|
mcp: McpSchema.optional(),
|
|
71
71
|
yolo: YoloSchema.optional(),
|
|
72
|
+
/**
|
|
73
|
+
* Pin a copillm account for launches that use this profile. The launcher
|
|
74
|
+
* routes the agent at this account unless overridden by `--account` /
|
|
75
|
+
* `COPILLM_ACCOUNT`. Must name an account from `copillm auth status`.
|
|
76
|
+
*/
|
|
77
|
+
account: z.string().min(1).optional(),
|
|
72
78
|
// v1 reserved sections: validated as objects but not interpreted.
|
|
73
79
|
skills: PassthroughRecord.optional(),
|
|
74
80
|
agents: PassthroughRecord.optional(),
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { findAccount, readAccountsIndex, removeAccount, setDefaultAccountId, upsertAccount, assertValidAccountId } from "./accounts.js";
|
|
2
|
+
import { clearStoredCredential, clearStoredCredentialForAccount, inspectStoredCredentialForAccount, saveStoredCredentialForAccount } from "./credentials.js";
|
|
3
|
+
/**
|
|
4
|
+
* Add or update an explicitly-identified account, materializing/extending the
|
|
5
|
+
* accounts index, and store its credential.
|
|
6
|
+
*
|
|
7
|
+
* Storage scheme follows the credential-store invariant: the **first** account
|
|
8
|
+
* (no index yet) takes legacy storage so it keeps the original keychain entry /
|
|
9
|
+
* `credentials.json`; every account added afterwards is namespaced. An existing
|
|
10
|
+
* account keeps whatever storage it already has.
|
|
11
|
+
*/
|
|
12
|
+
export async function addAccount(input) {
|
|
13
|
+
assertValidAccountId(input.id);
|
|
14
|
+
const index = readAccountsIndex();
|
|
15
|
+
const existing = findAccount(input.id);
|
|
16
|
+
const storage = existing ? existing.storage : index ? "namespaced" : "legacy";
|
|
17
|
+
upsertAccount({
|
|
18
|
+
id: input.id,
|
|
19
|
+
accountType: input.accountType,
|
|
20
|
+
storage,
|
|
21
|
+
addedAt: existing?.addedAt ?? new Date().toISOString()
|
|
22
|
+
});
|
|
23
|
+
// saveStoredCredentialForAccount resolves storage from the index record we
|
|
24
|
+
// just wrote, so it lands in the right (legacy vs namespaced) location.
|
|
25
|
+
const backend = await saveStoredCredentialForAccount(input.id, input.token, input.accountType, {
|
|
26
|
+
mode: input.mode ?? "auto"
|
|
27
|
+
});
|
|
28
|
+
let isDefault = readAccountsIndex()?.defaultAccount === input.id;
|
|
29
|
+
if (input.makeDefault && !isDefault) {
|
|
30
|
+
setDefaultAccountId(input.id);
|
|
31
|
+
isDefault = true;
|
|
32
|
+
}
|
|
33
|
+
return { id: input.id, accountType: input.accountType, storage, backend, isDefault };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Detailed, token-free view of every registered account for `auth status`.
|
|
37
|
+
* Returns `hasIndex: false` for single-account installs so the caller can use
|
|
38
|
+
* the legacy single-account output unchanged.
|
|
39
|
+
*/
|
|
40
|
+
export async function listAccountsDetailed() {
|
|
41
|
+
const index = readAccountsIndex();
|
|
42
|
+
if (!index) {
|
|
43
|
+
return { hasIndex: false, defaultAccount: null, accounts: [] };
|
|
44
|
+
}
|
|
45
|
+
const accounts = [];
|
|
46
|
+
for (const record of index.accounts) {
|
|
47
|
+
const info = await inspectStoredCredentialForAccount(record.id);
|
|
48
|
+
accounts.push({
|
|
49
|
+
id: record.id,
|
|
50
|
+
accountType: record.accountType,
|
|
51
|
+
storage: record.storage,
|
|
52
|
+
isDefault: record.id === index.defaultAccount,
|
|
53
|
+
stored: info.stored,
|
|
54
|
+
backend: info.backend
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return { hasIndex: true, defaultAccount: index.defaultAccount, accounts };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Remove one account: clear its credential first (while its index record still
|
|
61
|
+
* exists, so the correct storage location is targeted), then drop it from the
|
|
62
|
+
* index (which reassigns the default, or deletes the index when it was the
|
|
63
|
+
* last account). Clearing is best-effort — an absent credential is reported as
|
|
64
|
+
* `removed: false` rather than failing the removal.
|
|
65
|
+
*/
|
|
66
|
+
export async function removeAccountAndCredential(id) {
|
|
67
|
+
assertValidAccountId(id);
|
|
68
|
+
let removed = false;
|
|
69
|
+
let backend = "file";
|
|
70
|
+
try {
|
|
71
|
+
const cleared = await clearStoredCredentialForAccount(id);
|
|
72
|
+
removed = cleared.removed;
|
|
73
|
+
backend = cleared.backend;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// No backend available to clear (e.g. nothing was stored). The account is
|
|
77
|
+
// still removed from the index below.
|
|
78
|
+
removed = false;
|
|
79
|
+
}
|
|
80
|
+
const index = removeAccount(id);
|
|
81
|
+
return {
|
|
82
|
+
id,
|
|
83
|
+
removed,
|
|
84
|
+
backend,
|
|
85
|
+
newDefault: index?.defaultAccount ?? null,
|
|
86
|
+
indexDeleted: index === null
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Remove every account and delete the index. For a single-account install (no
|
|
91
|
+
* index) this just clears the legacy credential.
|
|
92
|
+
*/
|
|
93
|
+
export async function removeAllAccounts() {
|
|
94
|
+
const index = readAccountsIndex();
|
|
95
|
+
if (!index) {
|
|
96
|
+
const cleared = await clearStoredCredential();
|
|
97
|
+
return { clearedCount: cleared.removed ? 1 : 0, removedAccountIds: [], indexDeleted: false };
|
|
98
|
+
}
|
|
99
|
+
const ids = index.accounts.map((account) => account.id);
|
|
100
|
+
let clearedCount = 0;
|
|
101
|
+
for (const id of ids) {
|
|
102
|
+
try {
|
|
103
|
+
const cleared = await clearStoredCredentialForAccount(id);
|
|
104
|
+
if (cleared.removed) {
|
|
105
|
+
clearedCount += 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Best-effort: an account with nothing stored still gets removed.
|
|
110
|
+
}
|
|
111
|
+
removeAccount(id);
|
|
112
|
+
}
|
|
113
|
+
return { clearedCount, removedAccountIds: ids, indexDeleted: true };
|
|
114
|
+
}
|
|
115
|
+
/** Point the default at an existing account. Throws `UnknownAccountError`. */
|
|
116
|
+
export function switchDefaultAccount(id) {
|
|
117
|
+
return setDefaultAccountId(id);
|
|
118
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ensureAppHome } from "../config/config.js";
|
|
4
|
+
import { accountsIndexPath, accountsIndexReadPath } from "../config/home.js";
|
|
5
|
+
import { writeFileSecureAtomic } from "../config/fsSecurity.js";
|
|
6
|
+
import { ACCOUNT_ID_PATTERN, MAX_ACCOUNT_ID_LENGTH, assertValidAccountId, InvalidAccountIdError } from "../config/accountId.js";
|
|
7
|
+
// Re-exported for callers that historically imported account-id validation
|
|
8
|
+
// from this module (e.g. `credentials.ts`). The canonical definition now lives
|
|
9
|
+
// in `config/accountId.ts` so the `models` layer can share it.
|
|
10
|
+
export { assertValidAccountId, InvalidAccountIdError };
|
|
11
|
+
// GitHub logins are `[A-Za-z0-9-]` and copillm allows `.` / `_` for synthetic
|
|
12
|
+
// ids. The id is embedded in a filename (`credentials.<id>.json`) and a
|
|
13
|
+
// keychain account string; the canonical validation lives in
|
|
14
|
+
// `config/accountId.ts` (shared with the models layer).
|
|
15
|
+
const AccountRecordSchema = z.object({
|
|
16
|
+
id: z.string().min(1).max(MAX_ACCOUNT_ID_LENGTH).regex(ACCOUNT_ID_PATTERN),
|
|
17
|
+
accountType: z.enum(["individual", "business", "enterprise"]),
|
|
18
|
+
storage: z.enum(["legacy", "namespaced"]),
|
|
19
|
+
addedAt: z.string().min(1)
|
|
20
|
+
});
|
|
21
|
+
const AccountsIndexSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
version: z.literal(1),
|
|
24
|
+
defaultAccount: z.string().min(1),
|
|
25
|
+
accounts: z.array(AccountRecordSchema)
|
|
26
|
+
})
|
|
27
|
+
.superRefine((value, ctx) => {
|
|
28
|
+
const ids = value.accounts.map((account) => account.id);
|
|
29
|
+
if (new Set(ids).size !== ids.length) {
|
|
30
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "accounts.json contains duplicate account ids." });
|
|
31
|
+
}
|
|
32
|
+
if (!ids.includes(value.defaultAccount)) {
|
|
33
|
+
ctx.addIssue({
|
|
34
|
+
code: z.ZodIssueCode.custom,
|
|
35
|
+
message: `accounts.json defaultAccount "${value.defaultAccount}" is not present in accounts.`
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (value.accounts.filter((account) => account.storage === "legacy").length > 1) {
|
|
39
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "accounts.json may declare at most one legacy-storage account." });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
/**
|
|
43
|
+
* Read and validate the accounts index. Returns `null` when no index exists
|
|
44
|
+
* (the single-account / legacy case). Throws if the file exists but is
|
|
45
|
+
* corrupt, so a damaged index surfaces loudly rather than silently dropping
|
|
46
|
+
* accounts.
|
|
47
|
+
*/
|
|
48
|
+
export function readAccountsIndex() {
|
|
49
|
+
const path = accountsIndexReadPath();
|
|
50
|
+
if (!fs.existsSync(path)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
let raw;
|
|
54
|
+
try {
|
|
55
|
+
raw = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const detail = error instanceof Error ? error.message : "unknown error";
|
|
59
|
+
throw new Error(`Accounts index exists but contains invalid JSON at ${path}: ${detail}`);
|
|
60
|
+
}
|
|
61
|
+
const parsed = AccountsIndexSchema.safeParse(raw);
|
|
62
|
+
if (!parsed.success) {
|
|
63
|
+
throw new Error(`Accounts index exists but is invalid at ${path}: ${parsed.error.issues.map((i) => i.message).join("; ")}`);
|
|
64
|
+
}
|
|
65
|
+
return parsed.data;
|
|
66
|
+
}
|
|
67
|
+
export function writeAccountsIndex(index) {
|
|
68
|
+
const validated = AccountsIndexSchema.parse(index);
|
|
69
|
+
ensureAppHome();
|
|
70
|
+
writeFileSecureAtomic(accountsIndexPath(), JSON.stringify(validated, null, 2), 0o600);
|
|
71
|
+
}
|
|
72
|
+
export function listAccounts() {
|
|
73
|
+
return readAccountsIndex()?.accounts ?? [];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The default account id, or `null` when no index exists. A `null` return is
|
|
77
|
+
* the signal to callers to use the legacy single-account storage path.
|
|
78
|
+
*/
|
|
79
|
+
export function getDefaultAccountId() {
|
|
80
|
+
return readAccountsIndex()?.defaultAccount ?? null;
|
|
81
|
+
}
|
|
82
|
+
export function findAccount(accountId) {
|
|
83
|
+
return readAccountsIndex()?.accounts.find((account) => account.id === accountId) ?? null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Insert or update an account record, then persist the index. When the index
|
|
87
|
+
* does not yet exist it is created with this account as the default. Returns
|
|
88
|
+
* the resulting index.
|
|
89
|
+
*/
|
|
90
|
+
export function upsertAccount(record) {
|
|
91
|
+
assertValidAccountId(record.id);
|
|
92
|
+
AccountRecordSchema.parse(record);
|
|
93
|
+
const existing = readAccountsIndex();
|
|
94
|
+
if (!existing) {
|
|
95
|
+
const index = { version: 1, defaultAccount: record.id, accounts: [record] };
|
|
96
|
+
writeAccountsIndex(index);
|
|
97
|
+
return index;
|
|
98
|
+
}
|
|
99
|
+
const accounts = existing.accounts.filter((account) => account.id !== record.id);
|
|
100
|
+
accounts.push(record);
|
|
101
|
+
const index = { ...existing, accounts };
|
|
102
|
+
writeAccountsIndex(index);
|
|
103
|
+
return index;
|
|
104
|
+
}
|
|
105
|
+
export class UnknownAccountError extends Error {
|
|
106
|
+
accountId;
|
|
107
|
+
constructor(accountId) {
|
|
108
|
+
super(`Unknown account "${accountId}".`);
|
|
109
|
+
this.accountId = accountId;
|
|
110
|
+
this.name = "UnknownAccountError";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Point the default at an existing account. Throws `UnknownAccountError` if the
|
|
115
|
+
* id isn't registered, so a typo can't silently orphan the default.
|
|
116
|
+
*/
|
|
117
|
+
export function setDefaultAccountId(accountId) {
|
|
118
|
+
const existing = readAccountsIndex();
|
|
119
|
+
if (!existing || !existing.accounts.some((account) => account.id === accountId)) {
|
|
120
|
+
throw new UnknownAccountError(accountId);
|
|
121
|
+
}
|
|
122
|
+
const index = { ...existing, defaultAccount: accountId };
|
|
123
|
+
writeAccountsIndex(index);
|
|
124
|
+
return index;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Remove an account from the index. Returns the updated index, or `null` if no
|
|
128
|
+
* index existed. When the removed account was the default, the default falls
|
|
129
|
+
* back to the first remaining account (or the index is deleted entirely if no
|
|
130
|
+
* accounts remain). Token removal is the caller's responsibility.
|
|
131
|
+
*/
|
|
132
|
+
export function removeAccount(accountId) {
|
|
133
|
+
const existing = readAccountsIndex();
|
|
134
|
+
if (!existing) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const accounts = existing.accounts.filter((account) => account.id !== accountId);
|
|
138
|
+
if (accounts.length === existing.accounts.length) {
|
|
139
|
+
return existing;
|
|
140
|
+
}
|
|
141
|
+
if (accounts.length === 0) {
|
|
142
|
+
deleteAccountsIndex();
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const defaultAccount = accounts.some((account) => account.id === existing.defaultAccount)
|
|
146
|
+
? existing.defaultAccount
|
|
147
|
+
: accounts[0].id;
|
|
148
|
+
const index = { ...existing, defaultAccount, accounts };
|
|
149
|
+
writeAccountsIndex(index);
|
|
150
|
+
return index;
|
|
151
|
+
}
|
|
152
|
+
function deleteAccountsIndex() {
|
|
153
|
+
const canonical = accountsIndexPath();
|
|
154
|
+
if (fs.existsSync(canonical)) {
|
|
155
|
+
fs.unlinkSync(canonical);
|
|
156
|
+
}
|
|
157
|
+
const readable = accountsIndexReadPath();
|
|
158
|
+
if (readable !== canonical && fs.existsSync(readable)) {
|
|
159
|
+
fs.unlinkSync(readable);
|
|
160
|
+
}
|
|
161
|
+
}
|