@lanes-sh/link 0.2.2 → 0.3.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 (130) hide show
  1. package/README.md +21 -8
  2. package/instructions/skills/lanes-link/SKILL.md +42 -14
  3. package/package.json +1 -1
  4. package/src/cli/argv.ts +50 -0
  5. package/src/cli/brand.ts +178 -0
  6. package/src/cli/callback-page.ts +108 -128
  7. package/src/cli/commands/connect/accounts.ts +5 -0
  8. package/src/cli/commands/connect/assertion.ts +187 -0
  9. package/src/cli/commands/connect/authorise.ts +61 -17
  10. package/src/cli/commands/connect/client.ts +37 -9
  11. package/src/cli/commands/connect/discover.ts +94 -0
  12. package/src/cli/commands/connect/family.ts +72 -0
  13. package/src/cli/commands/connect/index.ts +113 -115
  14. package/src/cli/commands/connect/method.ts +237 -0
  15. package/src/cli/commands/connect/outcome.ts +42 -1
  16. package/src/cli/commands/connect/pasted-token.ts +66 -0
  17. package/src/cli/commands/connect/requirements.ts +60 -8
  18. package/src/cli/commands/connect/setup.ts +16 -5
  19. package/src/cli/commands/connect/target-note.ts +34 -0
  20. package/src/cli/commands/identity.ts +258 -0
  21. package/src/cli/commands/knowledge/index.ts +390 -0
  22. package/src/cli/commands/knowledge/migrate.ts +180 -0
  23. package/src/cli/commands/knowledge/setup.ts +144 -0
  24. package/src/cli/commands/knowledge.ts +10 -0
  25. package/src/cli/commands/mcp/harnesses.ts +16 -2
  26. package/src/cli/commands/mcp/register.ts +9 -1
  27. package/src/cli/commands/mcp/stdio.ts +21 -0
  28. package/src/cli/commands/operate/dashboard.ts +107 -0
  29. package/src/cli/commands/operate/findings.ts +151 -0
  30. package/src/cli/commands/operate/inspect.ts +56 -158
  31. package/src/cli/commands/operate/outputs.ts +38 -11
  32. package/src/cli/commands/operate/serve.ts +3 -0
  33. package/src/cli/commands/operate/token.ts +1 -1
  34. package/src/cli/commands/operate.ts +2 -0
  35. package/src/cli/commands/profile/declare.ts +154 -0
  36. package/src/cli/commands/profile/removal.ts +17 -0
  37. package/src/cli/commands/profile.ts +83 -35
  38. package/src/cli/commands/setup.ts +22 -6
  39. package/src/cli/commands/target.ts +65 -83
  40. package/src/cli/config-edit.ts +48 -144
  41. package/src/cli/config-repair.ts +186 -0
  42. package/src/cli/dashboard-page.ts +284 -0
  43. package/src/cli/dashboard-shell.ts +125 -0
  44. package/src/cli/identity.ts +12 -1
  45. package/src/cli/main.ts +68 -4
  46. package/src/cli/oauth-callback.ts +187 -0
  47. package/src/cli/oauth-exchange.ts +57 -15
  48. package/src/cli/oauth.ts +67 -177
  49. package/src/cli/output.ts +21 -5
  50. package/src/cli/provider-marks.ts +45 -0
  51. package/src/cli/runtime/open.ts +67 -49
  52. package/src/cli/runtime/registry.ts +60 -2
  53. package/src/cli/runtime/select.ts +26 -13
  54. package/src/cli/runtime/vault.ts +61 -0
  55. package/src/cli/runtime.ts +2 -1
  56. package/src/cli/selection.ts +357 -0
  57. package/src/cli/usage.ts +32 -9
  58. package/src/connectivity/auth/README.md +7 -1
  59. package/src/connectivity/auth/basic/index.ts +1 -1
  60. package/src/connectivity/auth/index.ts +14 -0
  61. package/src/connectivity/auth/oauth-authcode/broker.ts +26 -0
  62. package/src/connectivity/auth/oauth-authcode/index.ts +16 -2
  63. package/src/connectivity/auth/oauth-authcode/provider.ts +1 -1
  64. package/src/connectivity/auth/oauth-authcode/refresh.ts +3 -3
  65. package/src/connectivity/auth/oauth-jwt/README.md +33 -0
  66. package/src/connectivity/auth/oauth-jwt/index.ts +237 -0
  67. package/src/connectivity/auth/oauth-jwt/key.ts +148 -0
  68. package/src/connectivity/auth/resolve.ts +1 -1
  69. package/src/connectivity/auth/token.ts +11 -0
  70. package/src/connectivity/index.ts +2 -0
  71. package/src/connectivity/manifest/auth.ts +99 -2
  72. package/src/connectivity/manifest/identity.ts +12 -0
  73. package/src/connectivity/manifest/index.ts +3 -1
  74. package/src/connectivity/manifest/provider.ts +37 -8
  75. package/src/connectivity/manifest/requirements.ts +109 -6
  76. package/src/deployments/adapters/filesystem.ts +10 -1
  77. package/src/deployments/adapters/github-api.ts +106 -0
  78. package/src/deployments/adapters/github-commit.ts +103 -0
  79. package/src/deployments/adapters/github-repo.ts +356 -0
  80. package/src/deployments/adapters/github-testing.ts +258 -0
  81. package/src/deployments/adapters/github.ts +125 -0
  82. package/src/deployments/deploy.ts +48 -19
  83. package/src/deployments/driver.ts +8 -1
  84. package/src/deployments/gcp/driver.ts +3 -1
  85. package/src/deployments/knowledge.ts +119 -0
  86. package/src/deployments/prepare.ts +2 -2
  87. package/src/deployments/servable.ts +81 -0
  88. package/src/deployments/target.ts +3 -2
  89. package/src/deployments/upload.ts +2 -1
  90. package/src/dispatch/dispatch.ts +1 -1
  91. package/src/profile/identity.ts +60 -0
  92. package/src/profile/index.ts +15 -5
  93. package/src/profile/knowledge.ts +124 -0
  94. package/src/profile/load.ts +17 -5
  95. package/src/profile/primitives.ts +24 -1
  96. package/src/profile/schema.ts +46 -3
  97. package/src/profile/targets.ts +74 -114
  98. package/src/profile/workspace.ts +71 -84
  99. package/src/providers/google/calendar/index.ts +2 -0
  100. package/src/providers/google/contacts/index.ts +2 -0
  101. package/src/providers/google/docs/index.ts +2 -0
  102. package/src/providers/google/drive/index.ts +2 -0
  103. package/src/providers/google/gmail/index.ts +2 -0
  104. package/src/providers/google/gmail-imap/index.ts +125 -0
  105. package/src/providers/google/index.ts +2 -1
  106. package/src/providers/google/shared/oauth.ts +18 -6
  107. package/src/providers/google/shared/service-account.ts +110 -0
  108. package/src/providers/google/shared/setup.ts +5 -2
  109. package/src/providers/google/sheets/index.ts +2 -0
  110. package/src/providers/google/tasks/index.ts +2 -0
  111. package/src/providers/identity/provider.ts +166 -0
  112. package/src/providers/index.ts +3 -0
  113. package/src/providers/owner.ts +10 -2
  114. package/src/providers/scopes.ts +2 -0
  115. package/src/providers/setup/plan.ts +31 -9
  116. package/src/providers/setup/provider.ts +23 -0
  117. package/src/providers/slack/index.ts +81 -33
  118. package/src/providers/slack/oauth.ts +103 -0
  119. package/src/providers/slack/scopes.ts +37 -0
  120. package/src/server/container.ts +18 -1
  121. package/src/server/cors.ts +252 -0
  122. package/src/server/dashboard.ts +208 -0
  123. package/src/server/endpoint.ts +45 -1
  124. package/src/server/generations.ts +11 -2
  125. package/src/server/harness.ts +7 -0
  126. package/src/server/index.ts +45 -6
  127. package/src/server/mcp/index.ts +1 -0
  128. package/src/server/mcp/instructions.ts +28 -1
  129. package/src/server/mcp/visibility.ts +33 -0
  130. package/src/stores/blobs/route.ts +123 -0
