@lanes-sh/link 0.7.2 → 0.8.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/README.md +20 -10
- package/instructions/agents/lanes-link-scout.md +2 -2
- package/instructions/skills/lanes-link/SKILL.md +136 -61
- package/package.json +2 -1
- package/src/audit/index.ts +8 -1
- package/src/auth/index.ts +58 -2
- package/src/auth/lanes/assertion.ts +256 -0
- package/src/auth/lanes/callback.ts +135 -0
- package/src/auth/lanes/federation.ts +50 -0
- package/src/auth/lanes/login.ts +294 -0
- package/src/auth/lanes/members.ts +103 -0
- package/src/auth/lanes/session.ts +97 -0
- package/src/auth/oauth/grant.ts +183 -0
- package/src/auth/oauth/result.ts +27 -0
- package/src/auth/oauth/server.ts +176 -203
- package/src/auth/oauth/store.ts +65 -0
- package/src/auth/remote.ts +32 -9
- package/src/cli/accepts.ts +108 -0
- package/src/cli/argv.ts +57 -3
- package/src/cli/audit-change.ts +140 -0
- package/src/cli/callback-page.ts +36 -115
- package/src/cli/commands/auth-dispatch.ts +48 -0
- package/src/cli/commands/auth.ts +229 -0
- package/src/cli/commands/connect/accounts.ts +4 -4
- package/src/cli/commands/connect/authorise.ts +4 -4
- package/src/cli/commands/connect/bind-credential.ts +2 -1
- package/src/cli/commands/connect/custom/index.ts +1 -1
- package/src/cli/commands/connect/custom/write.ts +2 -2
- package/src/cli/commands/connect/grant.ts +29 -14
- package/src/cli/commands/connect/index.ts +88 -87
- package/src/cli/commands/connect/options.ts +83 -0
- package/src/cli/commands/connect/registration.ts +50 -0
- package/src/cli/commands/connect/requirements.ts +1 -1
- package/src/cli/commands/connect/settle.ts +4 -2
- package/src/cli/commands/connect/target-note.ts +7 -2
- package/src/cli/commands/connect/unknown.ts +1 -1
- package/src/cli/commands/connect/variables.ts +3 -2
- package/src/cli/commands/connection-list.ts +116 -0
- package/src/cli/commands/connection.ts +182 -165
- package/src/cli/commands/grant.ts +140 -0
- package/src/cli/commands/identity.ts +21 -9
- package/src/cli/commands/knowledge/index.ts +46 -79
- package/src/cli/commands/knowledge/migrate.ts +74 -13
- package/src/cli/commands/knowledge/show.ts +92 -0
- package/src/cli/commands/knowledge.ts +2 -1
- package/src/cli/commands/mcp/harnesses.ts +30 -8
- package/src/cli/commands/mcp/onboarding.ts +86 -0
- package/src/cli/commands/mcp/register.ts +16 -2
- package/src/cli/commands/mcp.ts +1 -0
- package/src/cli/commands/members.ts +288 -0
- package/src/cli/commands/operate/attach.ts +3 -3
- package/src/cli/commands/operate/audit.ts +11 -7
- package/src/cli/commands/operate/auth.ts +28 -11
- package/src/cli/commands/operate/findings.ts +2 -1
- package/src/cli/commands/operate/inspect.ts +37 -19
- package/src/cli/commands/operate/migrate.ts +29 -12
- package/src/cli/commands/operate/outputs.ts +3 -3
- package/src/cli/commands/operate/pair-certificate.ts +141 -0
- package/src/cli/commands/operate/pair.ts +324 -0
- package/src/cli/commands/operate/policy.ts +73 -22
- package/src/cli/commands/operate/serve.ts +52 -4
- package/src/cli/commands/operate/status.ts +18 -10
- package/src/cli/commands/operate/tools.ts +2 -2
- package/src/cli/commands/operate.ts +2 -0
- package/src/cli/commands/owner/shared.ts +13 -2
- package/src/cli/commands/owner/skills.ts +28 -8
- package/src/cli/commands/profile/removal.ts +79 -76
- package/src/cli/commands/profile/remove.ts +16 -1
- package/src/cli/commands/profile.ts +46 -10
- package/src/cli/commands/relabel.ts +112 -0
- package/src/cli/commands/secrets.ts +34 -12
- package/src/cli/commands/set-workspace.ts +96 -0
- package/src/cli/commands/setup.ts +2 -2
- package/src/cli/commands/sync.ts +8 -8
- package/src/cli/commands/target.ts +9 -7
- package/src/cli/commands/update.ts +58 -17
- package/src/cli/config-edit.ts +75 -140
- package/src/cli/config-migrate.ts +82 -64
- package/src/cli/config-repair.ts +89 -32
- package/src/cli/config-templates.ts +198 -0
- package/src/cli/contract3-data.ts +328 -0
- package/src/cli/contract3-shape.ts +186 -0
- package/src/cli/contract3.ts +282 -0
- package/src/cli/endpoint-url.ts +1 -1
- package/src/cli/lanes.ts +25 -1
- package/src/cli/main.ts +89 -14
- package/src/cli/migrate-plan.ts +12 -6
- package/src/cli/output.ts +34 -1
- package/src/cli/publish.ts +5 -2
- package/src/cli/runtime/open.ts +63 -98
- package/src/cli/runtime/registry.ts +6 -7
- package/src/cli/runtime/stores.ts +53 -0
- package/src/cli/runtime/types.ts +106 -0
- package/src/cli/runtime/vault.ts +19 -4
- package/src/cli/runtime/workspace.ts +60 -0
- package/src/cli/runtime.ts +2 -1
- package/src/cli/selection-require.ts +44 -13
- package/src/cli/selection.ts +127 -145
- package/src/cli/usage.ts +34 -18
- package/src/cli/workspace-migrate.ts +125 -16
- package/src/connectivity/manifest/provider.ts +3 -1
- package/src/connectivity/manifest/requirements.ts +1 -1
- package/src/deployments/bind.ts +1 -1
- package/src/deployments/deploy.ts +36 -27
- package/src/deployments/gcp/bucket.ts +18 -7
- package/src/deployments/gcp/provision.ts +7 -7
- package/src/deployments/prepare.ts +72 -24
- package/src/deployments/record.ts +1 -1
- package/src/deployments/report.ts +2 -2
- package/src/deployments/serving.ts +15 -74
- package/src/deployments/target.ts +15 -15
- package/src/deployments/upload.ts +46 -22
- package/src/dispatch/deps.ts +88 -0
- package/src/dispatch/dispatch.ts +21 -62
- package/src/policy/index.ts +47 -15
- package/src/profile/connections.ts +183 -0
- package/src/profile/deployments.ts +3 -3
- package/src/profile/index.ts +30 -5
- package/src/profile/layout.ts +86 -89
- package/src/profile/load.ts +80 -47
- package/src/profile/pairing.ts +32 -0
- package/src/profile/primitives.ts +35 -1
- package/src/profile/registry.ts +6 -6
- package/src/profile/schema.ts +172 -21
- package/src/profile/targets.ts +21 -9
- package/src/profile/testing.ts +69 -2
- package/src/profile/workspace.ts +58 -3
- package/src/providers/custom/index.ts +1 -1
- package/src/providers/custom/load.ts +2 -3
- package/src/providers/identity/provider.ts +1 -1
- package/src/providers/memory/provider.ts +20 -2
- package/src/providers/setup/plan.ts +1 -1
- package/src/providers/slack/index.ts +2 -2
- package/src/registry/policy-bridge.ts +33 -11
- package/src/registry/reconcile.ts +4 -4
- package/src/server/authorization.ts +94 -0
- package/src/server/edge.ts +14 -1
- package/src/server/endpoint.ts +85 -104
- package/src/server/generation.ts +10 -1
- package/src/server/harness.ts +71 -13
- package/src/server/index.ts +31 -0
- package/src/server/mcp/build.ts +20 -1
- package/src/server/mcp/client-info.ts +54 -0
- package/src/server/mcp/guide.ts +120 -0
- package/src/server/mcp/instructions.ts +1 -1
- package/src/server/mcp/prompts.ts +7 -3
- package/src/server/mcp/resources.ts +16 -8
- package/src/server/mcp/tools.ts +9 -3
- package/src/server/mcp/visibility.ts +18 -3
- package/src/server/oauth.ts +29 -109
- package/src/server/read/credential.ts +134 -0
- package/src/server/read/deployed.ts +56 -0
- package/src/server/read/listener.ts +54 -0
- package/src/server/read/open.ts +101 -0
- package/src/server/read/routes.ts +247 -0
- package/src/server/read/state.ts +171 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { GlobalFlags } from '../../runtime.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What `lanes link connect` accepts, and what it hands back.
|
|
5
|
+
*
|
|
6
|
+
* Split out of `index.ts` so that file stays inside the size budget. The seam is
|
|
7
|
+
* the one the command already has: this is the shape of the request, and that is
|
|
8
|
+
* the six steps that carry it out.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `lanes link connect <provider>` — the one command that adds an account.
|
|
13
|
+
*
|
|
14
|
+
* The same command regardless of connectivity. A local provider declares a
|
|
15
|
+
* connection and stops; an MCP provider authorises, asks the upstream server
|
|
16
|
+
* what it exposes, and grants it. Core learns nothing about any vendor: the
|
|
17
|
+
* manifest says how to reach them and what to ask the operator for.
|
|
18
|
+
*
|
|
19
|
+
* The five numbered steps below are the whole command, and each one that grew
|
|
20
|
+
* past a paragraph moved out: `authorise.ts` gets the token, `setup.ts` asks the
|
|
21
|
+
* operator for what the vendor's console produced, `settle.ts` works out whose
|
|
22
|
+
* account it was, and `accounts.ts` knows which connections are siblings.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface ConnectOptions extends GlobalFlags {
|
|
26
|
+
readonly id?: string | undefined;
|
|
27
|
+
readonly displayName?: string | undefined;
|
|
28
|
+
/** `--set key=value`, repeatable: where this connection's service is. */
|
|
29
|
+
readonly set?: readonly string[] | string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* `--label`: what to call this connection, instead of being asked.
|
|
32
|
+
*
|
|
33
|
+
* Distinct from `--display-name`, which answers *whose account this is* for a
|
|
34
|
+
* provider that cannot report it. This one never touches the identity, so it
|
|
35
|
+
* is safe to pass anything a person would say out loud.
|
|
36
|
+
*/
|
|
37
|
+
readonly label?: string | undefined;
|
|
38
|
+
/** Ask for the stored credential again — a key rotated, or a password revoked. */
|
|
39
|
+
readonly replace?: boolean | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Answer nothing from a terminal: resolve every declared value from the
|
|
42
|
+
* credential store, and refuse with instructions where one is missing.
|
|
43
|
+
*/
|
|
44
|
+
readonly nonInteractive?: boolean | undefined;
|
|
45
|
+
/** The operator has already agreed to scopes broader than the provider needs. */
|
|
46
|
+
readonly acceptBroadScopes?: boolean | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Register an OAuth client of your own rather than using a hosted one.
|
|
49
|
+
*
|
|
50
|
+
* Sticky by consequence rather than by flag: it writes the `oauth_apps` entry,
|
|
51
|
+
* and a profile that declares one is never moved off it. So this is typed once
|
|
52
|
+
* and then forgotten, which is the right shape for a decision about a client
|
|
53
|
+
* that is shared by every connection of that vendor.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* `--own-client`, the older spelling of one of the routes `--auth` now names.
|
|
57
|
+
*
|
|
58
|
+
* Kept because it is in scripts and in a year of documentation, and because
|
|
59
|
+
* it still says something true. It resolves to `--auth own_client`.
|
|
60
|
+
*/
|
|
61
|
+
readonly ownClient?: boolean | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* `--auth <method>`: which way in, for a provider that offers more than one.
|
|
64
|
+
*
|
|
65
|
+
* Unset means ask, where there is somebody to ask and something to ask
|
|
66
|
+
* about. It is not sticky the way `--own-client` is: `--own-client` writes an
|
|
67
|
+
* `oauth_apps` entry that every connection of the vendor then reads, whereas
|
|
68
|
+
* this decides one connection's credential and is recorded by that credential
|
|
69
|
+
* existing. Two accounts on the same profile may honestly differ.
|
|
70
|
+
*/
|
|
71
|
+
readonly auth?: string | undefined;
|
|
72
|
+
/** Injected for tests. The broker is the only thing `connect` fetches. */
|
|
73
|
+
readonly fetch?: typeof globalThis.fetch | undefined;
|
|
74
|
+
readonly json?: boolean | undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The connection id used before the provider has said whose account this is.
|
|
80
|
+
*
|
|
81
|
+
* It is a placeholder rather than a name, and `setup.ts` reads it as one: a
|
|
82
|
+
* credential filed under it belongs to a `connect` that did not finish.
|
|
83
|
+
*/
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ProviderManifest } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Turn a refused registration into something an operator can act on.
|
|
5
|
+
*
|
|
6
|
+
* A provider declaring `registration: 'dynamic'` is promising that its
|
|
7
|
+
* authorization server will register a client on demand — that is the whole
|
|
8
|
+
* reason those providers need no setup. When the server refuses, the SDK throws
|
|
9
|
+
* the status and nothing else, and "Dynamic Client Registration rejected (HTTP
|
|
10
|
+
* 403): Forbidden" tells the reader neither what was being attempted nor that
|
|
11
|
+
* there is nothing wrong with their machine.
|
|
12
|
+
*
|
|
13
|
+
* It is a real state rather than a bug: a server may advertise a registration
|
|
14
|
+
* endpoint and gate it behind a plan, an allowlist, or a signed-in session.
|
|
15
|
+
* Figma's does, which is how this was found. What the operator needs to know is
|
|
16
|
+
* that the refusal came from the provider, that we sent nothing wrong, and
|
|
17
|
+
* where the answer lives — which is the provider's own documentation, because
|
|
18
|
+
* whatever unlocks it is theirs to state.
|
|
19
|
+
*/
|
|
20
|
+
export async function registering<T>(
|
|
21
|
+
manifest: ProviderManifest,
|
|
22
|
+
serverUrl: string,
|
|
23
|
+
run: () => Promise<T>,
|
|
24
|
+
): Promise<T> {
|
|
25
|
+
if (manifest.auth.kind !== 'oauth' || manifest.auth.registration !== 'dynamic') return run();
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
return await run();
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
31
|
+
if (!/registration/i.test(message)) throw error;
|
|
32
|
+
|
|
33
|
+
const host = new URL(serverUrl).host;
|
|
34
|
+
throw new Error(
|
|
35
|
+
`${manifest.name} refused to register this machine as a client.\n` +
|
|
36
|
+
` ${message}\n` +
|
|
37
|
+
'\n' +
|
|
38
|
+
` Nothing was written, and nothing about your setup is wrong: ${host}\n` +
|
|
39
|
+
' advertises registration and then declined it. That is usually a plan, an\n' +
|
|
40
|
+
' allowlist, or an account that has to be signed in first.\n' +
|
|
41
|
+
` What unlocks it is theirs to say: ${docsFor(manifest) ?? host}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Where the provider tells you what it wants, when the manifest names it. */
|
|
47
|
+
function docsFor(manifest: ProviderManifest): string | undefined {
|
|
48
|
+
const setup = (manifest as { setup?: { docs_url?: unknown } }).setup;
|
|
49
|
+
return typeof setup?.docs_url === 'string' ? setup.docs_url : undefined;
|
|
50
|
+
}
|
|
@@ -102,7 +102,7 @@ export async function preflight(input: {
|
|
|
102
102
|
}): Promise<Blocked | null> {
|
|
103
103
|
const { manifest, connectionId, profile, target, spec } = input;
|
|
104
104
|
const method = input.method ?? 'oauth';
|
|
105
|
-
const rerun = `lanes link connect ${spec} --profile ${profile} --
|
|
105
|
+
const rerun = `lanes link connect ${spec} --profile ${profile} --workspace ${target}`;
|
|
106
106
|
const assertion = manifest.auth.kind === 'oauth' ? manifest.auth.assertion : undefined;
|
|
107
107
|
|
|
108
108
|
if (method === 'assertion' && assertion) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createMcpConnector } from '#connectivity/transports';
|
|
2
2
|
import { bearerTokenAsStored } from '#connectivity/auth/index.ts';
|
|
3
3
|
import type { SecretStore } from '#secrets';
|
|
4
|
-
import type { Config } from '#profile';
|
|
4
|
+
import type { ConnectionConfig, Config } from '#profile';
|
|
5
5
|
import type { AnyConnector, ProviderManifest } from '#connectivity';
|
|
6
6
|
import { idFromAccount, resolveAccount } from '../../identity.ts';
|
|
7
7
|
import { style } from '../../output.ts';
|
|
@@ -38,6 +38,8 @@ export async function settleIdentity(input: {
|
|
|
38
38
|
label?: string | undefined;
|
|
39
39
|
runtime: {
|
|
40
40
|
config: Config;
|
|
41
|
+
/** Every account the workspace holds, for sibling matching (ADR-057). */
|
|
42
|
+
workspaceConnections: readonly ConnectionConfig[];
|
|
41
43
|
credentials: SecretStore;
|
|
42
44
|
registry: { manifest(id: string): ProviderManifest | undefined };
|
|
43
45
|
connectorFor(providerId: string, connectionId: string): AnyConnector | undefined;
|
|
@@ -51,7 +53,7 @@ export async function settleIdentity(input: {
|
|
|
51
53
|
// Siblings across the whole vendor account, not just this provider — so
|
|
52
54
|
// connecting iCloud Calendar after iCloud Mail lands on the same id, and
|
|
53
55
|
// therefore the same credential, rather than a second `will2`.
|
|
54
|
-
const siblings = accountSiblings(manifest, runtime.
|
|
56
|
+
const siblings = accountSiblings(manifest, runtime.workspaceConnections, runtime.registry);
|
|
55
57
|
|
|
56
58
|
let account = input.account ?? null;
|
|
57
59
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Resolution } from '#profile';
|
|
2
|
-
import { announce } from '../../output.ts';
|
|
2
|
+
import { announce, announceWorkspace } from '../../output.ts';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The line `connect` prints before it acts.
|
|
@@ -23,6 +23,7 @@ import { announce } from '../../output.ts';
|
|
|
23
23
|
export function announceConnectTarget(
|
|
24
24
|
runtime: { readonly resolution: Resolution },
|
|
25
25
|
json?: boolean | undefined,
|
|
26
|
+
granting = true,
|
|
26
27
|
): void {
|
|
27
28
|
// `emit`'s early return only protects lines printed *at* the emit, and this
|
|
28
29
|
// one has to precede the browser — so it carries its own guard, the one
|
|
@@ -30,5 +31,9 @@ export function announceConnectTarget(
|
|
|
30
31
|
// beside `emit`: a line of prose in front of a JSON document corrupts it.
|
|
31
32
|
if (json === true) return;
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
// Without `--profile` this connects into the workspace and edits no profile,
|
|
35
|
+
// so naming the one a runtime happened to resolve would report a subject the
|
|
36
|
+
// command does not have.
|
|
37
|
+
if (granting) announce(runtime.resolution);
|
|
38
|
+
else announceWorkspace(runtime.resolution);
|
|
34
39
|
}
|
|
@@ -28,7 +28,7 @@ export function unknownProvider(input: {
|
|
|
28
28
|
const yours = available.filter((c) => c.origin === 'workspace').map((c) => c.manifest.id);
|
|
29
29
|
|
|
30
30
|
const selection = `--profile ${input.profile} --target ${input.target}`;
|
|
31
|
-
const directory = `${input.workspaceRoot}/${layout.providers(
|
|
31
|
+
const directory = `${input.workspaceRoot}/${layout.providers()}`;
|
|
32
32
|
|
|
33
33
|
return new Error(
|
|
34
34
|
`Unknown provider "${input.providerId}".\n` +
|
|
@@ -161,7 +161,8 @@ export async function resolveConnectionAddress(input: {
|
|
|
161
161
|
readonly runtime: {
|
|
162
162
|
readonly registry: ProviderRegistry;
|
|
163
163
|
readonly credentials: SecretStore;
|
|
164
|
-
|
|
164
|
+
/** Every account the workspace holds, for the address a sibling already gave (ADR-057). */
|
|
165
|
+
readonly workspaceConnections: readonly ConnectionRow[];
|
|
165
166
|
connectorFor(providerId: string, connectionId: string): AnyConnector | undefined;
|
|
166
167
|
};
|
|
167
168
|
}): Promise<ResolvedAddress> {
|
|
@@ -178,7 +179,7 @@ export async function resolveConnectionAddress(input: {
|
|
|
178
179
|
// the id is not settled yet, and a second Nextcloud on a *different* host is
|
|
179
180
|
// rarer than reconnecting the one there is. It is offered, not imposed —
|
|
180
181
|
// pressing Enter accepts it and typing replaces it.
|
|
181
|
-
existing: previousAddress(runtime.
|
|
182
|
+
existing: previousAddress(runtime.workspaceConnections, input.providerId, manifest),
|
|
182
183
|
interactive: input.interactive,
|
|
183
184
|
supplied: parseSet(input.set),
|
|
184
185
|
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { RESERVED_PROVIDER_IDS } from '#connectivity';
|
|
2
|
+
import {
|
|
3
|
+
connectionRefOf,
|
|
4
|
+
listProfiles,
|
|
5
|
+
loadProfileConfig,
|
|
6
|
+
readConnections,
|
|
7
|
+
resolveTargetWorkspace,
|
|
8
|
+
resolveWorkspaceRoot,
|
|
9
|
+
} from '#profile';
|
|
10
|
+
import { emit, heading, print, style, table } from '../output.ts';
|
|
11
|
+
import type { GlobalFlags } from '../runtime.ts';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `lanes link connection list` — every account this workspace holds, and which
|
|
15
|
+
* profiles reach each.
|
|
16
|
+
*
|
|
17
|
+
* The listing that had nowhere to live under contract 2, because there was no
|
|
18
|
+
* such thing as "the workspace's connections": each profile had its own list
|
|
19
|
+
* and `status` printed one profile's. A connection belongs to the workspace now
|
|
20
|
+
* (ADR-057), so the question "what have I authorised" has one answer.
|
|
21
|
+
*
|
|
22
|
+
* **The profiles column is the useful half.** An account granted to nobody is
|
|
23
|
+
* connected and unreachable, which looks identical to a working one from
|
|
24
|
+
* anywhere else — and an account granted to more profiles than the operator
|
|
25
|
+
* remembers is how a disconnect surprises somebody. Both are visible here and
|
|
26
|
+
* nowhere else.
|
|
27
|
+
*
|
|
28
|
+
* Workspace-scoped, so it takes no `--profile`. Passing one would narrow the
|
|
29
|
+
* column rather than the rows, which is a different command.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export interface ConnectionSummary {
|
|
33
|
+
readonly key: string;
|
|
34
|
+
readonly provider: string;
|
|
35
|
+
readonly account: string;
|
|
36
|
+
readonly label: string | null;
|
|
37
|
+
/** Whether this is part of the owner layer rather than somebody's account. */
|
|
38
|
+
readonly builtIn: boolean;
|
|
39
|
+
/** Every profile in the workspace whose grants name it. */
|
|
40
|
+
readonly grantedTo: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function connectionList(flags: GlobalFlags & { json?: boolean }): Promise<void> {
|
|
44
|
+
const local = resolveWorkspaceRoot();
|
|
45
|
+
const root = await resolveTargetWorkspace(local, flags.target ?? 'local');
|
|
46
|
+
|
|
47
|
+
const held = (await readConnections(root)).connections;
|
|
48
|
+
|
|
49
|
+
// Read once, ahead of the loop. The alternative is a file read per connection
|
|
50
|
+
// per profile, which on a workspace with fifteen accounts and three profiles
|
|
51
|
+
// is forty-five reads of five files.
|
|
52
|
+
const grants = new Map<string, string[]>();
|
|
53
|
+
for (const name of await listProfiles(root)) {
|
|
54
|
+
try {
|
|
55
|
+
const { config } = await loadProfileConfig(root, name);
|
|
56
|
+
for (const grant of config.grants) {
|
|
57
|
+
grants.set(grant.connection, [...(grants.get(grant.connection) ?? []), name]);
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// A profile that will not load is `check`'s to report. Dropping it here
|
|
61
|
+
// makes this listing answer for the ones that do rather than failing
|
|
62
|
+
// wholesale on one broken file.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const summaries: ConnectionSummary[] = held.map((connection) => {
|
|
67
|
+
const key = connectionRefOf(connection);
|
|
68
|
+
return {
|
|
69
|
+
key,
|
|
70
|
+
provider: connection.provider,
|
|
71
|
+
account: connection.account,
|
|
72
|
+
label: connection.label ?? null,
|
|
73
|
+
builtIn: RESERVED_PROVIDER_IDS.includes(connection.provider),
|
|
74
|
+
grantedTo: grants.get(key) ?? [],
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return emit(flags.json, { workspace: flags.target ?? 'local', connections: summaries }, () => {
|
|
79
|
+
print(style.dim(`workspace ${style.bold(flags.target ?? 'local')} ${root}`));
|
|
80
|
+
|
|
81
|
+
const accounts = summaries.filter((one) => !one.builtIn);
|
|
82
|
+
const builtIns = summaries.filter((one) => one.builtIn);
|
|
83
|
+
|
|
84
|
+
if (accounts.length === 0) {
|
|
85
|
+
print('');
|
|
86
|
+
print(style.dim('No accounts connected yet. Connect one with: lanes link connect <provider>'));
|
|
87
|
+
} else {
|
|
88
|
+
heading(`Accounts (${accounts.length})`);
|
|
89
|
+
table(accounts.map(row));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (builtIns.length > 0) {
|
|
93
|
+
heading(`Your own material (${builtIns.length})`);
|
|
94
|
+
table(builtIns.map(row));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
print('');
|
|
98
|
+
print(
|
|
99
|
+
style.dim(
|
|
100
|
+
'A connection reaches a profile only where that profile grants it:\n' +
|
|
101
|
+
' lanes link grant <provider>.<id> --profile <name>',
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function row(one: ConnectionSummary): string[] {
|
|
108
|
+
// "granted to nobody" is said rather than left blank, because a blank column
|
|
109
|
+
// reads as "not loaded yet" and this is a fact about the config.
|
|
110
|
+
const reach =
|
|
111
|
+
one.grantedTo.length === 0
|
|
112
|
+
? style.dim('no profile grants it')
|
|
113
|
+
: one.grantedTo.join(', ');
|
|
114
|
+
|
|
115
|
+
return [` ${style.bold(one.key)}`, one.label ?? one.account, reach];
|
|
116
|
+
}
|