@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,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reject credential values that have been written into a config file.
|
|
3
|
+
*
|
|
4
|
+
* Config carries `_ref` pointers into the credential store; it never carries a
|
|
5
|
+
* value. This module is what turns that from an intention into a check, and it
|
|
6
|
+
* runs on every load and before every CLI write, so a config file cannot become
|
|
7
|
+
* a place secrets accumulate.
|
|
8
|
+
*
|
|
9
|
+
* Findings name the exact path (`oauth_apps.google.client_secret`) because "your
|
|
10
|
+
* config contains a secret" is not actionable on a file with fifty keys.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately biased toward false negatives over false positives. A rule that
|
|
13
|
+
* rejects a legitimate display name makes the tool unusable, and this is a
|
|
14
|
+
* second line of defence — `.gitignore` is the first, and the credential store
|
|
15
|
+
* is what makes the whole class of mistake unnecessary.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface SecretFinding {
|
|
19
|
+
/** Dotted path, with array indices: `connections[0].credential_ref`. */
|
|
20
|
+
readonly path: string;
|
|
21
|
+
readonly rule: string;
|
|
22
|
+
readonly detail: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Vendor prefixes that are never anything but a live credential. */
|
|
26
|
+
const CREDENTIAL_PREFIXES: ReadonlyArray<{ prefix: string; label: string }> = [
|
|
27
|
+
{ prefix: 'sk-', label: 'an OpenAI-style secret key' },
|
|
28
|
+
{ prefix: 'xoxb-', label: 'a Slack bot token' },
|
|
29
|
+
{ prefix: 'xoxp-', label: 'a Slack user token' },
|
|
30
|
+
{ prefix: 'xapp-', label: 'a Slack app token' },
|
|
31
|
+
{ prefix: 'ya29.', label: 'a Google OAuth access token' },
|
|
32
|
+
{ prefix: 'ghp_', label: 'a GitHub personal access token' },
|
|
33
|
+
{ prefix: 'gho_', label: 'a GitHub OAuth token' },
|
|
34
|
+
{ prefix: 'github_pat_', label: 'a GitHub fine-grained token' },
|
|
35
|
+
{ prefix: 'AKIA', label: 'an AWS access key id' },
|
|
36
|
+
{ prefix: 'AIza', label: 'a Google API key' },
|
|
37
|
+
{ prefix: 'llk_', label: 'a Lanes Link profile token' },
|
|
38
|
+
{ prefix: '1//', label: 'a Google refresh token' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const PRIVATE_KEY_BLOCK = /-----BEGIN[ A-Z]*PRIVATE KEY-----/;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Key names that must hold a reference rather than a value.
|
|
45
|
+
*
|
|
46
|
+
* A key ending in `_ref` is exempt: `client_secret_ref` is exactly what config
|
|
47
|
+
* is supposed to contain, while `client_secret` is exactly what it must not.
|
|
48
|
+
*/
|
|
49
|
+
const VALUE_BEARING_KEY = /(?:^|_)(secret|token|password|passwd|apikey|credential)$|_key$/;
|
|
50
|
+
|
|
51
|
+
/** A single opaque token — no spaces, the shape credentials actually take. */
|
|
52
|
+
const OPAQUE_TOKEN = /^[A-Za-z0-9_\-+/=.]{24,}$/;
|
|
53
|
+
|
|
54
|
+
export function shannonEntropy(value: string): number {
|
|
55
|
+
if (value.length === 0) return 0;
|
|
56
|
+
const counts = new Map<string, number>();
|
|
57
|
+
for (const char of value) counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
58
|
+
|
|
59
|
+
let entropy = 0;
|
|
60
|
+
for (const count of counts.values()) {
|
|
61
|
+
const p = count / value.length;
|
|
62
|
+
entropy -= p * Math.log2(p);
|
|
63
|
+
}
|
|
64
|
+
return entropy;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function characterClasses(value: string): number {
|
|
68
|
+
let classes = 0;
|
|
69
|
+
if (/[a-z]/.test(value)) classes++;
|
|
70
|
+
if (/[A-Z]/.test(value)) classes++;
|
|
71
|
+
if (/[0-9]/.test(value)) classes++;
|
|
72
|
+
return classes;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Does this look like a random blob rather than a path, name, or identifier?
|
|
77
|
+
*
|
|
78
|
+
* Requiring all three of length, mixed character classes, and high entropy is
|
|
79
|
+
* what keeps `./data/personal.credentials.enc` (no uppercase, no digits) and
|
|
80
|
+
* `google/client_secret` (lowercase only) from being flagged, while catching a
|
|
81
|
+
* base64 or hex credential.
|
|
82
|
+
*/
|
|
83
|
+
function looksHighEntropy(value: string): boolean {
|
|
84
|
+
return (
|
|
85
|
+
OPAQUE_TOKEN.test(value) &&
|
|
86
|
+
characterClasses(value) >= 3 &&
|
|
87
|
+
shannonEntropy(value) >= 3.5 &&
|
|
88
|
+
!/^https?:/i.test(value)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function inspectValue(path: string, key: string, value: string): SecretFinding | null {
|
|
93
|
+
if (value.length === 0) return null;
|
|
94
|
+
|
|
95
|
+
if (PRIVATE_KEY_BLOCK.test(value)) {
|
|
96
|
+
return { path, rule: 'private-key-block', detail: 'contains an inline private key block' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const { prefix, label } of CREDENTIAL_PREFIXES) {
|
|
100
|
+
if (value.startsWith(prefix)) {
|
|
101
|
+
return {
|
|
102
|
+
path,
|
|
103
|
+
rule: 'credential-prefix',
|
|
104
|
+
detail: `starts with "${prefix}", which identifies ${label}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// A key that names a credential must carry a reference, not a value —
|
|
110
|
+
// regardless of how innocuous the value looks.
|
|
111
|
+
if (!key.endsWith('_ref') && VALUE_BEARING_KEY.test(key)) {
|
|
112
|
+
return {
|
|
113
|
+
path,
|
|
114
|
+
rule: 'value-bearing-key',
|
|
115
|
+
detail:
|
|
116
|
+
`"${key}" names a credential, so it must hold a reference rather than a value. ` +
|
|
117
|
+
`Use "${key}_ref" pointing at the credential store, e.g. "google/${key}".`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (looksHighEntropy(value)) {
|
|
122
|
+
return {
|
|
123
|
+
path,
|
|
124
|
+
rule: 'high-entropy',
|
|
125
|
+
detail: `is a ${value.length}-character high-entropy string, which looks like a credential`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Walk a parsed config and report every credential-shaped value in it. */
|
|
133
|
+
export function findSecrets(value: unknown, path = '', key = ''): SecretFinding[] {
|
|
134
|
+
if (typeof value === 'string') {
|
|
135
|
+
const finding = inspectValue(path, key, value);
|
|
136
|
+
return finding ? [finding] : [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (Array.isArray(value)) {
|
|
140
|
+
return value.flatMap((item, index) => findSecrets(item, `${path}[${index}]`, key));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (value !== null && typeof value === 'object') {
|
|
144
|
+
return Object.entries(value).flatMap(([childKey, childValue]) =>
|
|
145
|
+
findSecrets(childValue, path ? `${path}.${childKey}` : childKey, childKey),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function formatSecretFindings(findings: readonly SecretFinding[]): string {
|
|
153
|
+
const lines = findings.map((finding) => ` ${finding.path} ${finding.detail}`);
|
|
154
|
+
return (
|
|
155
|
+
`Configuration must not contain credential values, only "_ref" pointers into the credential store.\n` +
|
|
156
|
+
`${findings.length === 1 ? 'This value' : 'These values'} looked like a credential:\n` +
|
|
157
|
+
`${lines.join('\n')}\n\n` +
|
|
158
|
+
`Move the value into the credential store and reference it instead. ` +
|
|
159
|
+
`If this is a false positive, rename the key or shorten the value — ` +
|
|
160
|
+
`there is deliberately no way to suppress this check.`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { ConfigError } from './load.ts';
|
|
2
|
+
import type { Config } from './schema.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Which target a command acts on.
|
|
6
|
+
*
|
|
7
|
+
* A target names an adapter set — where credentials are kept and where bytes
|
|
8
|
+
* go — and choosing one is a different subject from finding the workspace and
|
|
9
|
+
* the profile, which is why it is a different file. `workspace.ts` reached the
|
|
10
|
+
* size budget holding both, and the budget's job is to say which of the two
|
|
11
|
+
* things a file is doing should leave.
|
|
12
|
+
*
|
|
13
|
+
* The order is `--target`, then `LANES_LINK_TARGET`, then
|
|
14
|
+
* `instance.default_target`. `deploy` resolves it differently and deliberately;
|
|
15
|
+
* see `resolveDeployTarget`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** The variable that names a target for every command in a shell. */
|
|
19
|
+
export const TARGET_ENV = 'LANES_LINK_TARGET';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The target somebody asked for, before the config gets a say.
|
|
23
|
+
*
|
|
24
|
+
* Split out because two callers need this precedence and must not disagree
|
|
25
|
+
* about it: `resolveSelection` records a provisional answer for `announce`
|
|
26
|
+
* before any config is loaded, and `resolveTarget` settles it afterwards. While
|
|
27
|
+
* the flag was the only source, both could spell it `targetFlag ?? …` and stay
|
|
28
|
+
* accidentally correct. With a second source, one of them learning about it and
|
|
29
|
+
* the other not is a line reading `config-default` beside a target the
|
|
30
|
+
* environment chose — which is the one line that exists to prevent exactly that.
|
|
31
|
+
*
|
|
32
|
+
* Returns no target rather than a default: what an unanswered question falls
|
|
33
|
+
* back to is `instance.default_target`, and that is the config's to supply.
|
|
34
|
+
*/
|
|
35
|
+
export function askedTarget(
|
|
36
|
+
targetFlag: string | undefined,
|
|
37
|
+
env: Record<string, string | undefined>,
|
|
38
|
+
): { target: string | undefined; source: 'flag' | 'environment' | undefined } {
|
|
39
|
+
if (targetFlag) return { target: targetFlag, source: 'flag' };
|
|
40
|
+
|
|
41
|
+
const fromEnv = env[TARGET_ENV];
|
|
42
|
+
if (fromEnv) return { target: fromEnv, source: 'environment' };
|
|
43
|
+
|
|
44
|
+
return { target: undefined, source: undefined };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The refusal for a target that is not in the file, in one spelling.
|
|
49
|
+
*
|
|
50
|
+
* It was two — here and in the CLI's `openSecretStoreFor` — which is one more
|
|
51
|
+
* than a sentence naming the available targets survives: the copies drift the
|
|
52
|
+
* moment either learns something the other does not. This one knows about
|
|
53
|
+
* `LANES_LINK_TARGET`, and that is precisely the knowledge a copy would lack —
|
|
54
|
+
* an exported typo otherwise fails every command in the shell with a message
|
|
55
|
+
* that reads as a problem with the config file.
|
|
56
|
+
*/
|
|
57
|
+
export function undeclaredTarget(
|
|
58
|
+
target: string,
|
|
59
|
+
config: Config,
|
|
60
|
+
source?: 'flag' | 'environment' | 'config-default',
|
|
61
|
+
): ConfigError {
|
|
62
|
+
const have = Object.keys(config.targets).join(', ') || 'none';
|
|
63
|
+
return new ConfigError(
|
|
64
|
+
`Target "${target}" is not declared in this profile (have: ${have})` +
|
|
65
|
+
(source === 'environment'
|
|
66
|
+
? `\n${TARGET_ENV}=${target} is set in this shell — unset it, or pass --target.`
|
|
67
|
+
: ''),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Fill in the target once the profile's config has been loaded.
|
|
73
|
+
*
|
|
74
|
+
* Order: `--target`, then `LANES_LINK_TARGET`, then `instance.default_target`.
|
|
75
|
+
* The middle one is the same bargain `LANES_LINK_PROFILE` already offers — a
|
|
76
|
+
* shell's worth of commands without retyping a flag, somewhere `env` shows it.
|
|
77
|
+
*
|
|
78
|
+
* `allowUndeclared` is for the one command whose job is to create the target it
|
|
79
|
+
* was given — `deploy`, on a first run. Every other command naming a target that
|
|
80
|
+
* does not exist has made a typo, and the list of what does exist is the useful
|
|
81
|
+
* answer; refusing there is what stops `--target clod` opening the default one.
|
|
82
|
+
*/
|
|
83
|
+
export function resolveTarget(
|
|
84
|
+
config: Config,
|
|
85
|
+
targetFlag?: string,
|
|
86
|
+
options: {
|
|
87
|
+
allowUndeclared?: boolean;
|
|
88
|
+
env?: Record<string, string | undefined>;
|
|
89
|
+
} = {},
|
|
90
|
+
): { target: string; source: 'flag' | 'environment' | 'config-default' } {
|
|
91
|
+
const env = options.env ?? (process.env as Record<string, string | undefined>);
|
|
92
|
+
const asked = askedTarget(targetFlag, env);
|
|
93
|
+
|
|
94
|
+
const target = asked.target ?? config.instance.default_target;
|
|
95
|
+
const source = asked.source ?? 'config-default';
|
|
96
|
+
|
|
97
|
+
if (options.allowUndeclared !== true && !(target in config.targets)) {
|
|
98
|
+
throw undeclaredTarget(target, config, source);
|
|
99
|
+
}
|
|
100
|
+
return { target, source };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The target a deploy means, when nobody said.
|
|
105
|
+
*
|
|
106
|
+
* `--target cloud` was required on every deploy, and the reason was an accident:
|
|
107
|
+
* an absent flag falls back to `instance.default_target`, which is `local` —
|
|
108
|
+
* a target that by definition is not deployed anywhere. So the one command whose
|
|
109
|
+
* subject is never ambiguous was the one command that made you say it, and the
|
|
110
|
+
* default it would otherwise have taken was not merely unhelpful but wrong.
|
|
111
|
+
*
|
|
112
|
+
* The rule is what someone would say out loud: deploy the target that has a
|
|
113
|
+
* deployment. One is the answer; none means the first run, which conventionally
|
|
114
|
+
* creates `cloud` and is what every example in the docs names; several is a
|
|
115
|
+
* genuine question, and asking beats rolling a revision to whichever came first
|
|
116
|
+
* in a YAML mapping.
|
|
117
|
+
*
|
|
118
|
+
* `--target` still wins, which is how you deploy the second one.
|
|
119
|
+
*
|
|
120
|
+
* **`LANES_LINK_TARGET` is deliberately not read here.** It is the same kind of
|
|
121
|
+
* answer as `instance.default_target` — a shell-wide "where do my commands
|
|
122
|
+
* run" — and the paragraph above is why that kind of answer is the wrong one
|
|
123
|
+
* for this question. It is also the only place where being wrong creates cloud
|
|
124
|
+
* resources rather than an error: `deploy` is the one caller that passes
|
|
125
|
+
* `allowUndeclared`, so an exported typo would not be refused, it would be
|
|
126
|
+
* surveyed, written into the profile, and rolled out as a new service. An
|
|
127
|
+
* environment variable must not be able to name a Cloud Run service into
|
|
128
|
+
* existence. Say `--target`; `deploy` is rare enough to afford it.
|
|
129
|
+
*/
|
|
130
|
+
export const CONVENTIONAL_DEPLOY_TARGET = 'cloud';
|
|
131
|
+
|
|
132
|
+
export function resolveDeployTarget(
|
|
133
|
+
config: Config,
|
|
134
|
+
targetFlag?: string,
|
|
135
|
+
): { target: string; source: 'flag' | 'deployable' } {
|
|
136
|
+
if (targetFlag) return { target: targetFlag, source: 'flag' };
|
|
137
|
+
|
|
138
|
+
// `deploy` alone: the legacy `cloudrun:` spelling is normalised into it by the
|
|
139
|
+
// loader, so exactly one shape reaches here.
|
|
140
|
+
const deployable = Object.entries(config.targets)
|
|
141
|
+
.filter(([, declared]) => declared.deploy !== undefined)
|
|
142
|
+
.map(([name]) => name);
|
|
143
|
+
|
|
144
|
+
if (deployable.length > 1) {
|
|
145
|
+
throw new ConfigError(
|
|
146
|
+
`This profile declares ${deployable.length} deployable targets (${deployable.join(', ')}). ` +
|
|
147
|
+
'Name the one you mean with --target.',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { target: deployable[0] ?? CONVENTIONAL_DEPLOY_TARGET, source: 'deployable' };
|
|
152
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { isRemoteWorkspace, readWorkspaceFile, workspaceFiles } from './files.ts';
|
|
3
|
+
import { parseConfig, type LoadedConfig } from './load.ts';
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { parse as parseYaml } from 'yaml';
|
|
7
|
+
import { ConfigError } from './load.ts';
|
|
8
|
+
import { workspaceSchema, type Config, type WorkspaceConfig } from './schema.ts';
|
|
9
|
+
import { askedTarget } from './targets.ts';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Workspace and profile resolution.
|
|
13
|
+
*
|
|
14
|
+
* A workspace is a directory holding one or more profiles:
|
|
15
|
+
*
|
|
16
|
+
* lanes-link.yaml workspace settings: contract, default_profile
|
|
17
|
+
* profiles/
|
|
18
|
+
* personal.yaml
|
|
19
|
+
* work.yaml
|
|
20
|
+
* data/ local state per profile, gitignored
|
|
21
|
+
*
|
|
22
|
+
* There is deliberately no sticky `lanes link use` that persists a *hidden*
|
|
23
|
+
* current selection. Persisted context state is the standard way operators run
|
|
24
|
+
* destructive commands against the wrong target, and the version of it that
|
|
25
|
+
* bites is the dotfile nothing prints.
|
|
26
|
+
*
|
|
27
|
+
* So both ways of not retyping a flag are visible ones.
|
|
28
|
+
* `export LANES_LINK_PROFILE=work` and `export LANES_LINK_TARGET=cloud` live in
|
|
29
|
+
* the shell, where `env` shows them; `default_profile` and
|
|
30
|
+
* `instance.default_target` live in files the operator reads and `check`
|
|
31
|
+
* validates, written by `lanes link profile default` and `lanes link target
|
|
32
|
+
* use`. Every command prints which of the four it landed on and where that came
|
|
33
|
+
* from — see `announce`.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export const WORKSPACE_FILE = 'lanes-link.yaml';
|
|
37
|
+
|
|
38
|
+
export interface Resolution {
|
|
39
|
+
readonly workspaceRoot: string;
|
|
40
|
+
readonly profile: string;
|
|
41
|
+
readonly profilePath: string;
|
|
42
|
+
readonly target: string;
|
|
43
|
+
/** Where the value came from, so every command can print how it got here. */
|
|
44
|
+
readonly profileSource: 'flag' | 'environment' | 'workspace-default';
|
|
45
|
+
/**
|
|
46
|
+
* `deployable` is `deploy` choosing the only target it could have meant.
|
|
47
|
+
*
|
|
48
|
+
* It is its own source rather than reusing `config-default` because it is a
|
|
49
|
+
* different claim: the config default is what *commands* run against, and for
|
|
50
|
+
* every other command that is the local target. Printing "config-default"
|
|
51
|
+
* beside a target the config does not default to would be a lie on the one
|
|
52
|
+
* line that exists to say how the command got here.
|
|
53
|
+
*
|
|
54
|
+
* `environment` is `LANES_LINK_TARGET`, and it earns its own name for the
|
|
55
|
+
* same reason: a target chosen by a variable exported in another terminal an
|
|
56
|
+
* hour ago is the one an operator is most likely to be surprised by, and the
|
|
57
|
+
* fix — `unset` — is not the fix for a config default. `deploy` never
|
|
58
|
+
* produces it; `resolveDeployTarget` says why.
|
|
59
|
+
*/
|
|
60
|
+
readonly targetSource: 'flag' | 'environment' | 'config-default' | 'deployable';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ResolveOptions {
|
|
64
|
+
readonly profileFlag?: string | undefined;
|
|
65
|
+
readonly targetFlag?: string | undefined;
|
|
66
|
+
readonly cwd?: string;
|
|
67
|
+
readonly env?: Record<string, string | undefined>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* `LANES_LINK_HOME`, else the nearest ancestor containing `lanes-link.yaml`,
|
|
72
|
+
* else `~/.lanes-link`.
|
|
73
|
+
*/
|
|
74
|
+
export function resolveWorkspaceRoot(options: ResolveOptions = {}): string {
|
|
75
|
+
const env = options.env ?? (process.env as Record<string, string | undefined>);
|
|
76
|
+
const explicit = env['LANES_LINK_HOME'];
|
|
77
|
+
// A bucket URL is already absolute; `resolve` would turn `gs://b/p` into a
|
|
78
|
+
// path under the current directory and the failure would arrive much later,
|
|
79
|
+
// as a missing file rather than as a mangled root.
|
|
80
|
+
if (explicit) return isRemoteWorkspace(explicit) ? explicit.replace(/\/$/, '') : resolve(explicit);
|
|
81
|
+
|
|
82
|
+
let directory = resolve(options.cwd ?? process.cwd());
|
|
83
|
+
for (;;) {
|
|
84
|
+
// `existsSync`, not `Bun.file(path).size`: a missing file reports size 0,
|
|
85
|
+
// so a `>= 0` check would call every candidate a workspace and stop at the
|
|
86
|
+
// first directory it looked at.
|
|
87
|
+
if (existsSync(join(directory, WORKSPACE_FILE))) return directory;
|
|
88
|
+
const parent = dirname(directory);
|
|
89
|
+
if (parent === directory) break;
|
|
90
|
+
directory = parent;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return join(homedir(), '.lanes-link');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Where Lanes Link itself is installed — the directory holding `package.json`,
|
|
98
|
+
* and with it `skills/` and `docs/`.
|
|
99
|
+
*
|
|
100
|
+
* Not the workspace: this is the code, not the operator's data. Found by
|
|
101
|
+
* walking up rather than by counting `..` segments, because the count is a
|
|
102
|
+
* function of where the calling file sits and a file that moves one level takes
|
|
103
|
+
* a silently wrong path with it. Both callers had already been through that
|
|
104
|
+
* once.
|
|
105
|
+
*/
|
|
106
|
+
export function installRoot(from: string): string {
|
|
107
|
+
let directory = resolve(from);
|
|
108
|
+
for (;;) {
|
|
109
|
+
if (existsSync(join(directory, 'package.json'))) return directory;
|
|
110
|
+
const parent = dirname(directory);
|
|
111
|
+
if (parent === directory) {
|
|
112
|
+
throw new Error(`No package.json in any directory above ${from}`);
|
|
113
|
+
}
|
|
114
|
+
directory = parent;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Where a profile's config lives, for display and for filesystem callers.
|
|
120
|
+
*
|
|
121
|
+
* `join` would collapse the `//` in a bucket URL, so a remote root composes by
|
|
122
|
+
* hand. Loading goes through `loadProfileConfig` rather than this — what a
|
|
123
|
+
* command prints and what it reads stopped being the same string when the
|
|
124
|
+
* workspace stopped being a directory.
|
|
125
|
+
*/
|
|
126
|
+
export function profilePath(workspaceRoot: string, profile: string): string {
|
|
127
|
+
const key = `profiles/${profile}.yaml`;
|
|
128
|
+
return isRemoteWorkspace(workspaceRoot) ? `${workspaceRoot}/${key}` : join(workspaceRoot, key);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A profile's config, however this workspace is stored. */
|
|
132
|
+
export async function loadProfileConfig(
|
|
133
|
+
workspaceRoot: string,
|
|
134
|
+
profile: string,
|
|
135
|
+
): Promise<LoadedConfig> {
|
|
136
|
+
const key = `profiles/${profile}.yaml`;
|
|
137
|
+
const text = await readWorkspaceFile(workspaceFiles(workspaceRoot), key);
|
|
138
|
+
const shown = profilePath(workspaceRoot, profile);
|
|
139
|
+
|
|
140
|
+
if (text === null) throw new ConfigError(`${shown}: no such config file`);
|
|
141
|
+
return parseConfig(text, shown);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function readWorkspace(workspaceRoot: string): Promise<WorkspaceConfig | null> {
|
|
145
|
+
const path = join(workspaceRoot, WORKSPACE_FILE);
|
|
146
|
+
const text = await readWorkspaceFile(workspaceFiles(workspaceRoot), WORKSPACE_FILE);
|
|
147
|
+
if (text === null) return null;
|
|
148
|
+
|
|
149
|
+
const parsed = workspaceSchema.safeParse(parseYaml(text));
|
|
150
|
+
if (!parsed.success) {
|
|
151
|
+
throw new ConfigError(
|
|
152
|
+
`${path}:\n${parsed.error.issues.map((i) => ` ${i.path.join('.')}: ${i.message}`).join('\n')}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return parsed.data;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function listProfiles(workspaceRoot: string): Promise<string[]> {
|
|
159
|
+
try {
|
|
160
|
+
const entries = await workspaceFiles(workspaceRoot).list('profiles/');
|
|
161
|
+
return entries
|
|
162
|
+
.map((entry) => entry.key.slice('profiles/'.length))
|
|
163
|
+
.filter((name) => name.endsWith('.yaml') && !name.endsWith('.example.yaml'))
|
|
164
|
+
// Direct children only: a nested directory under `profiles/` is not a
|
|
165
|
+
// profile, and a bucket listing is flat so it would otherwise look like one.
|
|
166
|
+
.filter((name) => !name.includes('/'))
|
|
167
|
+
.map((name) => name.slice(0, -'.yaml'.length))
|
|
168
|
+
.sort();
|
|
169
|
+
} catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Resolve which profile and target a command acts on.
|
|
176
|
+
*
|
|
177
|
+
* Order: `--profile`, then `LANES_LINK_PROFILE`, then the workspace's
|
|
178
|
+
* `default_profile`, then an error that lists what is available — never a
|
|
179
|
+
* silent pick, because the wrong guess here operates on the wrong accounts.
|
|
180
|
+
*/
|
|
181
|
+
export async function resolveSelection(options: ResolveOptions = {}): Promise<Resolution> {
|
|
182
|
+
const env = options.env ?? (process.env as Record<string, string | undefined>);
|
|
183
|
+
const workspaceRoot = resolveWorkspaceRoot(options);
|
|
184
|
+
|
|
185
|
+
let profile: string | undefined;
|
|
186
|
+
let profileSource: Resolution['profileSource'] = 'workspace-default';
|
|
187
|
+
|
|
188
|
+
if (options.profileFlag) {
|
|
189
|
+
profile = options.profileFlag;
|
|
190
|
+
profileSource = 'flag';
|
|
191
|
+
} else if (env['LANES_LINK_PROFILE']) {
|
|
192
|
+
profile = env['LANES_LINK_PROFILE'];
|
|
193
|
+
profileSource = 'environment';
|
|
194
|
+
} else {
|
|
195
|
+
profile = (await readWorkspace(workspaceRoot))?.default_profile;
|
|
196
|
+
profileSource = 'workspace-default';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!profile) {
|
|
200
|
+
const available = await listProfiles(workspaceRoot);
|
|
201
|
+
throw new ConfigError(
|
|
202
|
+
`No profile selected in workspace ${workspaceRoot}.\n` +
|
|
203
|
+
(available.length > 0
|
|
204
|
+
? `Available: ${available.join(', ')}\n` +
|
|
205
|
+
`Pass --profile <name>, set LANES_LINK_PROFILE, or set default_profile in ${WORKSPACE_FILE}.`
|
|
206
|
+
: `No profiles exist yet. Create one with: lanes link profile add <name> --default`),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Provisional: the config has not been read yet, so `instance.default_target`
|
|
211
|
+
// is not available to fall back to. `resolveTarget` settles it, from the same
|
|
212
|
+
// helper, so the two cannot disagree about what beats what.
|
|
213
|
+
const asked = askedTarget(options.targetFlag, env);
|
|
214
|
+
|
|
215
|
+
const path = profilePath(workspaceRoot, profile);
|
|
216
|
+
if (!(await workspaceFiles(workspaceRoot).has(`profiles/${profile}.yaml`))) {
|
|
217
|
+
const available = await listProfiles(workspaceRoot);
|
|
218
|
+
throw new ConfigError(
|
|
219
|
+
`Profile "${profile}" does not exist (looked for ${path}).\n` +
|
|
220
|
+
(available.length > 0 ? `Available: ${available.join(', ')}` : 'No profiles exist yet.'),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
workspaceRoot,
|
|
226
|
+
profile,
|
|
227
|
+
profilePath: path,
|
|
228
|
+
target: asked.target ?? '',
|
|
229
|
+
profileSource,
|
|
230
|
+
targetSource: asked.source ?? 'config-default',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Resolve a target-relative path against the workspace root.
|
|
236
|
+
*
|
|
237
|
+
* Only meaningful for the adapters that take a filesystem path — `file`
|
|
238
|
+
* credentials, the `filesystem` blob store. A workspace in a bucket selects
|
|
239
|
+
* none of those, so reaching here with a remote root means a target declared a
|
|
240
|
+
* local adapter against a remote workspace, and saying so beats handing a
|
|
241
|
+
* `gs://…/data/x` string to `Bun.file`.
|
|
242
|
+
*/
|
|
243
|
+
export function workspacePath(workspaceRoot: string, path: string): string {
|
|
244
|
+
if (isRemoteWorkspace(workspaceRoot)) {
|
|
245
|
+
throw new ConfigError(
|
|
246
|
+
`This target wants a filesystem path ("${path}"), but the workspace is ${workspaceRoot}. ` +
|
|
247
|
+
'A workspace in a bucket can only use adapters that address it as one: ' +
|
|
248
|
+
'credentials `gcp-secret-manager`, storage `gcs` or `s3`, vault `secret` or `blob`.',
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return isAbsolute(path) ? path : resolve(workspaceRoot, path);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The line every command prints before acting, read-only commands included.
|
|
256
|
+
*
|
|
257
|
+
* This is the primary guard against operating on the wrong instance, and it
|
|
258
|
+
* costs one line.
|
|
259
|
+
*/
|
|
260
|
+
export function describeSelection(resolution: Resolution): string {
|
|
261
|
+
return `profile: ${resolution.profile} (${resolution.profileSource}) target: ${resolution.target} (${resolution.targetSource})`;
|
|
262
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom providers — the ones an operator writes.
|
|
3
|
+
*
|
|
4
|
+
* A YAML manifest in `~/.lanes-link/data/<profile>/providers.d/`, validated by
|
|
5
|
+
* exactly the schema the built-ins are validated by, registered into exactly
|
|
6
|
+
* the same registry. A service nobody has integrated is a file, not a pull
|
|
7
|
+
* request someone waits on.
|
|
8
|
+
*
|
|
9
|
+
* That equivalence is the scalability claim of the manifest design, and it is
|
|
10
|
+
* only worth anything if it is literally true — so nothing here is a reduced
|
|
11
|
+
* form of a built-in. `template.ts` scaffolds one starting point per
|
|
12
|
+
* connectivity type; `load.ts` reads and validates them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
loadProfileProviders,
|
|
17
|
+
parseManifest,
|
|
18
|
+
parseManifestFile,
|
|
19
|
+
type LoadedManifest,
|
|
20
|
+
} from './load.ts';
|
|
21
|
+
export { manifestTemplate } from './template.ts';
|