@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.
Files changed (276) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +144 -0
  3. package/bin/lanes +42 -0
  4. package/instructions/agents/lanes-link-scout.md +73 -0
  5. package/instructions/skills/lanes-link/SKILL.md +187 -0
  6. package/package.json +95 -0
  7. package/src/audit/chain.ts +278 -0
  8. package/src/audit/conformance.ts +374 -0
  9. package/src/audit/fanout.ts +97 -0
  10. package/src/audit/index.ts +218 -0
  11. package/src/audit/stdout.ts +60 -0
  12. package/src/auth/index.ts +220 -0
  13. package/src/auth/oauth/metadata.ts +75 -0
  14. package/src/auth/oauth/server.ts +385 -0
  15. package/src/auth/oauth/store.ts +215 -0
  16. package/src/auth/oidc.ts +206 -0
  17. package/src/auth/remote.ts +72 -0
  18. package/src/cli/argv.ts +94 -0
  19. package/src/cli/callback-page.ts +256 -0
  20. package/src/cli/commands/connect/accounts.ts +94 -0
  21. package/src/cli/commands/connect/authorise.ts +298 -0
  22. package/src/cli/commands/connect/client.ts +284 -0
  23. package/src/cli/commands/connect/index.ts +398 -0
  24. package/src/cli/commands/connect/outcome.ts +119 -0
  25. package/src/cli/commands/connect/requirements.ts +103 -0
  26. package/src/cli/commands/connect/scopes-gate.ts +146 -0
  27. package/src/cli/commands/connect/settle.ts +136 -0
  28. package/src/cli/commands/connect/setup.ts +276 -0
  29. package/src/cli/commands/mcp/assets.ts +189 -0
  30. package/src/cli/commands/mcp/harnesses.ts +143 -0
  31. package/src/cli/commands/mcp/list.ts +70 -0
  32. package/src/cli/commands/mcp/register.ts +199 -0
  33. package/src/cli/commands/mcp/stdio.ts +57 -0
  34. package/src/cli/commands/mcp.ts +22 -0
  35. package/src/cli/commands/operate/attach.ts +121 -0
  36. package/src/cli/commands/operate/audit.ts +119 -0
  37. package/src/cli/commands/operate/inspect.ts +355 -0
  38. package/src/cli/commands/operate/outputs.ts +206 -0
  39. package/src/cli/commands/operate/policy.ts +80 -0
  40. package/src/cli/commands/operate/serve.ts +55 -0
  41. package/src/cli/commands/operate/status.ts +133 -0
  42. package/src/cli/commands/operate/token.ts +70 -0
  43. package/src/cli/commands/operate.ts +27 -0
  44. package/src/cli/commands/owner/memory.ts +110 -0
  45. package/src/cli/commands/owner/shared.ts +125 -0
  46. package/src/cli/commands/owner/skills.ts +92 -0
  47. package/src/cli/commands/owner/vault.ts +140 -0
  48. package/src/cli/commands/owner.ts +46 -0
  49. package/src/cli/commands/profile/removal.ts +278 -0
  50. package/src/cli/commands/profile/remove.ts +315 -0
  51. package/src/cli/commands/profile.ts +156 -0
  52. package/src/cli/commands/secrets.ts +176 -0
  53. package/src/cli/commands/setup.ts +150 -0
  54. package/src/cli/commands/target.ts +310 -0
  55. package/src/cli/config-edit.ts +397 -0
  56. package/src/cli/endpoint-url.ts +77 -0
  57. package/src/cli/identity.ts +109 -0
  58. package/src/cli/lanes.ts +78 -0
  59. package/src/cli/main.ts +333 -0
  60. package/src/cli/oauth-error.ts +13 -0
  61. package/src/cli/oauth-exchange.ts +146 -0
  62. package/src/cli/oauth.ts +354 -0
  63. package/src/cli/output.ts +184 -0
  64. package/src/cli/prompt.ts +180 -0
  65. package/src/cli/publish.ts +185 -0
  66. package/src/cli/runtime/discovery.ts +123 -0
  67. package/src/cli/runtime/open.ts +342 -0
  68. package/src/cli/runtime/registry.ts +185 -0
  69. package/src/cli/runtime/select.ts +124 -0
  70. package/src/cli/runtime.ts +34 -0
  71. package/src/cli/scopes.ts +63 -0
  72. package/src/cli/usage.ts +111 -0
  73. package/src/cli/version.ts +25 -0
  74. package/src/connectivity/auth/README.md +36 -0
  75. package/src/connectivity/auth/api-key/index.ts +43 -0
  76. package/src/connectivity/auth/authorize.ts +49 -0
  77. package/src/connectivity/auth/basic/index.ts +68 -0
  78. package/src/connectivity/auth/bearer/index.ts +13 -0
  79. package/src/connectivity/auth/credential.ts +19 -0
  80. package/src/connectivity/auth/header/index.ts +18 -0
  81. package/src/connectivity/auth/index.ts +35 -0
  82. package/src/connectivity/auth/none/index.ts +12 -0
  83. package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
  84. package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
  85. package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
  86. package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
  87. package/src/connectivity/auth/resolve.ts +61 -0
  88. package/src/connectivity/auth/strategy/index.ts +14 -0
  89. package/src/connectivity/capability.ts +164 -0
  90. package/src/connectivity/connector.ts +175 -0
  91. package/src/connectivity/context.ts +77 -0
  92. package/src/connectivity/index.ts +94 -0
  93. package/src/connectivity/mail/attachments.ts +368 -0
  94. package/src/connectivity/mail/compose.ts +73 -0
  95. package/src/connectivity/mail/index.ts +40 -0
  96. package/src/connectivity/mail/message.ts +82 -0
  97. package/src/connectivity/mail/nodemailer.d.ts +83 -0
  98. package/src/connectivity/mail/staging.ts +143 -0
  99. package/src/connectivity/mail/url.ts +301 -0
  100. package/src/connectivity/manifest/auth.ts +135 -0
  101. package/src/connectivity/manifest/bundles.ts +23 -0
  102. package/src/connectivity/manifest/connector.ts +168 -0
  103. package/src/connectivity/manifest/credential-ref.ts +73 -0
  104. package/src/connectivity/manifest/identity.ts +43 -0
  105. package/src/connectivity/manifest/index.ts +46 -0
  106. package/src/connectivity/manifest/primitives.ts +20 -0
  107. package/src/connectivity/manifest/provider.ts +208 -0
  108. package/src/connectivity/manifest/requirements.ts +146 -0
  109. package/src/connectivity/manifest/setup.ts +55 -0
  110. package/src/connectivity/provider.ts +163 -0
  111. package/src/connectivity/transports/README.md +33 -0
  112. package/src/connectivity/transports/composite/index.ts +68 -0
  113. package/src/connectivity/transports/dav/calendar.ts +217 -0
  114. package/src/connectivity/transports/dav/capabilities.ts +151 -0
  115. package/src/connectivity/transports/dav/client.ts +226 -0
  116. package/src/connectivity/transports/dav/contacts.ts +75 -0
  117. package/src/connectivity/transports/dav/ical.ts +412 -0
  118. package/src/connectivity/transports/dav/index.ts +143 -0
  119. package/src/connectivity/transports/dav/operations.ts +43 -0
  120. package/src/connectivity/transports/dav/request.ts +161 -0
  121. package/src/connectivity/transports/dav/xml.ts +123 -0
  122. package/src/connectivity/transports/factory.ts +181 -0
  123. package/src/connectivity/transports/fs/capabilities.ts +88 -0
  124. package/src/connectivity/transports/fs/commands.ts +258 -0
  125. package/src/connectivity/transports/fs/index.ts +121 -0
  126. package/src/connectivity/transports/fs/operations.ts +45 -0
  127. package/src/connectivity/transports/fs/paths.ts +120 -0
  128. package/src/connectivity/transports/fs/result.ts +12 -0
  129. package/src/connectivity/transports/http/index.ts +255 -0
  130. package/src/connectivity/transports/imap/attachment.ts +166 -0
  131. package/src/connectivity/transports/imap/capabilities.ts +158 -0
  132. package/src/connectivity/transports/imap/client.ts +398 -0
  133. package/src/connectivity/transports/imap/commands.ts +385 -0
  134. package/src/connectivity/transports/imap/index.ts +152 -0
  135. package/src/connectivity/transports/imap/operations.ts +64 -0
  136. package/src/connectivity/transports/imap/parse.ts +130 -0
  137. package/src/connectivity/transports/imap/parser.ts +272 -0
  138. package/src/connectivity/transports/imap/result.ts +15 -0
  139. package/src/connectivity/transports/imap/send.ts +92 -0
  140. package/src/connectivity/transports/imap/socket.ts +111 -0
  141. package/src/connectivity/transports/imap/utf7.ts +136 -0
  142. package/src/connectivity/transports/index.ts +20 -0
  143. package/src/connectivity/transports/local/index.ts +173 -0
  144. package/src/connectivity/transports/mcp/index.ts +215 -0
  145. package/src/deployments/README.md +63 -0
  146. package/src/deployments/adapters/audit-blob.ts +203 -0
  147. package/src/deployments/adapters/filesystem.ts +184 -0
  148. package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
  149. package/src/deployments/adapters/gcs.ts +191 -0
  150. package/src/deployments/adapters/otlp.ts +128 -0
  151. package/src/deployments/adapters/s3.ts +195 -0
  152. package/src/deployments/azure/README.md +21 -0
  153. package/src/deployments/bootstrap.ts +177 -0
  154. package/src/deployments/deploy.ts +290 -0
  155. package/src/deployments/driver.ts +157 -0
  156. package/src/deployments/drivers.ts +35 -0
  157. package/src/deployments/gcp/Dockerfile +70 -0
  158. package/src/deployments/gcp/cloudbuild.yaml +31 -0
  159. package/src/deployments/gcp/driver.ts +175 -0
  160. package/src/deployments/gcp/gcloud.ts +178 -0
  161. package/src/deployments/gcp/provision.ts +290 -0
  162. package/src/deployments/gcp/survey.ts +319 -0
  163. package/src/deployments/local/README.md +12 -0
  164. package/src/deployments/prepare.ts +257 -0
  165. package/src/deployments/steps.ts +137 -0
  166. package/src/deployments/target.ts +295 -0
  167. package/src/deployments/upload.ts +207 -0
  168. package/src/dispatch/context.ts +195 -0
  169. package/src/dispatch/dispatch.ts +350 -0
  170. package/src/dispatch/index.ts +32 -0
  171. package/src/dispatch/staging.ts +102 -0
  172. package/src/policy/index.ts +179 -0
  173. package/src/policy/limits.ts +77 -0
  174. package/src/profile/authorization.ts +81 -0
  175. package/src/profile/files.ts +71 -0
  176. package/src/profile/index.ts +76 -0
  177. package/src/profile/layout.ts +123 -0
  178. package/src/profile/load.ts +199 -0
  179. package/src/profile/primitives.ts +45 -0
  180. package/src/profile/schema.ts +347 -0
  181. package/src/profile/secret-detection.ts +162 -0
  182. package/src/profile/targets.ts +152 -0
  183. package/src/profile/workspace.ts +262 -0
  184. package/src/providers/custom/index.ts +21 -0
  185. package/src/providers/custom/load.ts +115 -0
  186. package/src/providers/custom/template.ts +156 -0
  187. package/src/providers/example/provider.ts +207 -0
  188. package/src/providers/google/calendar/index.ts +66 -0
  189. package/src/providers/google/calendar/redact.ts +40 -0
  190. package/src/providers/google/contacts/index.ts +50 -0
  191. package/src/providers/google/contacts/redact.ts +21 -0
  192. package/src/providers/google/docs/index.ts +45 -0
  193. package/src/providers/google/drive/hints.ts +28 -0
  194. package/src/providers/google/drive/index.ts +34 -0
  195. package/src/providers/google/drive/redact.ts +39 -0
  196. package/src/providers/google/drive-mcp/index.ts +21 -0
  197. package/src/providers/google/gmail/api.ts +42 -0
  198. package/src/providers/google/gmail/attachment.ts +142 -0
  199. package/src/providers/google/gmail/hints.ts +55 -0
  200. package/src/providers/google/gmail/index.ts +112 -0
  201. package/src/providers/google/gmail/redact.ts +56 -0
  202. package/src/providers/google/gmail/send.ts +365 -0
  203. package/src/providers/google/gmail-mcp/index.ts +35 -0
  204. package/src/providers/google/index.ts +10 -0
  205. package/src/providers/google/shared/oauth.ts +122 -0
  206. package/src/providers/google/shared/scopes.ts +99 -0
  207. package/src/providers/google/shared/setup.ts +80 -0
  208. package/src/providers/google/sheets/hints.ts +45 -0
  209. package/src/providers/google/sheets/index.ts +70 -0
  210. package/src/providers/google/sheets/redact.ts +45 -0
  211. package/src/providers/google/specs/calendar.v3.json +1829 -0
  212. package/src/providers/google/specs/docs.v1.json +381 -0
  213. package/src/providers/google/specs/drive.v3.json +2208 -0
  214. package/src/providers/google/specs/gmail.v1.json +2578 -0
  215. package/src/providers/google/specs/people.v1.json +506 -0
  216. package/src/providers/google/specs/sheets.v4.json +1269 -0
  217. package/src/providers/google/specs/tasks.v1.json +840 -0
  218. package/src/providers/google/specs/vendor.ts +661 -0
  219. package/src/providers/google/tasks/index.ts +53 -0
  220. package/src/providers/google/tasks/redact.ts +34 -0
  221. package/src/providers/harness.ts +95 -0
  222. package/src/providers/icloud/calendar/index.ts +27 -0
  223. package/src/providers/icloud/contacts/index.ts +17 -0
  224. package/src/providers/icloud/drive/index.ts +47 -0
  225. package/src/providers/icloud/index.ts +8 -0
  226. package/src/providers/icloud/mail/index.ts +37 -0
  227. package/src/providers/icloud/shared/setup.ts +66 -0
  228. package/src/providers/index.ts +93 -0
  229. package/src/providers/linear/index.ts +11 -0
  230. package/src/providers/linear/scopes.ts +7 -0
  231. package/src/providers/memory/provider.ts +429 -0
  232. package/src/providers/notion/index.ts +19 -0
  233. package/src/providers/owner.ts +49 -0
  234. package/src/providers/scopes.ts +26 -0
  235. package/src/providers/setup/plan.ts +141 -0
  236. package/src/providers/setup/provider.ts +323 -0
  237. package/src/providers/shared/frontmatter.ts +119 -0
  238. package/src/providers/skills/provider.ts +283 -0
  239. package/src/providers/skills/store.ts +252 -0
  240. package/src/providers/vault/provider.ts +194 -0
  241. package/src/registry/index.ts +36 -0
  242. package/src/registry/policy-bridge.ts +32 -0
  243. package/src/registry/reconcile.ts +313 -0
  244. package/src/registry/registry.ts +240 -0
  245. package/src/secrets/document.ts +293 -0
  246. package/src/secrets/index.ts +154 -0
  247. package/src/secrets/system.ts +173 -0
  248. package/src/secrets/vault.ts +336 -0
  249. package/src/server/attachments.ts +197 -0
  250. package/src/server/container.ts +96 -0
  251. package/src/server/edge.ts +53 -0
  252. package/src/server/endpoint.ts +352 -0
  253. package/src/server/generations.ts +362 -0
  254. package/src/server/harness.ts +400 -0
  255. package/src/server/index.ts +331 -0
  256. package/src/server/logging.ts +41 -0
  257. package/src/server/mcp/build.ts +68 -0
  258. package/src/server/mcp/icon.ts +145 -0
  259. package/src/server/mcp/index.ts +32 -0
  260. package/src/server/mcp/instructions.ts +245 -0
  261. package/src/server/mcp/naming.ts +39 -0
  262. package/src/server/mcp/prompts.ts +78 -0
  263. package/src/server/mcp/resources.ts +106 -0
  264. package/src/server/mcp/routing.ts +117 -0
  265. package/src/server/mcp/schema.ts +78 -0
  266. package/src/server/mcp/tools.ts +186 -0
  267. package/src/server/mcp/visibility.ts +132 -0
  268. package/src/server/oauth.ts +222 -0
  269. package/src/server/rebinding.ts +53 -0
  270. package/src/server/stdio.ts +192 -0
  271. package/src/stores/blobs/conformance.ts +223 -0
  272. package/src/stores/blobs/index.ts +125 -0
  273. package/src/stores/blobs/testing.ts +49 -0
  274. package/src/stores/state/index.ts +247 -0
  275. package/src/stores/state/keys.ts +68 -0
  276. 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
+ }