@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,315 @@
|
|
|
1
|
+
import type { SecretStore } from '#secrets';
|
|
2
|
+
import type { BlobStore } from '#stores/blobs';
|
|
3
|
+
import {
|
|
4
|
+
ConfigError,
|
|
5
|
+
WORKSPACE_FILE,
|
|
6
|
+
readWorkspace,
|
|
7
|
+
readWorkspaceFile,
|
|
8
|
+
workspaceFiles,
|
|
9
|
+
workspacePath,
|
|
10
|
+
writeWorkspaceFile,
|
|
11
|
+
} from '#profile';
|
|
12
|
+
import { parseDocument } from 'yaml';
|
|
13
|
+
import { rm } from 'node:fs/promises';
|
|
14
|
+
import { terminalPrompter, type Prompter } from '../../prompt.ts';
|
|
15
|
+
import { announce, emit, fail, ok, print, style } from '../../output.ts';
|
|
16
|
+
import {
|
|
17
|
+
buildRegistryWithWorkspace,
|
|
18
|
+
openBlobStoreFor,
|
|
19
|
+
openSecretStoreFor,
|
|
20
|
+
resolveProfile,
|
|
21
|
+
type GlobalFlags,
|
|
22
|
+
} from '../../runtime.ts';
|
|
23
|
+
import { removalPlan, renderPlan, type RemovalItem, type RemovalPlan } from './removal.ts';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Performing a removal, and being honest about the parts that did not happen.
|
|
27
|
+
*
|
|
28
|
+
* Best effort by choice: a target whose project has been deleted must not be
|
|
29
|
+
* able to strand a profile on the machine forever, so one refusal does not stop
|
|
30
|
+
* the rest. The price is real — a deletion that fails leaves a live credential
|
|
31
|
+
* behind — and everything here exists to make that visible rather than quiet.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export interface RemovalResult {
|
|
35
|
+
readonly item: RemovalItem;
|
|
36
|
+
/** `kept` is deliberate: not attempted, because something before it failed. */
|
|
37
|
+
readonly status: 'removed' | 'failed' | 'kept';
|
|
38
|
+
readonly error?: string;
|
|
39
|
+
/** The exact command that finishes this one by hand. */
|
|
40
|
+
readonly retry?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RemovalOutcome {
|
|
44
|
+
readonly profile: string;
|
|
45
|
+
readonly results: readonly RemovalResult[];
|
|
46
|
+
/** How many items are still there. Non-zero means a credential is still live. */
|
|
47
|
+
readonly survived: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RunDeps {
|
|
51
|
+
openSecrets: (target: string) => Promise<SecretStore>;
|
|
52
|
+
openBlobs: (target: string, area?: string) => Promise<BlobStore>;
|
|
53
|
+
removeConfig: (path: string) => Promise<void>;
|
|
54
|
+
/** The emptied profile directory. A blob delete cannot remove a directory. */
|
|
55
|
+
removeDirectory: (path: string) => Promise<void>;
|
|
56
|
+
clearDefaultProfile: () => Promise<void>;
|
|
57
|
+
retry?: ((item: RemovalItem, cause: string) => string | undefined) | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const reason = (cause: unknown): string => (cause instanceof Error ? cause.message : String(cause));
|
|
61
|
+
|
|
62
|
+
/** The items that only make sense once everything else is actually gone. */
|
|
63
|
+
const isRecordOfWhereThingsAre = (item: RemovalItem): boolean =>
|
|
64
|
+
item.target === null && (item.kind === 'config' || item.kind === 'workspace-key');
|
|
65
|
+
|
|
66
|
+
export async function executeRemoval(
|
|
67
|
+
plan: RemovalPlan,
|
|
68
|
+
deps: RunDeps,
|
|
69
|
+
): Promise<RemovalOutcome> {
|
|
70
|
+
const results: RemovalResult[] = [];
|
|
71
|
+
let failed = 0;
|
|
72
|
+
|
|
73
|
+
// One store per target, however many items it holds. Opening a Secret
|
|
74
|
+
// Manager client per secret would turn a tidy removal into a rate limit.
|
|
75
|
+
const secrets = new Map<string, Promise<SecretStore>>();
|
|
76
|
+
const blobs = new Map<string, Promise<BlobStore>>();
|
|
77
|
+
|
|
78
|
+
const secretStore = (target: string): Promise<SecretStore> => {
|
|
79
|
+
const existing = secrets.get(target) ?? deps.openSecrets(target);
|
|
80
|
+
secrets.set(target, existing);
|
|
81
|
+
return existing;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const blobStore = (target: string, area?: string): Promise<BlobStore> => {
|
|
85
|
+
const key = `${target}:${area ?? ''}`;
|
|
86
|
+
const existing = blobs.get(key) ?? deps.openBlobs(target, area);
|
|
87
|
+
blobs.set(key, existing);
|
|
88
|
+
return existing;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
for (const item of plan.items) {
|
|
92
|
+
// The config is the only record of where everything else lives. Deleting it
|
|
93
|
+
// after a failure would strand precisely the credential that failed: still
|
|
94
|
+
// live, and nothing left that knows where it is. Keeping it means the retry
|
|
95
|
+
// is this same command rather than a hand-assembled console session.
|
|
96
|
+
if (failed > 0 && isRecordOfWhereThingsAre(item)) {
|
|
97
|
+
results.push({ item, status: 'kept' });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
switch (item.kind) {
|
|
103
|
+
case 'secret':
|
|
104
|
+
await (await secretStore(item.target!)).delete(item.id);
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case 'blob':
|
|
108
|
+
await (await blobStore(item.target!)).delete(item.id);
|
|
109
|
+
break;
|
|
110
|
+
|
|
111
|
+
case 'config':
|
|
112
|
+
if (item.target === null) await deps.removeConfig(item.id);
|
|
113
|
+
else {
|
|
114
|
+
// `profiles/<name>.yaml` in the target's bucket — outside the
|
|
115
|
+
// profile's blob tree, so it needs its own area.
|
|
116
|
+
const [area, ...rest] = item.id.split('/');
|
|
117
|
+
await (await blobStore(item.target, area)).delete(rest.join('/'));
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
|
|
121
|
+
case 'workspace-key':
|
|
122
|
+
await deps.clearDefaultProfile();
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
case 'file':
|
|
126
|
+
await deps.removeDirectory(item.id);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
results.push({ item, status: 'removed' });
|
|
131
|
+
} catch (cause) {
|
|
132
|
+
const error = reason(cause);
|
|
133
|
+
const retry = deps.retry?.(item, error);
|
|
134
|
+
failed += 1;
|
|
135
|
+
results.push({ item, status: 'failed', error, ...(retry ? { retry } : {}) });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
profile: plan.profile,
|
|
141
|
+
results,
|
|
142
|
+
survived: results.filter((result) => result.status !== 'removed').length,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* What happened, and what is still out there.
|
|
148
|
+
*
|
|
149
|
+
* The exit code is the load-bearing part. Best effort means the command can
|
|
150
|
+
* finish having left a live credential behind, and to a script silence is
|
|
151
|
+
* indistinguishable from success — so anything that survived makes this exit
|
|
152
|
+
* non-zero, and names itself with the command that finishes it.
|
|
153
|
+
*/
|
|
154
|
+
export function renderOutcome(outcome: RemovalOutcome): void {
|
|
155
|
+
const removed = outcome.results.filter((result) => result.status === 'removed');
|
|
156
|
+
const failed = outcome.results.filter((result) => result.status === 'failed');
|
|
157
|
+
const kept = outcome.results.filter((result) => result.status === 'kept');
|
|
158
|
+
|
|
159
|
+
print();
|
|
160
|
+
if (outcome.survived === 0) {
|
|
161
|
+
print(ok(`Removed profile ${style.bold(outcome.profile)} — ${removed.length} item(s).`));
|
|
162
|
+
print();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
print(
|
|
167
|
+
fail(
|
|
168
|
+
`Removed ${removed.length} item(s) of profile ${style.bold(outcome.profile)}, and ${failed.length} refused.`,
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
print();
|
|
172
|
+
|
|
173
|
+
for (const result of failed) {
|
|
174
|
+
print(` ${result.item.id}`);
|
|
175
|
+
if (result.error) print(style.dim(` ${result.error}`));
|
|
176
|
+
if (result.retry) print(style.dim(` finish it with: ${result.retry}`));
|
|
177
|
+
}
|
|
178
|
+
print();
|
|
179
|
+
|
|
180
|
+
if (kept.length > 0) {
|
|
181
|
+
// Said plainly, because the alternative reading — that the profile is
|
|
182
|
+
// half-gone and needs unpicking by hand — is the one an operator will
|
|
183
|
+
// assume from a failure report.
|
|
184
|
+
print(
|
|
185
|
+
`The profile's config was kept, so nothing is stranded: fix the above and run the same command again.`,
|
|
186
|
+
);
|
|
187
|
+
print();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// A live credential left behind must not look like success to a script.
|
|
191
|
+
process.exitCode = 1;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The confirmation, which asks for the name rather than a keystroke.
|
|
196
|
+
*
|
|
197
|
+
* A step up from `agreed()`, deliberately. That helper is `y/N` and is right
|
|
198
|
+
* for `vault remove`, which drops one item the operator can put back. This
|
|
199
|
+
* drops every live OAuth refresh token a profile holds, and putting those back
|
|
200
|
+
* means visiting each vendor again — so the gesture should be one you cannot
|
|
201
|
+
* make by leaning on the keyboard.
|
|
202
|
+
*
|
|
203
|
+
* Local rather than shared for the same reason it is not `agreed()`: the shape
|
|
204
|
+
* differs, and bending the existing helper for a single caller in another
|
|
205
|
+
* command folder would leave both worse. If a second consumer appears, promote
|
|
206
|
+
* it then.
|
|
207
|
+
*/
|
|
208
|
+
export async function confirmedByName(
|
|
209
|
+
profile: string,
|
|
210
|
+
options: { yes?: boolean | undefined; prompter?: Prompter | undefined },
|
|
211
|
+
): Promise<boolean> {
|
|
212
|
+
if (options.yes) return true;
|
|
213
|
+
|
|
214
|
+
// A prompter that was passed in is the caller's answer to "is there anyone
|
|
215
|
+
// to ask" — the console passes one that replays a form. Only the default
|
|
216
|
+
// needs stdin consulted, and conflating the two makes an injected prompter
|
|
217
|
+
// untestable and a real terminal the only place this works.
|
|
218
|
+
const prompter = options.prompter ?? terminalPrompter;
|
|
219
|
+
const someoneToAsk = options.prompter ? prompter.interactive : process.stdin.isTTY;
|
|
220
|
+
|
|
221
|
+
if (!someoneToAsk) {
|
|
222
|
+
throw new ConfigError(
|
|
223
|
+
`Removing "${profile}" cannot be undone, and stdin is not a terminal, so there is nobody to ask. Pass --yes to proceed.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const typed = (await prompter.ask(`Type ${profile} to remove it, or anything else to stop: `))
|
|
228
|
+
.trim();
|
|
229
|
+
|
|
230
|
+
if (typed !== profile) {
|
|
231
|
+
print(style.dim(' cancelled — nothing was removed'));
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface RemoveFlags extends GlobalFlags {
|
|
238
|
+
readonly dryRun?: boolean | undefined;
|
|
239
|
+
readonly yes?: boolean | undefined;
|
|
240
|
+
readonly json?: boolean | undefined;
|
|
241
|
+
/** Injected by a caller that has already asked — the console, and tests. */
|
|
242
|
+
readonly prompter?: Prompter | undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* `lanes link profile remove <name>` — the profile, and everything it owns.
|
|
247
|
+
*
|
|
248
|
+
* Deliberately not reachable from MCP. This writes credentials and mutates
|
|
249
|
+
* config, which ADR-007 keeps CLI-only, and it is the most destructive thing
|
|
250
|
+
* in the tool.
|
|
251
|
+
*/
|
|
252
|
+
export async function removeProfile(name: string, flags: RemoveFlags): Promise<void> {
|
|
253
|
+
const { resolution, config } = await resolveProfile({ ...flags, profile: name });
|
|
254
|
+
announce(resolution);
|
|
255
|
+
|
|
256
|
+
const root = resolution.workspaceRoot;
|
|
257
|
+
const registry = await buildRegistryWithWorkspace(root, name);
|
|
258
|
+
const files = workspaceFiles(root);
|
|
259
|
+
|
|
260
|
+
const plan = await removalPlan(config, root, name, registry, {
|
|
261
|
+
target: flags.target,
|
|
262
|
+
openSecrets: (target) => openSecretStoreFor(config, root, target),
|
|
263
|
+
openBlobs: (target, area) => openBlobStoreFor(config, root, target, area),
|
|
264
|
+
readDefaultProfile: async () => (await readWorkspace(root))?.default_profile,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
renderPlan(plan);
|
|
268
|
+
|
|
269
|
+
if (flags.dryRun) {
|
|
270
|
+
print(style.dim(' --dry-run: nothing was removed, and no store was written to.'));
|
|
271
|
+
print();
|
|
272
|
+
return emit(flags.json, plan, () => {});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!(await confirmedByName(name, { yes: flags.yes, prompter: flags.prompter }))) return;
|
|
276
|
+
|
|
277
|
+
const outcome = await executeRemoval(plan, {
|
|
278
|
+
openSecrets: (target) => openSecretStoreFor(config, root, target),
|
|
279
|
+
openBlobs: (target, area) => openBlobStoreFor(config, root, target, area),
|
|
280
|
+
removeConfig: async (path) => await files.delete(relativeToRoot(root, path)),
|
|
281
|
+
removeDirectory: async (path) => await rm(workspacePath(root, path), { recursive: true, force: true }),
|
|
282
|
+
clearDefaultProfile: async () => await clearDefault(root),
|
|
283
|
+
retry: retryCommand,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return emit(flags.json, outcome, () => renderOutcome(outcome));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** `profiles/<name>.yaml`, however the path was spelled for display. */
|
|
290
|
+
function relativeToRoot(root: string, path: string): string {
|
|
291
|
+
const at = path.indexOf('profiles/');
|
|
292
|
+
return at === -1 ? path.replace(`${root}/`, '') : path.slice(at);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Clear the key, never repoint it.
|
|
297
|
+
*
|
|
298
|
+
* Choosing a new default on the operator's behalf would silently change what
|
|
299
|
+
* every other command in that workspace acts on — the one thing a removal
|
|
300
|
+
* should not decide for them.
|
|
301
|
+
*/
|
|
302
|
+
async function clearDefault(root: string): Promise<void> {
|
|
303
|
+
const text = await readWorkspaceFile(workspaceFiles(root), WORKSPACE_FILE);
|
|
304
|
+
if (text === null) return;
|
|
305
|
+
|
|
306
|
+
const document = parseDocument(text);
|
|
307
|
+
document.delete('default_profile');
|
|
308
|
+
await writeWorkspaceFile(workspaceFiles(root), WORKSPACE_FILE, String(document));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** The command that finishes a refusal by hand, where one can be named. */
|
|
312
|
+
function retryCommand(item: RemovalItem): string | undefined {
|
|
313
|
+
if (item.kind !== 'secret') return undefined;
|
|
314
|
+
return `lanes link profile remove <name> --target ${item.target} # or delete ${item.id} in that store`;
|
|
315
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { parseDocument } from 'yaml';
|
|
5
|
+
import {
|
|
6
|
+
WORKSPACE_FILE,
|
|
7
|
+
listProfiles,
|
|
8
|
+
profilePath,
|
|
9
|
+
readWorkspace,
|
|
10
|
+
resolveWorkspaceRoot,
|
|
11
|
+
} from '#profile';
|
|
12
|
+
import { newProfileTemplate, newWorkspaceTemplate } from '../config-edit.ts';
|
|
13
|
+
import { emit, ok, print, style, table } from '../output.ts';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Profile management.
|
|
17
|
+
*
|
|
18
|
+
* One profile = one config = one instance = one endpoint. Profiles never share
|
|
19
|
+
* a database, a credential store, or a URL, so each new one gets its own port
|
|
20
|
+
* by default — running personal and work side by side is the normal case, not
|
|
21
|
+
* an advanced one.
|
|
22
|
+
*
|
|
23
|
+
* Each command is a data function plus a printing wrapper. The split exists
|
|
24
|
+
* because `--json` needs the facts without the rendering, which is the same
|
|
25
|
+
* reason `startEndpoint` takes an `EndpointReporter` — a caller that is not a
|
|
26
|
+
* terminal should not have to parse one.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const FIRST_PORT = 7337;
|
|
30
|
+
|
|
31
|
+
export interface ProfileCreated {
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly path: string;
|
|
34
|
+
readonly port: number;
|
|
35
|
+
readonly isDefault: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ProfileListing {
|
|
39
|
+
readonly root: string;
|
|
40
|
+
readonly default: string | undefined;
|
|
41
|
+
readonly profiles: ReadonlyArray<{ readonly name: string; readonly path: string }>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Write a new profile, and the workspace file if this is the first one. */
|
|
45
|
+
export async function createProfile(
|
|
46
|
+
name: string,
|
|
47
|
+
options: { default?: boolean },
|
|
48
|
+
): Promise<ProfileCreated> {
|
|
49
|
+
const root = resolveWorkspaceRoot();
|
|
50
|
+
const path = profilePath(root, name);
|
|
51
|
+
|
|
52
|
+
if (existsSync(path)) throw new Error(`Profile "${name}" already exists at ${path}`);
|
|
53
|
+
|
|
54
|
+
await mkdir(join(root, 'profiles'), { recursive: true });
|
|
55
|
+
|
|
56
|
+
// Each profile gets its own port so two can serve at once without an
|
|
57
|
+
// operator having to think about it.
|
|
58
|
+
const existing = await listProfiles(root);
|
|
59
|
+
const port = FIRST_PORT + existing.length;
|
|
60
|
+
|
|
61
|
+
const workspaceFile = join(root, WORKSPACE_FILE);
|
|
62
|
+
const isFirst = existing.length === 0;
|
|
63
|
+
const isDefault = options.default === true || isFirst;
|
|
64
|
+
|
|
65
|
+
if (!existsSync(workspaceFile)) {
|
|
66
|
+
await writeFile(workspaceFile, newWorkspaceTemplate(isDefault ? name : undefined), {
|
|
67
|
+
mode: 0o600,
|
|
68
|
+
});
|
|
69
|
+
} else if (options.default) {
|
|
70
|
+
const document = parseDocument(await Bun.file(workspaceFile).text());
|
|
71
|
+
document.set('default_profile', name);
|
|
72
|
+
await writeFile(workspaceFile, document.toString({ lineWidth: 0 }), { mode: 0o600 });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await writeFile(path, newProfileTemplate(name, port), { mode: 0o600 });
|
|
76
|
+
|
|
77
|
+
return { name, path, port, isDefault };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Every profile in the workspace, and which one is the default.
|
|
82
|
+
*
|
|
83
|
+
* Names and paths only — deliberately not each profile's port, which would mean
|
|
84
|
+
* parsing every config. One unparseable profile would then fail the command
|
|
85
|
+
* that tells you which profiles exist, and that is exactly when you need it.
|
|
86
|
+
* `status --json` reports the endpoint for a profile you have named.
|
|
87
|
+
*/
|
|
88
|
+
export async function readProfiles(): Promise<ProfileListing> {
|
|
89
|
+
const root = resolveWorkspaceRoot();
|
|
90
|
+
const profiles = await listProfiles(root);
|
|
91
|
+
const workspace = await readWorkspace(root);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
root,
|
|
95
|
+
default: workspace?.default_profile,
|
|
96
|
+
profiles: profiles.map((name) => ({ name, path: profilePath(root, name) })),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function profileAdd(
|
|
101
|
+
name: string,
|
|
102
|
+
options: { default?: boolean; json?: boolean },
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
const created = await createProfile(name, options);
|
|
105
|
+
|
|
106
|
+
return emit(options.json, created, () => {
|
|
107
|
+
print(ok(`created profile ${style.bold(created.name)}`));
|
|
108
|
+
print(` config ${created.path}`);
|
|
109
|
+
print(` port ${created.port}`);
|
|
110
|
+
if (created.isDefault) print(` set as the workspace default`);
|
|
111
|
+
print();
|
|
112
|
+
print(
|
|
113
|
+
style.dim('Next: lanes link connect example # add a connection, no credentials needed'),
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function profileList(options: { json?: boolean } = {}): Promise<void> {
|
|
119
|
+
const listing = await readProfiles();
|
|
120
|
+
|
|
121
|
+
return emit(options.json, listing, () => {
|
|
122
|
+
if (listing.profiles.length === 0) {
|
|
123
|
+
print(style.dim(`No profiles in ${listing.root}.`));
|
|
124
|
+
print(style.dim('Create one with: lanes link profile add personal --default'));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
print(style.dim(listing.root));
|
|
129
|
+
table(
|
|
130
|
+
listing.profiles.map((profile) => [
|
|
131
|
+
profile.name === listing.default ? style.green('*') : ' ',
|
|
132
|
+
style.bold(profile.name),
|
|
133
|
+
style.dim(profile.path),
|
|
134
|
+
]),
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function profileDefault(name: string): Promise<void> {
|
|
140
|
+
const root = resolveWorkspaceRoot();
|
|
141
|
+
if (!existsSync(profilePath(root, name))) throw new Error(`Profile "${name}" does not exist`);
|
|
142
|
+
|
|
143
|
+
const workspaceFile = join(root, WORKSPACE_FILE);
|
|
144
|
+
const document = existsSync(workspaceFile)
|
|
145
|
+
? parseDocument(await Bun.file(workspaceFile).text())
|
|
146
|
+
: parseDocument(newWorkspaceTemplate());
|
|
147
|
+
|
|
148
|
+
document.set('default_profile', name);
|
|
149
|
+
await writeFile(workspaceFile, document.toString({ lineWidth: 0 }), { mode: 0o600 });
|
|
150
|
+
|
|
151
|
+
print(ok(`default profile is now ${style.bold(name)}`));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// The bundled agent skill used to be printed from here, because `installRoot`
|
|
155
|
+
// was already imported for it. It belongs with the other documents this CLI
|
|
156
|
+
// ships to a client — see `commands/mcp/assets.ts`.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { ConfigError } from '#profile';
|
|
2
|
+
import { assertValidSecretRef } from '#secrets';
|
|
3
|
+
import { announce, heading, ok, print, style } from '../output.ts';
|
|
4
|
+
import { openSecretStoreFor, resolveProfile, type GlobalFlags } from '../runtime.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `lanes link secrets` — moving credential values between a profile's targets.
|
|
8
|
+
*
|
|
9
|
+
* Credentials follow the target, because each target has its own credential
|
|
10
|
+
* store: `lanes link connect gmail.side --target cloud` writes the refresh token into
|
|
11
|
+
* Secret Manager, and the same command with `--target local` writes it into
|
|
12
|
+
* the encrypted file. That is the right default and it leaves one gap, which
|
|
13
|
+
* this command fills — a setup built locally first, and now wanted in the
|
|
14
|
+
* cloud, without re-running every OAuth flow.
|
|
15
|
+
*
|
|
16
|
+
* Every operation here is control plane: it writes credential values, so it is
|
|
17
|
+
* CLI-only by design and has no MCP surface (ADR-007). Values are never
|
|
18
|
+
* printed, never passed on argv where they would land in shell history, and
|
|
19
|
+
* never logged — only reference names appear.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface SecretsFlags extends GlobalFlags {
|
|
23
|
+
readonly from?: string | undefined;
|
|
24
|
+
readonly to?: string | undefined;
|
|
25
|
+
/** Replace a reference that already exists in the destination. */
|
|
26
|
+
readonly overwrite?: boolean | undefined;
|
|
27
|
+
readonly dryRun?: boolean | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function secretsPush(flags: SecretsFlags): Promise<void> {
|
|
31
|
+
if (!flags.from || !flags.to) {
|
|
32
|
+
throw new ConfigError('Usage: lanes link secrets push --from <target> --to <target>');
|
|
33
|
+
}
|
|
34
|
+
if (flags.from === flags.to) {
|
|
35
|
+
throw new ConfigError(`--from and --to are both "${flags.from}"; there is nothing to copy.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { resolution, config } = await resolveProfile(flags);
|
|
39
|
+
announce(resolution);
|
|
40
|
+
|
|
41
|
+
const source = await openSecretStoreFor(config, resolution.workspaceRoot, flags.from);
|
|
42
|
+
const destination = await openSecretStoreFor(config, resolution.workspaceRoot, flags.to);
|
|
43
|
+
|
|
44
|
+
const refs = await source.list();
|
|
45
|
+
if (refs.length === 0) {
|
|
46
|
+
print(style.dim(`No credentials in target "${flags.from}".`));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
heading(`${flags.from} → ${flags.to}`);
|
|
51
|
+
|
|
52
|
+
// Only asked when it can change the answer: against Secret Manager each of
|
|
53
|
+
// these is an API call, and `--overwrite` ignores the result anyway.
|
|
54
|
+
const existing = new Set<string>();
|
|
55
|
+
if (!flags.overwrite) {
|
|
56
|
+
for (const ref of refs) if (await destination.has(ref)) existing.add(ref);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { copy, skip } = pushDecision({ refs, existing, overwrite: flags.overwrite === true });
|
|
60
|
+
const copied: string[] = [];
|
|
61
|
+
const skipped = [...skip];
|
|
62
|
+
|
|
63
|
+
for (const ref of copy) {
|
|
64
|
+
if (!flags.dryRun) {
|
|
65
|
+
const value = await source.get(ref);
|
|
66
|
+
// A ref that lists but does not read means the store changed underneath
|
|
67
|
+
// us. Skipping beats writing an empty string over a live credential.
|
|
68
|
+
if (value === null) {
|
|
69
|
+
skipped.push(ref);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
await destination.set(ref, value);
|
|
73
|
+
}
|
|
74
|
+
copied.push(ref);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Copy, never move. `connect` has a copy-then-delete helper for renaming a
|
|
78
|
+
// connection within one store; across targets both copies are wanted — the
|
|
79
|
+
// local one is what `lanes link start` still runs on.
|
|
80
|
+
for (const ref of copied) print(` ${style.green('+')} ${ref}`);
|
|
81
|
+
for (const ref of skipped) print(` ${style.dim('=')} ${style.dim(`${ref} already present`)}`);
|
|
82
|
+
|
|
83
|
+
print('');
|
|
84
|
+
if (flags.dryRun) {
|
|
85
|
+
print(style.dim(` --dry-run: ${copied.length} would be copied, ${skipped.length} skipped.`));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
print(ok(`copied ${copied.length}, skipped ${skipped.length}`));
|
|
89
|
+
if (skipped.length > 0 && !flags.overwrite) {
|
|
90
|
+
print(style.dim(' --overwrite replaces what is already there.'));
|
|
91
|
+
}
|
|
92
|
+
print(style.dim(` The source target keeps its copy; nothing was deleted from "${flags.from}".`));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Store one value, read from stdin.
|
|
97
|
+
*
|
|
98
|
+
* Never from argv: an argument is in the shell history, in `ps` output while
|
|
99
|
+
* the process runs, and in any transcript of the session. The Postgres
|
|
100
|
+
* connection string is the reason this exists — the cloud target names it with
|
|
101
|
+
* `database.url_ref`, and it has to get into the store somehow.
|
|
102
|
+
*/
|
|
103
|
+
export async function secretsSet(ref: string | undefined, flags: GlobalFlags): Promise<void> {
|
|
104
|
+
if (!ref) {
|
|
105
|
+
throw new ConfigError(
|
|
106
|
+
'Usage: lanes link secrets set <ref> (the value is read from stdin)\n' +
|
|
107
|
+
' e.g. printf %s "$DATABASE_URL" | lanes link secrets set cloud/database_url --target cloud',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
assertValidSecretRef(ref);
|
|
111
|
+
|
|
112
|
+
// Without this the command waits on a terminal that will never send
|
|
113
|
+
// anything, with no output to explain why — a hang, not an error.
|
|
114
|
+
if (process.stdin.isTTY) {
|
|
115
|
+
throw new ConfigError(
|
|
116
|
+
`lanes link secrets set reads the value from stdin, and stdin is a terminal.\n` +
|
|
117
|
+
` printf %s "<value>" | lanes link secrets set ${ref}\n` +
|
|
118
|
+
' Pass it this way rather than as an argument: an argument is in your shell history.',
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const { resolution, config, target } = await resolveProfile(flags);
|
|
123
|
+
announce(resolution);
|
|
124
|
+
|
|
125
|
+
const value = (await Bun.stdin.text()).replace(/\n$/, '');
|
|
126
|
+
if (!value) {
|
|
127
|
+
throw new ConfigError(
|
|
128
|
+
`No value on stdin. Pipe one in: printf %s "<value>" | lanes link secrets set ${ref}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const credentials = await openSecretStoreFor(config, resolution.workspaceRoot, target);
|
|
133
|
+
const replacing = await credentials.has(ref);
|
|
134
|
+
await credentials.set(ref, value);
|
|
135
|
+
|
|
136
|
+
print(ok(`${replacing ? 'replaced' : 'stored'} ${style.bold(ref)} in target ${target}`));
|
|
137
|
+
if (replacing) {
|
|
138
|
+
print(style.dim(' Anything still using the old value will start failing.'));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function secretsList(flags: GlobalFlags): Promise<void> {
|
|
143
|
+
const { resolution, config, target } = await resolveProfile(flags);
|
|
144
|
+
announce(resolution);
|
|
145
|
+
|
|
146
|
+
const credentials = await openSecretStoreFor(config, resolution.workspaceRoot, target);
|
|
147
|
+
const refs = await credentials.list();
|
|
148
|
+
|
|
149
|
+
heading(`Credential references in target ${target} (${refs.length})`);
|
|
150
|
+
if (refs.length === 0) {
|
|
151
|
+
print(style.dim(' none'));
|
|
152
|
+
} else {
|
|
153
|
+
for (const ref of refs) print(` ${ref}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The interface has no operation that enumerates values, and this command is
|
|
157
|
+
// not going to become the first one.
|
|
158
|
+
print('');
|
|
159
|
+
print(style.dim(' Names only. There is no command that prints a stored credential value.'));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Which references a push copies and which it leaves alone. */
|
|
163
|
+
export function pushDecision(input: {
|
|
164
|
+
readonly refs: readonly string[];
|
|
165
|
+
readonly existing: ReadonlySet<string>;
|
|
166
|
+
readonly overwrite: boolean;
|
|
167
|
+
}): { copy: string[]; skip: string[] } {
|
|
168
|
+
const copy: string[] = [];
|
|
169
|
+
const skip: string[] = [];
|
|
170
|
+
|
|
171
|
+
for (const ref of input.refs) {
|
|
172
|
+
if (!input.overwrite && input.existing.has(ref)) skip.push(ref);
|
|
173
|
+
else copy.push(ref);
|
|
174
|
+
}
|
|
175
|
+
return { copy, skip };
|
|
176
|
+
}
|