@lanes-sh/link 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +144 -0
- package/bin/lanes +42 -0
- package/instructions/agents/lanes-link-scout.md +73 -0
- package/instructions/skills/lanes-link/SKILL.md +187 -0
- package/package.json +95 -0
- package/src/audit/chain.ts +278 -0
- package/src/audit/conformance.ts +374 -0
- package/src/audit/fanout.ts +97 -0
- package/src/audit/index.ts +218 -0
- package/src/audit/stdout.ts +60 -0
- package/src/auth/index.ts +220 -0
- package/src/auth/oauth/metadata.ts +75 -0
- package/src/auth/oauth/server.ts +385 -0
- package/src/auth/oauth/store.ts +215 -0
- package/src/auth/oidc.ts +206 -0
- package/src/auth/remote.ts +72 -0
- package/src/cli/argv.ts +94 -0
- package/src/cli/callback-page.ts +256 -0
- package/src/cli/commands/connect/accounts.ts +94 -0
- package/src/cli/commands/connect/authorise.ts +298 -0
- package/src/cli/commands/connect/client.ts +284 -0
- package/src/cli/commands/connect/index.ts +398 -0
- package/src/cli/commands/connect/outcome.ts +119 -0
- package/src/cli/commands/connect/requirements.ts +103 -0
- package/src/cli/commands/connect/scopes-gate.ts +146 -0
- package/src/cli/commands/connect/settle.ts +136 -0
- package/src/cli/commands/connect/setup.ts +276 -0
- package/src/cli/commands/mcp/assets.ts +189 -0
- package/src/cli/commands/mcp/harnesses.ts +143 -0
- package/src/cli/commands/mcp/list.ts +70 -0
- package/src/cli/commands/mcp/register.ts +199 -0
- package/src/cli/commands/mcp/stdio.ts +57 -0
- package/src/cli/commands/mcp.ts +22 -0
- package/src/cli/commands/operate/attach.ts +121 -0
- package/src/cli/commands/operate/audit.ts +119 -0
- package/src/cli/commands/operate/inspect.ts +355 -0
- package/src/cli/commands/operate/outputs.ts +206 -0
- package/src/cli/commands/operate/policy.ts +80 -0
- package/src/cli/commands/operate/serve.ts +55 -0
- package/src/cli/commands/operate/status.ts +133 -0
- package/src/cli/commands/operate/token.ts +70 -0
- package/src/cli/commands/operate.ts +27 -0
- package/src/cli/commands/owner/memory.ts +110 -0
- package/src/cli/commands/owner/shared.ts +125 -0
- package/src/cli/commands/owner/skills.ts +92 -0
- package/src/cli/commands/owner/vault.ts +140 -0
- package/src/cli/commands/owner.ts +46 -0
- package/src/cli/commands/profile/removal.ts +278 -0
- package/src/cli/commands/profile/remove.ts +315 -0
- package/src/cli/commands/profile.ts +156 -0
- package/src/cli/commands/secrets.ts +176 -0
- package/src/cli/commands/setup.ts +150 -0
- package/src/cli/commands/target.ts +310 -0
- package/src/cli/config-edit.ts +397 -0
- package/src/cli/endpoint-url.ts +77 -0
- package/src/cli/identity.ts +109 -0
- package/src/cli/lanes.ts +78 -0
- package/src/cli/main.ts +333 -0
- package/src/cli/oauth-error.ts +13 -0
- package/src/cli/oauth-exchange.ts +146 -0
- package/src/cli/oauth.ts +354 -0
- package/src/cli/output.ts +184 -0
- package/src/cli/prompt.ts +180 -0
- package/src/cli/publish.ts +185 -0
- package/src/cli/runtime/discovery.ts +123 -0
- package/src/cli/runtime/open.ts +342 -0
- package/src/cli/runtime/registry.ts +185 -0
- package/src/cli/runtime/select.ts +124 -0
- package/src/cli/runtime.ts +34 -0
- package/src/cli/scopes.ts +63 -0
- package/src/cli/usage.ts +111 -0
- package/src/cli/version.ts +25 -0
- package/src/connectivity/auth/README.md +36 -0
- package/src/connectivity/auth/api-key/index.ts +43 -0
- package/src/connectivity/auth/authorize.ts +49 -0
- package/src/connectivity/auth/basic/index.ts +68 -0
- package/src/connectivity/auth/bearer/index.ts +13 -0
- package/src/connectivity/auth/credential.ts +19 -0
- package/src/connectivity/auth/header/index.ts +18 -0
- package/src/connectivity/auth/index.ts +35 -0
- package/src/connectivity/auth/none/index.ts +12 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
- package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
- package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
- package/src/connectivity/auth/resolve.ts +61 -0
- package/src/connectivity/auth/strategy/index.ts +14 -0
- package/src/connectivity/capability.ts +164 -0
- package/src/connectivity/connector.ts +175 -0
- package/src/connectivity/context.ts +77 -0
- package/src/connectivity/index.ts +94 -0
- package/src/connectivity/mail/attachments.ts +368 -0
- package/src/connectivity/mail/compose.ts +73 -0
- package/src/connectivity/mail/index.ts +40 -0
- package/src/connectivity/mail/message.ts +82 -0
- package/src/connectivity/mail/nodemailer.d.ts +83 -0
- package/src/connectivity/mail/staging.ts +143 -0
- package/src/connectivity/mail/url.ts +301 -0
- package/src/connectivity/manifest/auth.ts +135 -0
- package/src/connectivity/manifest/bundles.ts +23 -0
- package/src/connectivity/manifest/connector.ts +168 -0
- package/src/connectivity/manifest/credential-ref.ts +73 -0
- package/src/connectivity/manifest/identity.ts +43 -0
- package/src/connectivity/manifest/index.ts +46 -0
- package/src/connectivity/manifest/primitives.ts +20 -0
- package/src/connectivity/manifest/provider.ts +208 -0
- package/src/connectivity/manifest/requirements.ts +146 -0
- package/src/connectivity/manifest/setup.ts +55 -0
- package/src/connectivity/provider.ts +163 -0
- package/src/connectivity/transports/README.md +33 -0
- package/src/connectivity/transports/composite/index.ts +68 -0
- package/src/connectivity/transports/dav/calendar.ts +217 -0
- package/src/connectivity/transports/dav/capabilities.ts +151 -0
- package/src/connectivity/transports/dav/client.ts +226 -0
- package/src/connectivity/transports/dav/contacts.ts +75 -0
- package/src/connectivity/transports/dav/ical.ts +412 -0
- package/src/connectivity/transports/dav/index.ts +143 -0
- package/src/connectivity/transports/dav/operations.ts +43 -0
- package/src/connectivity/transports/dav/request.ts +161 -0
- package/src/connectivity/transports/dav/xml.ts +123 -0
- package/src/connectivity/transports/factory.ts +181 -0
- package/src/connectivity/transports/fs/capabilities.ts +88 -0
- package/src/connectivity/transports/fs/commands.ts +258 -0
- package/src/connectivity/transports/fs/index.ts +121 -0
- package/src/connectivity/transports/fs/operations.ts +45 -0
- package/src/connectivity/transports/fs/paths.ts +120 -0
- package/src/connectivity/transports/fs/result.ts +12 -0
- package/src/connectivity/transports/http/index.ts +255 -0
- package/src/connectivity/transports/imap/attachment.ts +166 -0
- package/src/connectivity/transports/imap/capabilities.ts +158 -0
- package/src/connectivity/transports/imap/client.ts +398 -0
- package/src/connectivity/transports/imap/commands.ts +385 -0
- package/src/connectivity/transports/imap/index.ts +152 -0
- package/src/connectivity/transports/imap/operations.ts +64 -0
- package/src/connectivity/transports/imap/parse.ts +130 -0
- package/src/connectivity/transports/imap/parser.ts +272 -0
- package/src/connectivity/transports/imap/result.ts +15 -0
- package/src/connectivity/transports/imap/send.ts +92 -0
- package/src/connectivity/transports/imap/socket.ts +111 -0
- package/src/connectivity/transports/imap/utf7.ts +136 -0
- package/src/connectivity/transports/index.ts +20 -0
- package/src/connectivity/transports/local/index.ts +173 -0
- package/src/connectivity/transports/mcp/index.ts +215 -0
- package/src/deployments/README.md +63 -0
- package/src/deployments/adapters/audit-blob.ts +203 -0
- package/src/deployments/adapters/filesystem.ts +184 -0
- package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
- package/src/deployments/adapters/gcs.ts +191 -0
- package/src/deployments/adapters/otlp.ts +128 -0
- package/src/deployments/adapters/s3.ts +195 -0
- package/src/deployments/azure/README.md +21 -0
- package/src/deployments/bootstrap.ts +177 -0
- package/src/deployments/deploy.ts +290 -0
- package/src/deployments/driver.ts +157 -0
- package/src/deployments/drivers.ts +35 -0
- package/src/deployments/gcp/Dockerfile +70 -0
- package/src/deployments/gcp/cloudbuild.yaml +31 -0
- package/src/deployments/gcp/driver.ts +175 -0
- package/src/deployments/gcp/gcloud.ts +178 -0
- package/src/deployments/gcp/provision.ts +290 -0
- package/src/deployments/gcp/survey.ts +319 -0
- package/src/deployments/local/README.md +12 -0
- package/src/deployments/prepare.ts +257 -0
- package/src/deployments/steps.ts +137 -0
- package/src/deployments/target.ts +295 -0
- package/src/deployments/upload.ts +207 -0
- package/src/dispatch/context.ts +195 -0
- package/src/dispatch/dispatch.ts +350 -0
- package/src/dispatch/index.ts +32 -0
- package/src/dispatch/staging.ts +102 -0
- package/src/policy/index.ts +179 -0
- package/src/policy/limits.ts +77 -0
- package/src/profile/authorization.ts +81 -0
- package/src/profile/files.ts +71 -0
- package/src/profile/index.ts +76 -0
- package/src/profile/layout.ts +123 -0
- package/src/profile/load.ts +199 -0
- package/src/profile/primitives.ts +45 -0
- package/src/profile/schema.ts +347 -0
- package/src/profile/secret-detection.ts +162 -0
- package/src/profile/targets.ts +152 -0
- package/src/profile/workspace.ts +262 -0
- package/src/providers/custom/index.ts +21 -0
- package/src/providers/custom/load.ts +115 -0
- package/src/providers/custom/template.ts +156 -0
- package/src/providers/example/provider.ts +207 -0
- package/src/providers/google/calendar/index.ts +66 -0
- package/src/providers/google/calendar/redact.ts +40 -0
- package/src/providers/google/contacts/index.ts +50 -0
- package/src/providers/google/contacts/redact.ts +21 -0
- package/src/providers/google/docs/index.ts +45 -0
- package/src/providers/google/drive/hints.ts +28 -0
- package/src/providers/google/drive/index.ts +34 -0
- package/src/providers/google/drive/redact.ts +39 -0
- package/src/providers/google/drive-mcp/index.ts +21 -0
- package/src/providers/google/gmail/api.ts +42 -0
- package/src/providers/google/gmail/attachment.ts +142 -0
- package/src/providers/google/gmail/hints.ts +55 -0
- package/src/providers/google/gmail/index.ts +112 -0
- package/src/providers/google/gmail/redact.ts +56 -0
- package/src/providers/google/gmail/send.ts +365 -0
- package/src/providers/google/gmail-mcp/index.ts +35 -0
- package/src/providers/google/index.ts +10 -0
- package/src/providers/google/shared/oauth.ts +122 -0
- package/src/providers/google/shared/scopes.ts +99 -0
- package/src/providers/google/shared/setup.ts +80 -0
- package/src/providers/google/sheets/hints.ts +45 -0
- package/src/providers/google/sheets/index.ts +70 -0
- package/src/providers/google/sheets/redact.ts +45 -0
- package/src/providers/google/specs/calendar.v3.json +1829 -0
- package/src/providers/google/specs/docs.v1.json +381 -0
- package/src/providers/google/specs/drive.v3.json +2208 -0
- package/src/providers/google/specs/gmail.v1.json +2578 -0
- package/src/providers/google/specs/people.v1.json +506 -0
- package/src/providers/google/specs/sheets.v4.json +1269 -0
- package/src/providers/google/specs/tasks.v1.json +840 -0
- package/src/providers/google/specs/vendor.ts +661 -0
- package/src/providers/google/tasks/index.ts +53 -0
- package/src/providers/google/tasks/redact.ts +34 -0
- package/src/providers/harness.ts +95 -0
- package/src/providers/icloud/calendar/index.ts +27 -0
- package/src/providers/icloud/contacts/index.ts +17 -0
- package/src/providers/icloud/drive/index.ts +47 -0
- package/src/providers/icloud/index.ts +8 -0
- package/src/providers/icloud/mail/index.ts +37 -0
- package/src/providers/icloud/shared/setup.ts +66 -0
- package/src/providers/index.ts +93 -0
- package/src/providers/linear/index.ts +11 -0
- package/src/providers/linear/scopes.ts +7 -0
- package/src/providers/memory/provider.ts +429 -0
- package/src/providers/notion/index.ts +19 -0
- package/src/providers/owner.ts +49 -0
- package/src/providers/scopes.ts +26 -0
- package/src/providers/setup/plan.ts +141 -0
- package/src/providers/setup/provider.ts +323 -0
- package/src/providers/shared/frontmatter.ts +119 -0
- package/src/providers/skills/provider.ts +283 -0
- package/src/providers/skills/store.ts +252 -0
- package/src/providers/vault/provider.ts +194 -0
- package/src/registry/index.ts +36 -0
- package/src/registry/policy-bridge.ts +32 -0
- package/src/registry/reconcile.ts +313 -0
- package/src/registry/registry.ts +240 -0
- package/src/secrets/document.ts +293 -0
- package/src/secrets/index.ts +154 -0
- package/src/secrets/system.ts +173 -0
- package/src/secrets/vault.ts +336 -0
- package/src/server/attachments.ts +197 -0
- package/src/server/container.ts +96 -0
- package/src/server/edge.ts +53 -0
- package/src/server/endpoint.ts +352 -0
- package/src/server/generations.ts +362 -0
- package/src/server/harness.ts +400 -0
- package/src/server/index.ts +331 -0
- package/src/server/logging.ts +41 -0
- package/src/server/mcp/build.ts +68 -0
- package/src/server/mcp/icon.ts +145 -0
- package/src/server/mcp/index.ts +32 -0
- package/src/server/mcp/instructions.ts +245 -0
- package/src/server/mcp/naming.ts +39 -0
- package/src/server/mcp/prompts.ts +78 -0
- package/src/server/mcp/resources.ts +106 -0
- package/src/server/mcp/routing.ts +117 -0
- package/src/server/mcp/schema.ts +78 -0
- package/src/server/mcp/tools.ts +186 -0
- package/src/server/mcp/visibility.ts +132 -0
- package/src/server/oauth.ts +222 -0
- package/src/server/rebinding.ts +53 -0
- package/src/server/stdio.ts +192 -0
- package/src/stores/blobs/conformance.ts +223 -0
- package/src/stores/blobs/index.ts +125 -0
- package/src/stores/blobs/testing.ts +49 -0
- package/src/stores/state/index.ts +247 -0
- package/src/stores/state/keys.ts +68 -0
- package/src/stores/state/testing.ts +41 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { ConfigError, type Config } from '#profile';
|
|
2
|
+
import { announce, print, style } from '../../output.ts';
|
|
3
|
+
import { confirm } from '../../prompt.ts';
|
|
4
|
+
import { openRuntime, type GlobalFlags, type Runtime } from '../../runtime.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What `lanes link memory`, `lanes link skills` and `lanes link vault` all need: the flag shape, the
|
|
8
|
+
* runtime wrapper, connection resolution, and the two prompts.
|
|
9
|
+
*
|
|
10
|
+
* All four commands are the same shape — open a runtime, announce, act, close —
|
|
11
|
+
* so the wrapper lives here rather than three times over.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface OwnerFlags extends GlobalFlags {
|
|
15
|
+
/** Which connection of this provider, when a profile has more than one. */
|
|
16
|
+
readonly connection?: string | undefined;
|
|
17
|
+
readonly title?: string | undefined;
|
|
18
|
+
readonly tag?: string | undefined;
|
|
19
|
+
readonly description?: string | undefined;
|
|
20
|
+
readonly file?: string | undefined;
|
|
21
|
+
/** Reveal a vault value on a terminal. */
|
|
22
|
+
readonly show?: boolean | undefined;
|
|
23
|
+
/** Print only the value, for `$(…)`. */
|
|
24
|
+
readonly raw?: boolean | undefined;
|
|
25
|
+
/** Skip the confirmation a destructive command would otherwise ask for. */
|
|
26
|
+
readonly yes?: boolean | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function withRuntime(
|
|
30
|
+
flags: OwnerFlags,
|
|
31
|
+
body: (runtime: Runtime) => Promise<void>,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const runtime = await openRuntime(flags);
|
|
34
|
+
try {
|
|
35
|
+
// Every command announces its profile and target before acting — except
|
|
36
|
+
// under `--raw`, whose entire purpose is that `$(…)` captures the value and
|
|
37
|
+
// nothing else. The same exception `lanes link token show --raw` makes.
|
|
38
|
+
if (!flags.raw) announce(runtime.resolution);
|
|
39
|
+
await body(runtime);
|
|
40
|
+
} finally {
|
|
41
|
+
await runtime.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Which connection of an owner provider to act on.
|
|
47
|
+
*
|
|
48
|
+
* A profile usually has exactly one of each, and typing `--connection` to reach
|
|
49
|
+
* your only one would be pure ceremony — the same trade ADR-012 §1 made for
|
|
50
|
+
* prompt routing. Two or more is genuinely ambiguous, so it refuses and names
|
|
51
|
+
* the candidates rather than picking.
|
|
52
|
+
*/
|
|
53
|
+
export function ownerConnection(config: Config, provider: string, flags: OwnerFlags): string {
|
|
54
|
+
const candidates = config.connections.filter((connection) => connection.provider === provider);
|
|
55
|
+
|
|
56
|
+
if (flags.connection) {
|
|
57
|
+
const match = candidates.find((connection) => connection.id === flags.connection);
|
|
58
|
+
if (!match) {
|
|
59
|
+
throw new ConfigError(
|
|
60
|
+
`No ${provider} connection "${flags.connection}" in this profile` +
|
|
61
|
+
(candidates.length > 0
|
|
62
|
+
? ` (have: ${candidates.map((connection) => connection.id).join(', ')})`
|
|
63
|
+
: ''),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return match.id;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (candidates.length === 0) {
|
|
70
|
+
throw new ConfigError(
|
|
71
|
+
`This profile has no ${provider} connection. Add one with: lanes link connect ${provider}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (candidates.length > 1) {
|
|
75
|
+
throw new ConfigError(
|
|
76
|
+
`This profile has ${candidates.length} ${provider} connections ` +
|
|
77
|
+
`(${candidates.map((connection) => connection.id).join(', ')}). Name one with --connection.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return candidates[0]!.id;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read a value from stdin, refusing a terminal.
|
|
86
|
+
*
|
|
87
|
+
* The same shape as `lanes link secrets set`, for the same two reasons: a value on argv
|
|
88
|
+
* lands in shell history, and a command that silently waits on a terminal that
|
|
89
|
+
* will never send anything is a hang rather than an error.
|
|
90
|
+
*/
|
|
91
|
+
export async function readStdin(usage: string, what: string): Promise<string> {
|
|
92
|
+
if (process.stdin.isTTY) {
|
|
93
|
+
throw new ConfigError(
|
|
94
|
+
`${usage} reads ${what} from stdin, and stdin is a terminal.\n` +
|
|
95
|
+
` printf %s "<value>" | ${usage}\n` +
|
|
96
|
+
` or: ${usage} < file.md`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const text = (await Bun.stdin.text()).replace(/\n$/, '');
|
|
101
|
+
if (!text) throw new ConfigError(`Nothing on stdin. Pipe ${what} in: ${usage} < file`);
|
|
102
|
+
return text;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Confirm a destructive action, unless `--yes` already answered. */
|
|
106
|
+
export async function agreed(flags: OwnerFlags, question: string): Promise<boolean> {
|
|
107
|
+
if (flags.yes) return true;
|
|
108
|
+
|
|
109
|
+
if (!process.stdin.isTTY) {
|
|
110
|
+
throw new ConfigError(
|
|
111
|
+
`${question} — stdin is not a terminal, so there is nobody to ask. Pass --yes to proceed.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Defaulting to no: the same choice `connect` makes where a prompt precedes
|
|
116
|
+
// something that cannot be undone.
|
|
117
|
+
const yes = await confirm(question, false);
|
|
118
|
+
if (!yes) print(style.dim(' cancelled'));
|
|
119
|
+
return yes;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function required(value: string | undefined, usage: string): string {
|
|
123
|
+
if (!value) throw new ConfigError(`Usage: ${usage}`);
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ConfigError } from '#profile';
|
|
2
|
+
import {
|
|
3
|
+
loadProfileSkills,
|
|
4
|
+
readSkill,
|
|
5
|
+
removeSkill,
|
|
6
|
+
writeSkill,
|
|
7
|
+
} from '#providers/skills/store.ts';
|
|
8
|
+
import { heading, ok, print, style, table } from '../../output.ts';
|
|
9
|
+
import { agreed, readStdin, required, withRuntime, type OwnerFlags } from './shared.ts';
|
|
10
|
+
|
|
11
|
+
/** `lanes link skills` — the reusable procedures agents can invoke. */
|
|
12
|
+
|
|
13
|
+
export async function skillsList(flags: OwnerFlags): Promise<void> {
|
|
14
|
+
await withRuntime(flags, async (runtime) => {
|
|
15
|
+
const skills = await loadProfileSkills(runtime.skills);
|
|
16
|
+
|
|
17
|
+
heading(`Skills (${skills.length})`);
|
|
18
|
+
if (skills.length === 0) {
|
|
19
|
+
print(style.dim(' none — add one with: lanes link skills add <name>'));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
table(
|
|
24
|
+
skills.map((skill) => [
|
|
25
|
+
` ${skill.name}`,
|
|
26
|
+
skill.arguments.length > 0
|
|
27
|
+
? style.dim(
|
|
28
|
+
skill.arguments
|
|
29
|
+
.map((argument) => (argument.required ? argument.name : `${argument.name}?`))
|
|
30
|
+
.join(', '),
|
|
31
|
+
)
|
|
32
|
+
: '',
|
|
33
|
+
skill.description,
|
|
34
|
+
]),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
print('');
|
|
38
|
+
print(style.dim(' Each is the MCP prompt "skills_<name>", where policy allows it.'));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function skillsShow(name: string | undefined, flags: OwnerFlags): Promise<void> {
|
|
43
|
+
const skillName = required(name, 'lanes link skills show <name>');
|
|
44
|
+
|
|
45
|
+
await withRuntime(flags, async (runtime) => {
|
|
46
|
+
const skill = await readSkill(runtime.skills, skillName);
|
|
47
|
+
if (!skill) throw new ConfigError(`No skill "${skillName}" in this workspace.`);
|
|
48
|
+
|
|
49
|
+
const bytes = await runtime.skills.get(skill.path);
|
|
50
|
+
print('');
|
|
51
|
+
print(bytes ? new TextDecoder().decode(bytes) : skill.body);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function skillsAdd(name: string | undefined, flags: OwnerFlags): Promise<void> {
|
|
56
|
+
const skillName = required(name, 'lanes link skills add <name> (document on stdin, or --file)');
|
|
57
|
+
|
|
58
|
+
const text = flags.file
|
|
59
|
+
? await Bun.file(flags.file).text()
|
|
60
|
+
: await readStdin(
|
|
61
|
+
`lanes link skills add ${skillName}`,
|
|
62
|
+
'the skill document, frontmatter included',
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
await withRuntime(flags, async (runtime) => {
|
|
66
|
+
const existing = await readSkill(runtime.skills, skillName);
|
|
67
|
+
const skill = await writeSkill(runtime.skills, skillName, text);
|
|
68
|
+
|
|
69
|
+
print(ok(`${existing ? 'replaced' : 'added'} skill ${style.bold(skill.name)}`));
|
|
70
|
+
print(
|
|
71
|
+
style.dim(
|
|
72
|
+
` Available as the prompt "skills_${skill.name}". A running endpoint picks it up within a few seconds.`,
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function skillsRemove(name: string | undefined, flags: OwnerFlags): Promise<void> {
|
|
79
|
+
const skillName = required(name, 'lanes link skills remove <name>');
|
|
80
|
+
|
|
81
|
+
await withRuntime(flags, async (runtime) => {
|
|
82
|
+
const skill = await readSkill(runtime.skills, skillName);
|
|
83
|
+
if (!skill) throw new ConfigError(`No skill "${skillName}" in this workspace.`);
|
|
84
|
+
|
|
85
|
+
print(` ${style.bold(skill.name)} ${skill.description}`);
|
|
86
|
+
print(style.dim(` ${skill.path}`));
|
|
87
|
+
if (!(await agreed(flags, 'Remove this skill?'))) return;
|
|
88
|
+
|
|
89
|
+
await removeSkill(runtime.skills, skillName);
|
|
90
|
+
print(ok(`removed skill ${style.bold(skillName)}`));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { ConfigError } from '#profile';
|
|
2
|
+
import { assertItemId, generateVaultKey } from '#providers/owner.ts';
|
|
3
|
+
import { heading, ok, print, style, table } from '../../output.ts';
|
|
4
|
+
import {
|
|
5
|
+
agreed,
|
|
6
|
+
ownerConnection,
|
|
7
|
+
readStdin,
|
|
8
|
+
required,
|
|
9
|
+
withRuntime,
|
|
10
|
+
type OwnerFlags,
|
|
11
|
+
} from './shared.ts';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `lanes link vault` — the owner's own secret material.
|
|
15
|
+
*
|
|
16
|
+
* `lanes link vault get` prints a value, which `lanes link secrets` deliberately never does.
|
|
17
|
+
* That is the two-kinds-of-secret distinction doing its job
|
|
18
|
+
* (`docs/detailed/security.md`): a credential authorises the system and is never
|
|
19
|
+
* disclosed, and a vault the owner cannot read without an agent is not a vault.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export async function vaultList(flags: OwnerFlags): Promise<void> {
|
|
23
|
+
await withRuntime(flags, async (runtime) => {
|
|
24
|
+
const items = await runtime.vault.ids();
|
|
25
|
+
|
|
26
|
+
heading(`Vault items (${items.length})`);
|
|
27
|
+
if (items.length === 0) {
|
|
28
|
+
print(style.dim(' none — store one with: lanes link vault set <id>'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
table(
|
|
33
|
+
items.map((item) => [
|
|
34
|
+
` ${item.id}`,
|
|
35
|
+
style.dim(item.connectionId),
|
|
36
|
+
item.description ?? '',
|
|
37
|
+
]),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
print('');
|
|
41
|
+
print(
|
|
42
|
+
style.dim(
|
|
43
|
+
' Names only. Each is readable over MCP as "vault.get.<id>", one grant at a time.',
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function vaultGet(id: string | undefined, flags: OwnerFlags): Promise<void> {
|
|
50
|
+
const itemId = required(id, 'lanes link vault get <id> [--show]');
|
|
51
|
+
|
|
52
|
+
await withRuntime(flags, async (runtime) => {
|
|
53
|
+
const connection = ownerConnection(runtime.config, 'vault', flags);
|
|
54
|
+
const item = await runtime.vault.get(connection, itemId);
|
|
55
|
+
if (!item) throw new ConfigError(`No vault item "${itemId}" in this profile.`);
|
|
56
|
+
|
|
57
|
+
// `--raw` writes the value and nothing else, so `$(…)` is not contaminated
|
|
58
|
+
// by the announce line every other command prints. Same shape as
|
|
59
|
+
// `lanes link token show --raw`.
|
|
60
|
+
if (flags.raw) {
|
|
61
|
+
process.stdout.write(`${item.value}\n`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
print(
|
|
66
|
+
flags.show
|
|
67
|
+
? item.value
|
|
68
|
+
: `${item.value.slice(0, 3)}… ${style.dim(`(${item.value.length} chars — --show to reveal)`)}`,
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function vaultSet(id: string | undefined, flags: OwnerFlags): Promise<void> {
|
|
74
|
+
const itemId = required(id, 'lanes link vault set <id> (value on stdin)');
|
|
75
|
+
assertItemId(itemId);
|
|
76
|
+
|
|
77
|
+
const value = await readStdin(`lanes link vault set ${itemId}`, 'the secret');
|
|
78
|
+
|
|
79
|
+
await withRuntime(flags, async (runtime) => {
|
|
80
|
+
const connection = ownerConnection(runtime.config, 'vault', flags);
|
|
81
|
+
const replacing = (await runtime.vault.get(connection, itemId)) !== null;
|
|
82
|
+
|
|
83
|
+
await runtime.vault.put(connection, {
|
|
84
|
+
id: itemId,
|
|
85
|
+
value,
|
|
86
|
+
...(flags.description ? { description: flags.description } : {}),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
print(ok(`${replacing ? 'replaced' : 'stored'} vault item ${style.bold(itemId)}`));
|
|
90
|
+
if (!replacing) {
|
|
91
|
+
// Not a limitation being apologised for: the item list is read when the
|
|
92
|
+
// runtime is built, so a write cannot hand itself a read and granting a
|
|
93
|
+
// new secret is a deliberate act between two runs (ADR-012 §3).
|
|
94
|
+
print(
|
|
95
|
+
style.dim(
|
|
96
|
+
` Not readable over MCP yet. Allow "vault.get.${itemId}" in policy, then restart the endpoint.`,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function vaultRemove(id: string | undefined, flags: OwnerFlags): Promise<void> {
|
|
104
|
+
const itemId = required(id, 'lanes link vault remove <id>');
|
|
105
|
+
|
|
106
|
+
await withRuntime(flags, async (runtime) => {
|
|
107
|
+
const connection = ownerConnection(runtime.config, 'vault', flags);
|
|
108
|
+
const item = await runtime.vault.get(connection, itemId);
|
|
109
|
+
if (!item) throw new ConfigError(`No vault item "${itemId}" in this profile.`);
|
|
110
|
+
|
|
111
|
+
print(` ${style.bold(itemId)} ${item.description ?? style.dim('no description')}`);
|
|
112
|
+
if (!(await agreed(flags, 'Remove this item? The value cannot be recovered.'))) return;
|
|
113
|
+
|
|
114
|
+
await runtime.vault.delete(connection, itemId);
|
|
115
|
+
print(ok(`removed vault item ${style.bold(itemId)}`));
|
|
116
|
+
print(style.dim(' Its read capability disappears when the endpoint restarts.'));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Mint a vault key.
|
|
122
|
+
*
|
|
123
|
+
* Deliberately not written anywhere: a deployment's key belongs in that
|
|
124
|
+
* deployment's secret manager, and a command that helpfully stored it beside
|
|
125
|
+
* the ciphertext it protects would encrypt nothing.
|
|
126
|
+
*/
|
|
127
|
+
export function vaultKeyGenerate(flags: OwnerFlags): void {
|
|
128
|
+
const key = generateVaultKey();
|
|
129
|
+
|
|
130
|
+
if (flags.raw) {
|
|
131
|
+
process.stdout.write(`${key}\n`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
print(key);
|
|
136
|
+
print('');
|
|
137
|
+
print(style.dim(' Store it as LANES_LINK_VAULT_KEY. It is not written anywhere by this command:'));
|
|
138
|
+
print(style.dim(' a key kept beside the document it protects protects nothing.'));
|
|
139
|
+
print(style.dim(' Separate from the credential-store key, deliberately — docs/detailed/security.md.'));
|
|
140
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lanes link memory`, `lanes link skills`, `lanes link vault` — the owner layer's control plane.
|
|
3
|
+
*
|
|
4
|
+
* The layer shipped in M4 with no CLI at all, so the two stores holding the
|
|
5
|
+
* owner's *own* data were reachable only by an agent, and the one thing that
|
|
6
|
+
* shapes agent behaviour was reachable only by editing a file and restarting.
|
|
7
|
+
* Putting a password into the vault meant asking a language model to type it.
|
|
8
|
+
* ADR-014.
|
|
9
|
+
*
|
|
10
|
+
* **These commands reach the same bytes the providers do**, through the same
|
|
11
|
+
* stores — `runtime.skills`, `runtime.vault`, and the blob namespace core
|
|
12
|
+
* scopes memory to. Nothing here reimplements a storage layout, because two
|
|
13
|
+
* spellings of one layout is exactly how a control plane and its data plane
|
|
14
|
+
* drift apart.
|
|
15
|
+
*
|
|
16
|
+
* This does not widen ADR-007. Its exclusion list is policy, tokens,
|
|
17
|
+
* credentials, connections, config, and audit — the things that authorise
|
|
18
|
+
* *future agent behaviour*. A memory entry and a vault item are the owner's own
|
|
19
|
+
* data. Skills are the interesting case and are deliberately reachable from
|
|
20
|
+
* both sides now: from here because this is the owner's control plane, and over
|
|
21
|
+
* MCP because ADR-014 §1 decided a policy-gated grant beats a missing path.
|
|
22
|
+
*
|
|
23
|
+
* One noun per file — `memory.ts`, `skills.ts`, `vault.ts` — over the shape all
|
|
24
|
+
* three share in `shared.ts`: the flag type, the open-announce-act-close
|
|
25
|
+
* wrapper, connection resolution, and the two prompts.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
memoryForget,
|
|
30
|
+
memoryGet,
|
|
31
|
+
memoryList,
|
|
32
|
+
memoryStore,
|
|
33
|
+
memoryWrite,
|
|
34
|
+
} from './owner/memory.ts';
|
|
35
|
+
|
|
36
|
+
export { skillsAdd, skillsList, skillsRemove, skillsShow } from './owner/skills.ts';
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
vaultGet,
|
|
40
|
+
vaultKeyGenerate,
|
|
41
|
+
vaultList,
|
|
42
|
+
vaultRemove,
|
|
43
|
+
vaultSet,
|
|
44
|
+
} from './owner/vault.ts';
|
|
45
|
+
|
|
46
|
+
export { ownerConnection, type OwnerFlags } from './owner/shared.ts';
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { layout, profilePath, workspacePath, type Config, type TargetConfig } from '#profile';
|
|
2
|
+
import type { SecretStore } from '#secrets';
|
|
3
|
+
import type { BlobStore } from '#stores/blobs';
|
|
4
|
+
import { credentialRefFor, ownClientRefsFor, type ProviderRegistry } from '#registry';
|
|
5
|
+
import { print, style, warn } from '../../output.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What a profile's removal is allowed to delete, worked out before any of it
|
|
9
|
+
* happens.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is a read. The plan is a value, and the preview an operator
|
|
12
|
+
* confirms from is that value rendered — which is what stops what they were
|
|
13
|
+
* shown and what runs from drifting apart. `../../../deployments/driver.ts`
|
|
14
|
+
* gives the same reason for the same shape.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The credential references this profile declares, and only those.
|
|
19
|
+
*
|
|
20
|
+
* Locally the profile is the boundary: its credentials are a file inside its own
|
|
21
|
+
* directory, and deleting the directory is the whole operation. In Secret
|
|
22
|
+
* Manager they are flat names in one project, so two profiles deployed to the
|
|
23
|
+
* same project share a namespace and `list()` hands back the other one's as
|
|
24
|
+
* readily as its own. Deriving from what this profile declares is the only
|
|
25
|
+
* answer that cannot delete something that was never ours, and a secret deleted
|
|
26
|
+
* in the wrong project is not recoverable.
|
|
27
|
+
*
|
|
28
|
+
* The cost is that a genuinely orphaned ref — one whose connection was removed
|
|
29
|
+
* from config long ago — is not derivable and so is not deleted. The caller
|
|
30
|
+
* reports those rather than guessing, which is the honest position: a guess
|
|
31
|
+
* here is indistinguishable from another profile's credential.
|
|
32
|
+
*/
|
|
33
|
+
export function declaredRefs(
|
|
34
|
+
config: Config,
|
|
35
|
+
registry: ProviderRegistry,
|
|
36
|
+
declared: TargetConfig,
|
|
37
|
+
): string[] {
|
|
38
|
+
const refs = new Set<string>([config.auth.token_ref]);
|
|
39
|
+
|
|
40
|
+
// Read off the *target*, not the profile. `vaultTargetSchema` sits inside
|
|
41
|
+
// `targetSchema`, so two targets may seal the same profile's items in
|
|
42
|
+
// different places; taking it from the profile would attach one target's
|
|
43
|
+
// vault to another target's removal.
|
|
44
|
+
if (declared.vault?.adapter === 'secret') {
|
|
45
|
+
refs.add(declared.vault.ref ?? 'vault/document');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const connection of config.connections) {
|
|
49
|
+
const manifest = registry.manifest(connection.provider);
|
|
50
|
+
|
|
51
|
+
// `credentialRefFor` rather than `credentialRefForConnection`: it is the
|
|
52
|
+
// single authority on where a connection's credential lives, and it exists
|
|
53
|
+
// because two callers once derived the answer differently and disagreed.
|
|
54
|
+
// Asking the lower-level one here would open that gap again from a third
|
|
55
|
+
// side — and it would miss a `local` provider, whose only ref is the one
|
|
56
|
+
// the connection declares itself.
|
|
57
|
+
const ref = credentialRefFor(connection, manifest);
|
|
58
|
+
if (ref) refs.add(ref);
|
|
59
|
+
|
|
60
|
+
if (manifest) {
|
|
61
|
+
for (const own of ownClientRefsFor(manifest, config.oauth_apps)) refs.add(own);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return [...refs];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface RemovalItem {
|
|
69
|
+
/** `null` for a workspace-level item — the config file, the default-profile key. */
|
|
70
|
+
readonly target: string | null;
|
|
71
|
+
readonly kind: 'secret' | 'blob' | 'file' | 'config' | 'workspace-key';
|
|
72
|
+
readonly id: string;
|
|
73
|
+
readonly note?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface RemovalPlan {
|
|
77
|
+
readonly profile: string;
|
|
78
|
+
readonly items: readonly RemovalItem[];
|
|
79
|
+
/** Present in a target's store, not declared by this profile. Left alone. */
|
|
80
|
+
readonly untouched: readonly { readonly target: string; readonly refs: readonly string[] }[];
|
|
81
|
+
readonly warnings: readonly string[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface PlanOptions {
|
|
85
|
+
/** Restrict to one target. The profile itself then survives. */
|
|
86
|
+
readonly target?: string | undefined;
|
|
87
|
+
readonly openSecrets: (target: string) => Promise<SecretStore>;
|
|
88
|
+
readonly openBlobs: (target: string, area?: string) => Promise<BlobStore>;
|
|
89
|
+
readonly readDefaultProfile?: (() => Promise<string | undefined>) | undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const reason = (cause: unknown): string => (cause instanceof Error ? cause.message : String(cause));
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Everything removing this profile would delete, before any of it is deleted.
|
|
96
|
+
*
|
|
97
|
+
* Read-only. A store that will not open becomes a warning rather than a throw,
|
|
98
|
+
* because a target whose project is gone must not be able to strand a profile
|
|
99
|
+
* on the machine forever — and the operator sees that warning in the preview,
|
|
100
|
+
* before they confirm, rather than discovering it half way through.
|
|
101
|
+
*/
|
|
102
|
+
export async function removalPlan(
|
|
103
|
+
config: Config,
|
|
104
|
+
root: string,
|
|
105
|
+
profile: string,
|
|
106
|
+
registry: ProviderRegistry,
|
|
107
|
+
options: PlanOptions,
|
|
108
|
+
): Promise<RemovalPlan> {
|
|
109
|
+
const items: RemovalItem[] = [];
|
|
110
|
+
const untouched: { target: string; refs: string[] }[] = [];
|
|
111
|
+
const warnings: string[] = [];
|
|
112
|
+
|
|
113
|
+
const names = options.target ? [options.target] : Object.keys(config.targets);
|
|
114
|
+
|
|
115
|
+
for (const name of names) {
|
|
116
|
+
const declared = config.targets[name];
|
|
117
|
+
if (!declared) {
|
|
118
|
+
warnings.push(`Target "${name}" is not declared by this profile, so nothing was planned.`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Secrets first, and this ordering is load-bearing rather than tidy:
|
|
123
|
+
// `layout.credentials(p)` is `data/<p>/credentials.enc`, *inside* the blob
|
|
124
|
+
// root `data/<p>`. For the file adapter the credential store is itself a
|
|
125
|
+
// blob, so blobs-first would delete the store the secret deletions read
|
|
126
|
+
// through and turn every one of them into a failure.
|
|
127
|
+
try {
|
|
128
|
+
const secrets = await options.openSecrets(name);
|
|
129
|
+
const present = await secrets.list();
|
|
130
|
+
const mine = new Set(declaredRefs(config, registry, declared));
|
|
131
|
+
|
|
132
|
+
for (const ref of present) if (mine.has(ref)) items.push({ target: name, kind: 'secret', id: ref });
|
|
133
|
+
|
|
134
|
+
const theirs = present.filter((ref) => !mine.has(ref));
|
|
135
|
+
if (theirs.length > 0) untouched.push({ target: name, refs: theirs });
|
|
136
|
+
} catch (cause) {
|
|
137
|
+
warnings.push(
|
|
138
|
+
`Target "${name}": its credential store could not be opened (${reason(cause)}), so nothing in it will be removed.`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const blobs = await options.openBlobs(name);
|
|
144
|
+
for (const blob of await blobs.list()) items.push({ target: name, kind: 'blob', id: blob.key });
|
|
145
|
+
} catch (cause) {
|
|
146
|
+
warnings.push(
|
|
147
|
+
`Target "${name}": its storage could not be opened (${reason(cause)}), so nothing in it will be removed.`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// The blob store's root is the profile's own directory, and an adapter must
|
|
152
|
+
// never delete the root it was configured with — so emptying it leaves the
|
|
153
|
+
// directory. `layout.ts` frames that directory as the unit ("rm -r
|
|
154
|
+
// data/work is exactly remove the work profile's data"), and one left
|
|
155
|
+
// behind is silently reused by a later `profile add` of the same name.
|
|
156
|
+
if (declared.storage.adapter === 'filesystem') {
|
|
157
|
+
const blobRoot = declared.storage.path ?? layout.blobs(profile);
|
|
158
|
+
items.push({
|
|
159
|
+
target: name,
|
|
160
|
+
kind: 'file',
|
|
161
|
+
id: blobRoot,
|
|
162
|
+
note: 'the profile directory, once emptied',
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Skills and manifests sit at fixed areas under `data/<profile>/`, the
|
|
166
|
+
// same way state and the log do — an explicit area, so a profile that
|
|
167
|
+
// declares its own `storage.path` moves its provider blobs and leaves
|
|
168
|
+
// these where `layout` says they are. The sweep above walks the blob root
|
|
169
|
+
// and would then walk straight past them, so they are named. Ordinary
|
|
170
|
+
// case: both are already inside `blobRoot` and the sweep has them, which
|
|
171
|
+
// is why this only fires when the two have been pointed apart.
|
|
172
|
+
//
|
|
173
|
+
// Resolved before comparing, because they are not compared as paths
|
|
174
|
+
// otherwise: `newProfileTemplate` writes `./data/<profile>` and
|
|
175
|
+
// `layout.blobs` returns `data/<profile>`, which is one directory spelled
|
|
176
|
+
// two ways — the same leading `./` that `profile/layout.ts` has a
|
|
177
|
+
// paragraph about. Comparing the strings names every default profile's
|
|
178
|
+
// skills twice in the preview an operator confirms.
|
|
179
|
+
if (workspacePath(root, blobRoot) !== workspacePath(root, layout.blobs(profile))) {
|
|
180
|
+
for (const area of [layout.skills(profile), layout.providers(profile)]) {
|
|
181
|
+
items.push({
|
|
182
|
+
target: name,
|
|
183
|
+
kind: 'file',
|
|
184
|
+
id: area,
|
|
185
|
+
note: 'outside the declared storage path, so not swept with it',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// A deployed revision reads its config from the bucket rather than the
|
|
192
|
+
// image (ADR-023), so that copy is the profile too — and it is outside the
|
|
193
|
+
// profile's blob tree, which is why it needs its own area.
|
|
194
|
+
if (declared.storage.adapter === 'gcs' || declared.storage.adapter === 's3') {
|
|
195
|
+
items.push({
|
|
196
|
+
target: name,
|
|
197
|
+
kind: 'config',
|
|
198
|
+
id: `profiles/${profile}.yaml`,
|
|
199
|
+
note: 'the copy a deployed revision reads',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// `deploy` only: the schema folds the deprecated `cloudrun` block into it,
|
|
204
|
+
// so the resolved config has one answer rather than two that could differ.
|
|
205
|
+
if (declared.deploy) {
|
|
206
|
+
warnings.push(
|
|
207
|
+
`Target "${name}" is deployed. The service will keep answering, and every call will fail, because what it served is gone. Tearing it down is not part of this.`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Only when the whole profile is going. With `--target` it still exists.
|
|
213
|
+
if (!options.target) {
|
|
214
|
+
const defaultProfile = await options.readDefaultProfile?.();
|
|
215
|
+
if (defaultProfile === profile) {
|
|
216
|
+
items.push({
|
|
217
|
+
target: null,
|
|
218
|
+
kind: 'workspace-key',
|
|
219
|
+
id: 'default_profile',
|
|
220
|
+
note: 'cleared, not repointed at whatever remains',
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Last. It is the only record of where everything else lives, so a failure
|
|
225
|
+
// before this point leaves data a later run can still find.
|
|
226
|
+
items.push({ target: null, kind: 'config', id: profilePath(root, profile) });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { profile, items, untouched, warnings };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const KIND_LABEL: Record<RemovalItem['kind'], string> = {
|
|
233
|
+
secret: 'credential',
|
|
234
|
+
blob: 'object',
|
|
235
|
+
file: 'file',
|
|
236
|
+
config: 'config',
|
|
237
|
+
'workspace-key': 'workspace',
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The plan, as the thing an operator decides from.
|
|
242
|
+
*
|
|
243
|
+
* Rendered from the same value that is executed, so there is no second
|
|
244
|
+
* description of the work to fall out of step with the first. It prints
|
|
245
|
+
* references and keys and never a value — the plan holds no values to print.
|
|
246
|
+
*/
|
|
247
|
+
export function renderPlan(plan: RemovalPlan): void {
|
|
248
|
+
print();
|
|
249
|
+
print(`Removing profile ${style.bold(plan.profile)} would delete:`);
|
|
250
|
+
print();
|
|
251
|
+
|
|
252
|
+
if (plan.items.length === 0) {
|
|
253
|
+
print(style.dim(' nothing — there is no trace of this profile left to remove.'));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const targets = [...new Set(plan.items.map((item) => item.target))];
|
|
257
|
+
for (const target of targets) {
|
|
258
|
+
const items = plan.items.filter((item) => item.target === target);
|
|
259
|
+
print(` ${style.bold(target ?? 'workspace')}`);
|
|
260
|
+
for (const item of items) {
|
|
261
|
+
const note = item.note ? style.dim(` — ${item.note}`) : '';
|
|
262
|
+
print(` ${KIND_LABEL[item.kind].padEnd(10)} ${item.id}${note}`);
|
|
263
|
+
}
|
|
264
|
+
print();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const { target, refs } of plan.untouched) {
|
|
268
|
+
// Named rather than counted, because the operator is the only one who can
|
|
269
|
+
// tell an orphan from another profile's live credential — and this command
|
|
270
|
+
// deliberately will not guess.
|
|
271
|
+
print(` ${style.bold(target)}: present but not declared by this profile, so left alone`);
|
|
272
|
+
for (const ref of refs) print(style.dim(` ${ref}`));
|
|
273
|
+
print();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const warning of plan.warnings) print(warn(warning));
|
|
277
|
+
if (plan.warnings.length > 0) print();
|
|
278
|
+
}
|