@thisisamank/noenv-cli 0.1.1 → 0.2.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/noenv.js +783 -328
  3. package/package.json +3 -2
package/dist/noenv.js CHANGED
@@ -1,16 +1,93 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // ../packages/config/src/names.ts
4
+ var IDENTIFIER = /^[A-Za-z0-9_-]+$/;
5
+ var ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
6
+ var REPO = /^[a-z0-9.-]+(\/[a-z0-9._~-]+)+$/;
7
+
8
+ // src/deployment.ts
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+
12
+ // src/files.ts
13
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
14
+ import { dirname } from "node:path";
15
+ async function readJsonFile(path) {
16
+ try {
17
+ return JSON.parse(await readFile(path, "utf8"));
18
+ } catch (error) {
19
+ if (error.code === "ENOENT") return null;
20
+ throw error;
21
+ }
22
+ }
23
+ async function writeJsonFile(path, value, flag = "w") {
24
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
25
+ await writeFile(path, JSON.stringify(value), { mode: 384, flag });
26
+ }
27
+ async function removeFile(path) {
28
+ await rm(path, { force: true });
29
+ }
30
+
31
+ // src/deployment.ts
32
+ function configDir(env = process.env) {
33
+ return env.NOENV_CONFIG_DIR ?? join(env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "noenv");
34
+ }
35
+ function deploymentAt(value, env = process.env, account) {
36
+ const url = URL.parse(value);
37
+ if (!url || url.protocol !== "https:" && url.protocol !== "http:") {
38
+ throw new Error(`${value} is not an http or https URL`);
39
+ }
40
+ return { url: url.origin, dir: join(configDir(env), url.host.replace(":", "_")), fetch: globalThis.fetch, account };
41
+ }
42
+ async function readAccounts(env = process.env) {
43
+ return await readJsonFile(accountsPath(env)) ?? { accounts: {} };
44
+ }
45
+ async function writeAccounts(accounts, env = process.env) {
46
+ await writeJsonFile(accountsPath(env), accounts);
47
+ }
48
+ function matchAccount(accounts, repo) {
49
+ let best;
50
+ for (const [name2, account] of Object.entries(accounts.accounts)) {
51
+ for (const prefix of account.match) {
52
+ const clean = prefix.toLowerCase().replace(/\/+$/, "");
53
+ if ((repo === clean || repo.startsWith(`${clean}/`)) && clean.length > (best?.length ?? -1)) {
54
+ best = { name: name2, length: clean.length };
55
+ }
56
+ }
57
+ }
58
+ return best?.name;
59
+ }
60
+ async function resolveDeployment(repo, env = process.env) {
61
+ const accounts = await readAccounts(env);
62
+ const name2 = env.NOENV_ACCOUNT || (repo ? matchAccount(accounts, repo) : void 0) || accounts.default;
63
+ if (name2) {
64
+ const account = accounts.accounts[name2];
65
+ if (!account) throw new Error(`there is no account named "${name2}", run \`noenv account list\``);
66
+ return deploymentAt(account.url, env, name2);
67
+ }
68
+ return env.NOENV_URL ? deploymentAt(env.NOENV_URL, env) : null;
69
+ }
70
+ function accountsPath(env) {
71
+ return join(configDir(env), "accounts.json");
72
+ }
73
+
74
+ // src/session.ts
75
+ import { spawn } from "node:child_process";
76
+ import { hostname } from "node:os";
77
+ import { join as join3 } from "node:path";
78
+
3
79
  // package.json
