@lanes-sh/link 0.1.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/LICENSE +202 -0
- package/README.md +144 -0
- package/bin/lanes +42 -0
- package/instructions/agents/lanes-link-scout.md +73 -0
- package/instructions/skills/lanes-link/SKILL.md +187 -0
- package/package.json +95 -0
- package/src/audit/chain.ts +278 -0
- package/src/audit/conformance.ts +374 -0
- package/src/audit/fanout.ts +97 -0
- package/src/audit/index.ts +218 -0
- package/src/audit/stdout.ts +60 -0
- package/src/auth/index.ts +220 -0
- package/src/auth/oauth/metadata.ts +75 -0
- package/src/auth/oauth/server.ts +385 -0
- package/src/auth/oauth/store.ts +215 -0
- package/src/auth/oidc.ts +206 -0
- package/src/auth/remote.ts +72 -0
- package/src/cli/argv.ts +94 -0
- package/src/cli/callback-page.ts +256 -0
- package/src/cli/commands/connect/accounts.ts +94 -0
- package/src/cli/commands/connect/authorise.ts +298 -0
- package/src/cli/commands/connect/client.ts +284 -0
- package/src/cli/commands/connect/index.ts +398 -0
- package/src/cli/commands/connect/outcome.ts +119 -0
- package/src/cli/commands/connect/requirements.ts +103 -0
- package/src/cli/commands/connect/scopes-gate.ts +146 -0
- package/src/cli/commands/connect/settle.ts +136 -0
- package/src/cli/commands/connect/setup.ts +276 -0
- package/src/cli/commands/mcp/assets.ts +189 -0
- package/src/cli/commands/mcp/harnesses.ts +143 -0
- package/src/cli/commands/mcp/list.ts +70 -0
- package/src/cli/commands/mcp/register.ts +199 -0
- package/src/cli/commands/mcp/stdio.ts +57 -0
- package/src/cli/commands/mcp.ts +22 -0
- package/src/cli/commands/operate/attach.ts +121 -0
- package/src/cli/commands/operate/audit.ts +119 -0
- package/src/cli/commands/operate/inspect.ts +355 -0
- package/src/cli/commands/operate/outputs.ts +206 -0
- package/src/cli/commands/operate/policy.ts +80 -0
- package/src/cli/commands/operate/serve.ts +55 -0
- package/src/cli/commands/operate/status.ts +133 -0
- package/src/cli/commands/operate/token.ts +70 -0
- package/src/cli/commands/operate.ts +27 -0
- package/src/cli/commands/owner/memory.ts +110 -0
- package/src/cli/commands/owner/shared.ts +125 -0
- package/src/cli/commands/owner/skills.ts +92 -0
- package/src/cli/commands/owner/vault.ts +140 -0
- package/src/cli/commands/owner.ts +46 -0
- package/src/cli/commands/profile/removal.ts +278 -0
- package/src/cli/commands/profile/remove.ts +315 -0
- package/src/cli/commands/profile.ts +156 -0
- package/src/cli/commands/secrets.ts +176 -0
- package/src/cli/commands/setup.ts +150 -0
- package/src/cli/commands/target.ts +310 -0
- package/src/cli/config-edit.ts +397 -0
- package/src/cli/endpoint-url.ts +77 -0
- package/src/cli/identity.ts +109 -0
- package/src/cli/lanes.ts +78 -0
- package/src/cli/main.ts +333 -0
- package/src/cli/oauth-error.ts +13 -0
- package/src/cli/oauth-exchange.ts +146 -0
- package/src/cli/oauth.ts +354 -0
- package/src/cli/output.ts +184 -0
- package/src/cli/prompt.ts +180 -0
- package/src/cli/publish.ts +185 -0
- package/src/cli/runtime/discovery.ts +123 -0
- package/src/cli/runtime/open.ts +342 -0
- package/src/cli/runtime/registry.ts +185 -0
- package/src/cli/runtime/select.ts +124 -0
- package/src/cli/runtime.ts +34 -0
- package/src/cli/scopes.ts +63 -0
- package/src/cli/usage.ts +111 -0
- package/src/cli/version.ts +25 -0
- package/src/connectivity/auth/README.md +36 -0
- package/src/connectivity/auth/api-key/index.ts +43 -0
- package/src/connectivity/auth/authorize.ts +49 -0
- package/src/connectivity/auth/basic/index.ts +68 -0
- package/src/connectivity/auth/bearer/index.ts +13 -0
- package/src/connectivity/auth/credential.ts +19 -0
- package/src/connectivity/auth/header/index.ts +18 -0
- package/src/connectivity/auth/index.ts +35 -0
- package/src/connectivity/auth/none/index.ts +12 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
- package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
- package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
- package/src/connectivity/auth/resolve.ts +61 -0
- package/src/connectivity/auth/strategy/index.ts +14 -0
- package/src/connectivity/capability.ts +164 -0
- package/src/connectivity/connector.ts +175 -0
- package/src/connectivity/context.ts +77 -0
- package/src/connectivity/index.ts +94 -0
- package/src/connectivity/mail/attachments.ts +368 -0
- package/src/connectivity/mail/compose.ts +73 -0
- package/src/connectivity/mail/index.ts +40 -0
- package/src/connectivity/mail/message.ts +82 -0
- package/src/connectivity/mail/nodemailer.d.ts +83 -0
- package/src/connectivity/mail/staging.ts +143 -0
- package/src/connectivity/mail/url.ts +301 -0
- package/src/connectivity/manifest/auth.ts +135 -0
- package/src/connectivity/manifest/bundles.ts +23 -0
- package/src/connectivity/manifest/connector.ts +168 -0
- package/src/connectivity/manifest/credential-ref.ts +73 -0
- package/src/connectivity/manifest/identity.ts +43 -0
- package/src/connectivity/manifest/index.ts +46 -0
- package/src/connectivity/manifest/primitives.ts +20 -0
- package/src/connectivity/manifest/provider.ts +208 -0
- package/src/connectivity/manifest/requirements.ts +146 -0
- package/src/connectivity/manifest/setup.ts +55 -0
- package/src/connectivity/provider.ts +163 -0
- package/src/connectivity/transports/README.md +33 -0
- package/src/connectivity/transports/composite/index.ts +68 -0
- package/src/connectivity/transports/dav/calendar.ts +217 -0
- package/src/connectivity/transports/dav/capabilities.ts +151 -0
- package/src/connectivity/transports/dav/client.ts +226 -0
- package/src/connectivity/transports/dav/contacts.ts +75 -0
- package/src/connectivity/transports/dav/ical.ts +412 -0
- package/src/connectivity/transports/dav/index.ts +143 -0
- package/src/connectivity/transports/dav/operations.ts +43 -0
- package/src/connectivity/transports/dav/request.ts +161 -0
- package/src/connectivity/transports/dav/xml.ts +123 -0
- package/src/connectivity/transports/factory.ts +181 -0
- package/src/connectivity/transports/fs/capabilities.ts +88 -0
- package/src/connectivity/transports/fs/commands.ts +258 -0
- package/src/connectivity/transports/fs/index.ts +121 -0
- package/src/connectivity/transports/fs/operations.ts +45 -0
- package/src/connectivity/transports/fs/paths.ts +120 -0
- package/src/connectivity/transports/fs/result.ts +12 -0
- package/src/connectivity/transports/http/index.ts +255 -0
- package/src/connectivity/transports/imap/attachment.ts +166 -0
- package/src/connectivity/transports/imap/capabilities.ts +158 -0
- package/src/connectivity/transports/imap/client.ts +398 -0
- package/src/connectivity/transports/imap/commands.ts +385 -0
- package/src/connectivity/transports/imap/index.ts +152 -0
- package/src/connectivity/transports/imap/operations.ts +64 -0
- package/src/connectivity/transports/imap/parse.ts +130 -0
- package/src/connectivity/transports/imap/parser.ts +272 -0
- package/src/connectivity/transports/imap/result.ts +15 -0
- package/src/connectivity/transports/imap/send.ts +92 -0
- package/src/connectivity/transports/imap/socket.ts +111 -0
- package/src/connectivity/transports/imap/utf7.ts +136 -0
- package/src/connectivity/transports/index.ts +20 -0
- package/src/connectivity/transports/local/index.ts +173 -0
- package/src/connectivity/transports/mcp/index.ts +215 -0
- package/src/deployments/README.md +63 -0
- package/src/deployments/adapters/audit-blob.ts +203 -0
- package/src/deployments/adapters/filesystem.ts +184 -0
- package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
- package/src/deployments/adapters/gcs.ts +191 -0
- package/src/deployments/adapters/otlp.ts +128 -0
- package/src/deployments/adapters/s3.ts +195 -0
- package/src/deployments/azure/README.md +21 -0
- package/src/deployments/bootstrap.ts +177 -0
- package/src/deployments/deploy.ts +290 -0
- package/src/deployments/driver.ts +157 -0
- package/src/deployments/drivers.ts +35 -0
- package/src/deployments/gcp/Dockerfile +70 -0
- package/src/deployments/gcp/cloudbuild.yaml +31 -0
- package/src/deployments/gcp/driver.ts +175 -0
- package/src/deployments/gcp/gcloud.ts +178 -0
- package/src/deployments/gcp/provision.ts +290 -0
- package/src/deployments/gcp/survey.ts +319 -0
- package/src/deployments/local/README.md +12 -0
- package/src/deployments/prepare.ts +257 -0
- package/src/deployments/steps.ts +137 -0
- package/src/deployments/target.ts +295 -0
- package/src/deployments/upload.ts +207 -0
- package/src/dispatch/context.ts +195 -0
- package/src/dispatch/dispatch.ts +350 -0
- package/src/dispatch/index.ts +32 -0
- package/src/dispatch/staging.ts +102 -0
- package/src/policy/index.ts +179 -0
- package/src/policy/limits.ts +77 -0
- package/src/profile/authorization.ts +81 -0
- package/src/profile/files.ts +71 -0
- package/src/profile/index.ts +76 -0
- package/src/profile/layout.ts +123 -0
- package/src/profile/load.ts +199 -0
- package/src/profile/primitives.ts +45 -0
- package/src/profile/schema.ts +347 -0
- package/src/profile/secret-detection.ts +162 -0
- package/src/profile/targets.ts +152 -0
- package/src/profile/workspace.ts +262 -0
- package/src/providers/custom/index.ts +21 -0
- package/src/providers/custom/load.ts +115 -0
- package/src/providers/custom/template.ts +156 -0
- package/src/providers/example/provider.ts +207 -0
- package/src/providers/google/calendar/index.ts +66 -0
- package/src/providers/google/calendar/redact.ts +40 -0
- package/src/providers/google/contacts/index.ts +50 -0
- package/src/providers/google/contacts/redact.ts +21 -0
- package/src/providers/google/docs/index.ts +45 -0
- package/src/providers/google/drive/hints.ts +28 -0
- package/src/providers/google/drive/index.ts +34 -0
- package/src/providers/google/drive/redact.ts +39 -0
- package/src/providers/google/drive-mcp/index.ts +21 -0
- package/src/providers/google/gmail/api.ts +42 -0
- package/src/providers/google/gmail/attachment.ts +142 -0
- package/src/providers/google/gmail/hints.ts +55 -0
- package/src/providers/google/gmail/index.ts +112 -0
- package/src/providers/google/gmail/redact.ts +56 -0
- package/src/providers/google/gmail/send.ts +365 -0
- package/src/providers/google/gmail-mcp/index.ts +35 -0
- package/src/providers/google/index.ts +10 -0
- package/src/providers/google/shared/oauth.ts +122 -0
- package/src/providers/google/shared/scopes.ts +99 -0
- package/src/providers/google/shared/setup.ts +80 -0
- package/src/providers/google/sheets/hints.ts +45 -0
- package/src/providers/google/sheets/index.ts +70 -0
- package/src/providers/google/sheets/redact.ts +45 -0
- package/src/providers/google/specs/calendar.v3.json +1829 -0
- package/src/providers/google/specs/docs.v1.json +381 -0
- package/src/providers/google/specs/drive.v3.json +2208 -0
- package/src/providers/google/specs/gmail.v1.json +2578 -0
- package/src/providers/google/specs/people.v1.json +506 -0
- package/src/providers/google/specs/sheets.v4.json +1269 -0
- package/src/providers/google/specs/tasks.v1.json +840 -0
- package/src/providers/google/specs/vendor.ts +661 -0
- package/src/providers/google/tasks/index.ts +53 -0
- package/src/providers/google/tasks/redact.ts +34 -0
- package/src/providers/harness.ts +95 -0
- package/src/providers/icloud/calendar/index.ts +27 -0
- package/src/providers/icloud/contacts/index.ts +17 -0
- package/src/providers/icloud/drive/index.ts +47 -0
- package/src/providers/icloud/index.ts +8 -0
- package/src/providers/icloud/mail/index.ts +37 -0
- package/src/providers/icloud/shared/setup.ts +66 -0
- package/src/providers/index.ts +93 -0
- package/src/providers/linear/index.ts +11 -0
- package/src/providers/linear/scopes.ts +7 -0
- package/src/providers/memory/provider.ts +429 -0
- package/src/providers/notion/index.ts +19 -0
- package/src/providers/owner.ts +49 -0
- package/src/providers/scopes.ts +26 -0
- package/src/providers/setup/plan.ts +141 -0
- package/src/providers/setup/provider.ts +323 -0
- package/src/providers/shared/frontmatter.ts +119 -0
- package/src/providers/skills/provider.ts +283 -0
- package/src/providers/skills/store.ts +252 -0
- package/src/providers/vault/provider.ts +194 -0
- package/src/registry/index.ts +36 -0
- package/src/registry/policy-bridge.ts +32 -0
- package/src/registry/reconcile.ts +313 -0
- package/src/registry/registry.ts +240 -0
- package/src/secrets/document.ts +293 -0
- package/src/secrets/index.ts +154 -0
- package/src/secrets/system.ts +173 -0
- package/src/secrets/vault.ts +336 -0
- package/src/server/attachments.ts +197 -0
- package/src/server/container.ts +96 -0
- package/src/server/edge.ts +53 -0
- package/src/server/endpoint.ts +352 -0
- package/src/server/generations.ts +362 -0
- package/src/server/harness.ts +400 -0
- package/src/server/index.ts +331 -0
- package/src/server/logging.ts +41 -0
- package/src/server/mcp/build.ts +68 -0
- package/src/server/mcp/icon.ts +145 -0
- package/src/server/mcp/index.ts +32 -0
- package/src/server/mcp/instructions.ts +245 -0
- package/src/server/mcp/naming.ts +39 -0
- package/src/server/mcp/prompts.ts +78 -0
- package/src/server/mcp/resources.ts +106 -0
- package/src/server/mcp/routing.ts +117 -0
- package/src/server/mcp/schema.ts +78 -0
- package/src/server/mcp/tools.ts +186 -0
- package/src/server/mcp/visibility.ts +132 -0
- package/src/server/oauth.ts +222 -0
- package/src/server/rebinding.ts +53 -0
- package/src/server/stdio.ts +192 -0
- package/src/stores/blobs/conformance.ts +223 -0
- package/src/stores/blobs/index.ts +125 -0
- package/src/stores/blobs/testing.ts +49 -0
- package/src/stores/state/index.ts +247 -0
- package/src/stores/state/keys.ts +68 -0
- package/src/stores/state/testing.ts +41 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { planAll, planFor, type ProviderPlan } from '#providers/setup/plan.ts';
|
|
2
|
+
import { emit, heading, print, style, table } from '../output.ts';
|
|
3
|
+
import { missingRequirements } from './connect/requirements.ts';
|
|
4
|
+
import { openRuntime, type GlobalFlags } from '../runtime.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `lanes link setup plan [<provider>]` — what connecting something involves,
|
|
8
|
+
* before you start.
|
|
9
|
+
*
|
|
10
|
+
* The CLI twin of the `setup.provider` capability, sharing `planFor` so the two
|
|
11
|
+
* cannot disagree about what to run. This one also reports what is already
|
|
12
|
+
* *satisfied*, which the capability deliberately does not: reading the
|
|
13
|
+
* credential store to answer that is the control plane's job (ADR-007).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface SetupFlags extends GlobalFlags {
|
|
17
|
+
readonly json?: boolean | undefined;
|
|
18
|
+
readonly id?: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function setupPlan(provider: string | undefined, flags: SetupFlags): Promise<void> {
|
|
22
|
+
const runtime = await openRuntime(flags);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const context = {
|
|
26
|
+
profile: runtime.resolution.profile,
|
|
27
|
+
connections: runtime.config.connections.map((c) => `${c.provider}.${c.id}`),
|
|
28
|
+
// Declaring an `oauth_apps` entry is how a profile says "use my own
|
|
29
|
+
// client". Without this the plan would tell someone who has already
|
|
30
|
+
// registered one that they need nothing.
|
|
31
|
+
ownClients: Object.keys(runtime.config.oauth_apps),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const manifests = runtime.registry.list().map((entry) => entry.manifest);
|
|
35
|
+
|
|
36
|
+
if (provider) {
|
|
37
|
+
const manifest = manifests.find((candidate) => candidate.id === provider);
|
|
38
|
+
if (!manifest) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Unknown provider "${provider}".\n Run: lanes link setup plan to see every one this build ships.`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const plan = planFor(manifest, context, flags.id);
|
|
45
|
+
|
|
46
|
+
// With no id, a per-account ref is still the placeholder `<provider>/<id>`
|
|
47
|
+
// — not a reference the store would accept, and not one anything could
|
|
48
|
+
// have been written to. Asking is a malformed-reference error where the
|
|
49
|
+
// honest answer is "name the connection first".
|
|
50
|
+
const missing = plan.needsId
|
|
51
|
+
? new Set(plan.requires.map((requirement) => requirement.ref))
|
|
52
|
+
: new Set(
|
|
53
|
+
(await missingRequirements(plan.requires, runtime.credentials)).map(
|
|
54
|
+
(requirement) => requirement.ref,
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return emit(
|
|
59
|
+
flags.json,
|
|
60
|
+
{ ...plan, missing: [...missing] },
|
|
61
|
+
() => renderOne(plan, missing),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const plans = planAll(manifests, context);
|
|
66
|
+
|
|
67
|
+
return emit(flags.json, { profile: context.profile, providers: plans }, () => {
|
|
68
|
+
heading(`Connected in ${style.bold(context.profile)}`);
|
|
69
|
+
const done = plans.filter((plan) => plan.connected.length > 0);
|
|
70
|
+
if (done.length === 0) print(style.dim(' nothing yet'));
|
|
71
|
+
else table(done.map((plan) => [` ${style.bold(plan.name)}`, style.dim(plan.connected.join(', '))]));
|
|
72
|
+
|
|
73
|
+
heading('Available');
|
|
74
|
+
table(
|
|
75
|
+
plans
|
|
76
|
+
.filter((plan) => plan.connected.length === 0)
|
|
77
|
+
.map((plan) => [
|
|
78
|
+
` ${style.bold(plan.id)}`,
|
|
79
|
+
plan.browser ? style.yellow('browser') : style.green('no browser'),
|
|
80
|
+
style.dim(plan.description),
|
|
81
|
+
]),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
print();
|
|
85
|
+
print(style.dim('Detail for one: lanes link setup plan <provider>'));
|
|
86
|
+
});
|
|
87
|
+
} finally {
|
|
88
|
+
await runtime.close();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderOne(plan: ProviderPlan, missing: ReadonlySet<string>): void {
|
|
93
|
+
heading(plan.name);
|
|
94
|
+
print(` ${plan.description}`);
|
|
95
|
+
if (plan.summary) print(` ${plan.summary}`);
|
|
96
|
+
if (plan.docsUrl) print(` ${style.dim(plan.docsUrl)}`);
|
|
97
|
+
|
|
98
|
+
if (plan.connected.length > 0) {
|
|
99
|
+
print();
|
|
100
|
+
print(` ${style.green('connected')} ${plan.connected.join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (plan.steps.length > 0 && !plan.brokered) {
|
|
104
|
+
heading('First, in the vendor’s console');
|
|
105
|
+
plan.steps.forEach((step, index) => print(` ${index + 1}. ${step}`));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (plan.brokered) {
|
|
109
|
+
heading('Values it needs');
|
|
110
|
+
print(
|
|
111
|
+
` ${style.dim(`none — the OAuth client is operated by ${plan.clientOperator}, and its secret never reaches this machine.`)}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (plan.requires.length > 0) {
|
|
116
|
+
heading('Values it needs');
|
|
117
|
+
for (const requirement of plan.requires) {
|
|
118
|
+
const state = missing.has(requirement.ref)
|
|
119
|
+
? style.yellow('not stored')
|
|
120
|
+
: style.green('stored');
|
|
121
|
+
print(` ${style.bold(requirement.label)} ${state}`);
|
|
122
|
+
print(` ${style.dim(requirement.ref)}`);
|
|
123
|
+
if (missing.has(requirement.ref)) print(` ${requirement.command}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
heading('Then');
|
|
128
|
+
print(` ${plan.command}`);
|
|
129
|
+
|
|
130
|
+
// Last, and headed as the alternative it is. Someone reading this wants the
|
|
131
|
+
// one line that connects an account; the console walkthrough is for the
|
|
132
|
+
// minority who cannot use the hosted client, and putting it first taught
|
|
133
|
+
// everyone else that this provider is the hard one.
|
|
134
|
+
if (plan.brokered && plan.ownClientCommand) {
|
|
135
|
+
heading('Or register a client of your own');
|
|
136
|
+
print(` ${plan.ownClientCommand}`);
|
|
137
|
+
if (plan.steps.length > 0) {
|
|
138
|
+
print();
|
|
139
|
+
plan.steps.forEach((step, index) => print(` ${index + 1}. ${step}`));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (plan.browser) {
|
|
143
|
+
print();
|
|
144
|
+
print(
|
|
145
|
+
style.dim(
|
|
146
|
+
' This one authorises in a browser, so it has to be run by whoever owns the account.',
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TARGET_ENV,
|
|
3
|
+
askedTarget,
|
|
4
|
+
loadProfileConfig,
|
|
5
|
+
resolveSelection,
|
|
6
|
+
undeclaredTarget,
|
|
7
|
+
type Config,
|
|
8
|
+
type Resolution,
|
|
9
|
+
} from '#profile';
|
|
10
|
+
import { deployedUrl, deploymentIdentity, type DeploymentIdentity } from '../endpoint-url.ts';
|
|
11
|
+
import { ConfigDocument } from '../config-edit.ts';
|
|
12
|
+
import { announce, emit, heading, ok, print, style, table, waiting, warn } from '../output.ts';
|
|
13
|
+
import type { GlobalFlags } from '../runtime.ts';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `lanes link target` — the adapter sets a profile declares, and which one is in play.
|
|
17
|
+
*
|
|
18
|
+
* A target names *where a profile runs*: a credential store and a blob store,
|
|
19
|
+
* and optionally a deployment. Connections, providers, policy and limits are
|
|
20
|
+
* declared once and apply to every target, so changing one changes where the
|
|
21
|
+
* bytes go and nothing above them.
|
|
22
|
+
*
|
|
23
|
+
* Two answers are worth telling apart and this is the only place that does.
|
|
24
|
+
* `instance.default_target` is what commands run against when nobody says, and
|
|
25
|
+
* it lives in the profile. `LANES_LINK_TARGET` is what *this shell* says, and it
|
|
26
|
+
* wins. When they disagree — the case that sends someone hunting a bug — `list`
|
|
27
|
+
* marks both and names the variable.
|
|
28
|
+
*
|
|
29
|
+
* Each command is a data function plus a printing wrapper, the split
|
|
30
|
+
* `profile.ts` uses and for the same reason: `--json` wants the facts without
|
|
31
|
+
* the rendering.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export interface TargetSummary {
|
|
35
|
+
readonly name: string;
|
|
36
|
+
/** `instance.default_target` — what the file says. */
|
|
37
|
+
readonly isDefault: boolean;
|
|
38
|
+
/** What this shell resolves to right now, which may not be the above. */
|
|
39
|
+
readonly isSelected: boolean;
|
|
40
|
+
readonly credentials: string;
|
|
41
|
+
readonly storage: string;
|
|
42
|
+
readonly vault: string;
|
|
43
|
+
/** Whether a deployment is declared. Free, and always present. */
|
|
44
|
+
readonly deployed: boolean;
|
|
45
|
+
readonly deployment: DeploymentIdentity | null;
|
|
46
|
+
/** Only when asked for; absent is "not asked", null is "asked, no answer". */
|
|
47
|
+
readonly url?: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TargetListing {
|
|
51
|
+
readonly root: string;
|
|
52
|
+
readonly profile: string;
|
|
53
|
+
readonly path: string;
|
|
54
|
+
readonly default: string;
|
|
55
|
+
readonly selected: string;
|
|
56
|
+
readonly selectedSource: 'flag' | 'environment' | 'config-default';
|
|
57
|
+
/**
|
|
58
|
+
* Whether `selected` names a target that exists.
|
|
59
|
+
*
|
|
60
|
+
* False is the interesting case, and the reason this command does not resolve
|
|
61
|
+
* its target the ordinary way — see `survey`.
|
|
62
|
+
*/
|
|
63
|
+
readonly selectedDeclared: boolean;
|
|
64
|
+
readonly targets: readonly TargetSummary[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface TargetFlags extends GlobalFlags {
|
|
68
|
+
readonly json?: boolean | undefined;
|
|
69
|
+
readonly urls?: boolean | undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The profile, its config, and what it declares — in one pass.
|
|
74
|
+
*
|
|
75
|
+
* **Deliberately not `resolveProfile`.** That helper resolves the target the way
|
|
76
|
+
* every other command needs it resolved: by refusing a name that is not
|
|
77
|
+
* declared. Here that is exactly backwards. `LANES_LINK_TARGET=clod` is the
|
|
78
|
+
* state in which every other command has just started failing, and this is the
|
|
79
|
+
* command someone runs to find out why — so it has to survive the condition it
|
|
80
|
+
* exists to diagnose. It resolves the name with `askedTarget` and reports
|
|
81
|
+
* whether it landed on anything, rather than throwing. `readProfiles` in
|
|
82
|
+
* `profile.ts` declines to parse configs for the same reason.
|
|
83
|
+
*/
|
|
84
|
+
async function survey(
|
|
85
|
+
flags: TargetFlags,
|
|
86
|
+
options: { urls?: boolean; env?: Record<string, string | undefined> } = {},
|
|
87
|
+
): Promise<{ resolution: Resolution; config: Config; listing: TargetListing }> {
|
|
88
|
+
const env = options.env ?? (process.env as Record<string, string | undefined>);
|
|
89
|
+
|
|
90
|
+
const selection = await resolveSelection({
|
|
91
|
+
...(flags.profile !== undefined ? { profileFlag: flags.profile } : {}),
|
|
92
|
+
...(flags.target !== undefined ? { targetFlag: flags.target } : {}),
|
|
93
|
+
...(options.env !== undefined ? { env: options.env } : {}),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const { config } = await loadProfileConfig(selection.workspaceRoot, selection.profile);
|
|
97
|
+
|
|
98
|
+
const asked = askedTarget(flags.target, env);
|
|
99
|
+
const selected = asked.target ?? config.instance.default_target;
|
|
100
|
+
const names = Object.keys(config.targets);
|
|
101
|
+
|
|
102
|
+
const summaries: TargetSummary[] = names.map((name) => {
|
|
103
|
+
const declared = config.targets[name]!;
|
|
104
|
+
return {
|
|
105
|
+
name,
|
|
106
|
+
isDefault: name === config.instance.default_target,
|
|
107
|
+
isSelected: name === selected,
|
|
108
|
+
credentials: declared.credentials.adapter,
|
|
109
|
+
storage: declared.storage.adapter,
|
|
110
|
+
vault: declared.vault?.adapter ?? 'file',
|
|
111
|
+
deployed: declared.deploy !== undefined,
|
|
112
|
+
deployment: deploymentIdentity(declared.deploy),
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
resolution: { ...selection, target: selected, targetSource: asked.source ?? 'config-default' },
|
|
118
|
+
config,
|
|
119
|
+
listing: {
|
|
120
|
+
root: selection.workspaceRoot,
|
|
121
|
+
profile: selection.profile,
|
|
122
|
+
path: selection.profilePath,
|
|
123
|
+
default: config.instance.default_target,
|
|
124
|
+
selected,
|
|
125
|
+
selectedSource: asked.source ?? 'config-default',
|
|
126
|
+
selectedDeclared: names.includes(selected),
|
|
127
|
+
// Asking the platform is opt-in: one `gcloud` subprocess per deployable
|
|
128
|
+
// target, and the profiles that make this command worth running are
|
|
129
|
+
// exactly the ones with several. A discovery command that takes ten
|
|
130
|
+
// seconds and needs a cloud CLI installed is one nobody runs twice —
|
|
131
|
+
// `outputs --target X` is already the command that asks.
|
|
132
|
+
targets: options.urls === true ? await withUrls(config, summaries) : summaries,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** What `--json` and the tests want, without the rendering. */
|
|
138
|
+
export async function readTargets(
|
|
139
|
+
flags: TargetFlags,
|
|
140
|
+
options: { urls?: boolean; env?: Record<string, string | undefined> } = {},
|
|
141
|
+
): Promise<TargetListing> {
|
|
142
|
+
return (await survey(flags, options)).listing;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Every deployable target's address, asked for at once rather than in turn. */
|
|
146
|
+
async function withUrls(
|
|
147
|
+
config: Config,
|
|
148
|
+
summaries: readonly TargetSummary[],
|
|
149
|
+
): Promise<TargetSummary[]> {
|
|
150
|
+
return waiting('asking the platform for addresses', () =>
|
|
151
|
+
Promise.all(
|
|
152
|
+
summaries.map(async (summary) => ({
|
|
153
|
+
...summary,
|
|
154
|
+
// Resolves to null immediately for a target with no deployment, and
|
|
155
|
+
// swallows a missing or unauthenticated `gcloud` — neither is a reason
|
|
156
|
+
// for a listing to fail.
|
|
157
|
+
url: await deployedUrl(config.targets[summary.name]?.deploy),
|
|
158
|
+
})),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function targetList(flags: TargetFlags): Promise<void> {
|
|
164
|
+
const { resolution, listing } = await survey(flags, { urls: flags.urls === true });
|
|
165
|
+
|
|
166
|
+
return emit(flags.json, listing, () => {
|
|
167
|
+
announce(resolution);
|
|
168
|
+
print();
|
|
169
|
+
|
|
170
|
+
table(
|
|
171
|
+
listing.targets.map((target) => [
|
|
172
|
+
` ${target.isDefault ? style.green('*') : ' '} ${target.isSelected ? style.cyan('→') : ' '}`,
|
|
173
|
+
style.bold(target.name),
|
|
174
|
+
style.dim(target.credentials),
|
|
175
|
+
style.dim(target.storage),
|
|
176
|
+
deploymentCell(target),
|
|
177
|
+
]),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
print();
|
|
181
|
+
print(style.dim(` ${style.green('*')} instance.default_target — what commands run against`));
|
|
182
|
+
print(style.dim(` ${style.cyan('→')} what this shell resolves to right now`));
|
|
183
|
+
|
|
184
|
+
// Printed only when the two disagree, which is the whole reason both markers
|
|
185
|
+
// exist. Saying it every time would train people to stop reading it.
|
|
186
|
+
if (listing.selectedSource === 'environment') {
|
|
187
|
+
print(
|
|
188
|
+
style.dim(
|
|
189
|
+
` ${TARGET_ENV}=${listing.selected} is set in this shell and overrides the file.`,
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!listing.selectedDeclared) {
|
|
195
|
+
print();
|
|
196
|
+
print(
|
|
197
|
+
warn(
|
|
198
|
+
`"${listing.selected}" is not declared here — every command will refuse until it is ` +
|
|
199
|
+
(listing.selectedSource === 'environment' ? `unset (${TARGET_ENV}).` : 'corrected.'),
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function deploymentCell(target: TargetSummary): string {
|
|
207
|
+
if (target.url) return style.dim(target.url);
|
|
208
|
+
if (!target.deployment) return style.dim('—');
|
|
209
|
+
|
|
210
|
+
const { platform, service, region } = target.deployment;
|
|
211
|
+
// `url === null` means the platform was asked and did not answer; a missing
|
|
212
|
+
// `url` key means nobody asked. Saying which beats printing the same dash.
|
|
213
|
+
const asked = target.url === null ? style.dim(' (no address yet)') : '';
|
|
214
|
+
return `${style.dim(`${platform} ${service} ${region}`)}${asked}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* `lanes link target use <name>` — rewrite `instance.default_target`.
|
|
219
|
+
*
|
|
220
|
+
* The persisted half of the switch, and a *declared* one: the value lands in the
|
|
221
|
+
* profile YAML, where the operator can read it, `check` validates it, and every
|
|
222
|
+
* command prints that it came from there. That is the distinction `#profile`'s
|
|
223
|
+
* `workspace.ts` draws — what was rejected is hidden per-shell state in a
|
|
224
|
+
* dotfile nothing prints, not a default written where defaults already live.
|
|
225
|
+
*/
|
|
226
|
+
export async function targetUse(name: string | undefined, flags: GlobalFlags): Promise<void> {
|
|
227
|
+
if (!name) throw new Error('Usage: lanes link target use <name>');
|
|
228
|
+
|
|
229
|
+
const selection = await resolveSelection({
|
|
230
|
+
...(flags.profile !== undefined ? { profileFlag: flags.profile } : {}),
|
|
231
|
+
});
|
|
232
|
+
const { config } = await loadProfileConfig(selection.workspaceRoot, selection.profile);
|
|
233
|
+
|
|
234
|
+
// Where this profile stands before the edit, not what the edit will make true.
|
|
235
|
+
announce({
|
|
236
|
+
...selection,
|
|
237
|
+
target: config.instance.default_target,
|
|
238
|
+
targetSource: 'config-default',
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// `name` is an argument being validated, not a selection being resolved, so
|
|
242
|
+
// no environment gets a say in whether it exists.
|
|
243
|
+
if (!(name in config.targets)) throw undeclaredTarget(name, config);
|
|
244
|
+
|
|
245
|
+
if (config.instance.default_target === name) {
|
|
246
|
+
print(ok(`${style.bold(name)} is already the default`));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const document = await ConfigDocument.open(selection.workspaceRoot, selection.profile);
|
|
251
|
+
document.setIn(['instance', 'default_target'], name);
|
|
252
|
+
await document.save();
|
|
253
|
+
|
|
254
|
+
print(ok(`default target is now ${style.bold(name)}`));
|
|
255
|
+
|
|
256
|
+
// Otherwise the operator edits the file, sees nothing change, and concludes
|
|
257
|
+
// the command is broken. The variable is the thing that is winning.
|
|
258
|
+
const fromEnv = process.env[TARGET_ENV];
|
|
259
|
+
if (fromEnv && fromEnv !== name) {
|
|
260
|
+
print(warn(`${TARGET_ENV}=${fromEnv} is set in this shell, and still wins over the file.`));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* `lanes link target show [name]` — one target's adapters, and where it answers.
|
|
266
|
+
*
|
|
267
|
+
* The deep counterpart to `list`, and the one that *does* ask the platform: one
|
|
268
|
+
* target, one subprocess, and you named it. Nothing else prints a target's
|
|
269
|
+
* adapter set — `config show` dumps the whole file as JSON — which is what earns
|
|
270
|
+
* this its place beside `outputs`.
|
|
271
|
+
*/
|
|
272
|
+
export async function targetShow(name: string | undefined, flags: TargetFlags): Promise<void> {
|
|
273
|
+
const { config, listing } = await survey(flags);
|
|
274
|
+
const wanted = name ?? listing.selected;
|
|
275
|
+
const summary = listing.targets.find((candidate) => candidate.name === wanted);
|
|
276
|
+
|
|
277
|
+
if (!summary) throw undeclaredTarget(wanted, config, listing.selectedSource);
|
|
278
|
+
|
|
279
|
+
const url = summary.deployment
|
|
280
|
+
? await waiting('asking the platform for an address', () =>
|
|
281
|
+
deployedUrl(config.targets[wanted]?.deploy),
|
|
282
|
+
)
|
|
283
|
+
: null;
|
|
284
|
+
|
|
285
|
+
return emit(flags.json, { ...summary, url }, () => {
|
|
286
|
+
print(style.dim(`${listing.profile} ${listing.path}`));
|
|
287
|
+
|
|
288
|
+
heading(summary.name);
|
|
289
|
+
table([
|
|
290
|
+
[' credentials', summary.credentials],
|
|
291
|
+
[' storage', summary.storage],
|
|
292
|
+
[' vault', summary.vault],
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
if (!summary.deployment) {
|
|
296
|
+
print();
|
|
297
|
+
print(style.dim(' No deployment — this target runs wherever the CLI does.'));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
heading('Deployment');
|
|
302
|
+
table([
|
|
303
|
+
[' platform', summary.deployment.platform],
|
|
304
|
+
[' service', summary.deployment.service],
|
|
305
|
+
[' region', summary.deployment.region],
|
|
306
|
+
...(summary.deployment.project ? [[' project', summary.deployment.project]] : []),
|
|
307
|
+
[' address', url ?? style.dim('not answering — is it deployed?')],
|
|
308
|
+
]);
|
|
309
|
+
});
|
|
310
|
+
}
|