@shardflux/cli 0.20.0 → 0.22.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 +214 -1
- package/README.md +215 -75
- package/dist/account-args.js +7 -2
- package/dist/account-commands.js +56 -106
- package/dist/account-pool.d.ts +19 -15
- package/dist/account-pool.js +47 -96
- package/dist/account-scope.d.ts +65 -0
- package/dist/account-scope.js +246 -0
- package/dist/acp-login.d.ts +4 -3
- package/dist/acp-login.js +7 -5
- package/dist/acp-models.js +20 -15
- package/dist/acp.js +66 -13
- package/dist/agent-commands.d.ts +4 -1
- package/dist/agent-commands.js +86 -32
- package/dist/agent-config-return.d.ts +46 -2
- package/dist/agent-config-return.js +110 -4
- package/dist/agent-credentials.js +7 -3
- package/dist/agent-repository.d.ts +16 -3
- package/dist/agent-repository.js +149 -74
- package/dist/agent-setup.d.ts +14 -3
- package/dist/agent-setup.js +78 -36
- package/dist/agent-target-commands.js +24 -13
- package/dist/args.d.ts +1 -1
- package/dist/args.js +94 -54
- package/dist/bin.js +5 -1
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -14
- package/dist/claude-cloud-login.d.ts +24 -2
- package/dist/claude-cloud-login.js +151 -11
- package/dist/cli-proxy-forward.d.ts +2 -0
- package/dist/cli-proxy-forward.js +9 -2
- package/dist/cli-proxy-hub.d.ts +69 -25
- package/dist/cli-proxy-hub.js +231 -54
- package/dist/cli-proxy.d.ts +70 -1
- package/dist/cli-proxy.js +298 -32
- package/dist/client-settings.d.ts +39 -0
- package/dist/client-settings.js +91 -0
- package/dist/codex-cloud.d.ts +5 -0
- package/dist/codex-cloud.js +88 -17
- package/dist/command-center-actions.js +5 -1
- package/dist/commands.js +171 -22
- package/dist/conversation-commands.d.ts +5 -0
- package/dist/conversation-commands.js +8 -5
- package/dist/conversation-terminal.d.ts +10 -2
- package/dist/conversation-terminal.js +77 -13
- package/dist/conversations.d.ts +73 -1
- package/dist/conversations.js +257 -23
- package/dist/device-bridge.d.ts +1 -1
- package/dist/device-bridge.js +6 -4
- package/dist/device-helper.js +43 -22
- package/dist/errors.d.ts +27 -1
- package/dist/errors.js +45 -5
- package/dist/filesystem-commands.d.ts +16 -9
- package/dist/filesystem-commands.js +108 -54
- package/dist/format.d.ts +2 -1
- package/dist/format.js +10 -1
- package/dist/handlers.js +3 -1
- package/dist/initial-prompt.d.ts +8 -0
- package/dist/initial-prompt.js +43 -1
- package/dist/large-media.d.ts +14 -0
- package/dist/large-media.js +100 -0
- package/dist/linux-terminal.d.ts +5 -3
- package/dist/linux-terminal.js +6 -20
- package/dist/local-sessions.d.ts +26 -2
- package/dist/local-sessions.js +113 -24
- package/dist/machine-setup.d.ts +11 -1
- package/dist/machine-setup.js +69 -22
- package/dist/machine-status.d.ts +67 -0
- package/dist/machine-status.js +140 -0
- package/dist/main.js +11 -13
- package/dist/move-worktrees-restore.py +144 -0
- package/dist/move-worktrees.d.ts +63 -0
- package/dist/move-worktrees.js +201 -0
- package/dist/move.d.ts +10 -1
- package/dist/move.js +116 -16
- package/dist/onboarding.d.ts +11 -7
- package/dist/onboarding.js +59 -27
- package/dist/prepared-repository.d.ts +202 -0
- package/dist/prepared-repository.js +860 -0
- package/dist/prepared-repository.py +1306 -0
- package/dist/project-preparation.d.ts +48 -0
- package/dist/project-preparation.js +304 -0
- package/dist/project-sync-manifest.d.ts +4 -0
- package/dist/project-sync-manifest.js +5 -1
- package/dist/project-sync-transfer.js +1 -1
- package/dist/project-sync.d.ts +19 -4
- package/dist/project-sync.js +31 -8
- package/dist/project-template-setup.d.ts +33 -1
- package/dist/project-template-setup.js +59 -2
- package/dist/secret-ask.d.ts +72 -0
- package/dist/secret-ask.js +351 -0
- package/dist/seed-phases.d.ts +8 -1
- package/dist/seed-phases.js +17 -2
- package/dist/setup-cache.d.ts +7 -0
- package/dist/setup-cache.js +32 -0
- package/dist/t3-launch.js +1 -4
- package/dist/t3-mode.d.ts +8 -2
- package/dist/t3-mode.js +19 -4
- package/dist/t3-phone.d.ts +4 -2
- package/dist/t3-phone.js +12 -4
- package/dist/terminal-apps.d.ts +157 -0
- package/dist/terminal-apps.js +561 -0
- package/dist/terminal-onboarding.js +6 -1
- package/dist/this-computer.d.ts +11 -0
- package/dist/this-computer.js +36 -0
- package/dist/thread-on-computer.js +4 -1
- package/dist/update.js +11 -3
- package/dist/worktree-twin.d.ts +10 -2
- package/dist/worktree-twin.js +27 -9
- package/package.json +5 -5
package/dist/account-commands.js
CHANGED
|
@@ -21,6 +21,7 @@ import { CLI_VERSION, UUID, parseDuration, parsePositiveInt } from "./args.js";
|
|
|
21
21
|
import { openUrl } from "./browser.js";
|
|
22
22
|
import { defaultClientName, startDeviceAuthorization, waitForDeviceSession } from "./browser-login.js";
|
|
23
23
|
import { setupGuide, setupGuideText } from "./agent-setup.js";
|
|
24
|
+
import { allOrgs, allProjects, byRef, choices, findOrg, gone, keyOrganizationWarning, keyScope, resolveOrg, resolveProject, staleDefaultOrg } from "./account-scope.js";
|
|
24
25
|
import { readFileSync, rmSync } from 'node:fs';
|
|
25
26
|
import { join } from 'node:path';
|
|
26
27
|
import { legalFields, requireTermsAcceptance, showLegalNotice } from "./legal.js";
|
|
@@ -135,100 +136,6 @@ async function withStepUp(ctx, account, call) {
|
|
|
135
136
|
return call();
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
|
-
// ---- organization / project resolution ------------------------------------------------------------------------
|
|
139
|
-
async function allOrgs(account) {
|
|
140
|
-
const rows = [];
|
|
141
|
-
for await (const o of account.organizations.listAll())
|
|
142
|
-
rows.push(o);
|
|
143
|
-
return rows;
|
|
144
|
-
}
|
|
145
|
-
async function allProjects(account, organizationId) {
|
|
146
|
-
const rows = [];
|
|
147
|
-
for await (const p of account.projects.listAll(organizationId))
|
|
148
|
-
rows.push(p);
|
|
149
|
-
return rows;
|
|
150
|
-
}
|
|
151
|
-
const choices = (rows) => rows.map((r) => ` ${r.id} ${r.slug} ${r.name}`).join('\n');
|
|
152
|
-
function byRef(rows, ref) {
|
|
153
|
-
const exact = rows.find((r) => r.id === ref || r.slug === ref);
|
|
154
|
-
if (exact)
|
|
155
|
-
return exact;
|
|
156
|
-
const named = rows.filter((r) => r.name.toLowerCase() === ref.toLowerCase());
|
|
157
|
-
return named.length === 1 ? named[0] : undefined;
|
|
158
|
-
}
|
|
159
|
-
const gone = (err) => err instanceof ShardfluxApiError && (err.status === 404 || err.status === 403);
|
|
160
|
-
async function findOrg(account, ref) {
|
|
161
|
-
if (UUID.test(ref))
|
|
162
|
-
return account.organizations.get(ref);
|
|
163
|
-
const hit = byRef(await allOrgs(account), ref);
|
|
164
|
-
if (!hit)
|
|
165
|
-
throw new NotFoundError(`No organization "${ref}" among yours.`, 'list them: shard orgs list');
|
|
166
|
-
return hit;
|
|
167
|
-
}
|
|
168
|
-
/** `ref` (a positional), else --org, else the saved default, else the only organization. */
|
|
169
|
-
async function resolveOrg(ctx, account, values, ref) {
|
|
170
|
-
const given = ref ?? str(values.org);
|
|
171
|
-
if (given !== undefined)
|
|
172
|
-
return findOrg(account, given);
|
|
173
|
-
const saved = ctx.store.profile(ctx.apiUrl).organization_id;
|
|
174
|
-
if (saved) {
|
|
175
|
-
try {
|
|
176
|
-
return await account.organizations.get(saved);
|
|
177
|
-
}
|
|
178
|
-
catch (err) {
|
|
179
|
-
if (gone(err))
|
|
180
|
-
throw new NotFoundError(`The default organization ${saved} is no longer available to you.`, 'pick another: shard orgs use <org> (list: shard orgs list)');
|
|
181
|
-
throw err;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
const orgs = await allOrgs(account);
|
|
185
|
-
if (orgs.length === 1)
|
|
186
|
-
return orgs[0];
|
|
187
|
-
if (orgs.length === 0)
|
|
188
|
-
throw new UsageError('You have no organization yet.', 'create one with a project and an API key: shard setup (or shard orgs create <name>)');
|
|
189
|
-
throw new UsageError(`You belong to ${orgs.length} organizations; pick one with --org <id|slug>:\n${choices(orgs)}\n`, 'or save a default: shard orgs use <id|slug>');
|
|
190
|
-
}
|
|
191
|
-
/** `ref` (a positional), else --project, else the saved default (unless --org names another), else the only project. */
|
|
192
|
-
async function resolveProject(ctx, account, values, ref) {
|
|
193
|
-
const given = ref ?? str(values.project);
|
|
194
|
-
const orgFlag = str(values.org);
|
|
195
|
-
if (given !== undefined && UUID.test(given)) {
|
|
196
|
-
const p = await account.projects.get(given);
|
|
197
|
-
if (orgFlag !== undefined) {
|
|
198
|
-
const org = await findOrg(account, orgFlag);
|
|
199
|
-
if (p.organization_id !== org.id)
|
|
200
|
-
throw new UsageError(`project ${given} is not in organization ${org.slug}`);
|
|
201
|
-
}
|
|
202
|
-
return p;
|
|
203
|
-
}
|
|
204
|
-
const profile = ctx.store.profile(ctx.apiUrl);
|
|
205
|
-
if (given === undefined && profile.project_id && orgFlag === undefined) {
|
|
206
|
-
try {
|
|
207
|
-
return await account.projects.get(profile.project_id);
|
|
208
|
-
}
|
|
209
|
-
catch (err) {
|
|
210
|
-
if (gone(err))
|
|
211
|
-
throw new NotFoundError(`The default project ${profile.project_id} is no longer available to you.`, 'pick another: shard projects use <project> (list: shard projects list)');
|
|
212
|
-
throw err;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
const org = await resolveOrg(ctx, account, values);
|
|
216
|
-
const projects = await allProjects(account, org.id);
|
|
217
|
-
if (given !== undefined) {
|
|
218
|
-
const hit = byRef(projects, given);
|
|
219
|
-
if (!hit)
|
|
220
|
-
throw new NotFoundError(`No project "${given}" in ${org.name} (${org.slug}).`, 'list them: shard projects list');
|
|
221
|
-
return hit;
|
|
222
|
-
}
|
|
223
|
-
const saved = profile.project_id ? projects.find((p) => p.id === profile.project_id) : undefined;
|
|
224
|
-
if (saved)
|
|
225
|
-
return saved;
|
|
226
|
-
if (projects.length === 1)
|
|
227
|
-
return projects[0];
|
|
228
|
-
if (projects.length === 0)
|
|
229
|
-
throw new UsageError(`${org.name} (${org.slug}) has no project yet.`, 'create one: shard projects create <name> (or shard setup)');
|
|
230
|
-
throw new UsageError(`${org.name} has ${projects.length} projects; pick one with --project <id|slug>:\n${choices(projects)}\n`, 'or save a default: shard projects use <id|slug>');
|
|
231
|
-
}
|
|
232
139
|
/** Removes a saved default (and the saved key) that pointed into an organization that is gone. */
|
|
233
140
|
function forgetOrganization(ctx, organizationId, projectIds) {
|
|
234
141
|
ctx.store.update(ctx.apiUrl, (p) => {
|
|
@@ -593,30 +500,40 @@ async function runSetup(ctx, values, account) {
|
|
|
593
500
|
if (!gone(err))
|
|
594
501
|
throw err;
|
|
595
502
|
if (!ctx.json)
|
|
596
|
-
ctx.io.stderr.write(`note: the saved API key ${profile.api_key.key_id} belongs to
|
|
503
|
+
ctx.io.stderr.write(`note: the saved API key ${profile.api_key.key_id} belongs to organization ${keyCheck.organizationId}, which you are no longer in; making a key for one of yours.\n`);
|
|
597
504
|
keyCheck = { ok: false, reason: 'another account' };
|
|
598
505
|
}
|
|
599
506
|
}
|
|
507
|
+
// Several organizations: which one this setup picked, and how that choice is kept and changed (0.20.2+).
|
|
508
|
+
let orgCount = 1;
|
|
600
509
|
if (home) {
|
|
601
510
|
org = home.org;
|
|
602
511
|
project = home.project;
|
|
603
512
|
}
|
|
604
513
|
else {
|
|
605
514
|
const orgs = await allOrgs(account);
|
|
515
|
+
orgCount = orgs.length;
|
|
606
516
|
const orgRef = str(values.org);
|
|
607
517
|
const savedOrg = profile.organization_id ? orgs.find((o) => o.id === profile.organization_id) : undefined;
|
|
608
518
|
if (orgRef !== undefined)
|
|
609
519
|
org = await findOrg(account, orgRef);
|
|
610
520
|
else if (savedOrg)
|
|
611
521
|
org = savedOrg;
|
|
612
|
-
else if (orgs.length
|
|
522
|
+
else if (profile.organization_id && orgs.length > 1)
|
|
523
|
+
throw staleDefaultOrg(profile.organization_id, orgs);
|
|
524
|
+
else if (orgs.length === 1) {
|
|
613
525
|
org = orgs[0];
|
|
526
|
+
if (profile.organization_id && profile.organization_id !== org.id && !ctx.json)
|
|
527
|
+
ctx.io.stderr.write(`note: your saved default organization ${profile.organization_id} is no longer one of yours; using ${org.name} (${org.slug}).\n`);
|
|
528
|
+
}
|
|
614
529
|
else if (orgs.length === 0) {
|
|
615
530
|
org = await account.organizations.create({ name: str(values['org-name'])?.trim() || defaultOrgName(me.user) });
|
|
616
531
|
created.organization = true;
|
|
617
532
|
}
|
|
618
|
-
else
|
|
619
|
-
throw new UsageError(`You belong to ${orgs.length} organizations; pick one with --org <id|slug>:\n${choices(orgs)}\n
|
|
533
|
+
else {
|
|
534
|
+
throw new UsageError(`You belong to ${orgs.length} organizations; pick one with --org <id|slug>:\n${choices(orgs)}\n` +
|
|
535
|
+
`The one you pick becomes this computer's default, with an API key for it (saved in ${ctx.store.file}); switch later: shard setup --org <id|slug> --new-key\n`, 'or save a default first: shard orgs use <id|slug>');
|
|
536
|
+
}
|
|
620
537
|
const projects = created.organization ? [] : await allProjects(account, org.id);
|
|
621
538
|
const projectRef = str(values.project);
|
|
622
539
|
const savedProject = profile.project_id ? projects.find((p) => p.id === profile.project_id) : undefined;
|
|
@@ -680,6 +597,7 @@ async function runSetup(ctx, values, account) {
|
|
|
680
597
|
codex = { ...await cloud.codex.waitConnector({ timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal }) };
|
|
681
598
|
}
|
|
682
599
|
const envNote = ctx.io.env.SHARDFLUX_API_KEY?.trim() ? 'note: SHARDFLUX_API_KEY is set in this environment and wins over the saved key; unset it to use the saved key.\n' : '';
|
|
600
|
+
const warnings = envNote ? [{ code: 'api_key_environment_override', message: 'SHARDFLUX_API_KEY is set in this environment and wins over the saved key.', fix: 'unset SHARDFLUX_API_KEY' }] : [];
|
|
683
601
|
return {
|
|
684
602
|
text: (keep ? 'Already set up; the saved API key works.\n' : 'Set up.\n') +
|
|
685
603
|
kv([
|
|
@@ -688,6 +606,7 @@ async function runSetup(ctx, values, account) {
|
|
|
688
606
|
['api key', `"${key.name}" ${key.key_id} (tools: ${toolsText(permissions)}) ${keep ? 'saved earlier' : 'created'}, saved in ${ctx.store.file}`],
|
|
689
607
|
]) +
|
|
690
608
|
envNote +
|
|
609
|
+
(orgCount > 1 ? `${org.name} is your default organization on this computer (saved in ${ctx.store.file}, with the key); switch: shard setup --org <id|slug> --new-key\n` : '') +
|
|
691
610
|
(codex ? 'Codex Cloud is ready. One account executor; no pairing. Its residency is billed as workspace usage.\n' : '') +
|
|
692
611
|
nextLines(),
|
|
693
612
|
data: {
|
|
@@ -699,6 +618,7 @@ async function runSetup(ctx, values, account) {
|
|
|
699
618
|
credentials_file: ctx.store.file,
|
|
700
619
|
next: NEXT_WORKSPACE,
|
|
701
620
|
next_steps: NEXT_STEPS,
|
|
621
|
+
...(warnings.length ? { warnings } : {}),
|
|
702
622
|
},
|
|
703
623
|
};
|
|
704
624
|
}
|
|
@@ -767,7 +687,7 @@ async function browserSignIn(ctx, values, noticeShown = false) {
|
|
|
767
687
|
else if (!noticeShown)
|
|
768
688
|
showLegalNotice(ctx);
|
|
769
689
|
// A resumed request was opened (or printed) by the command that started it.
|
|
770
|
-
const opened = !resumed && values.open !== false && openUrl(d.verification_uri_complete, ctx.io.env);
|
|
690
|
+
const opened = !resumed && values.open !== false && (await openUrl(d.verification_uri_complete, ctx.io.env));
|
|
771
691
|
const minutes = Math.max(1, Math.round(d.expires_in / 60));
|
|
772
692
|
if (!ctx.json) {
|
|
773
693
|
ctx.io.stderr.write(`${resumed ? 'Continuing the sign-in started earlier' : opened ? 'Opening your browser to sign in' : 'Open this page to sign in'}: ${d.verification_uri_complete}\n` +
|
|
@@ -852,6 +772,32 @@ export function agentSetupGuide(ctx) {
|
|
|
852
772
|
const profile = ctx.store.profile(ctx.apiUrl);
|
|
853
773
|
return setupGuide({ signedIn: ctx.sessionSource() !== null, email: profile.session?.user?.email ?? null, apiKey: profile.api_key !== undefined || Boolean(ctx.io.env.SHARDFLUX_API_KEY?.trim()) });
|
|
854
774
|
}
|
|
775
|
+
/**
|
|
776
|
+
* After `orgs use` / `projects use` (0.20.2+): the defaults moved, but workspace, agent and usage commands use the API
|
|
777
|
+
* key, which may act in another organization. Says so; on a terminal offers to make a key for the new default (a
|
|
778
|
+
* `shard setup --new-key` for it), otherwise names that command. Nothing when the key is already there (or none).
|
|
779
|
+
*/
|
|
780
|
+
async function keyElsewhereOffer(ctx, account, orgId, loadOrg, setupValues) {
|
|
781
|
+
const key = await keyScope(ctx);
|
|
782
|
+
if (key === null || key.organizationId === orgId)
|
|
783
|
+
return { text: '', data: {} };
|
|
784
|
+
const org = await loadOrg();
|
|
785
|
+
const w = keyOrganizationWarning(key, await allOrgs(account), org, str(setupValues.project));
|
|
786
|
+
const { fix, message: warning } = w;
|
|
787
|
+
if (key.source === 'file' && ctx.input.interactive && !ctx.json) {
|
|
788
|
+
ctx.io.stderr.write(`${warning}\n`);
|
|
789
|
+
const answer = (await ctx.input.answer(`Make an API key for ${org.name} and save it in place of ${key.keyId}? [y/N] `))?.trim() ?? '';
|
|
790
|
+
if (/^y(es)?$/i.test(answer)) {
|
|
791
|
+
const r = await runSetup(ctx, { ...setupValues, 'new-key': true, codex: false }, account);
|
|
792
|
+
return { text: r.text, data: { setup: r.data } };
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
else if (!ctx.json)
|
|
796
|
+
ctx.io.stderr.write(`warning: ${warning}\n`);
|
|
797
|
+
if (!ctx.json)
|
|
798
|
+
ctx.io.stderr.write(`fix: ${fix}\n`);
|
|
799
|
+
return { text: '', data: { api_key: { key_id: key.keyId, source: key.source, organization_id: key.organizationId, project_id: key.projectId }, warnings: [w] } };
|
|
800
|
+
}
|
|
855
801
|
// ---- handlers --------------------------------------------------------------------------------------------------
|
|
856
802
|
export const ACCOUNT_HANDLERS = {
|
|
857
803
|
async 'auth register'(ctx, values) {
|
|
@@ -1358,13 +1304,15 @@ export const ACCOUNT_HANDLERS = {
|
|
|
1358
1304
|
return 0;
|
|
1359
1305
|
},
|
|
1360
1306
|
async 'orgs use'(ctx, values, [ref]) {
|
|
1361
|
-
const
|
|
1307
|
+
const account = ctx.account();
|
|
1308
|
+
const org = await resolveOrg(ctx, account, values, ref);
|
|
1362
1309
|
ctx.store.update(ctx.apiUrl, (p) => {
|
|
1363
1310
|
if (p.organization_id !== org.id)
|
|
1364
1311
|
delete p.project_id;
|
|
1365
1312
|
p.organization_id = org.id;
|
|
1366
1313
|
});
|
|
1367
|
-
|
|
1314
|
+
const key = await keyElsewhereOffer(ctx, account, org.id, () => Promise.resolve(org), { org: org.id });
|
|
1315
|
+
out(ctx, `Default organization: ${org.name} (${org.slug}, ${org.id}).\n${key.text}`, { default_organization_id: org.id, organization: org, ...key.data });
|
|
1368
1316
|
return 0;
|
|
1369
1317
|
},
|
|
1370
1318
|
async 'orgs export'(ctx, values, [ref]) {
|
|
@@ -1479,12 +1427,14 @@ export const ACCOUNT_HANDLERS = {
|
|
|
1479
1427
|
return 0;
|
|
1480
1428
|
},
|
|
1481
1429
|
async 'projects use'(ctx, values, [ref]) {
|
|
1482
|
-
const
|
|
1430
|
+
const account = ctx.account();
|
|
1431
|
+
const p = await resolveProject(ctx, account, values, ref);
|
|
1483
1432
|
ctx.store.update(ctx.apiUrl, (x) => {
|
|
1484
1433
|
x.organization_id = p.organization_id;
|
|
1485
1434
|
x.project_id = p.id;
|
|
1486
1435
|
});
|
|
1487
|
-
|
|
1436
|
+
const key = await keyElsewhereOffer(ctx, account, p.organization_id, () => account.organizations.get(p.organization_id), { project: p.id });
|
|
1437
|
+
out(ctx, `Default project: ${p.name} (${p.slug}, ${p.id}) of organization ${p.organization_id}.\n${key.text}`, { default_project_id: p.id, default_organization_id: p.organization_id, project: p, ...key.data });
|
|
1488
1438
|
return 0;
|
|
1489
1439
|
},
|
|
1490
1440
|
async 'api-keys list'(ctx, values) {
|
|
@@ -1673,13 +1623,13 @@ export const ACCOUNT_HANDLERS = {
|
|
|
1673
1623
|
if (!(err instanceof ShardfluxApiError && err.code === 'conflict' && err.details?.reason === 'subscription_exists'))
|
|
1674
1624
|
throw err;
|
|
1675
1625
|
const portal = await account.billing.portal(org.id);
|
|
1676
|
-
const opened = values.open === true && openUrl(portal.url, ctx.io.env);
|
|
1626
|
+
const opened = values.open === true && (await openUrl(portal.url, ctx.io.env));
|
|
1677
1627
|
out(ctx, `${org.name} already has a subscription: plan changes happen in the Stripe billing portal (it shows the new price before you confirm):\n${portal.url}\n${values.open === true ? (opened ? 'Opened it in your browser.\n' : 'Could not open a browser; open the URL yourself.\n') : ''}`, { subscription_exists: true, organization_id: org.id, plan_key: plan.plan_key, portal_url: portal.url, opened });
|
|
1678
1628
|
return 0;
|
|
1679
1629
|
}
|
|
1680
1630
|
if (!checkout.url && !checkout.subscription_active)
|
|
1681
1631
|
throw new UsageError(`checkout ${checkout.id} is ${checkout.status} and has no URL; start again: shard billing upgrade ${plan.plan_key}`);
|
|
1682
|
-
const opened = values.open === true && checkout.url !== null && openUrl(checkout.url, ctx.io.env);
|
|
1632
|
+
const opened = values.open === true && checkout.url !== null && (await openUrl(checkout.url, ctx.io.env));
|
|
1683
1633
|
if (!ctx.json) {
|
|
1684
1634
|
ctx.io.stdout.write(`Checkout for ${plan.name} (${plan.plan_key}, ${priceText(catalog, plan.plan_key)}) for ${org.name}:\n${checkout.url ?? '-'}\n` +
|
|
1685
1635
|
`A person pays there (Stripe Checkout${checkout.expires_at ? `; the link works until ${time(checkout.expires_at)}` : ''}). The plan is active once Stripe confirms the payment${values.wait === true ? '.' : ': shard billing status'}\n` +
|
|
@@ -1699,7 +1649,7 @@ export const ACCOUNT_HANDLERS = {
|
|
|
1699
1649
|
const account = ctx.account();
|
|
1700
1650
|
const org = await resolveOrg(ctx, account, values);
|
|
1701
1651
|
const portal = await account.billing.portal(org.id);
|
|
1702
|
-
const opened = values.open === true && openUrl(portal.url, ctx.io.env);
|
|
1652
|
+
const opened = values.open === true && (await openUrl(portal.url, ctx.io.env));
|
|
1703
1653
|
out(ctx, `Billing portal of ${org.name} (plan changes, payment methods, invoices, cancellation; the link is short-lived):\n${portal.url}\n${values.open === true ? (opened ? 'Opened it in your browser.\n' : 'Could not open a browser; open the URL yourself.\n') : ''}`, { organization_id: org.id, url: portal.url, opened });
|
|
1704
1654
|
return 0;
|
|
1705
1655
|
},
|
package/dist/account-pool.d.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `shard agents accounts` (0.18.0+)
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* `shard agents accounts` (0.18.0+): the organization's Claude and ChatGPT/Codex accounts, with each one's usage
|
|
3
|
+
* windows, reset times and status, and which one is active. Each provider has one active account: new conversations
|
|
4
|
+
* start with its login and running conversation VMs get it. The person chooses it (`shard agents accounts use`);
|
|
5
|
+
* nothing switches on usage (switching accounts automatically because usage runs out is against the providers' terms).
|
|
6
|
+
* With --json every write prints the updated `{accounts, pool}` block (the menu bar redraws from it), plus the account
|
|
7
|
+
* it changed. Sign-ins run the provider's own login in a workspace, as `shard agents credentials login claude` does.
|
|
6
8
|
*/
|
|
7
|
-
import type { AgentAccount, AgentAccountList } from '@shardflux/sdk';
|
|
9
|
+
import type { AgentAccount, AgentAccountList, AgentAccountWindow } from '@shardflux/sdk';
|
|
8
10
|
import type { Ctx, Handler } from './commands.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Codex names its windows primary and secondary; their length says which is the 5-hour and which the weekly one (a
|
|
13
|
+
* ChatGPT Pro Max login's primary window is its weekly one). Unknown length: the provider's name.
|
|
14
|
+
*/
|
|
15
|
+
export declare const windowLabel: (w: AgentAccountWindow) => string;
|
|
11
16
|
/** "in 2h 10m", "in 4d 8h", "in 41m"; "now" once passed. */
|
|
12
17
|
export declare function resetIn(resetsAt: string | null, now: number): string | null;
|
|
13
18
|
export declare const planName: (planType: string) => string;
|
|
19
|
+
/** The provider's active account; an API before 2026-10-12 sends only `preferred` (the same account). */
|
|
20
|
+
export declare const isActive: (a: AgentAccount) => boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the caller can make it the active account: the API says so (`can_activate`, 2026-10-12+); an API from before
|
|
23
|
+
* it, by the same rules as far as the list shows them (a working subscription sign-in, not active, team or yours).
|
|
24
|
+
*/
|
|
25
|
+
export declare const canActivate: (a: AgentAccount) => boolean;
|
|
14
26
|
/** Account references: the id, `<provider>:<n>` (its place in the list, from 1), the email or the label. */
|
|
15
27
|
export declare function findAccount(list: AgentAccountList, ref: string): AgentAccount;
|
|
16
28
|
/** The compact list: one row per account, grouped by provider. */
|
|
@@ -22,11 +34,3 @@ export declare function codexLoginUrls(output: string): string[];
|
|
|
22
34
|
export declare const ACCOUNT_POOL_HANDLERS: Record<string, Handler>;
|
|
23
35
|
/** The `accounts` block of `shard agents targets --json` (0.18.0+): the account list for the menu bar; null when it cannot be read. */
|
|
24
36
|
export declare function targetsAccounts(ctx: Ctx): Promise<AgentAccountList | null>;
|
|
25
|
-
/** The conversation's thread runs on the account pool (its `account.mode`, API with the pool). */
|
|
26
|
-
export declare function conversationPooled(conversation: unknown): boolean;
|
|
27
|
-
/** A pooled thread has no provider login on its session, so claude.ai, Claude Desktop and the Codex app cannot reach it. */
|
|
28
|
-
export declare const pooledNeedsT3: (ref: string) => CodedError;
|
|
29
|
-
/** Test seam: the hub cloud the pool toggle ensures (the SDK's by default). */
|
|
30
|
-
export declare const accountPoolSeams: {
|
|
31
|
-
hubCloud?: (ctx: Ctx) => HubCloud;
|
|
32
|
-
};
|
package/dist/account-pool.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { CodedError, UsageError, describeError } from "./errors.js";
|
|
1
|
+
import { out, resolveWorkspace } from "./commands.js";
|
|
2
|
+
import { UsageError } from "./errors.js";
|
|
4
3
|
import { claudeLoginUrls, requireClaudeTerminal, runWorkspaceLogin } from "./claude-cloud-login.js";
|
|
5
4
|
import { openMachine } from "./filesystem-commands.js";
|
|
6
|
-
import { t3Install } from "./t3-mode.js";
|
|
7
|
-
import { ensureCliProxyHub, sdkHubCloud } from "./cli-proxy-hub.js";
|
|
8
|
-
import { homeProject } from "./cli-proxy.js";
|
|
9
|
-
import { progress } from "./progress.js";
|
|
10
5
|
const PROVIDERS = { claude: 'Claude', codex: 'ChatGPT / Codex' };
|
|
11
6
|
const WINDOW_LABELS = { five_hour: '5-hour', seven_day: 'Weekly', seven_day_opus: 'Weekly Opus', seven_day_sonnet: 'Weekly Sonnet', primary: 'Primary', secondary: 'Secondary' };
|
|
12
|
-
/**
|
|
13
|
-
const
|
|
14
|
-
|
|
7
|
+
/** A window's length as a title, as Claude's windows read: "Weekly", "5-hour", "2-day", "90-minute". */
|
|
8
|
+
const lengthLabel = (m) => m === 10080 ? 'Weekly' : m % 1440 === 0 ? `${m / 1440}-day` : m % 60 === 0 ? `${m / 60}-hour` : `${m}-minute`;
|
|
9
|
+
/**
|
|
10
|
+
* Codex names its windows primary and secondary; their length says which is the 5-hour and which the weekly one (a
|
|
11
|
+
* ChatGPT Pro Max login's primary window is its weekly one). Unknown length: the provider's name.
|
|
12
|
+
*/
|
|
13
|
+
export const windowLabel = (w) => (w.window === 'primary' || w.window === 'secondary') && typeof w.window_minutes === 'number' && w.window_minutes > 0
|
|
14
|
+
? lengthLabel(w.window_minutes) : WINDOW_LABELS[w.window] ?? w.window;
|
|
15
15
|
const STATUS_LABELS = { signed_in: 'Signed in', low: 'Low', limited: 'Limited', expired: 'Expired' };
|
|
16
16
|
const CLAUDE_VERSION = '2.1.289';
|
|
17
17
|
function provider(value, what = 'provider') {
|
|
@@ -37,7 +37,14 @@ const pct = (w) => `${Math.round(w.used_percent)}%`;
|
|
|
37
37
|
const PLAN_NAMES = { free: 'Free', go: 'Go', plus: 'Plus', pro: 'Pro', prolite: 'Pro Lite', promax: 'Pro Max', max: 'Max' };
|
|
38
38
|
export const planName = (planType) => PLAN_NAMES[planType] ?? planType.charAt(0).toUpperCase() + planType.slice(1);
|
|
39
39
|
const plan = (a) => a.kind === 'api_key' ? 'API key' : a.plan_type ? planName(a.plan_type) : '';
|
|
40
|
-
const threads = (n) => `${n} ${n === 1 ? '
|
|
40
|
+
const threads = (n) => `${n} ${n === 1 ? 'conversation' : 'conversations'}`;
|
|
41
|
+
/** The provider's active account; an API before 2026-10-12 sends only `preferred` (the same account). */
|
|
42
|
+
export const isActive = (a) => a.active ?? a.preferred;
|
|
43
|
+
/**
|
|
44
|
+
* Whether the caller can make it the active account: the API says so (`can_activate`, 2026-10-12+); an API from before
|
|
45
|
+
* it, by the same rules as far as the list shows them (a working subscription sign-in, not active, team or yours).
|
|
46
|
+
*/
|
|
47
|
+
export const canActivate = (a) => a.can_activate ?? (a.kind === 'oauth' && !isActive(a) && a.status !== 'expired' && (a.scope === 'team' || a.mine));
|
|
41
48
|
const name = (a) => a.email && a.label !== a.email ? `${a.label} (${a.email})` : a.label;
|
|
42
49
|
function bar(used, width = 10) {
|
|
43
50
|
const filled = Math.max(0, Math.min(width, Math.round(used / 100 * width)));
|
|
@@ -72,14 +79,22 @@ export function formatAccounts(list, now) {
|
|
|
72
79
|
for (const a of rows) {
|
|
73
80
|
const windows = a.kind === 'api_key' ? 'Pay as you go' : a.windows.filter(w => ['five_hour', 'seven_day', 'primary', 'secondary'].includes(w.window))
|
|
74
81
|
.map(w => `${windowLabel(w)} ${pct(w)}${resetIn(w.resets_at, now) ? ` (resets ${resetIn(w.resets_at, now) === 'now' ? 'now' : `in ${resetIn(w.resets_at, now)}`})` : ''}`).join(' · ') || 'No usage read yet';
|
|
75
|
-
const tags = [plan(a), a
|
|
82
|
+
const tags = [plan(a), isActive(a) ? 'active' : ''].filter(Boolean).join(', ');
|
|
76
83
|
lines.push(` ${`${p}:${a.position + 1}`.padEnd(9)} ${name(a)}${tags ? ` (${tags})` : ''} · ${STATUS_LABELS[a.status]}${a.status === 'expired' ? ': sign in again with shard agents accounts add ' + p : ''}`);
|
|
77
84
|
lines.push(` ${''.padEnd(9)} ${windows}${asOf(a, now)}`);
|
|
78
85
|
}
|
|
79
86
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
// Only what the caller can do: the accounts the API says they can make active (can_activate).
|
|
88
|
+
for (const p of ['claude', 'codex']) {
|
|
89
|
+
const rows = list.accounts.filter(a => a.provider === p);
|
|
90
|
+
if (!rows.length)
|
|
91
|
+
continue;
|
|
92
|
+
const can = rows.filter(canActivate);
|
|
93
|
+
const ref = can.length === 1 ? `${p}:${can[0].position + 1}` : `${p}:<n>`;
|
|
94
|
+
if (can.length)
|
|
95
|
+
lines.push(rows.some(isActive) ? `Switch ${PROVIDERS[p]}: shard agents accounts use ${ref}` : `No ${PROVIDERS[p]} account is active: shard agents accounts use ${ref}`);
|
|
96
|
+
else if (!rows.some(isActive))
|
|
97
|
+
lines.push(`No ${PROVIDERS[p]} account is active: an organization owner or admin makes one active.`);
|
|
83
98
|
}
|
|
84
99
|
return `${lines.join('\n')}\n`;
|
|
85
100
|
}
|
|
@@ -89,7 +104,7 @@ export function formatUsage(list, now) {
|
|
|
89
104
|
return formatAccounts(list, now);
|
|
90
105
|
const lines = [];
|
|
91
106
|
for (const a of list.accounts) {
|
|
92
|
-
lines.push(`${PROVIDERS[a.provider]} · ${name(a)}${plan(a) ? ` · ${plan(a)}` : ''} · ${STATUS_LABELS[a.status]} · ${threads(a.threads)}
|
|
107
|
+
lines.push(`${PROVIDERS[a.provider]} · ${name(a)}${plan(a) ? ` · ${plan(a)}` : ''} · ${STATUS_LABELS[a.status]}${isActive(a) ? ` · active · ${threads(a.threads)}` : ''}${asOf(a, now)}`);
|
|
93
108
|
if (a.kind === 'api_key')
|
|
94
109
|
lines.push(' Pay as you go');
|
|
95
110
|
else if (!a.windows.length)
|
|
@@ -142,17 +157,17 @@ export function codexLoginUrls(output) {
|
|
|
142
157
|
/**
|
|
143
158
|
* Adds a subscription account: the provider's own sign-in runs in a fresh profile in the workspace (Claude:
|
|
144
159
|
* `claude auth login --claudeai`; Codex: `codex login --device-auth`), its login files go to the API, and the profile
|
|
145
|
-
* is deleted. The workspace's own logins are untouched; the API delivers the account
|
|
160
|
+
* is deleted. The workspace's own logins are untouched; the API delivers the active account's login.
|
|
146
161
|
*/
|
|
147
162
|
async function signIn(ctx, values, app) {
|
|
148
163
|
if (ctx.json || !ctx.io.stdin.isTTY || !ctx.io.stdin.setRawMode || !ctx.io.stdin.on) {
|
|
149
|
-
throw new UsageError(`Signing in to ${PROVIDERS[app]} runs in a terminal: shard agents accounts add ${app}
|
|
164
|
+
throw new UsageError(`Signing in to ${PROVIDERS[app]} runs in a terminal: shard agents accounts add ${app}`);
|
|
150
165
|
}
|
|
151
166
|
if (app === 'claude')
|
|
152
167
|
requireClaudeTerminal(ctx);
|
|
153
168
|
const ws = await signInWorkspace(ctx, values);
|
|
154
169
|
const api = ctx.cloud().agentAccounts;
|
|
155
|
-
const claim = await api.signIn(app, ws.id,
|
|
170
|
+
const claim = await api.signIn(app, ws.id, typeof values.label === 'string' ? { label: values.label } : {});
|
|
156
171
|
const root = `/tmp/shard-account-${claim.sign_in_id}`;
|
|
157
172
|
const session = `account-sign-in-${claim.sign_in_id}`;
|
|
158
173
|
const cell = ws.cell({ agentLabel: ctx.agentLabel, transitionTimeoutMs: ctx.wakeTimeoutMs });
|
|
@@ -191,16 +206,6 @@ async function signIn(ctx, values, app) {
|
|
|
191
206
|
await api.cancelSignIn(claim.sign_in_id).catch(() => undefined);
|
|
192
207
|
}
|
|
193
208
|
}
|
|
194
|
-
/**
|
|
195
|
-
* Pooled threads are used remotely in T3 Code (desktop, web, phone): they have no provider login for claude.ai or the
|
|
196
|
-
* ChatGPT app. Where new conversations open stays the person's choice (Settings, or agents targets --open-in).
|
|
197
|
-
*/
|
|
198
|
-
function remoteHint(ctx) {
|
|
199
|
-
return t3Install(ctx.io.env)
|
|
200
|
-
? 'Use pooled threads remotely in T3 Code: shard agents targets --open-in claude=t3,codex=t3 opens new conversations there.\n'
|
|
201
|
-
: 'Use pooled threads remotely in T3 Code (desktop, web and phone): install it from https://t3.codes.\n';
|
|
202
|
-
}
|
|
203
|
-
const notAvailable = () => new CodedError('pool_not_available', 'Pooling accounts is not available for this organization.', 'Your accounts keep serving threads directly; shard agents accounts shows their usage.');
|
|
204
209
|
export const ACCOUNT_POOL_HANDLERS = {
|
|
205
210
|
async 'agents accounts'(ctx, values) {
|
|
206
211
|
const list = await ctx.cloud().agentAccounts.list({ provider: optionalProvider(values.provider) });
|
|
@@ -218,20 +223,9 @@ export const ACCOUNT_POOL_HANDLERS = {
|
|
|
218
223
|
return 0;
|
|
219
224
|
},
|
|
220
225
|
async 'agents accounts add'(ctx, values, [app]) {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
let account;
|
|
225
|
-
if (values['api-key-stdin'] === true) {
|
|
226
|
-
const key = Buffer.from(await readInput(ctx, '-', 64 * 1024, '--api-key-stdin')).toString('utf8').trim();
|
|
227
|
-
if (!key || /\s/.test(key))
|
|
228
|
-
throw new UsageError('--api-key-stdin reads one API key from standard input, e.g. pbpaste | shard agents accounts add claude --api-key-stdin');
|
|
229
|
-
ctx.addSecret(key);
|
|
230
|
-
account = await ctx.cloud().agentAccounts.addApiKey(p, key, { ...(typeof values.label === 'string' ? { label: values.label } : {}), ...(values.scope === 'team' || values.scope === 'personal' ? { scope: values.scope } : {}) });
|
|
231
|
-
}
|
|
232
|
-
else
|
|
233
|
-
account = await signIn(ctx, values, p);
|
|
234
|
-
out(ctx, `Added ${name(account)} (${PROVIDERS[account.provider]}${plan(account) ? ` ${plan(account)}` : ''}) as ${account.provider}:${account.position + 1}.\n`, ctx.json ? { ...await ctx.cloud().agentAccounts.list(), account } : account);
|
|
226
|
+
const account = await signIn(ctx, values, provider(app));
|
|
227
|
+
const ref = `${account.provider}:${account.position + 1}`;
|
|
228
|
+
out(ctx, `Added ${name(account)} (${PROVIDERS[account.provider]}${plan(account) ? ` ${plan(account)}` : ''}) as ${ref}${isActive(account) ? ', the active account' : `. Make it active: shard agents accounts use ${ref}`}.\n`, ctx.json ? { ...await ctx.cloud().agentAccounts.list(), account } : account);
|
|
235
229
|
return 0;
|
|
236
230
|
},
|
|
237
231
|
async 'agents accounts remove'(ctx, _values, [ref]) {
|
|
@@ -241,33 +235,17 @@ export const ACCOUNT_POOL_HANDLERS = {
|
|
|
241
235
|
out(ctx, `Removed ${name(account)} (${PROVIDERS[account.provider]}).\n`, ctx.json ? { ...await api.list(), removed: account.id, provider: account.provider } : null);
|
|
242
236
|
return 0;
|
|
243
237
|
},
|
|
244
|
-
async 'agents accounts
|
|
245
|
-
if (direction !== 'up' && direction !== 'down')
|
|
246
|
-
throw new UsageError('move takes an account and up or down, e.g. shard agents accounts move claude:2 up');
|
|
238
|
+
async 'agents accounts use'(ctx, _values, [ref]) {
|
|
247
239
|
const api = ctx.cloud().agentAccounts;
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
let pool;
|
|
258
|
-
try {
|
|
259
|
-
pool = await ctx.cloud().agentPool.set({ pooled: state === 'on' });
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
if (error instanceof ShardfluxApiError && (error.status === 404 || error.details?.reason === 'pool_not_on_plan'))
|
|
263
|
-
throw notAvailable();
|
|
264
|
-
throw error;
|
|
265
|
-
}
|
|
266
|
-
// Pooled threads run through the account hub: it needs the router (codex-connector 5) and its hub token.
|
|
267
|
-
const hub = pool.pooled ? await ensurePoolHub(ctx) : null;
|
|
268
|
-
out(ctx, pool.pooled
|
|
269
|
-
? `Pooled: each new thread runs on the account with the most room. Running threads keep their account.\n${hub ? `The account hub is not ready (${hub}); new threads start directly until it is. Run shard agents accounts pool on again.\n` : ''}${remoteHint(ctx)}`
|
|
270
|
-
: 'Direct: new threads use your account as before. Pooled threads keep their account until they end.\n', ctx.json ? { ...await ctx.cloud().agentAccounts.list(), pool, ...(pool.pooled ? { hub: hub ? { ready: false, error: hub } : { ready: true } } : {}) } : pool);
|
|
240
|
+
const before = await api.list();
|
|
241
|
+
const account = findAccount(before, ref);
|
|
242
|
+
const previous = before.accounts.find(a => a.provider === account.provider && isActive(a) && a.id !== account.id);
|
|
243
|
+
const list = await api.activate(account.id);
|
|
244
|
+
const active = list.accounts.find(a => a.id === account.id) ?? account;
|
|
245
|
+
const follows = account.provider === 'claude'
|
|
246
|
+
? 'New conversations start with it; running ones continue on it, and Remote Control turns back on by itself.'
|
|
247
|
+
: `New conversations and every running conversation VM use it; Codex Cloud moves to it${previous ? ` (Cloud threads started on ${name(previous)} stay in that ChatGPT account)` : ''}.`;
|
|
248
|
+
out(ctx, previous || !isActive(account) ? `${name(active)} is the active ${PROVIDERS[account.provider]} account. ${follows}\n` : `${name(active)} is already the active ${PROVIDERS[account.provider]} account.\n`, { ...list, account: active });
|
|
271
249
|
return 0;
|
|
272
250
|
},
|
|
273
251
|
};
|
|
@@ -284,33 +262,6 @@ export async function targetsAccounts(ctx) {
|
|
|
284
262
|
clearTimeout(timer);
|
|
285
263
|
}
|
|
286
264
|
}
|
|
287
|
-
/** The conversation's thread runs on the account pool (its `account.mode`, API with the pool). */
|
|
288
|
-
export function conversationPooled(conversation) {
|
|
289
|
-
const account = conversation?.account;
|
|
290
|
-
return account?.mode === 'pooled';
|
|
291
|
-
}
|
|
292
|
-
/** A pooled thread has no provider login on its session, so claude.ai, Claude Desktop and the Codex app cannot reach it. */
|
|
293
|
-
export const pooledNeedsT3 = (ref) => new CodedError('t3_not_installed', `${ref} runs on pooled accounts; open it in T3 Code.`, `Install T3 Code from https://t3.codes, open it once, then run shard agents open t3 ${ref}.`);
|
|
294
265
|
// `shard accounts …` runs the same handlers (hidden aliases in args.ts).
|
|
295
266
|
for (const [path, handler] of Object.entries(ACCOUNT_POOL_HANDLERS))
|
|
296
267
|
ACCOUNT_POOL_HANDLERS[path.replace(/^agents /, '')] = handler;
|
|
297
|
-
/**
|
|
298
|
-
* The account hub in pool mode (W1): created or upgraded on codex-connector 5 with its hub token (POST
|
|
299
|
-
* /v1/agent-pool/hub-token, minted for the hub workspace). Null when ready, else what stopped it.
|
|
300
|
-
*/
|
|
301
|
-
async function ensurePoolHub(ctx) {
|
|
302
|
-
try {
|
|
303
|
-
const cloud = ctx.cloud();
|
|
304
|
-
const hub = (accountPoolSeams.hubCloud ?? ((c) => sdkHubCloud(c.cloud(), { signal: c.signal, wakeTimeoutMs: c.wakeTimeoutMs, agentLabel: c.agentLabel, homeProject: (org) => homeProject(c, org) })))(ctx);
|
|
305
|
-
const ensured = await ensureCliProxyHub(hub, { dir: ctx.store.dir, apiUrl: ctx.apiUrl, progress: (m) => progress(ctx, m),
|
|
306
|
-
pool: { hubToken: async (workspaceId) => (await cloud.request('POST', '/v1/agent-pool/hub-token', { json: { workspace_id: workspaceId } })).token } });
|
|
307
|
-
for (const value of [ensured.info.callback_url, ensured.info.management_key])
|
|
308
|
-
ctx.addSecret(value);
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
catch (error) {
|
|
312
|
-
return describeError(error).message;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
/** Test seam: the hub cloud the pool toggle ensures (the SDK's by default). */
|
|
316
|
-
export const accountPoolSeams = {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Organization, Project, ShardfluxAccount } from '@shardflux/sdk';
|
|
2
|
+
import type { Values } from './args.js';
|
|
3
|
+
import type { Ctx } from './commands.js';
|
|
4
|
+
import { UsageError } from './errors.js';
|
|
5
|
+
export declare function allOrgs(account: ShardfluxAccount): Promise<Organization[]>;
|
|
6
|
+
export declare function allProjects(account: ShardfluxAccount, organizationId: string): Promise<Project[]>;
|
|
7
|
+
export declare const choices: (rows: readonly {
|
|
8
|
+
id: string;
|
|
9
|
+
slug: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}[]) => string;
|
|
12
|
+
export declare function byRef<T extends {
|
|
13
|
+
id: string;
|
|
14
|
+
slug: string;
|
|
15
|
+
name: string;
|
|
16
|
+
}>(rows: readonly T[], ref: string): T | undefined;
|
|
17
|
+
export declare const gone: (err: unknown) => boolean;
|
|
18
|
+
export declare function findOrg(account: ShardfluxAccount, ref: string): Promise<Organization>;
|
|
19
|
+
/** The fix for a saved default organization the person is no longer in. */
|
|
20
|
+
export declare function staleDefaultOrg(saved: string, orgs: readonly Organization[]): UsageError;
|
|
21
|
+
/** `ref` (a positional), else --org, else the saved default, else the only organization. */
|
|
22
|
+
export declare function resolveOrg(ctx: Ctx, account: ShardfluxAccount, values: Values, ref?: string): Promise<Organization>;
|
|
23
|
+
/** `ref` (a positional), else --project, else the saved default (unless --org names another), else the only project. */
|
|
24
|
+
export declare function resolveProject(ctx: Ctx, account: ShardfluxAccount, values: Values, ref?: string): Promise<Project>;
|
|
25
|
+
/** The API key in use (SHARDFLUX_API_KEY, else the saved one): where it acts. Null without a key or when it is rejected. */
|
|
26
|
+
export interface KeyScope {
|
|
27
|
+
keyId: string;
|
|
28
|
+
/** Where the key comes from: SHARDFLUX_API_KEY or the credentials file. */
|
|
29
|
+
source: 'env' | 'file';
|
|
30
|
+
organizationId: string;
|
|
31
|
+
projectId: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function keyScope(ctx: Ctx): Promise<KeyScope | null>;
|
|
34
|
+
/** A default that points somewhere it should not, and the exact command that fixes it (`--json`: `warnings`). */
|
|
35
|
+
export interface ScopeWarning {
|
|
36
|
+
code: 'default_organization_gone' | 'api_key_organization_gone' | 'api_key_in_other_organization' | 'api_key_environment_override';
|
|
37
|
+
message: string;
|
|
38
|
+
fix: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Plain warnings about defaults that point into an organization the signed-in person is no longer in: the saved
|
|
42
|
+
* default organization, and the API key's organization. Each names the command that fixes it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function staleScopeWarnings(ctx: Ctx, orgs: readonly Organization[], key: KeyScope | null): ScopeWarning[];
|
|
45
|
+
/** Compare membership before defaults: a departed organization is different from another one still accessible. */
|
|
46
|
+
export declare function keyOrganizationWarning(key: KeyScope, orgs: readonly Organization[], target?: Organization, projectId?: string): ScopeWarning | null;
|
|
47
|
+
/**
|
|
48
|
+
* The stale-default warnings for the signed-in person, or none: without a session (or with one the API no longer
|
|
49
|
+
* accepts) there is no list of their organizations to compare with, and the command itself goes on either way.
|
|
50
|
+
*/
|
|
51
|
+
export declare function sessionScopeWarnings(ctx: Ctx, key: KeyScope | null): Promise<ScopeWarning[]>;
|
|
52
|
+
/** Warnings on stderr, one `warning:` line and its `fix:` each (text mode). */
|
|
53
|
+
export declare function writeWarnings(ctx: Ctx, warnings: readonly ScopeWarning[]): void;
|
|
54
|
+
/** The note for a default just saved in `org` while the API key acts in another organization (orgs use, projects use). */
|
|
55
|
+
export declare function keyElsewhere(key: KeyScope, org: Organization): string;
|
|
56
|
+
/**
|
|
57
|
+
* The project a T3 Connect sign-in is scoped by (0.20.2+). A sign-in is per person and organization, and a session in
|
|
58
|
+
* more than one organization must name one (the API answers 422 without `project_id`). --project, else a project of
|
|
59
|
+
* --org (the saved default or the key's project when it is in that organization, else its first), else the API key's
|
|
60
|
+
* project (T3 links the key's workspaces with the sign-in of the key's organization, and the onboarding row checks it
|
|
61
|
+
* with the key), else the saved default project. Undefined when none is known: one organization needs none.
|
|
62
|
+
*/
|
|
63
|
+
export declare function t3SignInProject(ctx: Ctx, values: Values): Promise<string | undefined>;
|
|
64
|
+
/** The API's 422 for a session in several organizations without a project (production before train 22): what to run. */
|
|
65
|
+
export declare function t3NeedsProject(err: unknown): UsageError | null;
|