4
80
  var package_default = {
5
81
  name: "@thisisamank/noenv-cli",
6
- version: "0.1.1",
82
+ version: "0.2.1",
7
83
  description: "Encrypted environment variables that load when you cd into a repo. CLI for noenv.",
8
84
  type: "module",
9
85
  bin: {
10
86
  noenv: "dist/noenv.js"
11
87
  },
12
88
  files: [
13
- "dist"
89
+ "dist",
90
+ "CHANGELOG.md"
14
91
  ],
15
92
  engines: {
16
93
  node: ">=22"
@@ -87,19 +164,6 @@ function environmentPath(repo, environment) {
87
164
  return `${repoPath(repo)}/environments/${environment}`;
88
165
  }
89
166
 
90
- // src/deployment.ts
91
- import { homedir } from "node:os";
92
- import { join } from "node:path";
93
- function currentDeployment(env = process.env) {
94
- if (!env.NOENV_URL) return null;
95
- const url = URL.parse(env.NOENV_URL);
96
- if (!url || url.protocol !== "https:" && url.protocol !== "http:") {
97
- throw new Error("NOENV_URL must be an http or https URL");
98
- }
99
- const base = env.NOENV_CONFIG_DIR ?? join(env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "noenv");
100
- return { url: url.origin, dir: join(base, url.host.replace(":", "_")), fetch: globalThis.fetch };
101
- }
102
-
103
167
  // src/identity.ts
104
168
  import { join as join2 } from "node:path";
105
169
 
@@ -206,25 +270,6 @@ async function unwrapPrivateKey(wrapped, recipientPrivateKey) {
206
270
  return crypto.subtle.importKey("pkcs8", pkcs8, ECDH, true, ["deriveBits"]);
207
271
  }
208
272
 
209
- // src/files.ts
210
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
211
- import { dirname } from "node:path";
212
- async function readJsonFile(path) {
213
- try {
214
- return JSON.parse(await readFile(path, "utf8"));
215
- } catch (error) {
216
- if (error.code === "ENOENT") return null;
217
- throw error;
218
- }
219
- }
220
- async function writeJsonFile(path, value, flag = "w") {
221
- await mkdir(dirname(path), { recursive: true, mode: 448 });
222
- await writeFile(path, JSON.stringify(value), { mode: 384, flag });
223
- }
224
- async function removeFile(path) {
225
- await rm(path, { force: true });
226
- }
227
-
228
273
  // src/identity.ts
229
274
  async function loadIdentity(deployment) {
230
275
  const stored = await readJsonFile(identityPath(deployment));
@@ -256,9 +301,6 @@ function identityPath(deployment) {
256
301
  }
257
302
 
258
303
  // src/session.ts
259
- import { spawn } from "node:child_process";
260
- import { hostname } from "node:os";
261
- import { join as join3 } from "node:path";
262
304
  async function loadSession(deployment) {
263
305
  const session = await readJsonFile(sessionPath(deployment));
264
306
  if (!session) return null;
@@ -270,7 +312,7 @@ async function clearSession(deployment) {
270
312
  async function requireSession(deployment) {
271
313
  const token = await loadSession(deployment);
272
314
  if (!token) {
273
- throw new Error("you are not signed in, open a new shell or run `noenv auth`");
315
+ throw new Error(`you are not signed in to ${deployment.account ?? deployment.url}, open a new shell or run \`noenv auth${deployment.account ? ` ${deployment.account}` : ""}\``);
274
316
  }
275
317
  return token;
276
318
  }
@@ -300,6 +342,25 @@ async function startSignIn(deployment) {
300
342
  return { pending, started: true };
301
343
  }
302
344
  async function waitForSignIn(deployment, pending) {
345
+ await writeJsonFile(pollerPath(deployment), { pid: process.pid });
346
+ try {
347
+ return await poll(deployment, pending);
348
+ } finally {
349
+ const poller = await readJsonFile(pollerPath(deployment));
350
+ if (poller?.pid === process.pid) await removeFile(pollerPath(deployment));
351
+ }
352
+ }
353
+ async function pollerRunning(deployment) {
354
+ const poller = await readJsonFile(pollerPath(deployment));
355
+ if (!poller) return false;
356
+ try {
357
+ process.kill(poller.pid, 0);
358
+ return true;
359
+ } catch (error) {
360
+ return error.code === "EPERM";
361
+ }
362
+ }
363
+ async function poll(deployment, pending) {
303
364
  while (pending.expiresAt > Date.now()) {
304
365
  await sleep(pending.interval * 1e3);
305
366
  try {
@@ -311,7 +372,7 @@ async function waitForSignIn(deployment, pending) {
311
372
  await ensureIdentity(deployment, session.token);
312
373
  return true;
313
374
  } catch (error) {
314
- if (!(error instanceof ApiError) || error.message !== "authorization_pending") break;
375
+ if (!isRetryable(error)) break;
315
376
  }
316
377
  }
317
378
  await removeFile(pendingPath(deployment));
@@ -325,12 +386,19 @@ function openBrowser(url) {
325
386
  spawn(command2, [url], { detached: true, stdio: "ignore" }).on("error", () => {
326
387
  }).unref();
327
388
  }
328
- function startBackgroundPoller() {
329
- spawn(process.execPath, [...process.execArgv, process.argv[1], "auth", "--wait"], {
389
+ function startBackgroundPoller(deployment) {
390
+ spawn(process.execPath, [...process.execArgv, process.argv[1], "auth", "--wait", deployment.url], {
330
391
  detached: true,
331
392
  stdio: "ignore"
332
393
  }).unref();
333
394
  }
395
+ function isRetryable(error) {
396
+ if (!(error instanceof ApiError)) return true;
397
+ return error.message === "authorization_pending" || error.status === 429 || error.status >= 500;
398
+ }
399
+ function pollerPath(deployment) {
400
+ return join3(deployment.dir, "sign-in-poller.json");
401
+ }
334
402
  function sessionPath(deployment) {
335
403
  return join3(deployment.dir, "session.json");
336
404
  }
@@ -338,7 +406,67 @@ function pendingPath(deployment) {
338
406
  return join3(deployment.dir, "pending-sign-in.json");
339
407
  }
340
408
  function sleep(ms) {
341
- return new Promise((resolve) => setTimeout(resolve, ms));
409
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
410
+ }
411
+
412
+ // src/commands/account.ts
413
+ var USAGE = "usage: noenv account add NAME URL [REPO_PREFIX...] | list | default NAME | remove NAME";
414
+ var PREFIX = /^[a-z0-9.-]+(\/[a-z0-9._~-]+)*\/?$/;
415
+ async function accountCommand([action, ...args2]) {
416
+ const handlers = {
417
+ add: addAccount,
418
+ list: listAccounts,
419
+ default: setDefault,
420
+ remove: removeAccount
421
+ };
422
+ const handler = handlers[action ?? ""];
423
+ if (!handler) throw new Error(USAGE);
424
+ await handler(args2);
425
+ }
426
+ async function addAccount([name2, url, ...match]) {
427
+ if (!name2 || !url) throw new Error(USAGE);
428
+ if (!IDENTIFIER.test(name2)) throw new Error('the account name must contain only letters, digits, "_" or "-"');
429
+ const deployment = deploymentAt(url);
430
+ const prefixes = match.map((prefix) => prefix.toLowerCase());
431
+ const invalid = prefixes.find((prefix) => !PREFIX.test(prefix));
432
+ if (invalid) throw new Error(`${invalid} is not a repository prefix such as github.com/acme`);
433
+ const accounts = await readAccounts();
434
+ const clash = Object.entries(accounts.accounts).find(([other, account]) => other !== name2 && deploymentAt(account.url).url === deployment.url);
435
+ if (clash) throw new Error(`the account "${clash[0]}" already uses ${deployment.url}`);
436
+ accounts.accounts[name2] = { url: deployment.url, match: prefixes };
437
+ accounts.default ??= name2;
438
+ await writeAccounts(accounts);
439
+ console.log(`Saved account ${name2} (${deployment.url})${accounts.default === name2 ? " as the default" : ""}`);
440
+ if (!await loadSession(deployment)) console.log(`Run \`noenv auth ${name2}\` to sign in`);
441
+ }
442
+ async function listAccounts() {
443
+ const accounts = await readAccounts();
444
+ const entries = Object.entries(accounts.accounts);
445
+ if (entries.length === 0) {
446
+ console.log("No accounts. Add one with `noenv account add NAME URL [REPO_PREFIX...]`");
447
+ return;
448
+ }
449
+ for (const [name2, account] of entries) {
450
+ const signedIn2 = await loadSession(deploymentAt(account.url)) ? "signed in" : "signed out";
451
+ const marker = accounts.default === name2 ? "*" : " ";
452
+ const match = account.match.length > 0 ? ` ${account.match.join(", ")}` : "";
453
+ console.log(`${marker} ${name2} ${account.url} ${signedIn2}${match}`);
454
+ }
455
+ }
456
+ async function setDefault([name2]) {
457
+ const accounts = await readAccounts();
458
+ if (!name2 || !accounts.accounts[name2]) throw new Error(`there is no account named "${name2 ?? ""}"`);
459
+ accounts.default = name2;
460
+ await writeAccounts(accounts);
461
+ console.log(`${name2} is now the default account`);
462
+ }
463
+ async function removeAccount([name2]) {
464
+ const accounts = await readAccounts();
465
+ if (!name2 || !accounts.accounts[name2]) throw new Error(`there is no account named "${name2 ?? ""}"`);
466
+ delete accounts.accounts[name2];
467
+ if (accounts.default === name2) delete accounts.default;
468
+ await writeAccounts(accounts);
469
+ console.log(`Removed account ${name2}. Its key stays on this machine, so adding it again keeps your access.`);
342
470
  }
343
471
 
344
472
  // ../packages/config/src/errors.ts
@@ -1103,15 +1231,11 @@ function parse(toml, options = {}) {
1103
1231
  // ../node_modules/smol-toml/dist/stringify.js
1104
1232
  var HAS_WELLFORMED = !!"".isWellFormed;
1105
1233
 
1106
- // ../packages/config/src/names.ts
1107
- var IDENTIFIER = /^[A-Za-z0-9_-]+$/;
1108
- var ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
1109
- var REPO = /^[a-z0-9.-]+(\/[a-z0-9._~-]+)+$/;
1110
-
1111
1234
  // ../packages/config/src/parse.ts
1112
1235
  var DEFAULT_CONFIG = {
1113
1236
  environment: "development",
1114
- requiredSecrets: []
1237
+ requiredSecrets: [],
1238
+ wrangler: { secrets: [] }
1115
1239
  };
1116
1240
  function parseConfig(text3) {
1117
1241
  const raw = parse(text3);
@@ -1127,7 +1251,24 @@ function parseConfig(text3) {
1127
1251
  if (!Array.isArray(required) || !required.every(isEnvVarName)) {
1128
1252
  throw new ConfigError("secrets.required must be a list of valid variable names");
1129
1253
  }
1130
- return { environment, requiredSecrets: required };
1254
+ return { environment, requiredSecrets: required, wrangler: parseWrangler(raw.wrangler) };
1255
+ }
1256
+ function parseWrangler(value) {
1257
+ const raw = value ?? {};
1258
+ const secrets = raw.secrets ?? [];
1259
+ if (!Array.isArray(secrets) || !secrets.every(isEnvVarName)) {
1260
+ throw new ConfigError("wrangler.secrets must be a list of valid variable names");
1261
+ }
1262
+ const config = { secrets };
1263
+ for (const key of ["environment", "env"]) {
1264
+ const field = raw[key];
1265
+ if (field === void 0) continue;
1266
+ if (typeof field !== "string" || !IDENTIFIER.test(field)) {
1267
+ throw new ConfigError(`wrangler.${key} must contain only letters, digits, "_" or "-"`);
1268
+ }
1269
+ config[key] = field;
1270
+ }
1271
+ return config;
1131
1272
  }
1132
1273
  function isEnvVarName(value) {
1133
1274
  return typeof value === "string" && ENV_VAR_NAME.test(value);
@@ -1167,8 +1308,7 @@ import { promisify } from "node:util";
1167
1308
  var run = promisify(execFile);
1168
1309
  async function readGitOrigin(dir) {
1169
1310
  try {
1170
- const root = await git(dir, "rev-parse", "--show-toplevel");
1171
- const url = await git(dir, "config", "--get", "remote.origin.url");
1311
+ const [root, url] = await Promise.all([git(dir, "rev-parse", "--show-toplevel"), git(dir, "config", "--get", "remote.origin.url")]);
1172
1312
  return { root, url };
1173
1313
  } catch {
1174
1314
  return null;
@@ -1219,30 +1359,33 @@ async function requireTarget(cwd = process.cwd(), env = process.env) {
1219
1359
  }
1220
1360
 
1221
1361
  // src/context.ts
1222
- function requireDeployment() {
1223
- const deployment = currentDeployment();
1362
+ async function requireDeployment(repo) {
1363
+ const found = repo ?? (await findTarget(process.cwd(), process.env).catch(() => null))?.repo ?? null;
1364
+ const deployment = await resolveDeployment(found);
1224
1365
  if (!deployment) {
1225
- throw new Error("NOENV_URL is not set, add the lines from your noenv landing page to your shell profile");
1366
+ throw new Error("no noenv account is set up, run `noenv account add NAME URL` or set NOENV_URL");
1226
1367
  }
1227
1368
  return deployment;
1228
1369
  }
1229
- async function signedIn() {
1230
- const deployment = requireDeployment();
1370
+ async function signedIn(repo) {
1371
+ const deployment = await requireDeployment(repo);
1231
1372
  const token = await requireSession(deployment);
1232
1373
  return { deployment, token, identity: await ensureIdentity(deployment, token) };
1233
1374
  }
1234
1375
  async function inProject() {
1235
- return { ...await signedIn(), target: await requireTarget() };
1376
+ const target = await requireTarget();
1377
+ return { ...await signedIn(target.repo), target };
1236
1378
  }
1237
1379
 
1238
1380
  // src/commands/auth.ts
1239
1381
  async function authCommand(args2) {
1240
- const deployment = requireDeployment();
1241
- if (args2.includes("--wait")) {
1242
- const pending2 = await readPendingSignIn(deployment);
1243
- if (pending2) await waitForSignIn(deployment, pending2);
1382
+ if (args2[0] === "--wait") {
1383
+ const deployment2 = args2[1] ? deploymentAt(args2[1]) : await requireDeployment();
1384
+ const pending2 = await readPendingSignIn(deployment2);
1385
+ if (pending2) await waitForSignIn(deployment2, pending2);
1244
1386
  return;
1245
1387
  }
1388
+ const deployment = await accountOrCurrent(args2[0]);
1246
1389
  const { pending, started } = await startSignIn(deployment);
1247
1390
  console.log(`Open ${pending.url} and confirm the code ${pending.userCode}`);
1248
1391
  if (started) openBrowser(pending.url);
@@ -1250,10 +1393,12 @@ async function authCommand(args2) {
1250
1393
  throw new Error("sign-in was not completed");
1251
1394
  }
1252
1395
  const me = await api(deployment, "GET", "/v1/me", { token: await loadSession(deployment) });
1253
- console.log(`Signed in as ${me.email}`);
1396
+ console.log(`Signed in to ${label(deployment)} as ${me.email}`);
1254
1397
  }
1255
1398
  async function statusCommand() {
1256
- const deployment = requireDeployment();
1399
+ const target = await findTarget(process.cwd(), process.env);
1400
+ const deployment = await requireDeployment(target?.repo);
1401
+ if (deployment.account) console.log(`Account: ${deployment.account}`);
1257
1402
  console.log(`Deployment: ${deployment.url}`);
1258
1403
  const token = await loadSession(deployment);
1259
1404
  if (!token) {
@@ -1262,7 +1407,6 @@ async function statusCommand() {
1262
1407
  }
1263
1408
  const me = await api(deployment, "GET", "/v1/me", { token });
1264
1409
  console.log(`Signed in: ${me.email}${me.isAdmin ? " (organization admin)" : ""}`);
1265
- const target = await findTarget(process.cwd(), process.env);
1266
1410
  if (!target) {
1267
1411
  console.log("Project: none (not in a git repository with an origin)");
1268
1412
  return;
@@ -1277,6 +1421,44 @@ async function statusCommand() {
1277
1421
  console.log("Secrets: no access");
1278
1422
  }
1279
1423
  }
1424
+ async function accountOrCurrent(name2) {
1425
+ if (!name2) return requireDeployment();
1426
+ return await resolveDeployment(null, { ...process.env, NOENV_ACCOUNT: name2 });
1427
+ }
1428
+ function label(deployment) {
1429
+ return deployment.account ?? deployment.url;
1430
+ }
1431
+
1432
+ // src/commands/exec.ts
1433
+ import { spawn as spawn3 } from "node:child_process";
1434
+ import { constants } from "node:os";
1435
+
1436
+ // src/bundle.ts
1437
+ async function fetchRawBundle(deployment, token, repo, environment) {
1438
+ return api(deployment, "GET", `${environmentPath(repo, environment)}/bundle`, { token });
1439
+ }
1440
+ async function openBundle(raw, privateKey, environment) {
1441
+ const environmentPrivateKey = await unwrapPrivateKey(raw.wrappedKey, privateKey);
1442
+ const secrets = {};
1443
+ for (const { name: name2, sealed } of raw.secrets) {
1444
+ secrets[name2] = await decryptSecret(environmentPrivateKey, sealed, { projectId: raw.projectId, environment, name: name2 });
1445
+ }
1446
+ return {
1447
+ projectId: raw.projectId,
1448
+ revision: raw.revision,
1449
+ policies: raw.policies,
1450
+ environmentPublicKey: await importPublicKey(raw.publicKey),
1451
+ environmentPrivateKey,
1452
+ secrets
1453
+ };
1454
+ }
1455
+ async function fetchBundle(deployment, token, privateKey, repo, environment) {
1456
+ return openBundle(await fetchRawBundle(deployment, token, repo, environment), privateKey, environment);
1457
+ }
1458
+
1459
+ // src/refresh.ts
1460
+ import { createHash as createHash2 } from "node:crypto";
1461
+ import { join as join7 } from "node:path";
1280
1462
 
1281
1463
  // ../packages/policy/src/defaults.ts
1282
1464
  var DEFAULT_POLICIES = {
@@ -1397,27 +1579,6 @@ async function deleteAfterMigration(path, store) {
1397
1579
  }
1398
1580
  }
1399
1581
 
1400
- // src/commands/migrate.ts
1401
- import { relative as relative2 } from "node:path";
1402
-
1403
- // src/bundle.ts
1404
- async function fetchBundle(deployment, token, privateKey, repo, environment) {
1405
- const raw = await api(deployment, "GET", `${environmentPath(repo, environment)}/bundle`, { token });
1406
- const environmentPrivateKey = await unwrapPrivateKey(raw.wrappedKey, privateKey);
1407
- const secrets = {};
1408
- for (const { name: name2, sealed } of raw.secrets) {
1409
- secrets[name2] = await decryptSecret(environmentPrivateKey, sealed, { projectId: raw.projectId, environment, name: name2 });
1410
- }
1411
- return {
1412
- projectId: raw.projectId,
1413
- revision: raw.revision,
1414
- policies: raw.policies,
1415
- environmentPublicKey: await importPublicKey(raw.publicKey),
1416
- environmentPrivateKey,
1417
- secrets
1418
- };
1419
- }
1420
-
1421
1582
  // src/compliance.ts
1422
1583
  import { relative } from "node:path";
1423
1584
  var DESCRIPTIONS = {
@@ -1443,48 +1604,6 @@ async function reportCompliance(deployment, token, target, result) {
1443
1604
  });
1444
1605
  }
1445
1606
 
1446
- // src/prompt.ts
1447
- import { createInterface } from "node:readline/promises";
1448
- import { text } from "node:stream/consumers";
1449
- async function readSecretValue(label) {
1450
- if (!process.stdin.isTTY) {
1451
- return (await text(process.stdin)).replace(/\r?\n$/, "");
1452
- }
1453
- process.stderr.write(label);
1454
- process.stdin.setRawMode(true);
1455
- process.stdin.resume();
1456
- return new Promise((resolve) => {
1457
- let value = "";
1458
- const onData = (chunk) => {
1459
- for (const char of chunk.toString("utf8")) {
1460
- if (char === "\r" || char === "\n") {
1461
- finish();
1462
- return resolve(value);
1463
- }
1464
- if (char === "") {
1465
- finish();
1466
- process.exit(130);
1467
- }
1468
- value = char === "\x7F" ? value.slice(0, -1) : value + char;
1469
- }
1470
- };
1471
- const finish = () => {
1472
- process.stdin.off("data", onData);
1473
- process.stdin.setRawMode(false);
1474
- process.stdin.pause();
1475
- process.stderr.write("\n");
1476
- };
1477
- process.stdin.on("data", onData);
1478
- });
1479
- }
1480
- async function confirm(question) {
1481
- if (!process.stdin.isTTY) return false;
1482
- const readline = createInterface({ input: process.stdin, output: process.stderr });
1483
- const answer = await readline.question(`${question} [y/N] `);
1484
- readline.close();
1485
- return answer.trim().toLowerCase() === "y";
1486
- }
1487
-
1488
1607
  // src/secret-store.ts
1489
1608
  function remoteSecretStore(deployment, token, identity, repo, environment, initial = null) {
1490
1609
  let cached = initial;
@@ -1533,7 +1652,7 @@ async function watchEnvironment(deployment, repo, environment) {
1533
1652
  }
1534
1653
  async function startWatcher(deployment) {
1535
1654
  if (await isWatcherRunning(deployment)) return;
1536
- spawn2(process.execPath, [...process.execArgv, process.argv[1], "watch"], { detached: true, stdio: "ignore" }).unref();
1655
+ spawn2(process.execPath, [...process.execArgv, process.argv[1], "watch", deployment.url], { detached: true, stdio: "ignore" }).unref();
1537
1656
  }
1538
1657
  async function runWatcher(deployment) {
1539
1658
  try {
@@ -1544,7 +1663,7 @@ async function runWatcher(deployment) {
1544
1663
  }
1545
1664
  try {
1546
1665
  while (await checkForUpdates(deployment)) {
1547
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
1666
+ await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
1548
1667
  }
1549
1668
  } finally {
1550
1669
  await removeFile(pidPath(deployment));
@@ -1566,7 +1685,8 @@ async function refreshMarkers(deployment, token, entries) {
1566
1685
  });
1567
1686
  for (const { repo, environment, revision } of revisions) {
1568
1687
  const path = markerPath(deployment, repo, environment);
1569
- if (await readMarker(path) !== String(revision)) await writeMarker(path, String(revision));
1688
+ const marker = await readMarker(path);
1689
+ if (marker !== CHANGED && marker.split(":")[0] !== String(revision)) await writeMarker(path, CHANGED);
1570
1690
  }
1571
1691
  }
1572
1692
  async function isWatcherRunning(deployment) {
@@ -1586,7 +1706,478 @@ function pidPath(deployment) {
1586
1706
  return join6(deployment.dir, "watcher.pid");
1587
1707
  }
1588
1708
 
1709
+ // src/refresh.ts
1710
+ async function readCache(deployment, target) {
1711
+ return readJsonFile(cachePath(deployment, target)).catch(() => null);
1712
+ }
1713
+ async function refresh(deployment, target) {
1714
+ const entry = await fetchEntry(deployment, target);
1715
+ if (!entry) return null;
1716
+ await writeJsonFile(cachePath(deployment, target), entry);
1717
+ await writeMarker(markerPath(deployment, target.repo, target.environment), `${"raw" in entry ? entry.raw.revision : "x"}:${Date.now()}`);
1718
+ return entry;
1719
+ }
1720
+ async function fetchEntry(deployment, target) {
1721
+ try {
1722
+ const token = await loadSession(deployment);
1723
+ if (!token) return { error: "you are not signed in", status: 401 };
1724
+ const identity = await loadIdentity(deployment) ?? await ensureIdentity(deployment, token);
1725
+ let raw = await fetchRawBundle(deployment, token, target.repo, target.environment);
1726
+ const bundle = await openBundle(raw, identity.privateKey, target.environment);
1727
+ const policy = bundle.policies.dotenv;
1728
+ const store = remoteSecretStore(deployment, token, identity, target.repo, target.environment, bundle);
1729
+ const result = await enforceDotenvPolicy(target.root, policy, store);
1730
+ const notes = result.files.map((file) => describeOutcome(target, file));
1731
+ if (result.files.length > 0) {
1732
+ await reportCompliance(deployment, token, target, result).catch(() => notes.push("could not report compliance"));
1733
+ }
1734
+ if (result.blocked) {
1735
+ notes.push(`secrets were not loaded: the "${policy.mode}" policy does not allow .env files, run \`noenv migrate\``);
1736
+ }
1737
+ if (result.files.some((file) => file.status === "deleted")) {
1738
+ raw = await fetchRawBundle(deployment, token, target.repo, target.environment);
1739
+ }
1740
+ return { raw, notes, blocked: result.blocked };
1741
+ } catch (error) {
1742
+ if (!(error instanceof ApiError)) return null;
1743
+ if (error.status === 401) await clearSession(deployment);
1744
+ return { error: error.message, status: error.status };
1745
+ }
1746
+ }
1747
+ function cachePath(deployment, target) {
1748
+ const name2 = createHash2("sha256").update(`${target.repo}\0${target.environment}`).digest("hex").slice(0, 32);
1749
+ return join7(deployment.dir, "bundles", `${name2}.json`);
1750
+ }
1751
+
1752
+ // src/shell.ts
1753
+ var FIND_ROOT = `_noenv_root() {
1754
+ local dir="$PWD"
1755
+ until [[ -e "$dir/.git" || "$dir" == "/" ]]; do dir="\${dir%/*}"; dir="\${dir:-/}"; done
1756
+ if [[ -e "$dir/.git" ]]; then REPLY="$dir"; else REPLY=""; fi
1757
+ }
1758
+ `;
1759
+ var HOOKS = {
1760
+ zsh: `${FIND_ROOT}_noenv_hook() {
1761
+ local revision="" REPLY
1762
+ _noenv_root
1763
+ [[ -n "$_NOENV_REV_FILE" && -r "$_NOENV_REV_FILE" ]] && revision="$(<"$_NOENV_REV_FILE")"
1764
+ if [[ "$REPLY" != "$_NOENV_ROOT" || -n "$_NOENV_RETRY" || "$revision" != "$_NOENV_REV" ]]; then
1765
+ _NOENV_ROOT="$REPLY"
1766
+ if [[ -z "$REPLY" && ( -n "$_NOENV_RESTORE" || -z "$_NOENV_STATE" ) ]]; then
1767
+ eval "$_NOENV_RESTORE"
1768
+ else
1769
+ eval "$(printf '%s' "$_NOENV_STATE" | command noenv hook-env zsh)"
1770
+ fi
1771
+ fi
1772
+ }
1773
+ _noenv_refresh() {
1774
+ local fd
1775
+ exec {fd}< <(command noenv refresh "$@" 2>/dev/null; print)
1776
+ zle -F -w "$fd" _noenv_ready 2>/dev/null || exec {fd}<&-
1777
+ }
1778
+ _noenv_ready() {
1779
+ local fd="$1"
1780
+ zle -F "$fd"
1781
+ exec {fd}<&-
1782
+ zle -I
1783
+ _noenv_hook
1784
+ zle reset-prompt
1785
+ }
1786
+ zle -N _noenv_ready 2>/dev/null
1787
+ typeset -ag precmd_functions chpwd_functions
1788
+ if (( ! \${precmd_functions[(I)_noenv_hook]} )); then precmd_functions=(_noenv_hook $precmd_functions); fi
1789
+ if (( ! \${chpwd_functions[(I)_noenv_hook]} )); then chpwd_functions=(_noenv_hook $chpwd_functions); fi
1790
+ `,
1791
+ bash: `${FIND_ROOT}_noenv_hook() {
1792
+ local previous_exit=$? revision="" REPLY
1793
+ _noenv_root
1794
+ [[ -n "$_NOENV_REV_FILE" && -r "$_NOENV_REV_FILE" ]] && revision="$(<"$_NOENV_REV_FILE")"
1795
+ if [[ "$REPLY" != "$_NOENV_ROOT" || -n "$_NOENV_RETRY" || "$revision" != "$_NOENV_REV" ]]; then
1796
+ _NOENV_ROOT="$REPLY"
1797
+ if [[ -z "$REPLY" && ( -n "$_NOENV_RESTORE" || -z "$_NOENV_STATE" ) ]]; then
1798
+ eval "$_NOENV_RESTORE"
1799
+ else
1800
+ eval "$(printf '%s' "$_NOENV_STATE" | command noenv hook-env bash)"
1801
+ fi
1802
+ fi
1803
+ return $previous_exit
1804
+ }
1805
+ _noenv_refresh() {
1806
+ (command noenv refresh "$@" >/dev/null 2>&1 &)
1807
+ }
1808
+ if [[ ";\${PROMPT_COMMAND:-};" != *";_noenv_hook;"* ]]; then
1809
+ PROMPT_COMMAND="_noenv_hook\${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
1810
+ fi
1811
+ `
1812
+ };
1813
+ function parseShell(value) {
1814
+ if (value !== "zsh" && value !== "bash") {
1815
+ throw new Error("shell must be zsh or bash");
1816
+ }
1817
+ return value;
1818
+ }
1819
+ function hookScript(shell) {
1820
+ return HOOKS[shell];
1821
+ }
1822
+ function quote(value) {
1823
+ return `'${value.replaceAll("'", `'\\''`)}'`;
1824
+ }
1825
+ function exportLine(name2, value) {
1826
+ return `export ${name2}=${quote(value)}`;
1827
+ }
1828
+ function setLine(name2, value) {
1829
+ return `${name2}=${quote(value)}`;
1830
+ }
1831
+ function unsetLine(name2) {
1832
+ return `unset ${name2}`;
1833
+ }
1834
+
1835
+ // src/hook-env.ts
1836
+ var STATE_VARIABLES = ["_NOENV_STATE", "_NOENV_RETRY", "_NOENV_REV_FILE", "_NOENV_REV", "_NOENV_RESTORE"];
1837
+ async function hookEnv(input) {
1838
+ const state = decodeState(input.state);
1839
+ const messages = [];
1840
+ const resolved = await resolve(input, messages);
1841
+ if (!resolved) {
1842
+ return { lines: state ? [...restoreLines(state), ...STATE_VARIABLES.map(unsetLine)] : [], messages };
1843
+ }
1844
+ const { target, deployment } = resolved;
1845
+ const marker = markerPath(deployment, target.repo, target.environment);
1846
+ const revision = await readMarker(marker);
1847
+ const step = await load(input, resolved, state, revision, messages);
1848
+ return {
1849
+ lines: [
1850
+ ...step.lines,
1851
+ exportLine("_NOENV_STATE", encodeState(step.state)),
1852
+ step.state.retry ? setLine("_NOENV_RETRY", "1") : unsetLine("_NOENV_RETRY"),
1853
+ setLine("_NOENV_REV_FILE", marker),
1854
+ setLine("_NOENV_REV", revision),
1855
+ setLine("_NOENV_RESTORE", [...restoreLines(step.state), ...STATE_VARIABLES.map(unsetLine)].join("\n"))
1856
+ ],
1857
+ messages,
1858
+ refresh: step.refresh ? { url: deployment.url, root: target.root, environment: target.environment } : void 0
1859
+ };
1860
+ }
1861
+ async function load(input, resolved, state, revision, messages) {
1862
+ const { target, deployment } = resolved;
1863
+ const entering = state === null || !sameTarget(state, resolved);
1864
+ const empty = { url: deployment.url, repo: target.repo, environment: target.environment, previous: {}, retry: false };
1865
+ const restore = state ? restoreLines(state) : [];
1866
+ if (!await loadSession(deployment)) {
1867
+ return { lines: restore, state: await signIn(input, deployment, empty, state, entering, messages), refresh: false };
1868
+ }
1869
+ const entry = await readCache(deployment, target);
1870
+ const identity = await loadIdentity(deployment);
1871
+ const refresh2 = entering || state.retry || revision === CHANGED || !entry || !identity;
1872
+ if (!entering && !state.retry && (!entry || !identity || "raw" in entry && entry.raw.revision === state.revision)) {
1873
+ return { lines: [], state, refresh: refresh2 };
1874
+ }
1875
+ if (!entry || !identity) {
1876
+ return { lines: restore, state: empty, refresh: refresh2 };
1877
+ }
1878
+ const opened = await openEntry(entry, identity.privateKey, target, messages);
1879
+ if (!opened) {
1880
+ return { lines: restore, state: empty, refresh: refresh2 };
1881
+ }
1882
+ const lines = [...restore];
1883
+ const previous = {};
1884
+ for (const [name2, value] of Object.entries(opened.secrets)) {
1885
+ previous[name2] = state && name2 in state.previous ? state.previous[name2] : input.env[name2] ?? null;
1886
+ lines.push(exportLine(name2, value));
1887
+ }
1888
+ const count = Object.keys(opened.secrets).length;
1889
+ const updated = !entering && state.revision !== void 0;
1890
+ messages.push(`${updated ? "updated" : "loaded"} ${count} secrets for ${target.repo} (${target.environment})`);
1891
+ const missing = target.requiredSecrets.filter((name2) => !(name2 in opened.secrets));
1892
+ if (missing.length > 0) messages.push(`missing required secrets: ${missing.join(", ")}`);
1893
+ await watchEnvironment(deployment, target.repo, target.environment);
1894
+ input.startWatcher(deployment);
1895
+ return { lines, state: { ...empty, previous, revision: opened.revision }, refresh: refresh2 };
1896
+ }
1897
+ async function openEntry(entry, privateKey, target, messages) {
1898
+ if ("error" in entry) {
1899
+ if (!(entry.status === 403 && !target.hasConfig) && entry.status !== 401) {
1900
+ messages.push(`could not load secrets for ${target.repo} (${target.environment}): ${entry.error}`);
1901
+ }
1902
+ return null;
1903
+ }
1904
+ messages.push(...entry.notes);
1905
+ if (entry.blocked) return null;
1906
+ try {
1907
+ const bundle = await openBundle(entry.raw, privateKey, target.environment);
1908
+ return { secrets: bundle.secrets, revision: bundle.revision };
1909
+ } catch (error) {
1910
+ messages.push(`could not decrypt secrets for ${target.repo} (${target.environment}): ${describe(error)}`);
1911
+ return null;
1912
+ }
1913
+ }
1914
+ function sameTarget(state, { target, deployment }) {
1915
+ return state.url === deployment.url && state.repo === target.repo && state.environment === target.environment;
1916
+ }
1917
+ async function signIn(input, deployment, empty, state, entering, messages) {
1918
+ const waiting = await readPendingSignIn(deployment);
1919
+ if (state?.notified && !entering && !(waiting && waiting.expiresAt > Date.now())) {
1920
+ messages.push("sign-in was not completed, enter the repository again or run `noenv auth` to retry");
1921
+ return empty;
1922
+ }
1923
+ try {
1924
+ const { pending, started } = await startSignIn(deployment);
1925
+ if (started) input.openBrowser(pending.url);
1926
+ if (started || !await input.pollerRunning(deployment)) input.startPoller(deployment);
1927
+ if (started || state?.notified !== pending.userCode) {
1928
+ const account = deployment.account ? ` to ${deployment.account}` : "";
1929
+ messages.push(`sign in${account} to load secrets: open ${pending.url} and confirm the code ${pending.userCode}`);
1930
+ }
1931
+ return { ...empty, retry: true, notified: pending.userCode };
1932
+ } catch (error) {
1933
+ messages.push(`could not start sign-in: ${describe(error)}`);
1934
+ return empty;
1935
+ }
1936
+ }
1937
+ async function resolve(input, messages) {
1938
+ try {
1939
+ const target = await findTarget(input.cwd, input.env);
1940
+ const deployment = target && await input.resolveDeployment(target.repo);
1941
+ return target && deployment ? { target, deployment } : null;
1942
+ } catch (error) {
1943
+ messages.push(describe(error));
1944
+ return null;
1945
+ }
1946
+ }
1947
+ function restoreLines(state) {
1948
+ return Object.entries(state.previous).map(([name2, value]) => value === null ? unsetLine(name2) : exportLine(name2, value));
1949
+ }
1950
+ function encodeState(state) {
1951
+ return Buffer.from(JSON.stringify(state)).toString("base64");
1952
+ }
1953
+ function decodeState(text3) {
1954
+ if (!text3?.trim()) return null;
1955
+ try {
1956
+ return JSON.parse(Buffer.from(text3.trim(), "base64").toString("utf8"));
1957
+ } catch {
1958
+ return null;
1959
+ }
1960
+ }
1961
+ function describe(error) {
1962
+ return error instanceof Error ? error.message : "unknown error";
1963
+ }
1964
+
1965
+ // src/commands/exec.ts
1966
+ var SHELL_STATE = ["_NOENV_STATE", "_NOENV_RETRY", "_NOENV_REV_FILE", "_NOENV_REV", "_NOENV_ROOT"];
1967
+ async function execCommand(args2) {
1968
+ const command2 = args2[0] === "--" ? args2.slice(1) : args2;
1969
+ if (command2.length === 0) throw new Error("usage: noenv exec -- COMMAND [ARGS...]");
1970
+ const env = withoutLoadedSecrets(process.env);
1971
+ const secrets = await projectSecrets(env);
1972
+ const child = spawn3(command2[0], command2.slice(1), { stdio: "inherit", env: { ...env, ...secrets } });
1973
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, () => child.kill(signal));
1974
+ const code = await new Promise((resolve2, reject) => {
1975
+ child.on("error", reject);
1976
+ child.on("exit", (exitCode, signal) => resolve2(exitCode ?? 128 + (signal ? constants.signals[signal] : 0)));
1977
+ });
1978
+ process.exit(code);
1979
+ }
1980
+ function withoutLoadedSecrets(env) {
1981
+ const state = decodeState(env._NOENV_STATE);
1982
+ const clean = { ...env };
1983
+ for (const name2 of SHELL_STATE) delete clean[name2];
1984
+ for (const [name2, value] of Object.entries(state?.previous ?? {})) {
1985
+ if (value === null) delete clean[name2];
1986
+ else clean[name2] = value;
1987
+ }
1988
+ return clean;
1989
+ }
1990
+ async function projectSecrets(env) {
1991
+ const target = await findTarget(process.cwd(), env);
1992
+ if (!target) return note("not in a git repository with an origin, running without secrets");
1993
+ const deployment = await resolveDeployment(target.repo, env);
1994
+ if (!deployment) return note("no account is set up, running without secrets");
1995
+ const token = await requireSession(deployment);
1996
+ const identity = await loadIdentity(deployment) ?? await ensureIdentity(deployment, token);
1997
+ const label2 = `${target.repo} (${target.environment})`;
1998
+ let entry = await refresh(deployment, target);
1999
+ if (!entry) {
2000
+ entry = await readCache(deployment, target);
2001
+ if (!entry) throw new Error(`could not reach ${deployment.url} and there are no cached secrets for ${label2}`);
2002
+ note(`could not reach ${deployment.url}, using cached secrets`);
2003
+ }
2004
+ if ("error" in entry) {
2005
+ if ((entry.status === 403 || entry.status === 404) && !target.hasConfig) {
2006
+ return note(`you have no noenv project for ${target.repo} on ${deployment.account ?? deployment.url}, running without secrets`);
2007
+ }
2008
+ throw new Error(`could not load secrets for ${label2}: ${entry.error}`);
2009
+ }
2010
+ for (const line of entry.notes) note(line);
2011
+ if (entry.blocked) throw new Error(`secrets for ${label2} were not loaded because of the .env policy`);
2012
+ const { secrets } = await openBundle(entry.raw, identity.privateKey, target.environment);
2013
+ note(`loaded ${Object.keys(secrets).length} secrets for ${label2}${deployment.account ? ` from ${deployment.account}` : ""}`);
2014
+ const missing = target.requiredSecrets.filter((name2) => !(name2 in secrets));
2015
+ if (missing.length > 0) note(`missing required secrets: ${missing.join(", ")}`);
2016
+ return secrets;
2017
+ }
2018
+ function note(message) {
2019
+ process.stderr.write(`noenv: ${message}
2020
+ `);
2021
+ return {};
2022
+ }
2023
+
2024
+ // src/prompt.ts
2025
+ import { emitKeypressEvents } from "node:readline";
2026
+ import { createInterface } from "node:readline/promises";
2027
+ import { text } from "node:stream/consumers";
2028
+ async function readSecretValue(label2) {
2029
+ if (!process.stdin.isTTY) {
2030
+ return (await text(process.stdin)).replace(/\r?\n$/, "");
2031
+ }
2032
+ process.stderr.write(label2);
2033
+ process.stdin.setRawMode(true);
2034
+ process.stdin.resume();
2035
+ return new Promise((resolve2) => {
2036
+ let value = "";
2037
+ const onData = (chunk) => {
2038
+ for (const char of chunk.toString("utf8")) {
2039
+ if (char === "\r" || char === "\n") {
2040
+ finish();
2041
+ return resolve2(value);
2042
+ }
2043
+ if (char === "") {
2044
+ finish();
2045
+ process.exit(130);
2046
+ }
2047
+ value = char === "\x7F" ? value.slice(0, -1) : value + char;
2048
+ }
2049
+ };
2050
+ const finish = () => {
2051
+ process.stdin.off("data", onData);
2052
+ process.stdin.setRawMode(false);
2053
+ process.stdin.pause();
2054
+ process.stderr.write("\n");
2055
+ };
2056
+ process.stdin.on("data", onData);
2057
+ });
2058
+ }
2059
+ async function confirm(question) {
2060
+ if (!process.stdin.isTTY) return false;
2061
+ const readline = createInterface({ input: process.stdin, output: process.stderr });
2062
+ const answer = await readline.question(`${question} [y/N] `);
2063
+ readline.close();
2064
+ return answer.trim().toLowerCase() === "y";
2065
+ }
2066
+ async function checklist(title, items, checked) {
2067
+ const input = process.stdin;
2068
+ const output = process.stderr;
2069
+ const selected = new Set([...checked].filter((item) => items.includes(item)));
2070
+ const height = Math.min(items.length, Math.max(5, (output.rows ?? 24) - 3));
2071
+ let cursor = 0;
2072
+ let top = 0;
2073
+ let drawn = 0;
2074
+ const render = () => {
2075
+ top = Math.min(Math.max(top, cursor - height + 1), cursor);
2076
+ const rows = items.slice(top, top + height).map((item, offset) => {
2077
+ const active = top + offset === cursor;
2078
+ const line = `${active ? "\u203A" : " "} ${selected.has(item) ? "[x]" : "[ ]"} ${item}`;
2079
+ return active ? `\x1B[1m${line}\x1B[22m` : line;
2080
+ });
2081
+ const help = `\x1B[2m${selected.size} of ${items.length} selected \xB7 space select \xB7 a all \xB7 enter confirm \xB7 esc cancel\x1B[22m`;
2082
+ const lines = [title, ...rows, help];
2083
+ output.write((drawn > 0 ? `\x1B[${drawn}A` : "") + lines.map((line) => `\x1B[2K${line}
2084
+ `).join(""));
2085
+ drawn = lines.length;
2086
+ };
2087
+ emitKeypressEvents(input);
2088
+ input.setRawMode(true);
2089
+ input.resume();
2090
+ output.write("\x1B[?25l");
2091
+ return new Promise((resolve2) => {
2092
+ const finish = (result) => {
2093
+ input.off("keypress", onKey);
2094
+ input.setRawMode(false);
2095
+ input.pause();
2096
+ output.write("\x1B[?25h");
2097
+ resolve2(result);
2098
+ };
2099
+ const onKey = (_text, key = {}) => {
2100
+ if (key.ctrl && key.name === "c" || key.name === "escape" || key.name === "q") return finish(null);
2101
+ if (key.name === "return") return finish(items.filter((item) => selected.has(item)));
2102
+ if (key.name === "up" || key.name === "k") cursor = (cursor - 1 + items.length) % items.length;
2103
+ if (key.name === "down" || key.name === "j") cursor = (cursor + 1) % items.length;
2104
+ if (key.name === "space") {
2105
+ const item = items[cursor];
2106
+ if (selected.has(item)) selected.delete(item);
2107
+ else selected.add(item);
2108
+ }
2109
+ if (key.name === "a") {
2110
+ const all = items.every((item) => selected.has(item));
2111
+ for (const item of items) all ? selected.delete(item) : selected.add(item);
2112
+ }
2113
+ render();
2114
+ };
2115
+ input.on("keypress", onKey);
2116
+ render();
2117
+ });
2118
+ }
2119
+
2120
+ // src/wrangler.ts
2121
+ import { spawn as spawn4 } from "node:child_process";
2122
+ import { access } from "node:fs/promises";
2123
+ import { dirname as dirname2, join as join8 } from "node:path";
2124
+ function wranglerSource(target) {
2125
+ return target.wrangler.environment ?? target.environment;
2126
+ }
2127
+ async function putWranglerSecrets(target, secrets) {
2128
+ const names = Object.keys(secrets);
2129
+ const env = target.wrangler.env ? ` --env ${target.wrangler.env}` : "";
2130
+ console.log(`Pushing ${names.length} secrets from noenv (${wranglerSource(target)}) to Wrangler${env}: ${names.join(", ")}`);
2131
+ const args2 = ["secret", "bulk", ...target.wrangler.env ? ["--env", target.wrangler.env] : []];
2132
+ const child = spawn4(await findWrangler(target.root), args2, { cwd: target.root, stdio: ["pipe", "inherit", "inherit"] });
2133
+ child.stdin.end(JSON.stringify(secrets));
2134
+ const code = await new Promise((resolve2, reject) => {
2135
+ child.on(
2136
+ "error",
2137
+ (error) => reject(error.code === "ENOENT" ? new Error("wrangler is not installed, run `npm install -D wrangler`") : error)
2138
+ );
2139
+ child.on("exit", (exitCode) => resolve2(exitCode ?? 1));
2140
+ });
2141
+ if (code !== 0) throw new Error(`wrangler secret bulk failed with exit code ${code}`);
2142
+ }
2143
+ async function findWrangler(root) {
2144
+ for (let dir = root; ; dir = dirname2(dir)) {
2145
+ const local = join8(dir, "node_modules", ".bin", "wrangler");
2146
+ if (await access(local).then(() => true, () => false)) return local;
2147
+ if (dirname2(dir) === dir) return "wrangler";
2148
+ }
2149
+ }
2150
+
2151
+ // src/commands/wrangler.ts
2152
+ async function wranglerCommand(args2) {
2153
+ const invalid = args2.find((name2) => !ENV_VAR_NAME.test(name2));
2154
+ if (invalid) throw new Error(`usage: noenv wrangler [KEY...] (${invalid} is not a variable name)`);
2155
+ const { deployment, token, identity, target } = await inProject();
2156
+ const environment = wranglerSource(target);
2157
+ const label2 = `${target.repo} (${environment})`;
2158
+ const { secrets } = await fetchBundle(deployment, token, identity.privateKey, target.repo, environment);
2159
+ const names = Object.keys(secrets).sort();
2160
+ if (names.length === 0) throw new Error(`there are no secrets in ${label2}`);
2161
+ const selected = await choose(args2, names, target.wrangler.secrets, label2);
2162
+ if (!selected) return console.log("Cancelled, nothing was pushed");
2163
+ if (selected.length === 0) return console.log("Nothing selected, nothing was pushed");
2164
+ const missing = selected.filter((name2) => !(name2 in secrets));
2165
+ if (missing.length > 0) throw new Error(`${label2} has no secret named ${missing.join(", ")}`);
2166
+ await putWranglerSecrets(target, Object.fromEntries(selected.map((name2) => [name2, secrets[name2]])));
2167
+ }
2168
+ async function choose(args2, names, configured, label2) {
2169
+ if (args2.length > 0) return args2;
2170
+ if (process.stdin.isTTY && process.stderr.isTTY) {
2171
+ return checklist(`Push secrets from ${label2} to Wrangler`, names, configured);
2172
+ }
2173
+ if (configured.length === 0) {
2174
+ throw new Error("pass the KEY names, or list them under [wrangler] secrets in .noenv");
2175
+ }
2176
+ return configured;
2177
+ }
2178
+
1589
2179
  // src/commands/migrate.ts
2180
+ import { relative as relative2 } from "node:path";
1590
2181
  async function migrateCommand() {
1591
2182
  const { deployment, token, identity, target } = await inProject();
1592
2183
  const bundle = await fetchBundle(deployment, token, identity.privateKey, target.repo, target.environment);
@@ -1633,7 +2224,8 @@ async function migrateCommand() {
1633
2224
  }
1634
2225
 
1635
2226
  // src/commands/project.ts
1636
- import { basename } from "node:path";
2227
+ import { writeFile as writeFile3 } from "node:fs/promises";
2228
+ import { basename, join as join9 } from "node:path";
1637
2229
  var ROLES = ["reader", "editor", "admin"];
1638
2230
  async function initCommand() {
1639
2231
  const { deployment, token, identity, target } = await inProject();
@@ -1655,6 +2247,14 @@ async function initCommand() {
1655
2247
  if (!(error instanceof ApiError && error.status === 409)) throw error;
1656
2248
  console.log(`Environment ${target.environment} already exists`);
1657
2249
  }
2250
+ try {
2251
+ await writeFile3(join9(target.root, CONFIG_FILE), `version = 1
2252
+ environment = "${target.environment}"
2253
+ `, { flag: "wx" });
2254
+ console.log(`Created ${CONFIG_FILE}, commit it so teammates load this project`);
2255
+ } catch (error) {
2256
+ if (error.code !== "EEXIST") throw error;
2257
+ }
1658
2258
  }
1659
2259
  async function inviteCommand([email]) {
1660
2260
  if (!email) throw new Error("usage: noenv invite EMAIL");
@@ -1719,6 +2319,7 @@ async function setSecret([name2]) {
1719
2319
  const version = await store.set(name2, value);
1720
2320
  await markChanged(deployment, target.repo, target.environment);
1721
2321
  console.log(`Saved ${name2} as version ${version} in ${target.repo} (${target.environment})`);
2322
+ await syncToWrangler(target, name2, async () => value);
1722
2323
  }
1723
2324
  async function listSecrets() {
1724
2325
  const { deployment, token, target } = await inProject();
@@ -1751,7 +2352,7 @@ async function rollbackSecret([name2, version]) {
1751
2352
  if (!/^\d+$/.test(version ?? "")) {
1752
2353
  throw new Error("usage: noenv secret rollback KEY VERSION");
1753
2354
  }
1754
- const { deployment, token, target } = await inProject();
2355
+ const { deployment, token, identity, target } = await inProject();
1755
2356
  const result = await api(
1756
2357
  deployment,
1757
2358
  "POST",
@@ -1760,250 +2361,104 @@ async function rollbackSecret([name2, version]) {
1760
2361
  );
1761
2362
  await markChanged(deployment, target.repo, target.environment);
1762
2363
  console.log(`Restored ${name2} v${version} as v${result.version}`);
2364
+ await syncToWrangler(target, name2, async () => {
2365
+ const { secrets } = await fetchBundle(deployment, token, identity.privateKey, target.repo, target.environment);
2366
+ return secrets[name2];
2367
+ });
1763
2368
  }
1764
- function requireName(name2) {
1765
- if (!name2 || !ENV_VAR_NAME.test(name2)) {
1766
- throw new Error("the secret name must be a valid environment variable name");
1767
- }
1768
- }
1769
-
1770
- // src/commands/shell.ts
1771
- import { text as text2 } from "node:stream/consumers";
1772
-
1773
- // src/shell.ts
1774
- var HOOKS = {
1775
- zsh: `_noenv_hook() {
1776
- local revision=""
1777
- [[ -n "$_NOENV_REV_FILE" && -r "$_NOENV_REV_FILE" ]] && revision="$(<"$_NOENV_REV_FILE")"
1778
- if [[ "$PWD" != "$_NOENV_PWD" || -n "$_NOENV_RETRY" || "$revision" != "$_NOENV_REV" ]]; then
1779
- _NOENV_PWD="$PWD"
1780
- eval "$(printf '%s' "$_NOENV_STATE" | command noenv hook-env zsh)"
1781
- fi
1782
- }
1783
- typeset -ag precmd_functions chpwd_functions
1784
- if (( ! \${precmd_functions[(I)_noenv_hook]} )); then precmd_functions=(_noenv_hook $precmd_functions); fi
1785
- if (( ! \${chpwd_functions[(I)_noenv_hook]} )); then chpwd_functions=(_noenv_hook $chpwd_functions); fi
1786
- `,
1787
- bash: `_noenv_hook() {
1788
- local previous_exit=$? revision=""
1789
- [[ -n "$_NOENV_REV_FILE" && -r "$_NOENV_REV_FILE" ]] && revision="$(<"$_NOENV_REV_FILE")"
1790
- if [[ "$PWD" != "$_NOENV_PWD" || -n "$_NOENV_RETRY" || "$revision" != "$_NOENV_REV" ]]; then
1791
- _NOENV_PWD="$PWD"
1792
- eval "$(printf '%s' "$_NOENV_STATE" | command noenv hook-env bash)"
1793
- fi
1794
- return $previous_exit
1795
- }
1796
- if [[ ";\${PROMPT_COMMAND:-};" != *";_noenv_hook;"* ]]; then
1797
- PROMPT_COMMAND="_noenv_hook\${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
1798
- fi
1799
- `
1800
- };
1801
- function parseShell(value) {
1802
- if (value !== "zsh" && value !== "bash") {
1803
- throw new Error("shell must be zsh or bash");
1804
- }
1805
- return value;
1806
- }
1807
- function hookScript(shell) {
1808
- return HOOKS[shell];
1809
- }
1810
- function quote(value) {
1811
- return `'${value.replaceAll("'", `'\\''`)}'`;
1812
- }
1813
- function exportLine(name2, value) {
1814
- return `export ${name2}=${quote(value)}`;
1815
- }
1816
- function setLine(name2, value) {
1817
- return `${name2}=${quote(value)}`;
1818
- }
1819
- function unsetLine(name2) {
1820
- return `unset ${name2}`;
1821
- }
1822
-
1823
- // src/hook-env.ts
1824
- async function hookEnv(input) {
1825
- const state = decodeState(input.state);
1826
- const messages = [];
1827
- const target = input.deployment ? await findTargetOrReport(input, messages) : null;
1828
- if (state && target && !state.retry && sameTarget(state, target) && await isCurrent(input, state)) {
1829
- return { lines: [], messages };
1830
- }
1831
- const lines = state ? restoreLines(state) : [];
1832
- if (!target) {
1833
- if (state) lines.push(...["_NOENV_STATE", "_NOENV_RETRY", "_NOENV_REV_FILE", "_NOENV_REV"].map(unsetLine));
1834
- return { lines, messages };
1835
- }
1836
- const loaded = await load(input, target, state, messages);
1837
- lines.push(
1838
- ...loaded.lines,
1839
- setLine("_NOENV_STATE", encodeState(loaded.state)),
1840
- loaded.state.retry ? setLine("_NOENV_RETRY", "1") : unsetLine("_NOENV_RETRY"),
1841
- ...revisionLines(input, target, loaded.state.revision)
1842
- );
1843
- return { lines, messages };
1844
- }
1845
- function sameTarget(state, target) {
1846
- return state.repo === target.repo && state.environment === target.environment;
1847
- }
1848
- async function isCurrent(input, state) {
1849
- if (state.revision === void 0 || !input.deployment) return true;
1850
- return await readMarker(markerPath(input.deployment, state.repo, state.environment)) === String(state.revision);
1851
- }
1852
- function revisionLines(input, target, revision) {
1853
- if (revision === void 0 || !input.deployment) return [unsetLine("_NOENV_REV_FILE"), unsetLine("_NOENV_REV")];
1854
- return [setLine("_NOENV_REV_FILE", markerPath(input.deployment, target.repo, target.environment)), setLine("_NOENV_REV", String(revision))];
1855
- }
1856
- async function load(input, target, state, messages) {
1857
- const { deployment } = input;
1858
- const empty = { repo: target.repo, environment: target.environment, previous: {}, retry: false };
1859
- try {
1860
- const token = await loadSession(deployment);
1861
- if (!token) {
1862
- return { lines: [], state: await signIn(input, empty, state, messages) };
1863
- }
1864
- const identity = await loadIdentity(deployment) ?? await ensureIdentity(deployment, token);
1865
- let bundle = await fetchBundle(deployment, token, identity.privateKey, target.repo, target.environment);
1866
- const dotenv = await enforceDotenv({ deployment, token, identity, target, bundle }, messages);
1867
- if (dotenv.blocked) {
1868
- return { lines: [], state: empty };
1869
- }
1870
- if (dotenv.changed) {
1871
- bundle = await fetchBundle(deployment, token, identity.privateKey, target.repo, target.environment);
1872
- }
1873
- const lines = [];
1874
- const previous = {};
1875
- for (const [name2, value] of Object.entries(bundle.secrets)) {
1876
- previous[name2] = state && name2 in state.previous ? state.previous[name2] : input.env[name2] ?? null;
1877
- lines.push(exportLine(name2, value));
1878
- }
1879
- const reloaded = state !== null && sameTarget(state, target) && state.revision !== void 0;
1880
- messages.push(`${reloaded ? "updated" : "loaded"} ${lines.length} secrets for ${target.repo} (${target.environment})`);
1881
- const missing = target.requiredSecrets.filter((name2) => !(name2 in bundle.secrets));
1882
- if (missing.length > 0) {
1883
- messages.push(`missing required secrets: ${missing.join(", ")}`);
1884
- }
1885
- await writeMarker(markerPath(deployment, target.repo, target.environment), String(bundle.revision));
1886
- await watchEnvironment(deployment, target.repo, target.environment);
1887
- input.startWatcher();
1888
- return { lines, state: { ...empty, previous, revision: bundle.revision } };
1889
- } catch (error) {
1890
- if (error instanceof ApiError && error.status === 401) {
1891
- await clearSession(deployment);
1892
- return { lines: [], state: { ...empty, retry: true } };
1893
- }
1894
- if (!(error instanceof ApiError && error.status === 403 && !target.hasConfig)) {
1895
- messages.push(`could not load secrets for ${target.repo} (${target.environment}): ${describe(error)}`);
1896
- }
1897
- return { lines: [], state: empty };
1898
- }
1899
- }
1900
- async function enforceDotenv(loaded, messages) {
1901
- const { deployment, token, identity, target, bundle } = loaded;
1902
- const policy = bundle.policies.dotenv;
1903
- const store = remoteSecretStore(deployment, token, identity, target.repo, target.environment, bundle);
1904
- const result = await enforceDotenvPolicy(target.root, policy, store);
1905
- if (result.files.length === 0) {
1906
- return { blocked: false, changed: false };
1907
- }
1908
- messages.push(...result.files.map((file) => describeOutcome(target, file)));
1909
- if (result.blocked) {
1910
- messages.push(`secrets were not loaded: the "${policy.mode}" policy does not allow .env files, run \`noenv migrate\``);
1911
- }
1912
- await reportCompliance(deployment, token, target, result).catch(() => messages.push("could not report compliance"));
1913
- return { blocked: result.blocked, changed: result.files.some((file) => file.status === "deleted") };
1914
- }
1915
- async function signIn(input, empty, state, messages) {
1916
- const waiting = await readPendingSignIn(input.deployment);
1917
- if (state?.notified && !(waiting && waiting.expiresAt > Date.now())) {
1918
- messages.push("sign-in was not completed, run `noenv auth` to try again");
1919
- return empty;
1920
- }
1921
- const { pending, started } = await startSignIn(input.deployment);
1922
- if (started) {
1923
- input.startPoller();
1924
- input.openBrowser(pending.url);
1925
- }
1926
- if (started || state?.notified !== pending.userCode) {
1927
- messages.push(`sign in to load secrets: open ${pending.url} and confirm the code ${pending.userCode}`);
2369
+ async function syncToWrangler(target, name2, value) {
2370
+ if (!target.wrangler.secrets.includes(name2)) return;
2371
+ if (wranglerSource(target) !== target.environment) {
2372
+ console.log(`Not pushed to Wrangler: .noenv pushes values from ${wranglerSource(target)}, not ${target.environment}`);
2373
+ return;
1928
2374
  }
1929
- return { ...empty, retry: true, notified: pending.userCode };
1930
- }
1931
- async function findTargetOrReport(input, messages) {
1932
2375
  try {
1933
- return await findTarget(input.cwd, input.env);
2376
+ await putWranglerSecrets(target, { [name2]: await value() });
1934
2377
  } catch (error) {
1935
- messages.push(describe(error));
1936
- return null;
2378
+ const reason = error instanceof Error ? error.message : "unknown error";
2379
+ throw new Error(`${name2} is saved in noenv, but the Wrangler update failed: ${reason}. Run \`noenv wrangler ${name2}\` to try again`);
1937
2380
  }
1938
2381
  }
1939
- function restoreLines(state) {
1940
- return Object.entries(state.previous).map(([name2, value]) => value === null ? unsetLine(name2) : exportLine(name2, value));
1941
- }
1942
- function encodeState(state) {
1943
- return Buffer.from(JSON.stringify(state)).toString("base64");
1944
- }
1945
- function decodeState(text3) {
1946
- if (!text3.trim()) return null;
1947
- try {
1948
- return JSON.parse(Buffer.from(text3.trim(), "base64").toString("utf8"));
1949
- } catch {
1950
- return null;
2382
+ function requireName(name2) {
2383
+ if (!name2 || !ENV_VAR_NAME.test(name2)) {
2384
+ throw new Error("the secret name must be a valid environment variable name");
1951
2385
  }
1952
2386
  }
1953
- function describe(error) {
1954
- return error instanceof Error ? error.message : "unknown error";
1955
- }
1956
2387
 
1957
2388
  // src/commands/shell.ts
2389
+ import { text as text2 } from "node:stream/consumers";
1958
2390
  async function shellCommand([shell]) {
1959
2391
  process.stdout.write(hookScript(parseShell(shell)));
1960
2392
  }
1961
2393
  async function hookEnvCommand([shell]) {
1962
2394
  parseShell(shell);
1963
2395
  const state = process.stdin.isTTY ? "" : await text2(process.stdin);
1964
- const deployment = currentDeployment();
1965
- const { lines, messages } = await hookEnv({
2396
+ const { lines, messages, refresh: job } = await hookEnv({
1966
2397
  cwd: process.cwd(),
1967
2398
  env: process.env,
1968
2399
  state,
1969
- deployment,
2400
+ resolveDeployment: (repo) => resolveDeployment(repo),
1970
2401
  startPoller: startBackgroundPoller,
1971
- startWatcher: () => void (deployment && startWatcher(deployment)),
2402
+ pollerRunning,
2403
+ startWatcher: (deployment) => void startWatcher(deployment),
1972
2404
  openBrowser
1973
2405
  });
1974
2406
  for (const message of messages) process.stderr.write(`noenv: ${message}
1975
2407
  `);
2408
+ if (job) lines.push(`_noenv_refresh ${quote(job.url)} ${quote(job.root)} ${quote(job.environment)}`);
1976
2409
  process.stdout.write(lines.join("\n"));
1977
2410
  }
1978
- async function watchCommand() {
1979
- await runWatcher(requireDeployment());
2411
+ async function refreshCommand([url, root, environment]) {
2412
+ if (!url || !root || !environment) throw new Error("usage: noenv refresh URL ROOT ENVIRONMENT");
2413
+ const target = await findTarget(root, { ...process.env, NOENV_ENVIRONMENT: environment });
2414
+ if (target) await refresh(deploymentAt(url), target);
2415
+ }
2416
+ async function watchCommand([url]) {
2417
+ if (!url) throw new Error("usage: noenv watch URL");
2418
+ await runWatcher(deploymentAt(url));
1980
2419
  }
1981
2420
 
1982
2421
  // src/main.ts
1983
- var USAGE = `usage: noenv <command>
2422
+ var USAGE2 = `usage: noenv <command>
1984
2423
 
1985
2424
  shell zsh|bash print the shell hook for your shell profile
1986
- auth sign in to the deployment in NOENV_URL
1987
- status show the deployment, user, and project
1988
- init create the project and environment for this repository
2425
+ account add NAME URL [PREFIX...]
2426
+ add an account, such as work or personal; repos
2427
+ under a PREFIX (github.com/acme) use it
2428
+ account list list accounts, * marks the default
2429
+ account default NAME use NAME where no prefix matches
2430
+ account remove NAME forget an account
2431
+ auth [NAME] sign in to an account (default: the one for this repo)
2432
+ status show the account, user, and project
2433
+ init create the project, the environment, and the .noenv file
2434
+ exec -- COMMAND run COMMAND with this repository's secrets, for agents,
2435
+ scripts, and CI; it picks the account and project from
2436
+ the current directory
1989
2437
  secret set KEY save a secret (the value is read without echo)
1990
2438
  secret list list secrets in this environment
1991
2439
  secret history KEY list the versions of a secret
1992
2440
  secret rollback KEY VER restore an older version of a secret
1993
2441
  migrate move .env files of this repository into noenv
2442
+ wrangler [KEY...] push secrets to the Worker with wrangler; without KEY,
2443
+ pick them in a list (keys under [wrangler] secrets in
2444
+ .noenv are checked; without a terminal, push those)
1994
2445
  invite EMAIL invite a user to this deployment
1995
2446
  grant EMAIL ROLE give a user access to this project and environment
1996
2447
  revoke EMAIL remove a user from this project
1997
2448
  `;
1998
2449
  var commands = {
2450
+ account: accountCommand,
1999
2451
  shell: shellCommand,
2000
2452
  "hook-env": hookEnvCommand,
2453
+ refresh: refreshCommand,
2454
+ exec: execCommand,
2001
2455
  watch: watchCommand,
2002
2456
  auth: authCommand,
2003
2457
  status: statusCommand,
2004
2458
  init: initCommand,
2005
2459
  secret: secretCommand,
2006
2460
  migrate: migrateCommand,
2461
+ wrangler: wranglerCommand,
2007
2462
  invite: inviteCommand,
2008
2463
  grant: grantCommand,
2009
2464
  revoke: revokeCommand
@@ -2011,7 +2466,7 @@ var commands = {
2011
2466
  var [name, ...args] = process.argv.slice(2);
2012
2467
  var command = commands[name ?? ""];
2013
2468
  if (!command) {
2014
- process.stdout.write(USAGE);
2469
+ process.stdout.write(USAGE2);
2015
2470
  process.exit(name ? 1 : 0);
2016
2471
  }
2017
2472
  try {