neon 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -5
- package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
- package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
- package/dist/_chunks/credentials-MYdHdKah.js +188 -0
- package/dist/_chunks/env-NbA61JR3.js +585 -0
- package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
- package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
- package/dist/_chunks/profiles-Ir29rqns.js +217 -0
- package/dist/_chunks/psql-DWH-kc69.js +2169 -0
- package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
- package/dist/analytics.js +163 -207
- package/dist/api.js +815 -758
- package/dist/auth.js +121 -141
- package/dist/auth_context.js +39 -53
- package/dist/cli.js +4 -7
- package/dist/commands/api.js +220 -250
- package/dist/commands/api_keys.js +251 -314
- package/dist/commands/auth.js +283 -328
- package/dist/commands/bootstrap.js +372 -437
- package/dist/commands/branches.js +304 -455
- package/dist/commands/bucket.js +374 -514
- package/dist/commands/checkout.js +213 -298
- package/dist/commands/config.js +573 -690
- package/dist/commands/connection_string.js +137 -165
- package/dist/commands/data_api.js +238 -260
- package/dist/commands/databases.js +67 -76
- package/dist/commands/deploy.js +31 -25
- package/dist/commands/dev.js +639 -719
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +255 -305
- package/dist/commands/functions.js +275 -355
- package/dist/commands/index.js +70 -65
- package/dist/commands/init.js +84 -119
- package/dist/commands/inspect.js +55 -55
- package/dist/commands/ip_allow.js +88 -120
- package/dist/commands/link.js +874 -1019
- package/dist/commands/logs.js +291 -0
- package/dist/commands/neon_auth.js +725 -933
- package/dist/commands/operations.js +34 -25
- package/dist/commands/orgs.js +28 -18
- package/dist/commands/profile.js +615 -846
- package/dist/commands/projects.js +313 -373
- package/dist/commands/psql.js +60 -58
- package/dist/commands/roles.js +55 -58
- package/dist/commands/schema_diff.js +87 -131
- package/dist/commands/set_context.js +34 -26
- package/dist/commands/snapshots.js +288 -413
- package/dist/commands/status.js +41 -37
- package/dist/commands/user.js +21 -10
- package/dist/commands/vpc_endpoints.js +85 -113
- package/dist/config.js +7 -6
- package/dist/config_format.js +50 -66
- package/dist/config_template.js +128 -157
- package/dist/context.js +183 -235
- package/dist/current_branch_fast_path.js +40 -49
- package/dist/dev/env.js +2 -446
- package/dist/dev/functions.js +54 -68
- package/dist/dev/inputs.js +46 -58
- package/dist/dev/runtime.js +135 -164
- package/dist/dev/websocket.js +766 -959
- package/dist/env.js +27 -33
- package/dist/env_file.js +118 -132
- package/dist/env_services.js +2 -51
- package/dist/errors.js +57 -68
- package/dist/functions_api.js +45 -43
- package/dist/help.js +189 -140
- package/dist/index.js +182 -257
- package/dist/init/agents.js +137 -118
- package/dist/init/auth.js +58 -68
- package/dist/init/bootstrap.js +325 -396
- package/dist/init/build_config.js +4 -2
- package/dist/init/detect_agent.js +56 -101
- package/dist/init/editors.js +35 -52
- package/dist/init/enrich_output.js +51 -66
- package/dist/init/extension.js +134 -171
- package/dist/init/inspect.js +179 -266
- package/dist/init/interactive.js +510 -622
- package/dist/init/neonctl.js +117 -168
- package/dist/init/orchestrate.js +157 -173
- package/dist/init/phases/auth.js +188 -202
- package/dist/init/phases/cleanup.js +23 -23
- package/dist/init/phases/db.js +251 -277
- package/dist/init/phases/getting_started.js +213 -223
- package/dist/init/phases/mcp.js +174 -224
- package/dist/init/phases/migrations.js +247 -248
- package/dist/init/phases/neon_auth.js +114 -133
- package/dist/init/phases/setup.js +546 -703
- package/dist/init/phases/skills.js +75 -86
- package/dist/init/phases/status.js +72 -67
- package/dist/init/resolve_context.js +102 -99
- package/dist/init/route_command.js +91 -98
- package/dist/init/skills.js +174 -218
- package/dist/init/vsix.js +77 -99
- package/dist/log.js +17 -16
- package/dist/neon_services.js +104 -129
- package/dist/parameters.gen.js +481 -471
- package/dist/pkg.js +17 -19
- package/dist/profile_keys.js +44 -47
- package/dist/psql/cli.js +44 -47
- package/dist/psql/command/cmd_cond.js +231 -406
- package/dist/psql/command/cmd_connect.js +557 -764
- package/dist/psql/command/cmd_copy.js +728 -984
- package/dist/psql/command/cmd_describe.js +1499 -1688
- package/dist/psql/command/cmd_format.js +733 -905
- package/dist/psql/command/cmd_io.js +2 -2193
- package/dist/psql/command/cmd_lo.js +297 -359
- package/dist/psql/command/cmd_meta.js +727 -878
- package/dist/psql/command/cmd_misc.js +138 -172
- package/dist/psql/command/cmd_pipeline.js +2 -1148
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +260 -325
- package/dist/psql/command/inputQueue.js +35 -33
- package/dist/psql/command/shared.js +49 -63
- package/dist/psql/complete/filenames.js +90 -133
- package/dist/psql/complete/index.js +59 -97
- package/dist/psql/complete/matcher.js +236 -300
- package/dist/psql/complete/psqlVars.js +218 -223
- package/dist/psql/complete/queries.js +159 -177
- package/dist/psql/complete/rules.js +1493 -2299
- package/dist/psql/core/common.js +2 -1253
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1303
- package/dist/psql/core/prompt.js +391 -408
- package/dist/psql/core/settings.js +429 -644
- package/dist/psql/core/sqlHelp.js +480 -554
- package/dist/psql/core/startup.js +2 -846
- package/dist/psql/core/syncVars.js +67 -110
- package/dist/psql/core/variables.js +156 -278
- package/dist/psql/describe/formatters.js +884 -1285
- package/dist/psql/describe/processNamePattern.js +173 -260
- package/dist/psql/describe/queries.js +1368 -2403
- package/dist/psql/describe/versionGate.js +32 -41
- package/dist/psql/index.js +2 -2030
- package/dist/psql/io/history.js +232 -271
- package/dist/psql/io/input.js +103 -108
- package/dist/psql/io/lineEditor/buffer.js +238 -319
- package/dist/psql/io/lineEditor/complete.js +135 -213
- package/dist/psql/io/lineEditor/filename.js +139 -148
- package/dist/psql/io/lineEditor/index.js +653 -870
- package/dist/psql/io/lineEditor/keymap.js +544 -702
- package/dist/psql/io/lineEditor/vt100.js +294 -341
- package/dist/psql/io/pgpass.js +158 -187
- package/dist/psql/io/pgservice.js +146 -183
- package/dist/psql/io/psqlrc.js +328 -403
- package/dist/psql/print/aligned.js +1020 -1683
- package/dist/psql/print/asciidoc.js +180 -214
- package/dist/psql/print/crosstab.js +281 -442
- package/dist/psql/print/csv.js +48 -70
- package/dist/psql/print/html.js +195 -226
- package/dist/psql/print/json.js +75 -88
- package/dist/psql/print/latex.js +291 -364
- package/dist/psql/print/pager.js +171 -242
- package/dist/psql/print/troff.js +194 -226
- package/dist/psql/print/unaligned.js +69 -95
- package/dist/psql/print/units.js +167 -169
- package/dist/psql/scanner/slash.js +428 -483
- package/dist/psql/scanner/sql.js +445 -889
- package/dist/psql/scanner/stringutils.js +309 -379
- package/dist/psql/types/index.js +8 -7
- package/dist/psql/types/scanner.js +25 -22
- package/dist/psql/wire/connection.js +2042 -2803
- package/dist/psql/wire/copy.js +84 -100
- package/dist/psql/wire/notify.js +39 -59
- package/dist/psql/wire/pipeline.js +305 -518
- package/dist/psql/wire/protocol.js +349 -417
- package/dist/psql/wire/sasl.js +180 -265
- package/dist/psql/wire/tls.js +400 -561
- package/dist/storage_api.js +115 -129
- package/dist/test_utils/fixtures.js +94 -113
- package/dist/test_utils/oauth_server.js +10 -7
- package/dist/test_utils/project_dir.js +33 -0
- package/dist/utils/ai_gateway_notice.js +131 -162
- package/dist/utils/api_enums.js +21 -28
- package/dist/utils/auth.js +10 -4
- package/dist/utils/branch_notice.js +20 -19
- package/dist/utils/branch_picker.js +83 -89
- package/dist/utils/cli_name.js +15 -12
- package/dist/utils/compute_units.js +20 -27
- package/dist/utils/config_diff.js +127 -158
- package/dist/utils/enrichers.js +95 -148
- package/dist/utils/esbuild.js +130 -189
- package/dist/utils/flags.js +35 -47
- package/dist/utils/formats.js +8 -15
- package/dist/utils/git_diff.js +69 -80
- package/dist/utils/inspect_db.js +101 -143
- package/dist/utils/inspect_queries.js +179 -142
- package/dist/utils/middlewares.js +39 -45
- package/dist/utils/openapi.js +87 -99
- package/dist/utils/package_manager.js +312 -110
- package/dist/utils/point_in_time.js +49 -53
- package/dist/utils/psql.js +89 -106
- package/dist/utils/service_picker.js +55 -58
- package/dist/utils/string.js +5 -5
- package/dist/utils/ui.js +38 -55
- package/dist/utils/write_sync.js +26 -35
- package/dist/utils/zip.js +4 -3
- package/dist/writer.js +67 -87
- package/package.json +11 -6
- package/dist/_shared/auth_selection.js +0 -86
- package/dist/_shared/credentials.js +0 -209
- package/dist/_shared/env-core/env.js +0 -558
- package/dist/_shared/env-core/reuse-secrets.js +0 -223
- package/dist/_shared/paths.js +0 -148
- package/dist/_shared/profiles.js +0 -276
- package/dist/_shared/secure_file.js +0 -43
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import { credentialScopesSatisfied, } from "@neon/config/v1";
|
|
2
|
-
import { createApiFromOptions, credentialEnvKeys, credentialName, fetchEnvKeys, NEON_ENV_VAR_KEYS, policyEnvKeys, previewCredentialScopes, resolveBranchPolicy, toEntries, } from "./env.js";
|
|
3
|
-
/**
|
|
4
|
-
* Resolve a branch's env while keeping one-time secrets the caller already holds.
|
|
5
|
-
*
|
|
6
|
-
* {@link fetchEnvKeys} — and the public `fetchEnv` — only ever *fetch*. The Neon API returns a
|
|
7
|
-
* credential's `api_token` / `s3_secret_access_key` exactly once, at mint time, so "fetching"
|
|
8
|
-
* them means minting a new credential; a plain `fetchEnv` on every `neon dev` start or `env
|
|
9
|
-
* pull` would leave a live credential behind each time. This is the wrapper that avoids that:
|
|
10
|
-
* it looks at what the caller already has, decides what is still usable, and asks `fetchEnv`
|
|
11
|
-
* for only the rest.
|
|
12
|
-
*
|
|
13
|
-
* The check is a real verification, not a presence test. A persisted secret is kept only when
|
|
14
|
-
* it names a credential that still exists on this branch, is not revoked or expired, and
|
|
15
|
-
* carries every scope the policy needs. A `.env.example` placeholder, a credential revoked in
|
|
16
|
-
* the console, one copied in from another branch, or one predating a newly-enabled feature all
|
|
17
|
-
* fail that check and get replaced.
|
|
18
|
-
*
|
|
19
|
-
* None of this needs local bookkeeping, because the secrets carry their own credential id:
|
|
20
|
-
* `AWS_ACCESS_KEY_ID` **is** the credential's `tokenId` (the storage gateway authenticates
|
|
21
|
-
* against the full id), and the AI Gateway token is minted as `nt_live_<tokenIdShort>_<secret>`,
|
|
22
|
-
* where `tokenIdShort` is what the credentials list reports. The env source being replaced is
|
|
23
|
-
* the record of what the last call issued.
|
|
24
|
-
*
|
|
25
|
-
* ```ts
|
|
26
|
-
* import { fetchEnvReusingSecrets } from "../_shared/env-core/reuse-secrets.js";
|
|
27
|
-
*
|
|
28
|
-
* const { vars, credential } = await fetchEnvReusingSecrets(config, {
|
|
29
|
-
* projectId,
|
|
30
|
-
* branch: "main",
|
|
31
|
-
* env: { ...process.env, ...readEnvFile(".env") },
|
|
32
|
-
* });
|
|
33
|
-
* if (credential.issued) console.log(`new values for ${credential.keys.join(", ")}`);
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
|
-
export async function fetchEnvReusingSecrets(config, options) {
|
|
37
|
-
const { env: source = process.env, revokeSuperseded = true, ...fetchOptions } = options;
|
|
38
|
-
const api = options.api ?? createApiFromOptions(options);
|
|
39
|
-
const { branch, desired } = await resolveBranchPolicy(config, options, api);
|
|
40
|
-
const storageEnabled = (desired.preview?.buckets.length ?? 0) > 0;
|
|
41
|
-
const gatewayEnabled = desired.preview?.aiGatewayEnabled ?? false;
|
|
42
|
-
const secretKeys = credentialEnvKeys({
|
|
43
|
-
storage: storageEnabled,
|
|
44
|
-
aiGateway: gatewayEnabled,
|
|
45
|
-
});
|
|
46
|
-
// Nothing credential-backed on this branch, so there is nothing to preserve and no
|
|
47
|
-
// credential to spend: fetch everything and skip the credentials endpoint entirely.
|
|
48
|
-
if (secretKeys.length === 0) {
|
|
49
|
-
const fetched = await fetchEnvKeys(config, fetchOptions, null);
|
|
50
|
-
return {
|
|
51
|
-
vars: preferPersisted(toEntries(fetched), source),
|
|
52
|
-
credential: {
|
|
53
|
-
issued: false,
|
|
54
|
-
keys: [],
|
|
55
|
-
revoked: [],
|
|
56
|
-
superseded: [],
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
const persisted = readPersistedSecrets(source);
|
|
61
|
-
const complete = (!storageEnabled ||
|
|
62
|
-
Boolean(persisted.accessKeyId && persisted.secretAccessKey)) &&
|
|
63
|
-
(!gatewayEnabled || Boolean(persisted.apiToken));
|
|
64
|
-
// Look the persisted secrets up whenever there are any — not only when they're complete.
|
|
65
|
-
// An incomplete set still names the credential a newly-enabled feature is about to
|
|
66
|
-
// supersede (a storage-only credential on a branch that just gained the AI Gateway), and
|
|
67
|
-
// that one should be revoked rather than left live.
|
|
68
|
-
const named = persisted.accessKeyId !== "" || persisted.apiToken !== ""
|
|
69
|
-
? namedCredentials(await api.listCredentials(options.projectId, branch.id), persisted)
|
|
70
|
-
: { storage: null, gateway: null };
|
|
71
|
-
const reusable = complete
|
|
72
|
-
? reusableCredential(named, { storageEnabled, gatewayEnabled })
|
|
73
|
-
: null;
|
|
74
|
-
const scopes = previewCredentialScopes(desired.preview);
|
|
75
|
-
const keep = reusable !== null && credentialScopesSatisfied(reusable.scopes, scopes);
|
|
76
|
-
// Ask for everything the policy produces, minus the secrets we're keeping — which is what
|
|
77
|
-
// stops `fetchEnv` from minting a credential it doesn't need.
|
|
78
|
-
const allKeys = policyEnvKeys(desired);
|
|
79
|
-
const fetchKeys = keep
|
|
80
|
-
? allKeys.filter((key) => !secretKeys.includes(key))
|
|
81
|
-
: allKeys;
|
|
82
|
-
const fetched = await fetchEnvKeys(config,
|
|
83
|
-
// Pass the resolved id so `fetchEnv` targets the same branch this call verified against,
|
|
84
|
-
// even if `options.branch` was a name that has since been reused.
|
|
85
|
-
{ ...fetchOptions, branchId: branch.id, api }, fetchKeys);
|
|
86
|
-
const vars = preferPersisted(toEntries(fetched), source);
|
|
87
|
-
if (keep) {
|
|
88
|
-
for (const key of secretKeys) {
|
|
89
|
-
const value = source[key];
|
|
90
|
-
if (value !== undefined)
|
|
91
|
-
vars[key] = value;
|
|
92
|
-
}
|
|
93
|
-
return {
|
|
94
|
-
vars,
|
|
95
|
-
credential: {
|
|
96
|
-
issued: false,
|
|
97
|
-
keys: secretKeys,
|
|
98
|
-
revoked: [],
|
|
99
|
-
superseded: [],
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
// A replacement was minted, so revoke what it supersedes: the credentials the old secrets
|
|
104
|
-
// named, minus any this tool did not issue. Their secrets lived nowhere but the env source
|
|
105
|
-
// this call replaces, so revoking them strands nothing — and it keeps a branch from
|
|
106
|
-
// accumulating a live credential per call. Everything else on the branch is left alone: it
|
|
107
|
-
// may belong to a teammate, another checkout, or a deployed function, and nothing
|
|
108
|
-
// observable distinguishes those from an orphan of our own.
|
|
109
|
-
//
|
|
110
|
-
// Revoked *after* the fetch, so a failed fetch leaves the caller's existing secrets working.
|
|
111
|
-
const ours = new Set();
|
|
112
|
-
for (const meta of [named.storage, named.gateway]) {
|
|
113
|
-
if (meta !== null &&
|
|
114
|
-
meta.principalType === "user" &&
|
|
115
|
-
meta.name === credentialName(branch.name)) {
|
|
116
|
-
ours.add(meta.tokenId);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (revokeSuperseded) {
|
|
120
|
-
for (const tokenId of ours) {
|
|
121
|
-
await api.revokeCredential(options.projectId, branch.id, tokenId);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return {
|
|
125
|
-
vars,
|
|
126
|
-
credential: {
|
|
127
|
-
issued: true,
|
|
128
|
-
keys: secretKeys,
|
|
129
|
-
revoked: revokeSuperseded ? [...ours] : [],
|
|
130
|
-
superseded: revokeSuperseded ? [] : [...ours],
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
/** Read the branch credential's secrets out of an env source. */
|
|
135
|
-
function readPersistedSecrets(source) {
|
|
136
|
-
const storage = NEON_ENV_VAR_KEYS.storage;
|
|
137
|
-
const gateway = NEON_ENV_VAR_KEYS.aiGateway;
|
|
138
|
-
return {
|
|
139
|
-
accessKeyId: source[storage.accessKeyId] ?? "",
|
|
140
|
-
secretAccessKey: source[storage.secretAccessKey] ?? "",
|
|
141
|
-
apiToken: source[gateway.apiKey] ?? "",
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Keep a persisted value rather than overwriting it with an empty fetched one.
|
|
146
|
-
*
|
|
147
|
-
* Neon Auth's `base_url` is the case that needs this: integrations created before the API
|
|
148
|
-
* returned it answer with an empty string, and the persisted copy is the only one left. An
|
|
149
|
-
* empty fetched value never carries more information than a non-empty persisted one, so
|
|
150
|
-
* preferring the latter is safe for every var — and it keeps a pull from blanking a working
|
|
151
|
-
* line in someone's `.env`.
|
|
152
|
-
*/
|
|
153
|
-
function preferPersisted(vars, source) {
|
|
154
|
-
const out = { ...vars };
|
|
155
|
-
for (const [key, value] of Object.entries(out)) {
|
|
156
|
-
if (value !== "")
|
|
157
|
-
continue;
|
|
158
|
-
const persisted = source[key];
|
|
159
|
-
if (persisted !== undefined && persisted !== "")
|
|
160
|
-
out[key] = persisted;
|
|
161
|
-
}
|
|
162
|
-
return out;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* The credential id embedded in an AI Gateway token. The API mints them as
|
|
166
|
-
* `nt_live_<tokenIdShort>_<secret>`, and `tokenIdShort` is the public identifier the credentials
|
|
167
|
-
* list reports — so a persisted token names the credential that issued it. Returns `null` for
|
|
168
|
-
* anything not in that shape (a `.env.example` placeholder, a hand-typed value), which callers
|
|
169
|
-
* treat as unverifiable.
|
|
170
|
-
*/
|
|
171
|
-
function gatewayTokenIdShort(apiToken) {
|
|
172
|
-
return /^nt_live_([^_]+)_.+$/.exec(apiToken)?.[1] ?? null;
|
|
173
|
-
}
|
|
174
|
-
/** Whether an issued credential can still be used: not revoked, not past its expiry. */
|
|
175
|
-
function isLiveCredential(meta, now) {
|
|
176
|
-
if (meta.revokedAt !== undefined)
|
|
177
|
-
return false;
|
|
178
|
-
if (meta.expiresAt === undefined)
|
|
179
|
-
return true;
|
|
180
|
-
const expiresAt = Date.parse(meta.expiresAt);
|
|
181
|
-
return Number.isNaN(expiresAt) || expiresAt > now;
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* The live credentials the persisted secrets name — at most one per half. A half that names
|
|
185
|
-
* nothing contributes nothing, which is what a placeholder, a credential revoked in the
|
|
186
|
-
* console, and one copied in from another branch all look like from here.
|
|
187
|
-
*/
|
|
188
|
-
function namedCredentials(live, persisted) {
|
|
189
|
-
const usable = live.filter((meta) => isLiveCredential(meta, Date.now()));
|
|
190
|
-
const shortId = persisted.apiToken
|
|
191
|
-
? gatewayTokenIdShort(persisted.apiToken)
|
|
192
|
-
: null;
|
|
193
|
-
return {
|
|
194
|
-
storage: persisted.accessKeyId
|
|
195
|
-
? (usable.find((meta) => meta.tokenId === persisted.accessKeyId) ??
|
|
196
|
-
null)
|
|
197
|
-
: null,
|
|
198
|
-
gateway: shortId
|
|
199
|
-
? (usable.find((meta) => meta.tokenIdShort === shortId) ?? null)
|
|
200
|
-
: null,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* The credential the persisted secrets can be *reused* as, or `null`.
|
|
205
|
-
*
|
|
206
|
-
* Strict on purpose: every half the policy enables has to name a live credential, and when both
|
|
207
|
-
* features are enabled they must name the *same* one — they share a single credential, so
|
|
208
|
-
* halves that disagree came from two different calls and neither can be trusted.
|
|
209
|
-
*/
|
|
210
|
-
function reusableCredential(named, enabled) {
|
|
211
|
-
if (enabled.storageEnabled && enabled.gatewayEnabled) {
|
|
212
|
-
return named.storage &&
|
|
213
|
-
named.gateway &&
|
|
214
|
-
named.storage.tokenId === named.gateway.tokenId
|
|
215
|
-
? named.storage
|
|
216
|
-
: null;
|
|
217
|
-
}
|
|
218
|
-
if (enabled.storageEnabled)
|
|
219
|
-
return named.storage;
|
|
220
|
-
if (enabled.gatewayEnabled)
|
|
221
|
-
return named.gateway;
|
|
222
|
-
return null;
|
|
223
|
-
}
|
package/dist/_shared/paths.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* # Where the Neon CLIs keep their files on disk
|
|
3
|
-
*
|
|
4
|
-
**Deliberately impure.** It reads environment variables and touches the filesystem, which
|
|
5
|
-
* `@neon/config` — the package this used to be a subpath of — must never do from its root
|
|
6
|
-
* export. It lives here instead of there precisely so that a policy-facing package does not
|
|
7
|
-
* carry implementor-only code.
|
|
8
|
-
*
|
|
9
|
-
* It exists because three separate readers each grew their own answer to "where is the
|
|
10
|
-
* config directory", and all three disagreed: `packages/cli` honoured `XDG_CONFIG_HOME` but
|
|
11
|
-
* not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and the init
|
|
12
|
-
* flow hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote
|
|
13
|
-
* credentials somewhere the other two never looked.
|
|
14
|
-
*
|
|
15
|
-
* ## The directory
|
|
16
|
-
*
|
|
17
|
-
* `neon` is the current name; `neonctl` is the legacy one, kept readable forever. Resolution,
|
|
18
|
-
* each entry winning over the next:
|
|
19
|
-
*
|
|
20
|
-
* 1. An explicit directory (a `--config-dir` flag) — **exact**, no legacy fallback.
|
|
21
|
-
* 2. `NEON_CONFIG_DIR` — exact.
|
|
22
|
-
* 3. `NEONCTL_CONFIG_DIR` (legacy name) — exact.
|
|
23
|
-
* 4. `$XDG_CONFIG_HOME/neon`, else `<home>/.config/neon`.
|
|
24
|
-
*
|
|
25
|
-
* An explicitly chosen directory is never paired with a fallback: `--config-dir /tmp/ci` that
|
|
26
|
-
* quietly read `~/.config/neonctl` would defeat the point of passing it.
|
|
27
|
-
*
|
|
28
|
-
* ## The files
|
|
29
|
-
*
|
|
30
|
-
* {@link resolveConfigFile} answers "which path should I use for this file", and it is the
|
|
31
|
-
* same answer for reading and writing:
|
|
32
|
-
*
|
|
33
|
-
* - Present in `neon/` → use it.
|
|
34
|
-
* - Present only in `neonctl/` → **use it there, in place.** An existing credentials file is
|
|
35
|
-
* never copied or moved, so nothing is left behind to go stale and no other tool starts
|
|
36
|
-
* reading an abandoned token.
|
|
37
|
-
* - Present in neither → the new location. New files only ever appear under `neon/`.
|
|
38
|
-
*/
|
|
39
|
-
import { existsSync } from "node:fs";
|
|
40
|
-
import { join, resolve } from "node:path";
|
|
41
|
-
/** Current directory name. New files are created here. */
|
|
42
|
-
export const CONFIG_DIR_NAME = "neon";
|
|
43
|
-
/** Legacy directory name, read forever so existing installs keep working untouched. */
|
|
44
|
-
export const LEGACY_CONFIG_DIR_NAME = "neonctl";
|
|
45
|
-
/** Where files are created. See the module docs for the precedence. */
|
|
46
|
-
export function configDir(options = {}) {
|
|
47
|
-
const explicit = explicitDir(options);
|
|
48
|
-
if (explicit)
|
|
49
|
-
return explicit;
|
|
50
|
-
return join(configHome(options.env ?? process.env), CONFIG_DIR_NAME);
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* The legacy directory, or `undefined` when the location was chosen explicitly (in which
|
|
54
|
-
* case there is no legacy counterpart to fall back to).
|
|
55
|
-
*/
|
|
56
|
-
export function legacyConfigDir(options = {}) {
|
|
57
|
-
if (explicitDir(options))
|
|
58
|
-
return undefined;
|
|
59
|
-
return join(configHome(options.env ?? process.env), LEGACY_CONFIG_DIR_NAME);
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Resolve one file inside the config directory. Prefers the current location, falls back to
|
|
63
|
-
* an existing legacy file **in place**, and otherwise points at the current location so new
|
|
64
|
-
* files are created there.
|
|
65
|
-
*/
|
|
66
|
-
export function resolveConfigFile(fileName, options = {}) {
|
|
67
|
-
const dir = configDir(options);
|
|
68
|
-
const current = resolve(dir, fileName);
|
|
69
|
-
if (existsSync(current))
|
|
70
|
-
return { path: current, dir, isLegacy: false, exists: true };
|
|
71
|
-
const legacyDir = legacyConfigDir(options);
|
|
72
|
-
if (legacyDir) {
|
|
73
|
-
const legacy = resolve(legacyDir, fileName);
|
|
74
|
-
if (existsSync(legacy))
|
|
75
|
-
return {
|
|
76
|
-
path: legacy,
|
|
77
|
-
dir: legacyDir,
|
|
78
|
-
isLegacy: true,
|
|
79
|
-
exists: true,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
return { path: current, dir, isLegacy: false, exists: false };
|
|
83
|
-
}
|
|
84
|
-
/** `$XDG_CONFIG_HOME`, else `<home>/.config`. Falls back to a relative `.config` with no home. */
|
|
85
|
-
function configHome(env) {
|
|
86
|
-
const xdg = nonEmpty(env.XDG_CONFIG_HOME);
|
|
87
|
-
if (xdg)
|
|
88
|
-
return xdg;
|
|
89
|
-
const home = nonEmpty(env.HOME) ?? nonEmpty(env.USERPROFILE);
|
|
90
|
-
return home ? join(home, ".config") : ".config";
|
|
91
|
-
}
|
|
92
|
-
function explicitDir(options) {
|
|
93
|
-
const env = options.env ?? process.env;
|
|
94
|
-
return (nonEmpty(options.dir) ??
|
|
95
|
-
nonEmpty(env.NEON_CONFIG_DIR) ??
|
|
96
|
-
nonEmpty(env.NEONCTL_CONFIG_DIR));
|
|
97
|
-
}
|
|
98
|
-
function nonEmpty(value) {
|
|
99
|
-
if (typeof value !== "string")
|
|
100
|
-
return undefined;
|
|
101
|
-
const trimmed = value.trim();
|
|
102
|
-
return trimmed === "" ? undefined : trimmed;
|
|
103
|
-
}
|
|
104
|
-
export const CREDENTIALS_FILE = "credentials.json";
|
|
105
|
-
/**
|
|
106
|
-
* Default for `--config-dir`: `$XDG_CONFIG_HOME/neon`, else `~/.config/neon`.
|
|
107
|
-
*
|
|
108
|
-
* The directory was called `neonctl` until the CLI was renamed. An existing one is still read —
|
|
109
|
-
* see {@link credentialsPath} — but it is never written to, moved, or deleted.
|
|
110
|
-
*/
|
|
111
|
-
export const defaultDir = configDir();
|
|
112
|
-
/**
|
|
113
|
-
* Where this invocation's `credentials.json` lives.
|
|
114
|
-
*
|
|
115
|
-
* When `--config-dir` was left at its default, an existing file in the legacy `neonctl`
|
|
116
|
-
* directory is used **in place**: an install that predates the rename keeps working, and its
|
|
117
|
-
* credentials are never duplicated into a second location where one copy could go stale while
|
|
118
|
-
* another tool still reads it.
|
|
119
|
-
*
|
|
120
|
-
* A `--config-dir` the user actually passed is used exactly as given. Falling back out of an
|
|
121
|
-
* explicitly chosen directory would defeat the reason for choosing it — a CI run pointed at a
|
|
122
|
-
* scratch directory must never pick up a developer's real credentials.
|
|
123
|
-
*/
|
|
124
|
-
export const credentialsPath = (dir) => resolveConfigFile(CREDENTIALS_FILE, dir === defaultDir ? {} : { dir }).path;
|
|
125
|
-
/**
|
|
126
|
-
* Whether a credentials file is one the CLI created, rather than a path a profile adopted.
|
|
127
|
-
*
|
|
128
|
-
* Anything that deletes a credential has to ask this first. A profile entry may point anywhere —
|
|
129
|
-
* that is what makes adopting an existing directory a one-line edit — and a file we did not
|
|
130
|
-
* create is not ours to remove.
|
|
131
|
-
*/
|
|
132
|
-
export const isInsideConfigDir = (configDirectory, file) => `${resolve(file)}/`.startsWith(`${resolve(configDirectory)}/`);
|
|
133
|
-
/**
|
|
134
|
-
* Whether a credentials file is one the CLI owns, counting the legacy `neonctl` directory.
|
|
135
|
-
*
|
|
136
|
-
* {@link credentialsPath} deliberately reads an existing legacy file in place rather than
|
|
137
|
-
* migrating it, so for a default config directory that file is ours even though it sits outside
|
|
138
|
-
* `neon/`. Judging ownership on the current directory alone would call an install that predates
|
|
139
|
-
* the rename "adopted".
|
|
140
|
-
*/
|
|
141
|
-
export const isOwnedCredentialPath = (configDirectory, file) => {
|
|
142
|
-
if (isInsideConfigDir(configDirectory, file))
|
|
143
|
-
return true;
|
|
144
|
-
if (configDirectory !== defaultDir)
|
|
145
|
-
return false;
|
|
146
|
-
const legacy = legacyConfigDir();
|
|
147
|
-
return legacy !== undefined && isInsideConfigDir(legacy, file);
|
|
148
|
-
};
|
package/dist/_shared/profiles.js
DELETED
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* # Profiles — several Neon accounts in one config directory
|
|
3
|
-
*
|
|
4
|
-
* A profile is **a pointer to a credentials file**. Nothing more. That constraint is what
|
|
5
|
-
* keeps the feature small: there is no mirror, no per-profile directory tree, no persistent
|
|
6
|
-
* "active profile" state to fall out of sync, and no migration.
|
|
7
|
-
*
|
|
8
|
-
* ```
|
|
9
|
-
* ~/.config/neon/
|
|
10
|
-
* ├── credentials.json # this IS the DEFAULT profile, not a copy of it
|
|
11
|
-
* ├── credentials.work.json # created by `neon auth --profile work`
|
|
12
|
-
* └── profiles.json # created only once a second profile exists
|
|
13
|
-
* ```
|
|
14
|
-
*
|
|
15
|
-
* `profiles.json` maps a name to a path, and the path may point anywhere — which is what
|
|
16
|
-
* makes adopting an existing directory a one-line edit rather than an import command:
|
|
17
|
-
*
|
|
18
|
-
* ```json
|
|
19
|
-
* {
|
|
20
|
-
* "version": 1,
|
|
21
|
-
* "profiles": {
|
|
22
|
-
* "DEFAULT": { "credentials": "credentials.json" },
|
|
23
|
-
* "work": {
|
|
24
|
-
* "credentials": "../neonctl-databricks/credentials.json",
|
|
25
|
-
* "label": "someone@example.com"
|
|
26
|
-
* }
|
|
27
|
-
* }
|
|
28
|
-
* }
|
|
29
|
-
* ```
|
|
30
|
-
*
|
|
31
|
-
* ## Selection
|
|
32
|
-
*
|
|
33
|
-
* `--profile` → `NEON_PROFILE` → `DEFAULT`. Per invocation, like `AWS_PROFILE`; there is no
|
|
34
|
-
* `profile use` command, so nothing persists that could disagree with what you typed.
|
|
35
|
-
*
|
|
36
|
-
* ## Compatibility
|
|
37
|
-
*
|
|
38
|
-
* An install with no `profiles.json` is already a valid `DEFAULT`-only state: `DEFAULT`
|
|
39
|
-
* resolves to `credentials.json` in the config directory (including an existing one in the
|
|
40
|
-
* legacy `neonctl` directory — see `./paths.ts`). Nothing is created until a second
|
|
41
|
-
* profile is, and nothing is ever moved.
|
|
42
|
-
*/
|
|
43
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
44
|
-
import { isAbsolute, relative, resolve } from "node:path";
|
|
45
|
-
import { credentialsPath, defaultDir, resolveConfigFile } from "./paths.js";
|
|
46
|
-
import { writeSecretFile } from "./secure_file.js";
|
|
47
|
-
export const PROFILES_FILE = "profiles.json";
|
|
48
|
-
/** The implicit profile. Backed by plain `credentials.json`, with or without a profiles file. */
|
|
49
|
-
export const DEFAULT_PROFILE = "DEFAULT";
|
|
50
|
-
/** Profile names become part of a filename, so keep them boring. */
|
|
51
|
-
const NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
52
|
-
/** Which profile this invocation should use: `--profile` → `NEON_PROFILE` → `DEFAULT`. */
|
|
53
|
-
export const selectProfileName = (flag, env = process.env) => nonEmpty(flag) ?? nonEmpty(env.NEON_PROFILE) ?? DEFAULT_PROFILE;
|
|
54
|
-
export const assertValidProfileName = (name) => {
|
|
55
|
-
if (!NAME_PATTERN.test(name)) {
|
|
56
|
-
throw new Error(`Invalid profile name "${name}". Use letters, digits, dot, dash or underscore, starting with a letter or digit.`);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
/** Where `profiles.json` lives for this config directory (whether or not it exists yet). */
|
|
60
|
-
export const profilesFilePath = (dir) => resolveConfigFile(PROFILES_FILE, dir === defaultDir ? {} : { dir }).path;
|
|
61
|
-
/**
|
|
62
|
-
* Read and classify `profiles.json` without deciding what to do about it.
|
|
63
|
-
*
|
|
64
|
-
* Entry keys and shapes are validated here rather than at each use. A key is a profile name,
|
|
65
|
-
* and a name that `assertValidProfileName` would reject cannot have been written by this CLI —
|
|
66
|
-
* it would travel into error messages as a recovery command nobody can run, and into a
|
|
67
|
-
* `credentials.<name>.json` filename.
|
|
68
|
-
*/
|
|
69
|
-
export const inspectProfiles = (dir) => {
|
|
70
|
-
const path = profilesFilePath(dir);
|
|
71
|
-
if (!existsSync(path))
|
|
72
|
-
return { kind: "absent" };
|
|
73
|
-
const broken = (why) => ({
|
|
74
|
-
kind: "unusable",
|
|
75
|
-
reason: `${path} could not be read as a profiles file: ${why}`,
|
|
76
|
-
});
|
|
77
|
-
// Reading and parsing are separate failures with separate answers. Sharing one catch
|
|
78
|
-
// reported `EACCES` as "not valid JSON", which sends the user to edit a file that is
|
|
79
|
-
// perfectly valid and that they cannot open.
|
|
80
|
-
let contents;
|
|
81
|
-
try {
|
|
82
|
-
contents = readFileSync(path, "utf8");
|
|
83
|
-
}
|
|
84
|
-
catch (err) {
|
|
85
|
-
const code = err.code;
|
|
86
|
-
return broken(code ? `reading it failed with ${code}` : "reading it failed");
|
|
87
|
-
}
|
|
88
|
-
let parsed;
|
|
89
|
-
try {
|
|
90
|
-
parsed = JSON.parse(contents);
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
return broken("it is not valid JSON");
|
|
94
|
-
}
|
|
95
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
96
|
-
return broken("it does not contain an object");
|
|
97
|
-
const profiles = parsed.profiles;
|
|
98
|
-
if (profiles === null ||
|
|
99
|
-
typeof profiles !== "object" ||
|
|
100
|
-
Array.isArray(profiles))
|
|
101
|
-
return broken("it has no `profiles` object");
|
|
102
|
-
for (const [name, entry] of Object.entries(profiles)) {
|
|
103
|
-
if (!NAME_PATTERN.test(name))
|
|
104
|
-
return broken(`"${name}" is not a valid profile name`);
|
|
105
|
-
if (entry === null ||
|
|
106
|
-
typeof entry !== "object" ||
|
|
107
|
-
typeof entry.credentials !== "string" ||
|
|
108
|
-
entry.credentials.trim() === "") {
|
|
109
|
-
return broken(`profile "${name}" has no \`credentials\` path`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return { kind: "ok", file: { version: 1, profiles } };
|
|
113
|
-
};
|
|
114
|
-
/**
|
|
115
|
-
* Read `profiles.json`, or `null` when there is nothing usable there.
|
|
116
|
-
*
|
|
117
|
-
* A malformed file is reported through `onWarn` and treated as absent, because for a *read* the
|
|
118
|
-
* worst case is a named profile turning up missing, which is recoverable — whereas throwing
|
|
119
|
-
* would lock the user out of `neon auth` itself. Writing is the opposite: see
|
|
120
|
-
* {@link upsertProfile}, which refuses rather than rebuilding a file it cannot read.
|
|
121
|
-
*/
|
|
122
|
-
export const readProfiles = (dir,
|
|
123
|
-
/** Called with the reason a profiles file was ignored. The consumer owns how it reports. */
|
|
124
|
-
onWarn = () => { }) => {
|
|
125
|
-
const read = inspectProfiles(dir);
|
|
126
|
-
if (read.kind === "ok")
|
|
127
|
-
return read.file;
|
|
128
|
-
if (read.kind === "unusable")
|
|
129
|
-
onWarn(read.reason);
|
|
130
|
-
return null;
|
|
131
|
-
};
|
|
132
|
-
/**
|
|
133
|
-
* Refuse to act on a named profile when the file that defines it cannot be read.
|
|
134
|
-
*
|
|
135
|
-
* Call this **before** anything that writes a credential, opens a browser, or spends an API
|
|
136
|
-
* call. {@link upsertProfile} refuses too, but it runs last: by then `create` has already
|
|
137
|
-
* overwritten `credentials.<name>.json` and revoked the key it replaced, and `neon auth
|
|
138
|
-
* --profile` has already signed in over it — a refusal that arrives after the destruction it
|
|
139
|
-
* exists to prevent. The path resolution itself is the unsound part, since with the metadata
|
|
140
|
-
* unreadable the conventional filename is a guess about which account that file belongs to.
|
|
141
|
-
*
|
|
142
|
-
* `DEFAULT` is exempt: it is defined by the absence of metadata rather than by an entry, so
|
|
143
|
-
* signing in normally must keep working while a broken `profiles.json` is repaired.
|
|
144
|
-
*/
|
|
145
|
-
export const assertProfilesUsable = (dir, name) => {
|
|
146
|
-
if (name === DEFAULT_PROFILE)
|
|
147
|
-
return;
|
|
148
|
-
const read = inspectProfiles(dir);
|
|
149
|
-
if (read.kind === "unusable") {
|
|
150
|
-
throw new Error(`${read.reason}. Fix or delete the file before working with profile "${name}" — it is the only record of where each account's credentials live.`);
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
/** Resolve a profile to an absolute credentials path. Throws when a named profile is unknown. */
|
|
154
|
-
export const resolveProfile = (dir, name) => {
|
|
155
|
-
const read = inspectProfiles(dir);
|
|
156
|
-
// A broken file must not be reported as `Unknown profile "work"`. That names the wrong
|
|
157
|
-
// problem, and the user goes looking for a profile they can see in the file in front of them.
|
|
158
|
-
if (read.kind === "unusable" && name !== DEFAULT_PROFILE) {
|
|
159
|
-
throw new Error(`${read.reason}. Fix or delete the file — every named profile is defined in it.`);
|
|
160
|
-
}
|
|
161
|
-
const file = read.kind === "ok" ? read.file : null;
|
|
162
|
-
const entry = file?.profiles[name];
|
|
163
|
-
if (entry) {
|
|
164
|
-
return {
|
|
165
|
-
name,
|
|
166
|
-
credentialsPath: resolveEntryPath(dir, entry.credentials),
|
|
167
|
-
...(entry.label ? { label: entry.label } : {}),
|
|
168
|
-
...(entry.userId ? { userId: entry.userId } : {}),
|
|
169
|
-
declared: true,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
// DEFAULT works with no profiles.json at all, and keeps working when one exists but
|
|
173
|
-
// doesn't mention it — that is the pre-profiles behaviour, unchanged.
|
|
174
|
-
if (name === DEFAULT_PROFILE) {
|
|
175
|
-
return {
|
|
176
|
-
name,
|
|
177
|
-
credentialsPath: credentialsPath(dir),
|
|
178
|
-
declared: false,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
const known = file
|
|
182
|
-
? Object.keys(file.profiles).join(", ")
|
|
183
|
-
: DEFAULT_PROFILE;
|
|
184
|
-
throw new Error(`Unknown profile "${name}". Known profiles: ${known}. Create it with \`neon profile create ${name}\`.`);
|
|
185
|
-
};
|
|
186
|
-
/** Default location for a new named profile's credentials file. */
|
|
187
|
-
export const newProfileCredentialsPath = (dir, name) => resolve(dir, `credentials.${name}.json`);
|
|
188
|
-
/**
|
|
189
|
-
* Record a profile, creating `profiles.json` if this is the first named one.
|
|
190
|
-
*
|
|
191
|
-
* When the file is created, `DEFAULT` is written explicitly and pointed at wherever
|
|
192
|
-
* `credentials.json` actually is. That matters for an install predating the directory
|
|
193
|
-
* rename: `profiles.json` is created in `neon/` while the credentials are still in
|
|
194
|
-
* `neonctl/`, so `DEFAULT` is recorded as `../neonctl/credentials.json` rather than a
|
|
195
|
-
* relative name that would resolve to a file that isn't there.
|
|
196
|
-
*/
|
|
197
|
-
export const upsertProfile = (dir, name, entry) => {
|
|
198
|
-
assertValidProfileName(name);
|
|
199
|
-
const path = profilesFilePath(dir);
|
|
200
|
-
const read = inspectProfiles(dir);
|
|
201
|
-
// Refusing is the point. Treating a broken file as absent here rebuilt it from a single
|
|
202
|
-
// `DEFAULT` entry and dropped every named profile in it — silent data loss, in the file
|
|
203
|
-
// that is the only record of where each account's credentials live. The credentials
|
|
204
|
-
// themselves survive, so fixing the file by hand recovers everything.
|
|
205
|
-
if (read.kind === "unusable") {
|
|
206
|
-
throw new Error(`${read.reason}. Refusing to rewrite it, because doing so would discard the profiles it defines. Fix or delete the file, then re-run.`);
|
|
207
|
-
}
|
|
208
|
-
const file = read.kind === "ok"
|
|
209
|
-
? read.file
|
|
210
|
-
: {
|
|
211
|
-
version: 1,
|
|
212
|
-
profiles: {
|
|
213
|
-
[DEFAULT_PROFILE]: {
|
|
214
|
-
credentials: relativeToProfiles(path, credentialsPath(dir)),
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
file.profiles[name] = {
|
|
219
|
-
credentials: relativeToProfiles(path, entry.credentials),
|
|
220
|
-
...(entry.label ? { label: entry.label } : {}),
|
|
221
|
-
...(entry.userId ? { userId: entry.userId } : {}),
|
|
222
|
-
};
|
|
223
|
-
writeProfiles(path, file);
|
|
224
|
-
};
|
|
225
|
-
/** Remove an entry. Returns false when it wasn't there. */
|
|
226
|
-
export const removeProfileEntry = (dir, name) => {
|
|
227
|
-
const path = profilesFilePath(dir);
|
|
228
|
-
const file = readProfiles(dir);
|
|
229
|
-
if (!file?.profiles[name])
|
|
230
|
-
return false;
|
|
231
|
-
delete file.profiles[name];
|
|
232
|
-
writeProfiles(path, file);
|
|
233
|
-
return true;
|
|
234
|
-
};
|
|
235
|
-
/**
|
|
236
|
-
* True when only `DEFAULT` is left, so `profiles.json` no longer earns its place. Mirrors
|
|
237
|
-
* lazy creation: a single-account install has no profiles file, before or after.
|
|
238
|
-
*/
|
|
239
|
-
export const onlyDefaultRemains = (file) => {
|
|
240
|
-
const names = Object.keys(file.profiles);
|
|
241
|
-
return (names.length === 0 ||
|
|
242
|
-
(names.length === 1 && names[0] === DEFAULT_PROFILE));
|
|
243
|
-
};
|
|
244
|
-
export const listProfiles = (dir) => {
|
|
245
|
-
const read = inspectProfiles(dir);
|
|
246
|
-
// Listing is the command run to find out what is there, so a broken file is the answer
|
|
247
|
-
// rather than an obstacle. Showing only `DEFAULT` would state, as fact, that the profiles
|
|
248
|
-
// in that file do not exist.
|
|
249
|
-
if (read.kind === "unusable") {
|
|
250
|
-
throw new Error(`${read.reason}. Fix or delete the file — every named profile is defined in it.`);
|
|
251
|
-
}
|
|
252
|
-
const file = read.kind === "ok" ? read.file : null;
|
|
253
|
-
if (!file)
|
|
254
|
-
return [resolveProfile(dir, DEFAULT_PROFILE)];
|
|
255
|
-
const names = Object.keys(file.profiles);
|
|
256
|
-
if (!names.includes(DEFAULT_PROFILE))
|
|
257
|
-
names.unshift(DEFAULT_PROFILE);
|
|
258
|
-
return names.map((name) => resolveProfile(dir, name));
|
|
259
|
-
};
|
|
260
|
-
const writeProfiles = (path, file) => {
|
|
261
|
-
writeSecretFile(path, `${JSON.stringify(file, null, 2)}\n`);
|
|
262
|
-
};
|
|
263
|
-
const resolveEntryPath = (dir, entry) => isAbsolute(entry) ? entry : resolve(profilesDir(dir), entry);
|
|
264
|
-
/** `profiles.json` may sit in the legacy directory, so entries resolve against its own dir. */
|
|
265
|
-
const profilesDir = (dir) => resolve(profilesFilePath(dir), "..");
|
|
266
|
-
/** Keep entries relative when they sit near `profiles.json`; absolute paths stay absolute. */
|
|
267
|
-
const relativeToProfiles = (profilesPath, target) => {
|
|
268
|
-
const rel = relative(resolve(profilesPath, ".."), target);
|
|
269
|
-
return rel && !isAbsolute(rel) ? rel : target;
|
|
270
|
-
};
|
|
271
|
-
function nonEmpty(value) {
|
|
272
|
-
if (typeof value !== "string")
|
|
273
|
-
return undefined;
|
|
274
|
-
const trimmed = value.trim();
|
|
275
|
-
return trimmed === "" ? undefined : trimmed;
|
|
276
|
-
}
|