@@ -1,8 +1,8 @@
1
1
  import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { parseDocument } from 'yaml';
5
4
  import {
5
+ ConfigError,
6
6
  WORKSPACE_FILE,
7
7
  listProfiles,
8
8
  profilePath,
@@ -10,6 +10,7 @@ import {
10
10
  resolveWorkspaceRoot,
11
11
  } from '#profile';
12
12
  import { newProfileTemplate, newWorkspaceTemplate } from '../config-edit.ts';
13
+ import { askTarget, localBlock, siblingTarget } from './profile/declare.ts';
13
14
  import { emit, ok, print, style, table } from '../output.ts';
14
15
 
15
16
  /**
@@ -32,7 +33,10 @@ export interface ProfileCreated {
32
33
  readonly name: string;
33
34
  readonly path: string;
34
35
  readonly port: number;
35
- readonly isDefault: boolean;
36
+ /** Every target the new profile declares, in the order they were named. */
37
+ readonly targets: readonly string[];
38
+ /** Which sibling supplied each non-local target's adapters, where one did. */
39
+ readonly copiedFrom: Readonly<Record<string, string>>;
36
40
  }
37
41
 
38
42
  export interface ProfileListing {
@@ -41,10 +45,17 @@ export interface ProfileListing {
41
45
  readonly profiles: ReadonlyArray<{ readonly name: string; readonly path: string }>;
42
46
  }
43
47
 
44
- /** Write a new profile, and the workspace file if this is the first one. */
48
+ /**
49
+ * Write a new profile, and the workspace file if this is the first one.
50
+ *
51
+ * The targets are the argument that used to be missing. `--target` was accepted
52
+ * and dropped here, and the template could only ever emit `local` — so the
53
+ * command reported success and produced a profile that could not reach the
54
+ * deployment the operator had just told it about.
55
+ */
45
56
  export async function createProfile(
46
57
  name: string,
47
- options: { default?: boolean },
58
+ options: { targets: readonly string[]; nonInteractive?: boolean },
48
59
  ): Promise<ProfileCreated> {
49
60
  const root = resolveWorkspaceRoot();
50
61
  const path = profilePath(root, name);
@@ -58,23 +69,37 @@ export async function createProfile(
58
69
  const existing = await listProfiles(root);
59
70
  const port = FIRST_PORT + existing.length;
60
71
 
61
- const workspaceFile = join(root, WORKSPACE_FILE);
62
- const isFirst = existing.length === 0;
63
- const isDefault = options.default === true || isFirst;
72
+ // Everything that can fail — a target nobody declares, a prompt nobody can
73
+ // answer happens before the first write, so a refusal leaves the workspace
74
+ // exactly as it was rather than half a profile behind.
75
+ const blocks: string[] = [];
76
+ const copiedFrom: Record<string, string> = {};
64
77
 
65
- if (!existsSync(workspaceFile)) {
66
- await writeFile(workspaceFile, newWorkspaceTemplate(isDefault ? name : undefined), {
67
- mode: 0o600,
78
+ for (const target of options.targets) {
79
+ if (target === 'local') {
80
+ blocks.push(localBlock(name));
81
+ continue;
82
+ }
83
+
84
+ const declared = await askTarget({
85
+ target,
86
+ profile: name,
87
+ sibling: await siblingTarget(root, target, name),
88
+ ...(options.nonInteractive === true ? { nonInteractive: true } : {}),
68
89
  });
69
- } else if (options.default) {
70
- const document = parseDocument(await Bun.file(workspaceFile).text());
71
- document.set('default_profile', name);
72
- await writeFile(workspaceFile, document.toString({ lineWidth: 0 }), { mode: 0o600 });
90
+
91
+ blocks.push(declared.block);
92
+ if (declared.from) copiedFrom[target] = declared.from;
73
93
  }
74
94
 
75
- await writeFile(path, newProfileTemplate(name, port), { mode: 0o600 });
95
+ const workspaceFile = join(root, WORKSPACE_FILE);
96
+ if (!existsSync(workspaceFile)) {
97
+ await writeFile(workspaceFile, newWorkspaceTemplate(), { mode: 0o600 });
98
+ }
76
99
 
77
- return { name, path, port, isDefault };
100
+ await writeFile(path, newProfileTemplate(name, port, blocks.join('')), { mode: 0o600 });
101
+
102
+ return { name, path, port, targets: options.targets, copiedFrom };
78
103
  }
79
104
 
80
105
  /**
@@ -99,18 +124,36 @@ export async function readProfiles(): Promise<ProfileListing> {
99
124
 
100
125
  export async function profileAdd(
101
126
  name: string,
102
- options: { default?: boolean; json?: boolean },
127
+ options: { targets: readonly string[]; nonInteractive?: boolean; json?: boolean },
103
128
  ): Promise<void> {
129
+ if (options.targets.length === 0) {
130
+ throw new ConfigError(
131
+ 'Say which targets this profile declares:\n' +
132
+ ` lanes link profile add ${name} --target local\n` +
133
+ ` lanes link profile add ${name} --target local --target cloud\n` +
134
+ '\n' +
135
+ ' A target is where a profile runs — a credential store and a blob store.\n' +
136
+ ' There is no default to inherit before the profile exists.',
137
+ );
138
+ }
139
+
104
140
  const created = await createProfile(name, options);
105
141
 
106
142
  return emit(options.json, created, () => {
107
143
  print(ok(`created profile ${style.bold(created.name)}`));
108
- print(` config ${created.path}`);
109
- print(` port ${created.port}`);
110
- if (created.isDefault) print(` set as the workspace default`);
144
+ print(` config ${created.path}`);
145
+ print(` port ${created.port}`);
146
+ print(` targets ${created.targets.join(', ')}`);
147
+
148
+ for (const [target, from] of Object.entries(created.copiedFrom)) {
149
+ print(` ${style.dim(`${target} adapters copied from profile "${from}"`)}`);
150
+ }
151
+
111
152
  print();
112
153
  print(
113
- style.dim('Next: lanes link connect example # add a connection, no credentials needed'),
154
+ style.dim(
155
+ `Next: lanes link connect example --profile ${created.name} --target ${created.targets[0]}`,
156
+ ),
114
157
  );
115
158
  });
116
159
  }
@@ -121,7 +164,7 @@ export async function profileList(options: { json?: boolean } = {}): Promise<voi
121
164
  return emit(options.json, listing, () => {
122
165
  if (listing.profiles.length === 0) {
123
166
  print(style.dim(`No profiles in ${listing.root}.`));
124
- print(style.dim('Create one with: lanes link profile add personal --default'));
167
+ print(style.dim('Create one with: lanes link profile add personal --target local'));
125
168
  return;
126
169
  }
127
170
 
@@ -136,19 +179,24 @@ export async function profileList(options: { json?: boolean } = {}): Promise<voi
136
179
  });
137
180
  }
138
181
 
139
- export async function profileDefault(name: string): Promise<void> {
140
- const root = resolveWorkspaceRoot();
141
- if (!existsSync(profilePath(root, name))) throw new Error(`Profile "${name}" does not exist`);
142
-
143
- const workspaceFile = join(root, WORKSPACE_FILE);
144
- const document = existsSync(workspaceFile)
145
- ? parseDocument(await Bun.file(workspaceFile).text())
146
- : parseDocument(newWorkspaceTemplate());
147
-
148
- document.set('default_profile', name);
149
- await writeFile(workspaceFile, document.toString({ lineWidth: 0 }), { mode: 0o600 });
150
-
151
- print(ok(`default profile is now ${style.bold(name)}`));
182
+ /**
183
+ * `lanes link profile default <name>` — removed.
184
+ *
185
+ * It wrote `default_profile`, which nothing reads (ADR-037). A command that
186
+ * writes a key nothing reads reports success and changes nothing observable,
187
+ * which is the failure this change exists to remove.
188
+ *
189
+ * A refusal rather than a deletion, for one release: falling through to
190
+ * "Unknown: lanes link profile default" would send someone hunting a typo in a
191
+ * command they have run for months.
192
+ */
193
+ export function profileDefault(name: string | undefined): never {
194
+ throw new ConfigError(
195
+ 'lanes link profile default was removed.\n' +
196
+ ' Nothing reads default_profile any more — pass --profile on every command:\n' +
197
+ ` lanes link status --profile ${name ?? '<name>'} --target <target>\n` +
198
+ ' If the key is still in lanes-link.yaml it is inert, and safe to delete.',
199
+ );
152
200
  }
153
201
 
154
202
  // The bundled agent skill used to be printed from here, because `installRoot`
@@ -1,5 +1,5 @@
1
1
  import { planAll, planFor, type ProviderPlan } from '#providers/setup/plan.ts';
2
- import { emit, heading, print, style, table } from '../output.ts';
2
+ import { announce, emit, heading, print, style, table } from '../output.ts';
3
3
  import { missingRequirements } from './connect/requirements.ts';
4
4
  import { openRuntime, type GlobalFlags } from '../runtime.ts';
5
5
 
@@ -24,6 +24,7 @@ export async function setupPlan(provider: string | undefined, flags: SetupFlags)
24
24
  try {
25
25
  const context = {
26
26
  profile: runtime.resolution.profile,
27
+ target: runtime.resolution.target,
27
28
  connections: runtime.config.connections.map((c) => `${c.provider}.${c.id}`),
28
29
  // Declaring an `oauth_apps` entry is how a profile says "use my own
29
30
  // client". Without this the plan would tell someone who has already
@@ -55,16 +56,19 @@ export async function setupPlan(provider: string | undefined, flags: SetupFlags)
55
56
  ),
56
57
  );
57
58
 
58
- return emit(
59
- flags.json,
60
- { ...plan, missing: [...missing] },
61
- () => renderOne(plan, missing),
62
- );
59
+ // Inside the render callback, not before it: `emit` returns the JSON
60
+ // early, so this is where a line of prose is safe. `connect` needs its
61
+ // own guard because its announce has to precede a browser.
62
+ return emit(flags.json, { ...plan, missing: [...missing] }, () => {
63
+ announce(runtime.resolution);
64
+ renderOne(plan, missing);
65
+ });
63
66
  }
64
67
 
65
68
  const plans = planAll(manifests, context);
66
69
 
67
70
  return emit(flags.json, { profile: context.profile, providers: plans }, () => {
71
+ announce(runtime.resolution);
68
72
  heading(`Connected in ${style.bold(context.profile)}`);
69
73
  const done = plans.filter((plan) => plan.connected.length > 0);
70
74
  if (done.length === 0) print(style.dim(' nothing yet'));
@@ -139,6 +143,18 @@ function renderOne(plan: ProviderPlan, missing: ReadonlySet<string>): void {
139
143
  plan.steps.forEach((step, index) => print(` ${index + 1}. ${step}`));
140
144
  }
141
145
  }
146
+
147
+ // The other alternative, and for the same reason it is down here: it is what
148
+ // to do when the line above is refused by somebody who is not the reader.
149
+ if (plan.tokenCommand && plan.pastedCredential) {
150
+ heading('Or paste a token you already hold');
151
+ print(` ${plan.tokenCommand}`);
152
+ print(` ${style.dim(`Asks for the ${plan.pastedCredential}.`)}`);
153
+ if (plan.steps.length > 0 && !plan.ownClientCommand) {
154
+ print();
155
+ plan.steps.forEach((step, index) => print(` ${index + 1}. ${step}`));
156
+ }
157
+ }
142
158
  if (plan.browser) {
143
159
  print();
144
160
  print(
@@ -1,12 +1,12 @@
1
+ import { describeKnowledge } from '#deployments/knowledge.ts';
1
2
  import {
2
- TARGET_ENV,
3
- askedTarget,
4
3
  loadProfileConfig,
5
4
  resolveSelection,
6
5
  undeclaredTarget,
7
6
  type Config,
8
- type Resolution,
7
+ type ProfileSelection,
9
8
  } from '#profile';
9
+ import { ConfigError } from '#profile';
10
10
  import { deployedUrl, deploymentIdentity, type DeploymentIdentity } from '../endpoint-url.ts';
11
11
  import { ConfigDocument } from '../config-edit.ts';
12
12
  import { announce, emit, heading, ok, print, style, table, waiting, warn } from '../output.ts';
@@ -33,13 +33,20 @@ import type { GlobalFlags } from '../runtime.ts';
33
33
 
34
34
  export interface TargetSummary {
35
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. */
36
+ /** Whether this is the one `--target` named, when it named any. */
39
37
  readonly isSelected: boolean;
40
38
  readonly credentials: string;
41
39
  readonly storage: string;
42
40
  readonly vault: string;
41
+ /**
42
+ * Where memory and skills are kept, when that is not `storage` above.
43
+ *
44
+ * Null is the ordinary answer. It is here because a `knowledge:` block moves
45
+ * two of the four things this target holds, and the config file would
46
+ * otherwise be the only witness — a `storage: filesystem` row that is true of
47
+ * the log and the state and false of the owner's own notes.
48
+ */
49
+ readonly knowledge: string | null;
43
50
  /** Whether a deployment is declared. Free, and always present. */
44
51
  readonly deployed: boolean;
45
52
  readonly deployment: DeploymentIdentity | null;
@@ -51,9 +58,8 @@ export interface TargetListing {
51
58
  readonly root: string;
52
59
  readonly profile: string;
53
60
  readonly path: string;
54
- readonly default: string;
55
- readonly selected: string;
56
- readonly selectedSource: 'flag' | 'environment' | 'config-default';
61
+ /** What `--target` named, when it named anything. */
62
+ readonly selected: string | null;
57
63
  /**
58
64
  * Whether `selected` names a target that exists.
59
65
  *
@@ -84,46 +90,40 @@ export interface TargetFlags extends GlobalFlags {
84
90
  async function survey(
85
91
  flags: TargetFlags,
86
92
  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
-
93
+ ): Promise<{ selection: ProfileSelection; config: Config; listing: TargetListing }> {
90
94
  const selection = await resolveSelection({
91
95
  ...(flags.profile !== undefined ? { profileFlag: flags.profile } : {}),
92
- ...(flags.target !== undefined ? { targetFlag: flags.target } : {}),
93
96
  ...(options.env !== undefined ? { env: options.env } : {}),
94
97
  });
95
98
 
96
99
  const { config } = await loadProfileConfig(selection.workspaceRoot, selection.profile);
97
100
 
98
- const asked = askedTarget(flags.target, env);
99
- const selected = asked.target ?? config.instance.default_target;
101
+ const selected = flags.target ?? null;
100
102
  const names = Object.keys(config.targets);
101
103
 
102
104
  const summaries: TargetSummary[] = names.map((name) => {
103
105
  const declared = config.targets[name]!;
104
106
  return {
105
107
  name,
106
- isDefault: name === config.instance.default_target,
107
108
  isSelected: name === selected,
108
109
  credentials: declared.credentials.adapter,
109
110
  storage: declared.storage.adapter,
110
111
  vault: declared.vault?.adapter ?? 'file',
112
+ knowledge: declared.knowledge ? describeKnowledge(declared.knowledge) : null,
111
113
  deployed: declared.deploy !== undefined,
112
114
  deployment: deploymentIdentity(declared.deploy),
113
115
  };
114
116
  });
115
117
 
116
118
  return {
117
- resolution: { ...selection, target: selected, targetSource: asked.source ?? 'config-default' },
119
+ selection,
118
120
  config,
119
121
  listing: {
120
122
  root: selection.workspaceRoot,
121
123
  profile: selection.profile,
122
124
  path: selection.profilePath,
123
- default: config.instance.default_target,
124
125
  selected,
125
- selectedSource: asked.source ?? 'config-default',
126
- selectedDeclared: names.includes(selected),
126
+ selectedDeclared: selected === null || names.includes(selected),
127
127
  // Asking the platform is opt-in: one `gcloud` subprocess per deployable
128
128
  // target, and the profiles that make this command worth running are
129
129
  // exactly the ones with several. A discovery command that takes ten
@@ -161,15 +161,15 @@ async function withUrls(
161
161
  }
162
162
 
163
163
  export async function targetList(flags: TargetFlags): Promise<void> {
164
- const { resolution, listing } = await survey(flags, { urls: flags.urls === true });
164
+ const { listing } = await survey(flags, { urls: flags.urls === true });
165
165
 
166
166
  return emit(flags.json, listing, () => {
167
- announce(resolution);
167
+ print(style.dim(`${listing.profile} ${listing.path}`));
168
168
  print();
169
169
 
170
170
  table(
171
171
  listing.targets.map((target) => [
172
- ` ${target.isDefault ? style.green('*') : ' '} ${target.isSelected ? style.cyan('→') : ' '}`,
172
+ ` ${target.isSelected ? style.cyan('→') : ' '}`,
173
173
  style.bold(target.name),
174
174
  style.dim(target.credentials),
175
175
  style.dim(target.storage),
@@ -178,27 +178,21 @@ export async function targetList(flags: TargetFlags): Promise<void> {
178
178
  );
179
179
 
180
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
181
 
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
- );
182
+ // This command takes no required `--target`, and that is not an oversight:
183
+ // it is the command you run to find out what to pass. Requiring the answer
184
+ // as input would be circular, and it has to keep working in the state every
185
+ // other command fails in — which is what `selectedDeclared` reports.
186
+ if (listing.selected === null) {
187
+ print(style.dim(' Every other command names one of these with --target.'));
188
+ return;
192
189
  }
193
190
 
191
+ print(style.dim(` ${style.cyan('→')} the target you named`));
192
+
194
193
  if (!listing.selectedDeclared) {
195
194
  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
- );
195
+ print(warn(`"${listing.selected}" is not declared here — every command naming it will refuse.`));
202
196
  }
203
197
  });
204
198
  }
@@ -215,50 +209,25 @@ function deploymentCell(target: TargetSummary): string {
215
209
  }
216
210
 
217
211
  /**
218
- * `lanes link target use <name>` — rewrite `instance.default_target`.
212
+ * `lanes link target use <name>` — removed.
219
213
  *
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.
214
+ * It wrote `instance.default_target`, which nothing reads (ADR-037). A command
215
+ * that writes a key nothing reads is worse than no command at all: it reports
216
+ * success and changes nothing observable, which is the exact failure this whole
217
+ * change exists to remove.
218
+ *
219
+ * Kept as a refusal rather than deleted outright, for one release. Falling
220
+ * through to `Unknown: lanes link target use` would send someone hunting a typo
221
+ * in a command they have run for months.
225
222
  */
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
- }
223
+ export function targetUse(name: string | undefined): never {
224
+ throw new ConfigError(
225
+ 'lanes link target use was removed.\n' +
226
+ ' Nothing reads instance.default_target any more — pass --target on every\n' +
227
+ ' command instead:\n' +
228
+ ` lanes link status --profile <name> --target ${name ?? '<target>'}\n` +
229
+ ' If the key is still in your profile it is inert, and safe to delete.',
230
+ );
262
231
  }
263
232
 
264
233
  /**
@@ -271,10 +240,20 @@ export async function targetUse(name: string | undefined, flags: GlobalFlags): P
271
240
  */
272
241
  export async function targetShow(name: string | undefined, flags: TargetFlags): Promise<void> {
273
242
  const { config, listing } = await survey(flags);
243
+
244
+ // Positionally or by flag, but one of them: this command's whole subject is a
245
+ // single target, and there is no default left to mean "the one you would have
246
+ // got". `list` is the command for "I do not know which".
274
247
  const wanted = name ?? listing.selected;
275
- const summary = listing.targets.find((candidate) => candidate.name === wanted);
248
+ if (!wanted) {
249
+ throw new ConfigError(
250
+ 'Usage: lanes link target show <name> --profile <name>\n' +
251
+ ` Declared here: ${listing.targets.map((one) => one.name).join(', ') || 'none'}`,
252
+ );
253
+ }
276
254
 
277
- if (!summary) throw undeclaredTarget(wanted, config, listing.selectedSource);
255
+ const summary = listing.targets.find((candidate) => candidate.name === wanted);
256
+ if (!summary) throw undeclaredTarget(wanted, config, listing.profile);
278
257
 
279
258
  const url = summary.deployment
280
259
  ? await waiting('asking the platform for an address', () =>
@@ -290,6 +269,9 @@ export async function targetShow(name: string | undefined, flags: TargetFlags):
290
269
  [' credentials', summary.credentials],
291
270
  [' storage', summary.storage],
292
271
  [' vault', summary.vault],
272
+ ...(summary.knowledge
273
+ ? [[' knowledge', summary.knowledge, style.dim('memory and skills')]]
274
+ : []),
293
275
  ]);
294
276
 
295
277
  if (!summary.deployment) {