@lanes-sh/link 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -8
- package/instructions/skills/lanes-link/SKILL.md +42 -14
- package/package.json +1 -1
- package/src/cli/argv.ts +50 -0
- package/src/cli/brand.ts +178 -0
- package/src/cli/callback-page.ts +108 -128
- package/src/cli/commands/connect/accounts.ts +5 -0
- package/src/cli/commands/connect/assertion.ts +187 -0
- package/src/cli/commands/connect/authorise.ts +61 -17
- package/src/cli/commands/connect/client.ts +37 -9
- package/src/cli/commands/connect/discover.ts +94 -0
- package/src/cli/commands/connect/family.ts +72 -0
- package/src/cli/commands/connect/index.ts +113 -115
- package/src/cli/commands/connect/method.ts +237 -0
- package/src/cli/commands/connect/outcome.ts +42 -1
- package/src/cli/commands/connect/pasted-token.ts +66 -0
- package/src/cli/commands/connect/requirements.ts +60 -8
- package/src/cli/commands/connect/setup.ts +16 -5
- package/src/cli/commands/connect/target-note.ts +34 -0
- package/src/cli/commands/identity.ts +258 -0
- package/src/cli/commands/knowledge/index.ts +390 -0
- package/src/cli/commands/knowledge/migrate.ts +180 -0
- package/src/cli/commands/knowledge/setup.ts +144 -0
- package/src/cli/commands/knowledge.ts +10 -0
- package/src/cli/commands/mcp/harnesses.ts +16 -2
- package/src/cli/commands/mcp/register.ts +9 -1
- package/src/cli/commands/mcp/stdio.ts +21 -0
- package/src/cli/commands/operate/dashboard.ts +107 -0
- package/src/cli/commands/operate/findings.ts +151 -0
- package/src/cli/commands/operate/inspect.ts +63 -164
- package/src/cli/commands/operate/outputs.ts +38 -11
- package/src/cli/commands/operate/policy.ts +7 -7
- package/src/cli/commands/operate/serve.ts +3 -0
- package/src/cli/commands/operate/status.ts +108 -1
- package/src/cli/commands/operate/token.ts +1 -1
- package/src/cli/commands/operate.ts +2 -0
- package/src/cli/commands/profile/declare.ts +154 -0
- package/src/cli/commands/profile/removal.ts +17 -0
- package/src/cli/commands/profile/remove.ts +5 -5
- package/src/cli/commands/profile.ts +83 -35
- package/src/cli/commands/secrets.ts +6 -6
- package/src/cli/commands/setup.ts +22 -6
- package/src/cli/commands/sync.ts +262 -0
- package/src/cli/commands/target.ts +65 -83
- package/src/cli/config-edit.ts +53 -144
- package/src/cli/config-repair.ts +186 -0
- package/src/cli/dashboard-page.ts +284 -0
- package/src/cli/dashboard-shell.ts +125 -0
- package/src/cli/dispatch-owner.ts +93 -0
- package/src/cli/identity.ts +12 -1
- package/src/cli/main.ts +90 -61
- package/src/cli/nearest.ts +45 -0
- package/src/cli/oauth-callback.ts +187 -0
- package/src/cli/oauth-exchange.ts +57 -15
- package/src/cli/oauth.ts +67 -177
- package/src/cli/output.ts +21 -5
- package/src/cli/provider-marks.ts +45 -0
- package/src/cli/runtime/open.ts +74 -51
- package/src/cli/runtime/registry.ts +60 -2
- package/src/cli/runtime/select.ts +26 -13
- package/src/cli/runtime/vault.ts +61 -0
- package/src/cli/runtime.ts +2 -1
- package/src/cli/selection.ts +365 -0
- package/src/cli/usage.ts +42 -11
- package/src/connectivity/auth/README.md +7 -1
- package/src/connectivity/auth/basic/index.ts +1 -1
- package/src/connectivity/auth/index.ts +14 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +26 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +16 -2
- package/src/connectivity/auth/oauth-authcode/provider.ts +1 -1
- package/src/connectivity/auth/oauth-authcode/refresh.ts +3 -3
- package/src/connectivity/auth/oauth-jwt/README.md +33 -0
- package/src/connectivity/auth/oauth-jwt/index.ts +237 -0
- package/src/connectivity/auth/oauth-jwt/key.ts +148 -0
- package/src/connectivity/auth/resolve.ts +1 -1
- package/src/connectivity/auth/token.ts +11 -0
- package/src/connectivity/index.ts +2 -0
- package/src/connectivity/manifest/auth.ts +99 -2
- package/src/connectivity/manifest/identity.ts +12 -0
- package/src/connectivity/manifest/index.ts +3 -1
- package/src/connectivity/manifest/provider.ts +37 -8
- package/src/connectivity/manifest/requirements.ts +109 -6
- package/src/deployments/adapters/filesystem.ts +10 -1
- package/src/deployments/adapters/github-api.ts +106 -0
- package/src/deployments/adapters/github-commit.ts +103 -0
- package/src/deployments/adapters/github-repo.ts +356 -0
- package/src/deployments/adapters/github-testing.ts +258 -0
- package/src/deployments/adapters/github.ts +125 -0
- package/src/deployments/deploy.ts +94 -114
- package/src/deployments/discover.ts +103 -0
- package/src/deployments/driver.ts +8 -1
- package/src/deployments/gcp/driver.ts +3 -1
- package/src/deployments/knowledge.ts +119 -0
- package/src/deployments/prepare.ts +12 -6
- package/src/deployments/report.ts +117 -0
- package/src/deployments/servable.ts +82 -0
- package/src/deployments/serving.ts +165 -0
- package/src/deployments/sync-apply.ts +276 -0
- package/src/deployments/sync.ts +136 -0
- package/src/deployments/target.ts +3 -2
- package/src/deployments/upload.ts +19 -12
- package/src/dispatch/dispatch.ts +1 -1
- package/src/profile/deployments.ts +80 -0
- package/src/profile/identity.ts +60 -0
- package/src/profile/index.ts +23 -5
- package/src/profile/knowledge.ts +124 -0
- package/src/profile/load.ts +17 -5
- package/src/profile/primitives.ts +24 -1
- package/src/profile/schema.ts +81 -3
- package/src/profile/targets.ts +122 -109
- package/src/profile/workspace.ts +139 -79
- package/src/providers/google/calendar/index.ts +2 -0
- package/src/providers/google/contacts/index.ts +2 -0
- package/src/providers/google/docs/index.ts +2 -0
- package/src/providers/google/drive/index.ts +2 -0
- package/src/providers/google/gmail/index.ts +2 -0
- package/src/providers/google/gmail-imap/index.ts +125 -0
- package/src/providers/google/index.ts +2 -1
- package/src/providers/google/shared/oauth.ts +18 -6
- package/src/providers/google/shared/service-account.ts +110 -0
- package/src/providers/google/shared/setup.ts +21 -3
- package/src/providers/google/sheets/index.ts +2 -0
- package/src/providers/google/tasks/index.ts +2 -0
- package/src/providers/identity/provider.ts +166 -0
- package/src/providers/index.ts +3 -0
- package/src/providers/owner.ts +10 -2
- package/src/providers/scopes.ts +2 -0
- package/src/providers/setup/plan.ts +31 -9
- package/src/providers/setup/provider.ts +23 -0
- package/src/providers/slack/index.ts +81 -33
- package/src/providers/slack/oauth.ts +103 -0
- package/src/providers/slack/scopes.ts +37 -0
- package/src/server/container.ts +18 -1
- package/src/server/cors.ts +252 -0
- package/src/server/dashboard.ts +208 -0
- package/src/server/endpoint.ts +45 -1
- package/src/server/generations.ts +11 -2
- package/src/server/harness.ts +7 -0
- package/src/server/index.ts +45 -6
- package/src/server/mcp/index.ts +1 -0
- package/src/server/mcp/instructions.ts +28 -1
- package/src/server/mcp/visibility.ts +33 -0
- package/src/stores/blobs/route.ts +123 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConfigError,
|
|
3
|
+
findDeployment,
|
|
4
|
+
loadWorkspaceProfiles,
|
|
5
|
+
recordDeployment,
|
|
6
|
+
resolveWorkspaceRoot,
|
|
7
|
+
} from '#profile';
|
|
8
|
+
import {
|
|
9
|
+
applyBlobs,
|
|
10
|
+
applyPulls,
|
|
11
|
+
applyPushes,
|
|
12
|
+
planBlobs,
|
|
13
|
+
planProfile,
|
|
14
|
+
profilesInEither,
|
|
15
|
+
resolved,
|
|
16
|
+
type Prefer,
|
|
17
|
+
type ProfileSync,
|
|
18
|
+
} from '#deployments/sync-apply.ts';
|
|
19
|
+
import { conflictsIn, type Change } from '#deployments/sync.ts';
|
|
20
|
+
import { deployedWorkspace } from '#deployments/upload.ts';
|
|
21
|
+
import { discoverDeployments, holdsWorkspace } from '#deployments/discover.ts';
|
|
22
|
+
import { emit, heading, ok, print, style, waiting, warn } from '../output.ts';
|
|
23
|
+
import { confirm, isInteractive } from '../prompt.ts';
|
|
24
|
+
import type { GlobalFlags } from '../runtime.ts';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `lanes link sync targets` — the workspace and a target's copy of it, reconciled.
|
|
28
|
+
*
|
|
29
|
+
* A deploy leaves two copies of every profile: the one in the workspace and the
|
|
30
|
+
* one in the bucket the endpoint reads. They are meant to agree, and when they
|
|
31
|
+
* stop there was no way to find out, let alone to say which side had lost
|
|
32
|
+
* something. The reported case: a local profile was rewritten and lost its
|
|
33
|
+
* cloud target, `auth.authorization`, and six connections, while the bucket
|
|
34
|
+
* still held every one of them and the service went on answering.
|
|
35
|
+
*
|
|
36
|
+
* Union, and refuse where a union is impossible (ADR-044). Nothing here
|
|
37
|
+
* overwrites a value that exists on both sides — that is `--prefer`, asked for
|
|
38
|
+
* explicitly, because silently choosing one copy over the other is the failure
|
|
39
|
+
* this command was written in response to.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export interface SyncFlags extends GlobalFlags {
|
|
43
|
+
readonly json?: boolean | undefined;
|
|
44
|
+
readonly dryRun?: boolean | undefined;
|
|
45
|
+
readonly from?: string | undefined;
|
|
46
|
+
readonly discover?: boolean | undefined;
|
|
47
|
+
readonly prefer?: string | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parsePrefer(value: string | undefined): Prefer | undefined {
|
|
51
|
+
if (value === undefined) return undefined;
|
|
52
|
+
if (value !== 'local' && value !== 'remote') {
|
|
53
|
+
throw new ConfigError(`--prefer must be "local" or "remote", not "${value}"`);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Where the target's copy lives, tried cheapest first.
|
|
60
|
+
*
|
|
61
|
+
* The order is the point. A declared target answers instantly and is right
|
|
62
|
+
* whenever anything is; the index answers instantly and is right when the
|
|
63
|
+
* declaration is what went missing; `--from` is for when the operator knows and
|
|
64
|
+
* the workspace does not; and discovery is the one that works from nothing, at
|
|
65
|
+
* the cost of a `gcloud` call per project.
|
|
66
|
+
*/
|
|
67
|
+
async function locateRemote(
|
|
68
|
+
root: string,
|
|
69
|
+
target: string,
|
|
70
|
+
flags: SyncFlags,
|
|
71
|
+
): Promise<{ workspace: string; how: string }> {
|
|
72
|
+
if (flags.from) {
|
|
73
|
+
if (!(await holdsWorkspace(flags.from))) {
|
|
74
|
+
throw new ConfigError(
|
|
75
|
+
`${flags.from} does not hold a workspace — no ${'lanes-link.yaml'} in it.\n` +
|
|
76
|
+
' Check the bucket name, or run with --discover to search for one.',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return { workspace: flags.from.replace(/\/$/, ''), how: '--from' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const entry of (await loadWorkspaceProfiles(root)).loaded) {
|
|
83
|
+
const declared = entry.config.targets[target];
|
|
84
|
+
const workspace = declared ? deployedWorkspace(declared) : undefined;
|
|
85
|
+
if (workspace) return { workspace, how: `declared by ${entry.profile}` };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const recorded = await findDeployment(root, target);
|
|
89
|
+
if (recorded) return { workspace: recorded.workspace, how: 'recorded by a previous deploy' };
|
|
90
|
+
|
|
91
|
+
if (flags.discover !== true) {
|
|
92
|
+
throw new ConfigError(
|
|
93
|
+
`Nothing here says where "${target}" lives.\n` +
|
|
94
|
+
' No profile declares it, and no deploy recorded it in lanes-link.yaml.\n\n' +
|
|
95
|
+
' If you know the bucket: lanes link sync targets --target ' +
|
|
96
|
+
`${target} --from gs://<bucket>\n` +
|
|
97
|
+
` If you do not: lanes link sync targets --target ${target} --discover`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const candidates = (
|
|
102
|
+
await waiting('searching your projects for a deployment', () =>
|
|
103
|
+
discoverDeployments(),
|
|
104
|
+
)
|
|
105
|
+
).filter((candidate) => candidate.workspace !== undefined);
|
|
106
|
+
|
|
107
|
+
if (candidates.length === 0) {
|
|
108
|
+
throw new ConfigError(
|
|
109
|
+
'Found no deployment holding a workspace in any project this login can see.\n' +
|
|
110
|
+
' Check you are logged in as the right account: gcloud auth list',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
print('');
|
|
115
|
+
for (const candidate of candidates) {
|
|
116
|
+
print(` ${style.bold(candidate.service)} ${candidate.region} ${candidate.project}`);
|
|
117
|
+
print(style.dim(` workspace ${candidate.workspace}`));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// One is offered rather than chosen: adopting the wrong deployment would
|
|
121
|
+
// merge a stranger's config into this workspace.
|
|
122
|
+
const first = candidates[0]!;
|
|
123
|
+
if (candidates.length > 1 || !isInteractive()) {
|
|
124
|
+
throw new ConfigError(
|
|
125
|
+
`Found ${candidates.length} deployment(s). Name the one you mean:\n` +
|
|
126
|
+
` lanes link sync targets --target ${target} --from ${first.workspace}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!(await confirm(` Sync "${target}" against ${first.workspace}?`))) {
|
|
131
|
+
throw new ConfigError('Nothing was read or written.');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { workspace: first.workspace!, how: 'discovered' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const arrow = (direction: Change['direction']): string =>
|
|
138
|
+
direction === 'pull' ? style.green('←') : direction === 'push' ? style.cyan('→') : style.yellow('!');
|
|
139
|
+
|
|
140
|
+
const describe = (change: Change): string => {
|
|
141
|
+
const path = change.path.length === 0 ? 'the whole profile' : change.path.join('.');
|
|
142
|
+
if (change.direction === 'pull') return `${path} ${style.dim('missing locally')}`;
|
|
143
|
+
if (change.direction === 'push') return `${path} ${style.dim('missing remotely')}`;
|
|
144
|
+
return `${path} ${style.dim(`local ${short(change.local)} ≠ remote ${short(change.remote)}`)}`;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const short = (value: unknown): string => {
|
|
148
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value);
|
|
149
|
+
return (text ?? 'nothing').length > 40 ? `${(text ?? '').slice(0, 37)}…` : (text ?? 'nothing');
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export async function syncTargets(flags: SyncFlags): Promise<void> {
|
|
153
|
+
const target = flags.target!;
|
|
154
|
+
const prefer = parsePrefer(flags.prefer);
|
|
155
|
+
const root = resolveWorkspaceRoot();
|
|
156
|
+
|
|
157
|
+
const { workspace: remote, how } = await locateRemote(root, target, flags);
|
|
158
|
+
|
|
159
|
+
const names = flags.profile ? [flags.profile] : await profilesInEither(root, remote);
|
|
160
|
+
const plans: ProfileSync[] = [];
|
|
161
|
+
for (const profile of names) plans.push(await planProfile(root, remote, profile, prefer));
|
|
162
|
+
|
|
163
|
+
const blobs = await planBlobs(root, remote);
|
|
164
|
+
const conflicts = [
|
|
165
|
+
...plans.flatMap((plan) => conflictsIn(plan.changes)),
|
|
166
|
+
...blobs.filter((blob) => blob.direction === 'conflict'),
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const payload = {
|
|
170
|
+
workspace: root,
|
|
171
|
+
remote,
|
|
172
|
+
target,
|
|
173
|
+
profiles: plans.map((plan) => ({ profile: plan.profile, changes: plan.changes })),
|
|
174
|
+
blobs,
|
|
175
|
+
conflicts: conflicts.length,
|
|
176
|
+
applied: false,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const render = (): void => {
|
|
180
|
+
print(style.dim(`workspace ${style.bold(root)} target ${style.bold(target)}`));
|
|
181
|
+
print(style.dim(`remote ${remote} (${how})`));
|
|
182
|
+
|
|
183
|
+
let anything = false;
|
|
184
|
+
for (const plan of plans) {
|
|
185
|
+
if (plan.changes.length === 0) continue;
|
|
186
|
+
anything = true;
|
|
187
|
+
|
|
188
|
+
heading(plan.profile);
|
|
189
|
+
for (const change of plan.changes) print(` ${arrow(change.direction)} ${describe(change)}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (blobs.length > 0) {
|
|
193
|
+
anything = true;
|
|
194
|
+
heading('Skills and manifests');
|
|
195
|
+
for (const blob of blobs) print(` ${arrow(blob.direction)} ${blob.key}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!anything) print(ok('already in step — nothing to copy in either direction'));
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
if (conflicts.length > 0 && prefer === undefined) {
|
|
202
|
+
render();
|
|
203
|
+
print('');
|
|
204
|
+
throw new ConfigError(
|
|
205
|
+
`${conflicts.length} conflict(s): both copies hold these, and they disagree.\n` +
|
|
206
|
+
' Nothing was written. Re-run with --prefer local or --prefer remote,\n' +
|
|
207
|
+
' or edit one side so they agree.',
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (flags.dryRun === true) {
|
|
212
|
+
return emit(flags.json, payload, () => {
|
|
213
|
+
render();
|
|
214
|
+
print('');
|
|
215
|
+
print(style.dim(' --dry-run: nothing was written on either side.'));
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (flags.json !== true) render();
|
|
220
|
+
|
|
221
|
+
let pulled = 0;
|
|
222
|
+
let pushed = 0;
|
|
223
|
+
for (const plan of plans) {
|
|
224
|
+
const changes = resolved(plan.changes, prefer);
|
|
225
|
+
pulled += await applyPulls(root, remote, plan.profile, changes);
|
|
226
|
+
if (await applyPushes(root, remote, plan.profile, changes)) pushed += 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const copied = await applyBlobs(
|
|
230
|
+
root,
|
|
231
|
+
remote,
|
|
232
|
+
resolvedBlobs(blobs, prefer),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Recorded now that a bucket has been confirmed to hold this target's
|
|
236
|
+
// workspace: the next recovery does not have to discover it again.
|
|
237
|
+
await recordDeployment(root, { target, workspace: remote });
|
|
238
|
+
|
|
239
|
+
return emit(flags.json, { ...payload, applied: true, pulled, pushed, copied }, () => {
|
|
240
|
+
print('');
|
|
241
|
+
if (pulled === 0 && pushed === 0 && copied === 0) return;
|
|
242
|
+
|
|
243
|
+
print(ok(`${pulled} key(s) pulled, ${pushed} profile(s) pushed, ${copied} file(s) copied`));
|
|
244
|
+
print(style.dim(` recorded ${remote} in lanes-link.yaml, so this is findable next time`));
|
|
245
|
+
if (pushed > 0) {
|
|
246
|
+
print(style.dim(` the endpoint re-reads its config on the next call — or run:`));
|
|
247
|
+
print(style.dim(` lanes link status --target ${target}`));
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** A conflicting file follows `--prefer` like a conflicting key does. */
|
|
253
|
+
function resolvedBlobs(
|
|
254
|
+
blobs: readonly { key: string; direction: 'pull' | 'push' | 'conflict' }[],
|
|
255
|
+
prefer: Prefer | undefined,
|
|
256
|
+
): { key: string; direction: 'pull' | 'push' }[] {
|
|
257
|
+
return blobs.flatMap((blob) => {
|
|
258
|
+
if (blob.direction !== 'conflict') return [{ key: blob.key, direction: blob.direction }];
|
|
259
|
+
if (prefer === undefined) return [];
|
|
260
|
+
return [{ key: blob.key, direction: prefer === 'remote' ? ('pull' as const) : ('push' as const) }];
|
|
261
|
+
});
|
|
262
|
+
}
|
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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<{
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
164
|
+
const { listing } = await survey(flags, { urls: flags.urls === true });
|
|
165
165
|
|
|
166
166
|
return emit(flags.json, listing, () => {
|
|
167
|
-
|
|
167
|
+
print(style.dim(`${listing.profile} ${listing.path}`));
|
|
168
168
|
print();
|
|
169
169
|
|
|
170
170
|
table(
|
|
171
171
|
listing.targets.map((target) => [
|
|
172
|
-
` ${target.
|
|
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
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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>` —
|
|
212
|
+
* `lanes link target use <name>` — removed.
|
|
219
213
|
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
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
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|