@jondotsoy/envs 0.1.5 → 0.1.7
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 +42 -15
- package/envs.js +163 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ Run the commands inside the repo or any of its worktrees. Inside a worktree, `.e
|
|
|
42
42
|
| `envs init` | Creates `.envs/`, `.envs/.gitignore` (containing `*`) and an empty `.envs/values.yml`. Does not overwrite existing files. |
|
|
43
43
|
| `envs pull` | Reads the `.env` of the main repo and of each worktree and writes it to `values.yml`. Variables that are no longer in a `.env` are removed from `values.yml` for that worktree. |
|
|
44
44
|
| `envs push` | Writes `values.yml` to the `.env` of each worktree. Updates existing variables in place, appends new ones at the end, and keeps comments and variables that are not in `values.yml`. |
|
|
45
|
+
| `envs use [profile]` | Sets the profile of the current worktree (`uses.<worktree>`) and pushes that worktree. Without arguments it lists the profile of each worktree. The profile must already exist in `values.yml`. |
|
|
45
46
|
| `envs lint` | Checks security and prints a warning for each problem. Exits with code 1 if there is any (useful in CI). See below. |
|
|
46
47
|
| `envs help` | Shows the help (also `--help`, `-h`, or no command). |
|
|
47
48
|
|
|
@@ -87,33 +88,59 @@ It never prints values, only the variable and its location (`envs.DB_PASSWORD.ma
|
|
|
87
88
|
| `executable-files` | `values.yml` or a `.env` has the execute permission (use `chmod -x`). |
|
|
88
89
|
| `writable-files` | `values.yml` or a `.env` is writable by the group or other users (use `chmod go-w`). |
|
|
89
90
|
| `weak-secret` | Sensitive variable (`PASSWORD`, `TOKEN`, `SECRET`, `API_KEY`…) that is empty or has a typical value (`changeme`, `admin`…). |
|
|
90
|
-
| `shared-secret` | Sensitive variable in `defaults`, which is written to all worktrees. |
|
|
91
|
+
| `shared-secret` | Sensitive variable in `defaults`, which is written to all worktrees using that profile. |
|
|
92
|
+
| `unknown-profile` | `uses` names a profile that is not in `profiles`. |
|
|
91
93
|
| `secret-pattern` | Value with a known secret format (AWS, GitHub, Slack, `sk-…`, private key). |
|
|
92
94
|
| `url-credentials`, `insecure-url` | Remote URL with an embedded password, or using `http://`, `ws://` or `ftp://`. |
|
|
93
95
|
|
|
94
96
|
## `values.yml` format
|
|
95
97
|
|
|
96
98
|
```yaml
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
99
|
+
uses:
|
|
100
|
+
main: local
|
|
101
|
+
feature-x: dev
|
|
102
|
+
profiles:
|
|
103
|
+
default:
|
|
104
|
+
defaults:
|
|
105
|
+
LOG_LEVEL: info
|
|
106
|
+
envs:
|
|
107
|
+
PORT:
|
|
108
|
+
main: 3000
|
|
109
|
+
feature-x: 3001
|
|
110
|
+
local:
|
|
111
|
+
defaults:
|
|
112
|
+
DATABASE_URL: postgres://localhost/app
|
|
113
|
+
envs: {}
|
|
114
|
+
dev:
|
|
115
|
+
defaults:
|
|
116
|
+
DATABASE_URL: postgres://dev.internal/app
|
|
117
|
+
envs:
|
|
118
|
+
PORT:
|
|
119
|
+
feature-x: 4001
|
|
107
120
|
```
|
|
108
121
|
|
|
109
|
-
-
|
|
110
|
-
- `
|
|
122
|
+
- Every profile is written with both `defaults` and `envs` (empty when it has nothing); a profile is never `null`.
|
|
123
|
+
- `profiles.<profile>.defaults`: default value of each variable for the worktrees that use the profile.
|
|
124
|
+
- `profiles.<profile>.envs.<VARIABLE>.<worktree>`: value of the variable in that worktree. It overrides the profile's `defaults`.
|
|
125
|
+
- `uses.<worktree>`: profile of that worktree. A worktree that is not listed uses `default`; `pull` adds it as `default`.
|
|
111
126
|
- Values can be strings, numbers or booleans (`PORT: 3000`, `DEBUG: true`); `push` writes them to the `.env` as text (`PORT=3000`, `DEBUG=true`). `pull` converts `true`/`false` and canonical numbers (`3000`, `0.5`) to booleans and numbers; anything else (`007`, `1e3`) stays a string.
|
|
112
127
|
|
|
113
|
-
|
|
128
|
+
A worktree receives, from lowest to highest precedence: `default` defaults, `default` envs, its profile's defaults, its profile's envs. With the `default` profile only the first two apply.
|
|
129
|
+
|
|
130
|
+
The `.env` files are the source of truth: `pull` (and `edit`) rewrites `values.yml` from them, keeping only `uses` and `profiles`.
|
|
131
|
+
|
|
132
|
+
### Creating a profile
|
|
133
|
+
|
|
134
|
+
Write a profile that does not exist yet in `uses.<worktree>` while running `envs edit`: when you close the editor it is created as a copy of `default` (and the worktree receives it). A profile you add by hand under `profiles`, with values or empty, keeps what you wrote (nothing is copied from `default`) and is completed with an empty `defaults`/`envs` if one is missing. `envs push` and `envs use` do not create profiles: they fail with an unknown profile error. `envs lint` reports it as `unknown-profile`.
|
|
135
|
+
|
|
136
|
+
### Previous format
|
|
137
|
+
|
|
138
|
+
Files with `defaults` and `envs` at the root keep working: they are read as `profiles.default.defaults` and `profiles.default.envs`, and the next `envs edit` or `envs pull` rewrites the file in the new format.
|
|
139
|
+
|
|
140
|
+
The schema (JSON Schema) is at [`schema/values.schema.json`](schema/values.schema.json). It is also published at <https://jondotsoy.github.io/envs/schema.json>. `init`, `pull` and `edit` write this line at the top of `values.yml`, so VS Code validates the file with the YAML extension:
|
|
114
141
|
|
|
115
142
|
```yaml
|
|
116
|
-
# yaml-language-server: $schema
|
|
143
|
+
# yaml-language-server: $schema=https://jondotsoy.github.io/envs/schema.json
|
|
117
144
|
```
|
|
118
145
|
|
|
119
146
|
## Development
|
package/envs.js
CHANGED
|
@@ -40,18 +40,24 @@ function isSensitiveKey(key) {
|
|
|
40
40
|
}
|
|
41
41
|
function entries({ values }) {
|
|
42
42
|
const result = [];
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
for (const [
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
43
|
+
const collect = (prefix, section) => {
|
|
44
|
+
for (const [key, value] of Object.entries(section?.defaults ?? {})) {
|
|
45
|
+
result.push({ key, text: String(value), location: `${prefix}defaults.${key}`, shared: true });
|
|
46
|
+
}
|
|
47
|
+
for (const [key, byWorktree] of Object.entries(section?.envs ?? {})) {
|
|
48
|
+
for (const [worktree, value] of Object.entries(byWorktree ?? {})) {
|
|
49
|
+
result.push({
|
|
50
|
+
key,
|
|
51
|
+
text: String(value),
|
|
52
|
+
location: `${prefix}envs.${key}.${worktree}`,
|
|
53
|
+
shared: false
|
|
54
|
+
});
|
|
55
|
+
}
|
|
54
56
|
}
|
|
57
|
+
};
|
|
58
|
+
collect("", values);
|
|
59
|
+
for (const [name, profile] of Object.entries(values.profiles ?? {})) {
|
|
60
|
+
collect(`profiles.${name}.`, profile);
|
|
55
61
|
}
|
|
56
62
|
return result;
|
|
57
63
|
}
|
|
@@ -180,7 +186,7 @@ var rules = [
|
|
|
180
186
|
},
|
|
181
187
|
{
|
|
182
188
|
id: "shared-secret",
|
|
183
|
-
description: "A sensitive variable is in defaults, so it goes to every worktree.",
|
|
189
|
+
description: "A sensitive variable is in defaults, so it goes to every worktree using that profile.",
|
|
184
190
|
check: (ctx) => entries(ctx).filter((e) => e.shared && isSensitiveKey(e.key)).map((e) => ({
|
|
185
191
|
location: e.location,
|
|
186
192
|
message: `${e.key} looks sensitive but is written to every worktree; set it per worktree under envs instead.`
|
|
@@ -215,6 +221,14 @@ var rules = [
|
|
|
215
221
|
location: e.location,
|
|
216
222
|
message: `${e.key} uses ${parseUrl(e.text).scheme}:// instead of an encrypted protocol.`
|
|
217
223
|
}))
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: "unknown-profile",
|
|
227
|
+
description: "A worktree uses a profile that is not defined.",
|
|
228
|
+
check: ({ values }) => Object.entries(values.uses ?? {}).filter(([, profile]) => profile !== "default" && !Object.hasOwn(values.profiles ?? {}, profile)).map(([worktree, profile]) => ({
|
|
229
|
+
location: `uses.${worktree}`,
|
|
230
|
+
message: `Profile "${profile}" is not defined under profiles; \`envs edit\` creates it from default.`
|
|
231
|
+
}))
|
|
218
232
|
}
|
|
219
233
|
];
|
|
220
234
|
|
|
@@ -227,6 +241,7 @@ Commands:
|
|
|
227
241
|
push Write defaults and .envs/values.yml into the .env of each worktree
|
|
228
242
|
lint Warn about insecure values and unprotected secret files
|
|
229
243
|
edit Pull, open .envs/values.yml in VS Code (code -w), then push
|
|
244
|
+
use Select the profile of the current worktree (envs use <profile>), or list them
|
|
230
245
|
help Show this help message
|
|
231
246
|
`;
|
|
232
247
|
var colorsDisabled = () => Boolean(process.env.NO_COLOR);
|
|
@@ -247,6 +262,72 @@ var defaultContext = () => ({
|
|
|
247
262
|
return await proc.exited;
|
|
248
263
|
}
|
|
249
264
|
});
|
|
265
|
+
var DEFAULT_PROFILE = "default";
|
|
266
|
+
var hasProfile = (values, name) => Object.hasOwn(values.profiles, name);
|
|
267
|
+
function normalizeValues(raw) {
|
|
268
|
+
const profiles = {};
|
|
269
|
+
const add = (name, profile) => {
|
|
270
|
+
const target = profiles[name] ??= { defaults: {}, envs: {} };
|
|
271
|
+
Object.assign(target.defaults, profile?.defaults);
|
|
272
|
+
for (const [key, byWorktree] of Object.entries(profile?.envs ?? {})) {
|
|
273
|
+
Object.assign(target.envs[key] ??= {}, byWorktree);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
add(DEFAULT_PROFILE, { defaults: raw.defaults, envs: raw.envs });
|
|
277
|
+
for (const [name, profile] of Object.entries(raw.profiles ?? {})) {
|
|
278
|
+
add(name, profile);
|
|
279
|
+
}
|
|
280
|
+
return { uses: { ...raw.uses }, profiles };
|
|
281
|
+
}
|
|
282
|
+
function materializeProfiles(values) {
|
|
283
|
+
const created = [];
|
|
284
|
+
for (const name of Object.values(values.uses).map(String)) {
|
|
285
|
+
if (hasProfile(values, name))
|
|
286
|
+
continue;
|
|
287
|
+
values.profiles[name] = structuredClone(values.profiles[DEFAULT_PROFILE]);
|
|
288
|
+
created.push(name);
|
|
289
|
+
}
|
|
290
|
+
return created;
|
|
291
|
+
}
|
|
292
|
+
function resolveEntries(values, worktree, profile, includeProfileEnvs = true) {
|
|
293
|
+
const entries = {};
|
|
294
|
+
for (const name of new Set([DEFAULT_PROFILE, profile])) {
|
|
295
|
+
const layer = values.profiles[name];
|
|
296
|
+
for (const [key, value] of Object.entries(layer.defaults)) {
|
|
297
|
+
entries[key] = String(value);
|
|
298
|
+
}
|
|
299
|
+
if (name === profile && !includeProfileEnvs)
|
|
300
|
+
continue;
|
|
301
|
+
for (const [key, byWorktree] of Object.entries(layer.envs)) {
|
|
302
|
+
if (byWorktree && worktree in byWorktree) {
|
|
303
|
+
entries[key] = String(byWorktree[worktree]);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return entries;
|
|
308
|
+
}
|
|
309
|
+
var hasCompleteProfiles = (raw) => Object.values(raw.profiles ?? {}).every((profile) => typeof profile?.defaults === "object" && profile.defaults !== null && typeof profile.envs === "object" && profile.envs !== null);
|
|
310
|
+
function stringifyValues(values) {
|
|
311
|
+
const profiles = Object.fromEntries(Object.entries(values.profiles).map(([name, { defaults, envs }]) => {
|
|
312
|
+
const kept = Object.fromEntries(Object.entries(envs).filter(([, byWorktree]) => Object.keys(byWorktree).length > 0));
|
|
313
|
+
return [name, { defaults, envs: kept }];
|
|
314
|
+
}));
|
|
315
|
+
const out = Object.keys(values.uses).length > 0 ? { uses: values.uses, profiles } : { profiles };
|
|
316
|
+
return SCHEMA_COMMENT + Bun.YAML.stringify(out, null, 2);
|
|
317
|
+
}
|
|
318
|
+
var SCHEMA_COMMENT = `# yaml-language-server: $schema=https://jondotsoy.github.io/envs/schema.json
|
|
319
|
+
`;
|
|
320
|
+
function checkProfiles(ctx, values, worktrees) {
|
|
321
|
+
let ok = true;
|
|
322
|
+
for (const { name } of worktrees) {
|
|
323
|
+
const profile = values.uses[name] ?? DEFAULT_PROFILE;
|
|
324
|
+
if (hasProfile(values, profile))
|
|
325
|
+
continue;
|
|
326
|
+
ok = false;
|
|
327
|
+
ctx.error(`Unknown profile "${profile}" for worktree ${name}. Available: ${Object.keys(values.profiles).join(", ")}. Run \`envs edit\` to create it.`);
|
|
328
|
+
}
|
|
329
|
+
return ok;
|
|
330
|
+
}
|
|
250
331
|
async function findRoot(cwd) {
|
|
251
332
|
const result = await $`git rev-parse --path-format=absolute --git-common-dir`.cwd(cwd).nothrow().quiet();
|
|
252
333
|
if (result.exitCode !== 0)
|
|
@@ -336,7 +417,7 @@ async function readValues(ctx) {
|
|
|
336
417
|
async function init(ctx) {
|
|
337
418
|
const root = await findRoot(ctx.cwd);
|
|
338
419
|
const files = { ".envs/.gitignore": `*
|
|
339
|
-
`, ".envs/values.yml":
|
|
420
|
+
`, ".envs/values.yml": SCHEMA_COMMENT };
|
|
340
421
|
await mkdir(join(root, ".envs"), { recursive: true });
|
|
341
422
|
for (const [name, content] of Object.entries(files)) {
|
|
342
423
|
const path = join(root, name);
|
|
@@ -349,23 +430,21 @@ async function init(ctx) {
|
|
|
349
430
|
}
|
|
350
431
|
return 0;
|
|
351
432
|
}
|
|
352
|
-
async function push(ctx) {
|
|
433
|
+
async function push(ctx, only) {
|
|
353
434
|
const read = await readValues(ctx);
|
|
354
435
|
if (!read)
|
|
355
436
|
return 1;
|
|
356
|
-
const
|
|
437
|
+
const values = normalizeValues(read.values);
|
|
438
|
+
const worktrees = (await listWorktrees(ctx.cwd)).filter(({ name }) => only === undefined || name === only);
|
|
439
|
+
if (!checkProfiles(ctx, values, worktrees))
|
|
440
|
+
return 1;
|
|
357
441
|
const changes = new Map;
|
|
358
|
-
for (const { name, path } of
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
entries[key] = String(value);
|
|
362
|
-
}
|
|
363
|
-
for (const [key, byWorktree] of Object.entries(values.envs ?? {})) {
|
|
364
|
-
if (name in byWorktree)
|
|
365
|
-
entries[key] = String(byWorktree[name]);
|
|
366
|
-
}
|
|
442
|
+
for (const { name, path } of worktrees) {
|
|
443
|
+
const profile = values.uses[name] ?? DEFAULT_PROFILE;
|
|
444
|
+
const entries = resolveEntries(values, name, profile);
|
|
367
445
|
if (Object.keys(entries).length === 0)
|
|
368
446
|
continue;
|
|
447
|
+
const label = profile === DEFAULT_PROFILE ? name : `${name} [${profile}]`;
|
|
369
448
|
const dotenv = Bun.file(join(path, ".env"));
|
|
370
449
|
const current = await dotenv.exists() ? await dotenv.text() : "";
|
|
371
450
|
const existing = parseDotenv(current);
|
|
@@ -374,7 +453,7 @@ async function push(ctx) {
|
|
|
374
453
|
continue;
|
|
375
454
|
const shown = isMaskedKey(key) ? MASK : formatDotenvValue(value);
|
|
376
455
|
const line = `${key}=${shown}`;
|
|
377
|
-
changes.set(line, [...changes.get(line) ?? [],
|
|
456
|
+
changes.set(line, [...changes.get(line) ?? [], label]);
|
|
378
457
|
}
|
|
379
458
|
await Bun.write(dotenv, updateDotenv(current, entries));
|
|
380
459
|
}
|
|
@@ -387,12 +466,13 @@ async function pull(ctx) {
|
|
|
387
466
|
const read = await readValues(ctx);
|
|
388
467
|
if (!read)
|
|
389
468
|
return 1;
|
|
390
|
-
const
|
|
391
|
-
const
|
|
392
|
-
|
|
469
|
+
const values = normalizeValues(read.values);
|
|
470
|
+
const worktrees = await listWorktrees(ctx.cwd);
|
|
471
|
+
materializeProfiles(values);
|
|
393
472
|
const pulledNames = [];
|
|
394
473
|
const pulledKeys = new Set;
|
|
395
|
-
for (const { name, path } of
|
|
474
|
+
for (const { name, path } of worktrees) {
|
|
475
|
+
const profile = values.uses[name] ??= DEFAULT_PROFILE;
|
|
396
476
|
const dotenv = Bun.file(join(path, ".env"));
|
|
397
477
|
if (!await dotenv.exists())
|
|
398
478
|
continue;
|
|
@@ -400,14 +480,15 @@ async function pull(ctx) {
|
|
|
400
480
|
pulledNames.push(name);
|
|
401
481
|
for (const key of Object.keys(parsed))
|
|
402
482
|
pulledKeys.add(key);
|
|
483
|
+
const { envs } = values.profiles[profile];
|
|
403
484
|
for (const [key, byWorktree] of Object.entries(envs)) {
|
|
404
485
|
if (!(key in parsed))
|
|
405
486
|
delete byWorktree[name];
|
|
406
487
|
}
|
|
488
|
+
const base = resolveEntries(values, name, profile, false);
|
|
407
489
|
for (const [key, value] of Object.entries(parsed)) {
|
|
408
490
|
const parsedValue = parseValue(value);
|
|
409
|
-
|
|
410
|
-
if (fromDefaults) {
|
|
491
|
+
if (key in base && base[key] === String(parsedValue)) {
|
|
411
492
|
if (envs[key])
|
|
412
493
|
delete envs[key][name];
|
|
413
494
|
} else {
|
|
@@ -415,16 +496,40 @@ async function pull(ctx) {
|
|
|
415
496
|
}
|
|
416
497
|
}
|
|
417
498
|
}
|
|
418
|
-
|
|
419
|
-
if (Object.keys(byWorktree).length === 0)
|
|
420
|
-
delete envs[key];
|
|
421
|
-
}
|
|
422
|
-
await Bun.write(valuesPath, Bun.YAML.stringify(values, null, 2));
|
|
499
|
+
await Bun.write(read.path, stringifyValues(values));
|
|
423
500
|
if (pulledNames.length > 0) {
|
|
424
501
|
ctx.log(green(`\u2193 pulling ${pulledNames.join(", ")} - ${pulledKeys.size} variables`));
|
|
425
502
|
}
|
|
426
503
|
return 0;
|
|
427
504
|
}
|
|
505
|
+
async function use(ctx, profile) {
|
|
506
|
+
const read = await readValues(ctx);
|
|
507
|
+
if (!read)
|
|
508
|
+
return 1;
|
|
509
|
+
const values = normalizeValues(read.values);
|
|
510
|
+
const worktrees = await listWorktrees(ctx.cwd);
|
|
511
|
+
if (profile === undefined) {
|
|
512
|
+
for (const { name } of worktrees) {
|
|
513
|
+
ctx.log(`${name}: ${values.uses[name] ?? DEFAULT_PROFILE}`);
|
|
514
|
+
}
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
if (!hasProfile(values, profile)) {
|
|
518
|
+
ctx.error(`Unknown profile "${profile}". Available: ${Object.keys(values.profiles).join(", ")}. Add it in \`envs edit\` first.`);
|
|
519
|
+
return 1;
|
|
520
|
+
}
|
|
521
|
+
const top = await $`git rev-parse --show-toplevel`.cwd(ctx.cwd).nothrow().quiet();
|
|
522
|
+
const current = worktrees.find(({ path }) => path === top.stdout.toString().trim());
|
|
523
|
+
if (!current) {
|
|
524
|
+
ctx.error("Run `envs use` inside a git worktree.");
|
|
525
|
+
return 1;
|
|
526
|
+
}
|
|
527
|
+
for (const { name } of worktrees)
|
|
528
|
+
values.uses[name] ??= DEFAULT_PROFILE;
|
|
529
|
+
values.uses[current.name] = profile;
|
|
530
|
+
await Bun.write(read.path, stringifyValues(values));
|
|
531
|
+
return await push(ctx, current.name);
|
|
532
|
+
}
|
|
428
533
|
async function isIgnored(dir, file) {
|
|
429
534
|
const result = await $`git check-ignore -q ${file}`.cwd(dir).nothrow().quiet();
|
|
430
535
|
return result.exitCode === 0;
|
|
@@ -477,11 +582,31 @@ async function edit(ctx) {
|
|
|
477
582
|
const pulled = await pull(ctx);
|
|
478
583
|
if (pulled !== 0)
|
|
479
584
|
return pulled;
|
|
585
|
+
const original = await Bun.file(valuesPath).text();
|
|
586
|
+
await Bun.write(valuesPath, EDIT_NOTICE + original);
|
|
480
587
|
const code = await ctx.openEditor(valuesPath);
|
|
588
|
+
const edited = await Bun.file(valuesPath).text();
|
|
589
|
+
if (edited.startsWith(EDIT_NOTICE)) {
|
|
590
|
+
await Bun.write(valuesPath, edited.slice(EDIT_NOTICE.length));
|
|
591
|
+
}
|
|
481
592
|
if (code !== 0)
|
|
482
593
|
return code;
|
|
594
|
+
const raw = Bun.YAML.parse(await Bun.file(valuesPath).text()) ?? {};
|
|
595
|
+
const values = normalizeValues(raw);
|
|
596
|
+
const created = materializeProfiles(values);
|
|
597
|
+
if (created.length > 0 || !hasCompleteProfiles(raw)) {
|
|
598
|
+
await Bun.write(valuesPath, stringifyValues(values));
|
|
599
|
+
}
|
|
600
|
+
if (created.length > 0) {
|
|
601
|
+
ctx.log(green(`+ created ${created.join(", ")} from ${DEFAULT_PROFILE}`));
|
|
602
|
+
}
|
|
483
603
|
return await push(ctx);
|
|
484
604
|
}
|
|
605
|
+
var EDIT_NOTICE = `# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
606
|
+
# Close this file when your changes are ready, then they are pushed.
|
|
607
|
+
# To cancel the edit, press Ctrl+C in the terminal and then close this file.
|
|
608
|
+
# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
609
|
+
`;
|
|
485
610
|
async function run(args, ctx = defaultContext()) {
|
|
486
611
|
const [command = "help"] = args;
|
|
487
612
|
switch (command) {
|
|
@@ -500,6 +625,8 @@ async function run(args, ctx = defaultContext()) {
|
|
|
500
625
|
return await lint(ctx);
|
|
501
626
|
case "edit":
|
|
502
627
|
return await edit(ctx);
|
|
628
|
+
case "use":
|
|
629
|
+
return await use(ctx, args[1]);
|
|
503
630
|
default:
|
|
504
631
|
ctx.error(`Unknown command: ${command}
|
|
505
632
|
|