@indigoai-us/hq-cli 5.242.0 → 5.243.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/commands/auth.js +73 -16
- package/dist/commands/lanes-accounts.d.ts +25 -0
- package/dist/commands/lanes-accounts.js +527 -0
- package/dist/commands/lanes-questions.d.ts +34 -0
- package/dist/commands/lanes-questions.js +876 -0
- package/dist/commands/lanes-shared.d.ts +155 -0
- package/dist/commands/lanes-shared.js +2 -0
- package/dist/commands/lanes.d.ts +4 -6
- package/dist/commands/lanes.js +128 -1437
- package/dist/commands/safe-extract.d.ts +11 -2
- package/dist/commands/safe-extract.js +37 -8
- package/dist/lib/auth-login-provider-state.d.ts +8 -0
- package/dist/lib/auth-login-provider-state.js +63 -0
- package/dist/lib/install-path-compatibility-flag.d.ts +9 -0
- package/dist/lib/install-path-compatibility-flag.js +12 -0
- package/dist/main.js +9 -6
- package/dist/utils/login-provider.d.ts +7 -0
- package/dist/utils/login-provider.js +34 -0
- package/dist/utils/version-gate.d.ts +3 -3
- package/dist/utils/version-gate.js +19 -10
- package/package.json +2 -1
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { buildOutpostBalanceMoveCommand, dispatchErrorMessage } from "../lib/lanes/outpost-dispatch.js";
|
|
4
|
+
import { BALANCE_MOVABLE_STATES, accountLoadsForReport, planClaudeBalanceMoves, selectClaudeAccount } from "../lib/lanes/claude-balance.js";
|
|
5
|
+
import { CompanySelectionError } from "../utils/company-selection-error.js";
|
|
6
|
+
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
7
|
+
import { AccountResolutionError, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, PROVIDER_HOME_ENV, accountStatus, canonicalizeHome, installDrainHook, isLanesError, listAccountStatuses, loadLanesConfig, parseLaneProvider, readLane, writeLanesConfig } from "../lib/lanes/index.js";
|
|
8
|
+
import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
9
|
+
import { resolveLaneEffort } from "../lib/lanes/effort.js";
|
|
10
|
+
import { LanesError } from "../lib/lanes/errors.js";
|
|
11
|
+
import { accountQuotaView, loadQuotaState } from "../lib/lanes/quota.js";
|
|
12
|
+
import { resolveLaneModel } from "../lib/lanes/spawn.js";
|
|
13
|
+
export function createLaneAccountsHandlers(deps) {
|
|
14
|
+
const { automaticBalanceMoveHistoryReason, ensureDir, laneWithRecoveredProviderSession, loadClaudeBalanceHostSnapshots, parseRemoteJson, prepareAccountMove, printJson, reconcileHostFromCtx, resolveCallerPath } = deps;
|
|
15
|
+
const VENDOR_LOGIN = {
|
|
16
|
+
claude: { command: "claude", args: ["auth", "login"] },
|
|
17
|
+
codex: { command: "codex", args: ["login"] },
|
|
18
|
+
grok: { command: "grok", args: ["login"] },
|
|
19
|
+
};
|
|
20
|
+
async function resolveSelectedAccountHomeCompany(ctx, selector) {
|
|
21
|
+
const company = await ctx.resolveActiveCompany(selector);
|
|
22
|
+
if (!company) {
|
|
23
|
+
throw new CompanySelectionError(`No unique active company membership for ${selector}. Pass an active company UID with --company.`);
|
|
24
|
+
}
|
|
25
|
+
return company;
|
|
26
|
+
}
|
|
27
|
+
function copyHookRails(fromHome, toHome) {
|
|
28
|
+
const src = path.join(fromHome, "hooks");
|
|
29
|
+
if (!fs.existsSync(src))
|
|
30
|
+
return false;
|
|
31
|
+
fs.cpSync(src, path.join(toHome, "hooks"), { recursive: true });
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
function railsPresent(home) {
|
|
35
|
+
return fs.existsSync(path.join(home, "hooks"));
|
|
36
|
+
}
|
|
37
|
+
async function applyClaudeBalanceMove(ctx, suggestion) {
|
|
38
|
+
const currentHost = reconcileHostFromCtx(ctx);
|
|
39
|
+
const suggestionRunsHere = suggestion.host.kind === "outpost"
|
|
40
|
+
? currentHost.outpostId === suggestion.host.id
|
|
41
|
+
: currentHost.outpostId === null && currentHost.hostname === suggestion.host.id;
|
|
42
|
+
if (suggestion.host.kind === "outpost" && !suggestionRunsHere) {
|
|
43
|
+
let result;
|
|
44
|
+
try {
|
|
45
|
+
result = await ctx.dispatchOutpostExec({
|
|
46
|
+
outpostId: suggestion.host.id,
|
|
47
|
+
command: buildOutpostBalanceMoveCommand({
|
|
48
|
+
laneId: suggestion.lane_id,
|
|
49
|
+
account: suggestion.to,
|
|
50
|
+
fromAccount: suggestion.from,
|
|
51
|
+
expectedState: suggestion.state,
|
|
52
|
+
}),
|
|
53
|
+
timeoutSeconds: 120,
|
|
54
|
+
waitMs: 120_000,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
throw new LanesError("claude_account_balance_move_failed", `Could not move lane ${suggestion.lane_id} on Outpost ${suggestion.host.id}: ${dispatchErrorMessage(err)}`, { lane_id: suggestion.lane_id, host: suggestion.host, account: suggestion.to });
|
|
59
|
+
}
|
|
60
|
+
if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
|
|
61
|
+
throw new LanesError("claude_account_balance_move_failed", `Outpost ${suggestion.host.id} did not confirm the account move for lane ${suggestion.lane_id}`, { lane_id: suggestion.lane_id, host: suggestion.host, account: suggestion.to, status: result.status, exit_code: result.exitCode });
|
|
62
|
+
}
|
|
63
|
+
const remote = parseRemoteJson(result.stdout, "Claude account balance move");
|
|
64
|
+
if (remote?.ok !== true || remote.action !== "balance-move-target" ||
|
|
65
|
+
remote.lane_id !== suggestion.lane_id || remote.from !== suggestion.from ||
|
|
66
|
+
remote.account !== suggestion.to ||
|
|
67
|
+
remote.state !== suggestion.state) {
|
|
68
|
+
throw new LanesError("claude_account_balance_move_unconfirmed", `Outpost ${suggestion.host.id} returned an invalid account-move result`, {
|
|
69
|
+
lane_id: suggestion.lane_id,
|
|
70
|
+
host: suggestion.host,
|
|
71
|
+
account: suggestion.to,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return { lane_id: suggestion.lane_id, host: suggestion.host, from: suggestion.from, to: suggestion.to, state: suggestion.state };
|
|
75
|
+
}
|
|
76
|
+
const lane = laneWithRecoveredProviderSession(ctx.hqRoot(), readLane(ctx.hqRoot(), suggestion.lane_id));
|
|
77
|
+
if (lane.state === "running") {
|
|
78
|
+
throw new LanesError("lane_running", `Lane ${lane.lane_id} is running; balance never moves a running turn`, {
|
|
79
|
+
lane_id: lane.lane_id,
|
|
80
|
+
state: lane.state,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (lane.host.kind !== suggestion.host.kind || lane.host.id !== suggestion.host.id ||
|
|
84
|
+
lane.account !== suggestion.from || lane.state !== suggestion.state) {
|
|
85
|
+
throw new LanesError("claude_account_balance_lane_changed", `Lane ${lane.lane_id} changed after balance was calculated`, {
|
|
86
|
+
lane_id: lane.lane_id,
|
|
87
|
+
host: lane.host,
|
|
88
|
+
account: lane.account ?? null,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (!BALANCE_MOVABLE_STATES.includes(lane.state)) {
|
|
92
|
+
throw new LanesError("claude_account_balance_state_not_movable", `Lane ${lane.lane_id} is no longer between turns`, {
|
|
93
|
+
lane_id: lane.lane_id,
|
|
94
|
+
state: lane.state,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const moved = await prepareAccountMove(ctx, lane, {
|
|
98
|
+
account: suggestion.to,
|
|
99
|
+
cliModel: undefined,
|
|
100
|
+
accountHistoryReason: automaticBalanceMoveHistoryReason(ctx, lane),
|
|
101
|
+
deferTurnStart: true,
|
|
102
|
+
});
|
|
103
|
+
return { lane_id: moved.lane.lane_id, host: moved.lane.host, from: moved.from ?? suggestion.from, to: moved.to, state: moved.lane.state };
|
|
104
|
+
}
|
|
105
|
+
async function runAccountsList(ctx, opts) {
|
|
106
|
+
const hqRoot = ctx.hqRoot();
|
|
107
|
+
const config = loadLanesConfig(hqRoot);
|
|
108
|
+
const providers = opts.provider
|
|
109
|
+
? [parseLaneProvider(opts.provider)]
|
|
110
|
+
: [...LANE_PROVIDERS];
|
|
111
|
+
// model and effort are what a spawn on each account would start from
|
|
112
|
+
// (spec: --model/--effort, then the account, then the provider).
|
|
113
|
+
const quota = loadQuotaState(hqRoot);
|
|
114
|
+
const accounts = providers.flatMap((provider) => listAccountStatuses(provider, config, {
|
|
115
|
+
existsSync: ctx.existsSync,
|
|
116
|
+
hostname: ctx.hostname(),
|
|
117
|
+
probe: ctx.probeProvider,
|
|
118
|
+
env: ctx.env,
|
|
119
|
+
}).map((status) => {
|
|
120
|
+
const accountCfg = config.providers[provider].accounts[status.id];
|
|
121
|
+
const model = resolveLaneModel({
|
|
122
|
+
accountModel: accountCfg?.model,
|
|
123
|
+
providerModel: config.providers[provider].model,
|
|
124
|
+
});
|
|
125
|
+
const effort = resolveLaneEffort({
|
|
126
|
+
accountEffort: accountCfg?.effort,
|
|
127
|
+
providerEffort: config.providers[provider].effort,
|
|
128
|
+
model,
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
...status,
|
|
132
|
+
effective_home: status.home,
|
|
133
|
+
model: accountCfg?.model ?? null,
|
|
134
|
+
effort: accountCfg?.effort ?? null,
|
|
135
|
+
effective_model: model ?? null,
|
|
136
|
+
effective_effort: effort.effort ?? null,
|
|
137
|
+
effective_effort_source: effort.source,
|
|
138
|
+
...accountQuotaView(quota, provider, status.id),
|
|
139
|
+
};
|
|
140
|
+
}));
|
|
141
|
+
const providerDefaults = providers.map((provider) => ({
|
|
142
|
+
provider,
|
|
143
|
+
model: config.providers[provider].model ?? null,
|
|
144
|
+
effort: config.providers[provider].effort ?? null,
|
|
145
|
+
compact_after_ms: providerCompactAfterMs(config.providers[provider]),
|
|
146
|
+
}));
|
|
147
|
+
printJson({
|
|
148
|
+
ok: true,
|
|
149
|
+
action: "accounts.list",
|
|
150
|
+
providers: providerDefaults,
|
|
151
|
+
accounts,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async function runAccountsBalance(ctx, opts) {
|
|
155
|
+
const provider = parseLaneProvider(opts.provider ?? "");
|
|
156
|
+
if (provider !== "claude") {
|
|
157
|
+
throw new LanesError("claude_account_balance_provider_unsupported", "Account balance is available only for --provider claude", {
|
|
158
|
+
provider,
|
|
159
|
+
accepted: ["claude"],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const hqRoot = ctx.hqRoot();
|
|
163
|
+
const config = loadLanesConfig(hqRoot);
|
|
164
|
+
const loaded = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
|
|
165
|
+
// Retain the established response field for clients; balancing is now
|
|
166
|
+
// unconditional, so this compatibility value is always true.
|
|
167
|
+
const balanceGateEnabled = true;
|
|
168
|
+
const loads = accountLoadsForReport({ hosts: loaded.snapshots, config });
|
|
169
|
+
const suggestions = planClaudeBalanceMoves({ hosts: loaded.snapshots, config });
|
|
170
|
+
const selections = loaded.snapshots.map((host) => {
|
|
171
|
+
try {
|
|
172
|
+
const selection = selectClaudeAccount({
|
|
173
|
+
hosts: loaded.snapshots,
|
|
174
|
+
targetHost: host.capacity.host,
|
|
175
|
+
config,
|
|
176
|
+
unavailableHosts: loaded.unavailableHosts,
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
target_host: host.capacity.host,
|
|
180
|
+
account: selection.account,
|
|
181
|
+
signal: selection.signal,
|
|
182
|
+
skipped_accounts: selection.skipped_accounts,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
return {
|
|
187
|
+
target_host: host.capacity.host,
|
|
188
|
+
error: isLanesError(err) ? err.error : "claude_account_balance_selection_failed",
|
|
189
|
+
message: dispatchErrorMessage(err),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
const accounts = Object.keys(config.providers.claude.accounts).sort().map((account) => {
|
|
194
|
+
const load = loads.find((row) => row.account === account);
|
|
195
|
+
return {
|
|
196
|
+
account,
|
|
197
|
+
enabled: config.providers.claude.accounts[account]?.enabled === true,
|
|
198
|
+
live_lanes: load?.live_lanes ?? 0,
|
|
199
|
+
counts_complete: load?.counts_complete ?? false,
|
|
200
|
+
hosts: loaded.snapshots.map((host) => {
|
|
201
|
+
const accountStatus = host.capacity.providers.claude.accounts[account];
|
|
202
|
+
const accountLoad = load?.hosts.find((row) => row.host.kind === host.capacity.host.kind && row.host.id === host.capacity.host.id);
|
|
203
|
+
return {
|
|
204
|
+
host: host.capacity.host,
|
|
205
|
+
enabled: accountStatus?.enabled ?? false,
|
|
206
|
+
logged_in: accountStatus?.logged_in ?? null,
|
|
207
|
+
live_lanes: accountLoad?.live_lanes ?? null,
|
|
208
|
+
lane_counts: accountLoad?.lane_counts ?? { running: null, queued: null, awaiting_input: null },
|
|
209
|
+
count_source: accountLoad?.count_source ?? "unavailable",
|
|
210
|
+
usage: accountStatus?.usage ?? null,
|
|
211
|
+
};
|
|
212
|
+
}),
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
const moved = [];
|
|
216
|
+
const skipped = [];
|
|
217
|
+
if (opts.apply) {
|
|
218
|
+
for (const suggestion of suggestions) {
|
|
219
|
+
if (!suggestion.apply) {
|
|
220
|
+
skipped.push({
|
|
221
|
+
lane_id: suggestion.lane_id,
|
|
222
|
+
host: suggestion.host,
|
|
223
|
+
from: suggestion.from,
|
|
224
|
+
to: suggestion.to,
|
|
225
|
+
reason: suggestion.skip_reason ?? "not_movable",
|
|
226
|
+
});
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
moved.push(await applyClaudeBalanceMove(ctx, suggestion));
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
skipped.push({
|
|
234
|
+
lane_id: suggestion.lane_id,
|
|
235
|
+
host: suggestion.host,
|
|
236
|
+
from: suggestion.from,
|
|
237
|
+
to: suggestion.to,
|
|
238
|
+
reason: isLanesError(err) ? `${err.error}: ${err.message}` : dispatchErrorMessage(err),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
printJson({
|
|
244
|
+
ok: true,
|
|
245
|
+
action: "accounts.balance",
|
|
246
|
+
provider: "claude",
|
|
247
|
+
balance_gate_enabled: balanceGateEnabled,
|
|
248
|
+
accounts,
|
|
249
|
+
selections,
|
|
250
|
+
unavailable_hosts: loaded.unavailableHosts,
|
|
251
|
+
suggestions,
|
|
252
|
+
...(opts.apply ? { moved, skipped } : {}),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
256
|
+
const callerCwd = ctx.callerCwd();
|
|
257
|
+
const hqRoot = ctx.hqRoot();
|
|
258
|
+
const provider = parseLaneProvider(providerRaw);
|
|
259
|
+
const config = loadLanesConfig(hqRoot);
|
|
260
|
+
const id = accountId.trim();
|
|
261
|
+
if (!id) {
|
|
262
|
+
throw new LanesError("invalid_account", "account-id must be non-empty", {});
|
|
263
|
+
}
|
|
264
|
+
if (opts.deviceAuth && provider !== "codex") {
|
|
265
|
+
throw new LanesError("device_auth_unsupported", "--device-auth is supported only for codex account login", { provider });
|
|
266
|
+
}
|
|
267
|
+
const registeredAccount = config.providers[provider].accounts[id];
|
|
268
|
+
const hostname = ctx.hostname().trim();
|
|
269
|
+
if (!hostname) {
|
|
270
|
+
throw new LanesError("host_identity_unknown", "Cannot resolve this host's hostname for account login", {});
|
|
271
|
+
}
|
|
272
|
+
const requestedHome = opts.home?.trim();
|
|
273
|
+
if (opts.home !== undefined && !requestedHome) {
|
|
274
|
+
throw new LanesError("invalid_account_home", "--home must be a non-empty path", { provider, account: id });
|
|
275
|
+
}
|
|
276
|
+
if (opts.company !== undefined) {
|
|
277
|
+
await resolveSelectedAccountHomeCompany(ctx, opts.company);
|
|
278
|
+
}
|
|
279
|
+
const homeRaw = requestedHome ??
|
|
280
|
+
(registeredAccount !== undefined
|
|
281
|
+
? effectiveAccountHome(registeredAccount, hostname).home
|
|
282
|
+
: id === "a"
|
|
283
|
+
? DEFAULT_ACCOUNT_HOMES[provider]
|
|
284
|
+
: `~/.${provider}-${id}`);
|
|
285
|
+
const homeResolved = homeRaw.startsWith("~") || path.isAbsolute(homeRaw)
|
|
286
|
+
? homeRaw
|
|
287
|
+
: resolveCallerPath(callerCwd, homeRaw);
|
|
288
|
+
const home = canonicalizeHome(homeResolved, {
|
|
289
|
+
homedir: ctx.homedir,
|
|
290
|
+
realpathSync: (p) => fs.realpathSync(p),
|
|
291
|
+
existsSync: (p) => fs.existsSync(p),
|
|
292
|
+
});
|
|
293
|
+
const nextAccount = registeredAccount === undefined
|
|
294
|
+
? { home, enabled: true }
|
|
295
|
+
: { ...registeredAccount, enabled: true };
|
|
296
|
+
if (registeredAccount !== undefined && requestedHome !== undefined) {
|
|
297
|
+
if (home === registeredAccount.home) {
|
|
298
|
+
if (nextAccount.hosts !== undefined) {
|
|
299
|
+
delete nextAccount.hosts[hostname];
|
|
300
|
+
if (Object.keys(nextAccount.hosts).length === 0)
|
|
301
|
+
delete nextAccount.hosts;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
nextAccount.hosts = {
|
|
306
|
+
...(registeredAccount.hosts ?? {}),
|
|
307
|
+
[hostname]: { home },
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
delete nextAccount.disabled_reason;
|
|
312
|
+
assertUniqueAccountHomes(provider, {
|
|
313
|
+
...config.providers[provider].accounts,
|
|
314
|
+
[id]: nextAccount,
|
|
315
|
+
});
|
|
316
|
+
ensureDir(home);
|
|
317
|
+
const defaultId = config.providers[provider].default_account;
|
|
318
|
+
const defaultAccount = defaultId !== undefined
|
|
319
|
+
? config.providers[provider].accounts[defaultId]
|
|
320
|
+
: Object.values(config.providers[provider].accounts).find((account) => account.enabled);
|
|
321
|
+
const defaultHome = (defaultAccount !== undefined
|
|
322
|
+
? effectiveAccountHome(defaultAccount, hostname).home
|
|
323
|
+
: undefined) ??
|
|
324
|
+
canonicalizeHome(DEFAULT_ACCOUNT_HOMES[provider], {
|
|
325
|
+
homedir: ctx.homedir,
|
|
326
|
+
realpathSync: (p) => fs.realpathSync(p),
|
|
327
|
+
existsSync: (p) => fs.existsSync(p),
|
|
328
|
+
});
|
|
329
|
+
const copied = defaultHome !== home ? copyHookRails(defaultHome, home) : railsPresent(home);
|
|
330
|
+
installDrainHook(provider, home);
|
|
331
|
+
const envVar = PROVIDER_HOME_ENV[provider];
|
|
332
|
+
const login = VENDOR_LOGIN[provider];
|
|
333
|
+
const loginArgs = provider === "codex" && opts.deviceAuth
|
|
334
|
+
? [...login.args, "--device-auth"]
|
|
335
|
+
: login.args;
|
|
336
|
+
const result = await ctx.spawnLogin({
|
|
337
|
+
command: login.command,
|
|
338
|
+
args: loginArgs,
|
|
339
|
+
env: { ...ctx.env, [envVar]: home },
|
|
340
|
+
cwd: home,
|
|
341
|
+
});
|
|
342
|
+
if (result.status !== 0) {
|
|
343
|
+
throw new LanesError("login_failed", `${login.command} ${loginArgs.join(" ")} exited ${result.status}`, { provider, account: id, status: result.status });
|
|
344
|
+
}
|
|
345
|
+
const grokNeedsRails = provider === "grok";
|
|
346
|
+
if ((grokNeedsRails || copied) && !railsPresent(home)) {
|
|
347
|
+
throw new LanesError("rails_missing", `Hook rails are missing under ${home}; account not enabled`, { provider, account: id, home });
|
|
348
|
+
}
|
|
349
|
+
config.providers[provider].accounts[id] = nextAccount;
|
|
350
|
+
const status = accountStatus(provider, id, config, {
|
|
351
|
+
existsSync: ctx.existsSync,
|
|
352
|
+
hostname,
|
|
353
|
+
probe: ctx.probeProvider,
|
|
354
|
+
env: ctx.env,
|
|
355
|
+
});
|
|
356
|
+
if (status.logged_in !== true) {
|
|
357
|
+
throw new LanesError(status.logged_in === false ? "account_not_logged_in" : "account_login_status_unknown", status.logged_in === false
|
|
358
|
+
? `Account ${id} for ${provider} is not logged in`
|
|
359
|
+
: `Could not verify that account ${id} for ${provider} is logged in`, { provider, account: id, home });
|
|
360
|
+
}
|
|
361
|
+
writeLanesConfig(hqRoot, config);
|
|
362
|
+
printJson({ ok: true, action: "accounts.login", account: status });
|
|
363
|
+
}
|
|
364
|
+
async function runAccountsSetHome(ctx, providerRaw, accountIdRaw, homeRaw, opts) {
|
|
365
|
+
if (opts.company !== undefined)
|
|
366
|
+
await resolveSelectedAccountHomeCompany(ctx, opts.company);
|
|
367
|
+
const callerCwd = ctx.callerCwd();
|
|
368
|
+
const hqRoot = ctx.hqRoot();
|
|
369
|
+
const provider = parseLaneProvider(providerRaw);
|
|
370
|
+
const id = accountIdRaw.trim();
|
|
371
|
+
if (!id) {
|
|
372
|
+
throw new AccountResolutionError("unknown_account", "account-id must be non-empty", {
|
|
373
|
+
provider,
|
|
374
|
+
account: accountIdRaw,
|
|
375
|
+
enabled: [],
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
const hostname = opts.host === undefined ? ctx.hostname().trim() : opts.host.trim();
|
|
379
|
+
if (!hostname || (opts.host !== undefined && hostname !== opts.host)) {
|
|
380
|
+
throw new LanesError("invalid_host", "--host must be a non-empty hostname without surrounding whitespace", {
|
|
381
|
+
host: opts.host ?? "",
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (!homeRaw.trim()) {
|
|
385
|
+
throw new LanesError("invalid_account_home", "home path must be a non-empty path", {
|
|
386
|
+
provider,
|
|
387
|
+
account: id,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
const config = loadLanesConfig(hqRoot);
|
|
391
|
+
const account = config.providers[provider].accounts[id];
|
|
392
|
+
if (account === undefined) {
|
|
393
|
+
throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(id)} for ${provider}`, {
|
|
394
|
+
provider,
|
|
395
|
+
account: id,
|
|
396
|
+
enabled: Object.keys(config.providers[provider].accounts).filter((key) => config.providers[provider].accounts[key]?.enabled),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const homePath = homeRaw.trim();
|
|
400
|
+
const isCurrentHost = hostname === ctx.hostname().trim();
|
|
401
|
+
let home;
|
|
402
|
+
if (isCurrentHost) {
|
|
403
|
+
const homeResolved = homePath.startsWith("~") || path.isAbsolute(homePath)
|
|
404
|
+
? homePath
|
|
405
|
+
: resolveCallerPath(callerCwd, homePath);
|
|
406
|
+
home = canonicalizeHome(homeResolved, {
|
|
407
|
+
homedir: ctx.homedir,
|
|
408
|
+
realpathSync: (p) => fs.realpathSync(p),
|
|
409
|
+
existsSync: (p) => fs.existsSync(p),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
else if (homePath === "~" || homePath.startsWith("~/")) {
|
|
413
|
+
home = homePath;
|
|
414
|
+
}
|
|
415
|
+
else if (path.isAbsolute(homePath)) {
|
|
416
|
+
home = path.normalize(homePath);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
throw new LanesError("invalid_remote_account_home", "A home for another host must be an absolute path or start with ~/", { provider, account: id, hostname, home: homePath });
|
|
420
|
+
}
|
|
421
|
+
for (const [otherId, other] of Object.entries(config.providers[provider].accounts)) {
|
|
422
|
+
if (otherId !== id && effectiveAccountHome(other, hostname).home === home) {
|
|
423
|
+
throw new LanesError("duplicate_account_home", `Home ${home} is already registered as ${provider} account ${otherId} on host ${hostname}`, { provider, accounts: [otherId, id], home, hostname });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (home === account.home) {
|
|
427
|
+
if (account.hosts !== undefined) {
|
|
428
|
+
delete account.hosts[hostname];
|
|
429
|
+
if (Object.keys(account.hosts).length === 0)
|
|
430
|
+
delete account.hosts;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
const hosts = Object.assign(Object.create(null), account.hosts ?? {});
|
|
435
|
+
hosts[hostname] = { home };
|
|
436
|
+
account.hosts = hosts;
|
|
437
|
+
}
|
|
438
|
+
assertUniqueAccountHomes(provider, config.providers[provider].accounts);
|
|
439
|
+
writeLanesConfig(hqRoot, config);
|
|
440
|
+
printJson({
|
|
441
|
+
ok: true,
|
|
442
|
+
action: "accounts.set-home",
|
|
443
|
+
provider,
|
|
444
|
+
account: id,
|
|
445
|
+
hostname,
|
|
446
|
+
home,
|
|
447
|
+
home_source: home === account.home ? "default" : "host",
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
async function runAccountsEnable(ctx, providerRaw, accountId) {
|
|
451
|
+
const hqRoot = ctx.hqRoot();
|
|
452
|
+
const provider = parseLaneProvider(providerRaw);
|
|
453
|
+
const config = loadLanesConfig(hqRoot);
|
|
454
|
+
const account = config.providers[provider].accounts[accountId];
|
|
455
|
+
if (!account) {
|
|
456
|
+
throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(accountId)} for ${provider}`, {
|
|
457
|
+
provider,
|
|
458
|
+
account: accountId,
|
|
459
|
+
enabled: Object.entries(config.providers[provider].accounts)
|
|
460
|
+
.filter(([, a]) => a.enabled)
|
|
461
|
+
.map(([id]) => id),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
const status = accountStatus(provider, accountId, config, {
|
|
465
|
+
existsSync: ctx.existsSync,
|
|
466
|
+
hostname: ctx.hostname(),
|
|
467
|
+
probe: ctx.probeProvider,
|
|
468
|
+
env: ctx.env,
|
|
469
|
+
});
|
|
470
|
+
if (status.logged_in !== true) {
|
|
471
|
+
throw new LanesError(status.logged_in === false ? "account_not_logged_in" : "account_login_status_unknown", status.logged_in === false
|
|
472
|
+
? `Account ${accountId} for ${provider} is not logged in`
|
|
473
|
+
: `Could not verify that account ${accountId} for ${provider} is logged in`, { provider, account: accountId });
|
|
474
|
+
}
|
|
475
|
+
account.enabled = true;
|
|
476
|
+
delete account.disabled_reason;
|
|
477
|
+
writeLanesConfig(hqRoot, config);
|
|
478
|
+
printJson({
|
|
479
|
+
ok: true,
|
|
480
|
+
action: "accounts.enable",
|
|
481
|
+
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
|
|
482
|
+
existsSync: ctx.existsSync,
|
|
483
|
+
hostname: ctx.hostname(),
|
|
484
|
+
probe: ctx.probeProvider,
|
|
485
|
+
env: ctx.env,
|
|
486
|
+
}),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
async function runAccountsDisable(ctx, providerRaw, accountId, opts) {
|
|
490
|
+
const hqRoot = ctx.hqRoot();
|
|
491
|
+
const provider = parseLaneProvider(providerRaw);
|
|
492
|
+
const config = loadLanesConfig(hqRoot);
|
|
493
|
+
const account = config.providers[provider].accounts[accountId];
|
|
494
|
+
if (!account) {
|
|
495
|
+
throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(accountId)} for ${provider}`, {
|
|
496
|
+
provider,
|
|
497
|
+
account: accountId,
|
|
498
|
+
enabled: Object.entries(config.providers[provider].accounts)
|
|
499
|
+
.filter(([, a]) => a.enabled)
|
|
500
|
+
.map(([id]) => id),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
account.enabled = false;
|
|
504
|
+
if (opts.reason)
|
|
505
|
+
account.disabled_reason = opts.reason;
|
|
506
|
+
writeLanesConfig(hqRoot, config);
|
|
507
|
+
printJson({
|
|
508
|
+
ok: true,
|
|
509
|
+
action: "accounts.disable",
|
|
510
|
+
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
|
|
511
|
+
existsSync: ctx.existsSync,
|
|
512
|
+
hostname: ctx.hostname(),
|
|
513
|
+
probe: ctx.probeProvider,
|
|
514
|
+
env: ctx.env,
|
|
515
|
+
}),
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
runAccountsList,
|
|
520
|
+
runAccountsBalance,
|
|
521
|
+
runAccountsLogin,
|
|
522
|
+
runAccountsSetHome,
|
|
523
|
+
runAccountsEnable,
|
|
524
|
+
runAccountsDisable
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
//# sourceMappingURL=lanes-accounts.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type QuestionBody, type QuestionRecord } from "../lib/lanes/index.js";
|
|
2
|
+
import type { Ctx, LaneQuestionsHandlerDeps } from "./lanes-shared.js";
|
|
3
|
+
export declare function resolveAnswerChoice(choice: string, options: string[], freeform: boolean): string;
|
|
4
|
+
export declare function createLaneQuestionsHandlers(deps: LaneQuestionsHandlerDeps): {
|
|
5
|
+
runQuestionsList: (ctx: Ctx, opts: {
|
|
6
|
+
allHosts?: boolean;
|
|
7
|
+
for?: string;
|
|
8
|
+
from?: string;
|
|
9
|
+
company?: string;
|
|
10
|
+
project?: string;
|
|
11
|
+
story?: string;
|
|
12
|
+
status?: string;
|
|
13
|
+
}) => Promise<void>;
|
|
14
|
+
runQuestionsShow: (ctx: Ctx, questionId: string) => Promise<void>;
|
|
15
|
+
runQuestionsAnswer: (ctx: Ctx, questionId: string, opts: {
|
|
16
|
+
choice?: string;
|
|
17
|
+
notes?: string;
|
|
18
|
+
freeform?: string;
|
|
19
|
+
company?: string;
|
|
20
|
+
}) => Promise<void>;
|
|
21
|
+
runQuestionsEscalate: (ctx: Ctx, questionId: string, opts: {
|
|
22
|
+
note?: string;
|
|
23
|
+
}) => Promise<void>;
|
|
24
|
+
runQuestionsCancel: (ctx: Ctx, questionId: string, opts: {
|
|
25
|
+
reason?: string;
|
|
26
|
+
}) => Promise<void>;
|
|
27
|
+
runSessionsList: (ctx: Ctx, opts: {
|
|
28
|
+
state?: string;
|
|
29
|
+
company?: string;
|
|
30
|
+
}) => Promise<void>;
|
|
31
|
+
runSessionsShow: (ctx: Ctx, sessionId: string) => Promise<void>;
|
|
32
|
+
requireQuestionBody: (hqRoot: string, question: QuestionRecord) => QuestionBody;
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=lanes-questions.d.ts.map
|