@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,154 @@
|
|
|
1
|
+
import { listProfiles, loadProfileConfig, ConfigError, type TargetConfig } from '#profile';
|
|
2
|
+
import { ask, isInteractive } from '../../prompt.ts';
|
|
3
|
+
import { print, style } from '../../output.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The targets a new profile declares, and where their values come from.
|
|
7
|
+
*
|
|
8
|
+
* `profile add` used to emit exactly one target — `local`, hard-coded in the
|
|
9
|
+
* template — while accepting a `--target` it dropped on the floor. So the
|
|
10
|
+
* command that creates a profile could not create the thing a profile is mostly
|
|
11
|
+
* *for*: somewhere to run.
|
|
12
|
+
*
|
|
13
|
+
* A `local` target is derivable and never asked about: a path under the
|
|
14
|
+
* workspace, an encrypted file beside it. Anything else names a cloud account,
|
|
15
|
+
* and this is the one place in the CLI where guessing those is actively harmful.
|
|
16
|
+
* `deploy`'s survey proposes a *fresh* project id (`lanes-link-<random>`),
|
|
17
|
+
* which is right for a first deploy and exactly wrong here — pressing return
|
|
18
|
+
* would build a second, separate deployment rather than adding this profile to
|
|
19
|
+
* the one the operator already has. So the defaults come from a sibling profile
|
|
20
|
+
* that already declares the same target name, and the service name is the only
|
|
21
|
+
* one derived, because that is the one field that has to differ per profile.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export interface DeclaredTarget {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
/** Rendered YAML, indented two spaces, ready to sit under `targets:`. */
|
|
27
|
+
readonly block: string;
|
|
28
|
+
/** Which profile the values were taken from, when they were taken from one. */
|
|
29
|
+
readonly from?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The `local` target, which is the same shape in every profile. */
|
|
33
|
+
export function localBlock(profile: string, name = 'local'): string {
|
|
34
|
+
return (
|
|
35
|
+
` ${name}:\n` +
|
|
36
|
+
` credentials: { adapter: file, path: ./data/${profile}/credentials.enc }\n` +
|
|
37
|
+
` storage: { adapter: filesystem, path: ./data/${profile} }\n`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A sibling profile's declaration of the same target name, if there is one.
|
|
43
|
+
*
|
|
44
|
+
* The first match wins rather than the operator being asked which — two
|
|
45
|
+
* profiles declaring `cloud` against different projects is possible and rare,
|
|
46
|
+
* and the values are shown and editable at the prompt either way.
|
|
47
|
+
*/
|
|
48
|
+
export async function siblingTarget(
|
|
49
|
+
workspaceRoot: string,
|
|
50
|
+
target: string,
|
|
51
|
+
exclude: string,
|
|
52
|
+
): Promise<{ profile: string; declared: TargetConfig } | null> {
|
|
53
|
+
for (const name of await listProfiles(workspaceRoot)) {
|
|
54
|
+
if (name === exclude) continue;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const { config } = await loadProfileConfig(workspaceRoot, name);
|
|
58
|
+
const declared = config.targets[target];
|
|
59
|
+
if (declared) return { profile: name, declared };
|
|
60
|
+
} catch {
|
|
61
|
+
// A profile that will not parse is not a source of defaults, and saying
|
|
62
|
+
// so here would report a broken sibling as a problem with this command.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Ask for what a non-local target needs, pre-filled from a sibling.
|
|
71
|
+
*
|
|
72
|
+
* Refuses rather than inventing when there is nobody to ask and nothing to copy
|
|
73
|
+
* — the same bargain `connect --non-interactive` makes. A profile written with
|
|
74
|
+
* a guessed project id parses, deploys, and points at the wrong account.
|
|
75
|
+
*/
|
|
76
|
+
export async function askTarget(input: {
|
|
77
|
+
readonly target: string;
|
|
78
|
+
readonly profile: string;
|
|
79
|
+
readonly sibling: { profile: string; declared: TargetConfig } | null;
|
|
80
|
+
readonly nonInteractive?: boolean;
|
|
81
|
+
}): Promise<DeclaredTarget> {
|
|
82
|
+
const { target, profile, sibling } = input;
|
|
83
|
+
|
|
84
|
+
if (!sibling) {
|
|
85
|
+
throw new ConfigError(
|
|
86
|
+
`No profile in this workspace declares a target called "${target}", so there is\n` +
|
|
87
|
+
'nothing to copy its adapters from.\n' +
|
|
88
|
+
` lanes link profile add ${profile} --target local\n` +
|
|
89
|
+
` lanes link deploy --profile ${profile} --target ${target}\n` +
|
|
90
|
+
' The deploy surveys for what it does not know and writes the block.',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const declared = sibling.declared;
|
|
95
|
+
const project = declared.credentials.project ?? '';
|
|
96
|
+
const bucket = declared.storage.bucket ?? '';
|
|
97
|
+
|
|
98
|
+
// The one field that must differ: two profiles served by one project need two
|
|
99
|
+
// services, which is what makes them separately deployable.
|
|
100
|
+
const service = `lanes-link-${profile}-mcp`;
|
|
101
|
+
|
|
102
|
+
if (input.nonInteractive === true || !isInteractive()) {
|
|
103
|
+
return {
|
|
104
|
+
name: target,
|
|
105
|
+
block: cloudBlock({ target, project, bucket, service, declared }),
|
|
106
|
+
from: sibling.profile,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
print(style.dim(` ${target} — defaults from profile "${sibling.profile}"`));
|
|
111
|
+
|
|
112
|
+
const answeredProject = (await ask(` Cloud project [${project}]: `)) || project;
|
|
113
|
+
const answeredBucket = (await ask(` Bucket [${bucket}]: `)) || bucket;
|
|
114
|
+
const answeredService = (await ask(` Service [${service}]: `)) || service;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
name: target,
|
|
118
|
+
block: cloudBlock({
|
|
119
|
+
target,
|
|
120
|
+
project: answeredProject,
|
|
121
|
+
bucket: answeredBucket,
|
|
122
|
+
service: answeredService,
|
|
123
|
+
declared,
|
|
124
|
+
}),
|
|
125
|
+
from: sibling.profile,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** One target's YAML, carrying over the adapters the sibling chose. */
|
|
130
|
+
function cloudBlock(input: {
|
|
131
|
+
target: string;
|
|
132
|
+
project: string;
|
|
133
|
+
bucket: string;
|
|
134
|
+
service: string;
|
|
135
|
+
declared: TargetConfig;
|
|
136
|
+
}): string {
|
|
137
|
+
const { declared } = input;
|
|
138
|
+
const deploy = declared.deploy;
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
` ${input.target}:\n` +
|
|
142
|
+
` credentials: { adapter: ${declared.credentials.adapter}, project: ${input.project} }\n` +
|
|
143
|
+
` storage: { adapter: ${declared.storage.adapter}, bucket: ${input.bucket} }\n` +
|
|
144
|
+
` vault: { adapter: ${declared.vault?.adapter ?? 'secret'} }\n` +
|
|
145
|
+
(deploy
|
|
146
|
+
? ` deploy:\n` +
|
|
147
|
+
` platform: ${deploy.platform}\n` +
|
|
148
|
+
` project: ${input.project}\n` +
|
|
149
|
+
` region: ${deploy.region}\n` +
|
|
150
|
+
` service: ${input.service}\n` +
|
|
151
|
+
` access: ${deploy.access}\n`
|
|
152
|
+
: '')
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -119,6 +119,23 @@ export async function removalPlan(
|
|
|
119
119
|
continue;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// A repository this profile keeps memory and skills in is not this
|
|
123
|
+
// command's to empty, and it is not reachable from here either: the routing
|
|
124
|
+
// that points memory at a repository is applied by `openRuntime`, and this
|
|
125
|
+
// plan is built on `openBlobStoreFor`, which opens the target's own declared
|
|
126
|
+
// storage. That is the right answer — deleting a profile must not delete
|
|
127
|
+
// somebody's repository — and it is a surprising one, because
|
|
128
|
+
// `rm -r data/<profile>` used to be the whole of "what could this profile
|
|
129
|
+
// reach". So it is said before the operator confirms rather than discovered
|
|
130
|
+
// afterwards.
|
|
131
|
+
if (declared.knowledge) {
|
|
132
|
+
warnings.push(
|
|
133
|
+
`Target "${name}" keeps this profile's memory and skills in ${declared.knowledge.repo}. ` +
|
|
134
|
+
'Nothing here touches a repository, so they survive this removal — delete them there ' +
|
|
135
|
+
'if you want them gone.',
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
122
139
|
// Secrets first, and this ordering is load-bearing rather than tidy:
|
|
123
140
|
// `layout.credentials(p)` is `data/<p>/credentials.enc`, *inside* the blob
|
|
124
141
|
// root `data/<p>`. For the file adapter the credential store is itself a
|
|
@@ -12,12 +12,12 @@ import {
|
|
|
12
12
|
import { parseDocument } from 'yaml';
|
|
13
13
|
import { rm } from 'node:fs/promises';
|
|
14
14
|
import { terminalPrompter, type Prompter } from '../../prompt.ts';
|
|
15
|
-
import {
|
|
15
|
+
import { announceProfile, emit, fail, ok, print, style } from '../../output.ts';
|
|
16
16
|
import {
|
|
17
17
|
buildRegistryWithWorkspace,
|
|
18
18
|
openBlobStoreFor,
|
|
19
19
|
openSecretStoreFor,
|
|
20
|
-
|
|
20
|
+
resolveProfileOnly,
|
|
21
21
|
type GlobalFlags,
|
|
22
22
|
} from '../../runtime.ts';
|
|
23
23
|
import { removalPlan, renderPlan, type RemovalItem, type RemovalPlan } from './removal.ts';
|
|
@@ -250,10 +250,10 @@ export interface RemoveFlags extends GlobalFlags {
|
|
|
250
250
|
* in the tool.
|
|
251
251
|
*/
|
|
252
252
|
export async function removeProfile(name: string, flags: RemoveFlags): Promise<void> {
|
|
253
|
-
const {
|
|
254
|
-
|
|
253
|
+
const { selection, config } = await resolveProfileOnly({ ...flags, profile: name });
|
|
254
|
+
announceProfile(selection);
|
|
255
255
|
|
|
256
|
-
const root =
|
|
256
|
+
const root = selection.workspaceRoot;
|
|
257
257
|
const registry = await buildRegistryWithWorkspace(root, name);
|
|
258
258
|
const files = workspaceFiles(root);
|
|
259
259
|
|
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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: {
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
95
|
+
const workspaceFile = join(root, WORKSPACE_FILE);
|
|
96
|
+
if (!existsSync(workspaceFile)) {
|
|
97
|
+
await writeFile(workspaceFile, newWorkspaceTemplate(), { mode: 0o600 });
|
|
98
|
+
}
|
|
76
99
|
|
|
77
|
-
|
|
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: {
|
|
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
|
|
109
|
-
print(` port
|
|
110
|
-
|
|
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(
|
|
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 --
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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,7 +1,7 @@
|
|
|
1
1
|
import { ConfigError } from '#profile';
|
|
2
2
|
import { assertValidSecretRef } from '#secrets';
|
|
3
|
-
import { announce, heading, ok, print, style } from '../output.ts';
|
|
4
|
-
import { openSecretStoreFor, resolveProfile, type GlobalFlags } from '../runtime.ts';
|
|
3
|
+
import { announce, announceProfile, heading, ok, print, style } from '../output.ts';
|
|
4
|
+
import { openSecretStoreFor, resolveProfile, resolveProfileOnly, type GlobalFlags } from '../runtime.ts';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* `lanes link secrets` — moving credential values between a profile's targets.
|
|
@@ -35,11 +35,11 @@ export async function secretsPush(flags: SecretsFlags): Promise<void> {
|
|
|
35
35
|
throw new ConfigError(`--from and --to are both "${flags.from}"; there is nothing to copy.`);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const {
|
|
39
|
-
|
|
38
|
+
const { selection, config } = await resolveProfileOnly(flags);
|
|
39
|
+
announceProfile(selection);
|
|
40
40
|
|
|
41
|
-
const source = await openSecretStoreFor(config,
|
|
42
|
-
const destination = await openSecretStoreFor(config,
|
|
41
|
+
const source = await openSecretStoreFor(config, selection.workspaceRoot, flags.from);
|
|
42
|
+
const destination = await openSecretStoreFor(config, selection.workspaceRoot, flags.to);
|
|
43
43
|
|
|
44
44
|
const refs = await source.list();
|
|
45
45
|
if (refs.length === 0) {
|
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|