affect-cli 0.0.3 → 0.0.4

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/README.md +16 -1
  2. package/dist/index.js +161 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,12 +10,27 @@ bunx --no-install affect --help
10
10
  Run commands from the application repository root:
11
11
 
12
12
  - `affect env` prints shell exports from `env/{development,preview,production,ci}/.env`. It requires the matching `DOTENV_PRIVATE_KEY` when that file exists. Treat its output as secrets.
13
+ - `affect env sync-dev [--force]` reads only `env/development/.env` with its matching `DOTENV_PRIVATE_KEY` and synchronizes declared server settings to its verified hosted development backend. It never deploys code or builds the frontend. It ignores inherited CI, production, account-token and provider-host selection. Missing required settings fail; absent optional settings preserve remote values; explicit empty strings remain values. Existing differing values refuse the entire batch unless `--force` is supplied. Omission never deletes a remote setting. Output contains only the verified target and setting counts.
13
14
  - `affect run -- <command> [args...]` loads that environment and runs a command. Existing environment values take precedence; arguments, standard I/O, and exit codes are preserved.
14
15
  - `affect dotenvx -- …` forwards arguments to the bundled native dotenvx CLI, for example `affect dotenvx -- encrypt -f env/development/.env --no-armor --no-native`.
15
16
  - `affect setup` initializes an anonymous local Convex backend only for a fresh project, or retries its environment import. It preserves existing linked deployments and credentials.
16
17
  - `affect deploy` uses Convex's native deploy/build sequence, imports the selected backend environment, and builds the frontend. Vercel supplies `VERCEL_ENV=preview|production` and the preview branch name automatically; put the matching deploy key in the encrypted file. `--build` is its internal Convex callback.
17
18
 
18
- Selection uses `VERCEL_ENV`, then `ci` for GitHub Actions/CI, otherwise `development`. Files stay in the application; cryptography stays in dotenvx. Deployment follows Affect's `apps/web` layout.
19
+ Except for the explicitly development-only `env sync-dev`, selection uses `VERCEL_ENV`, then `ci` for GitHub Actions/CI, otherwise `development`. Files stay in the application; cryptography stays in dotenvx. Deployment follows Affect's `apps/web` layout.
20
+
21
+ `env sync-dev` requires a declaration-only `convex/env.config.ts` with a default Convex `EnvDefinition` export. Share that same object with `defineApp({ env })`; keep it independent of generated files, app/component initialization and runtime environment reads. The Bun-only CLI loads this trusted module directly. It does not execute `convex.config.ts` or generate an allowlist from source text. For example:
22
+
23
+ ```ts
24
+ import type { EnvDefinition } from "convex/server";
25
+ import { v } from "convex/values";
26
+
27
+ export default {
28
+ API_TOKEN: v.string(),
29
+ OPTIONAL_SETTING: v.optional(v.string()),
30
+ } satisfies EnvDefinition;
31
+ ```
32
+
33
+ Use the profile's development-scoped deploy key and canonical public Convex cloud/site origins. Before any update the command verifies the provider's cloud/development identity, owning team/project and both canonical URLs. Reserved credential/control/client-public names are rejected even if declared. Only present declared server settings are sent, using the native Convex JSON environment API without temporary plaintext files; native Convex remains responsible for provider limits and application value validation. The command needs metadata and environment read/write permissions; it does not create credentials or fall back to a broader login. Profile values use the same dotenvx decryption and interpolation as `affect env`.
19
34
 
20
35
  For example, an application's package scripts can load their environment automatically:
21
36
 
package/dist/index.js CHANGED
@@ -1684,6 +1684,7 @@ var normalize = (n) => n > 0 ? Math.floor(n) : 0;
1684
1684
  // node_modules/effect/dist/Result.js
1685
1685
  var succeed2 = succeed;
1686
1686
  var fail2 = fail;
1687
+ var failVoid = /* @__PURE__ */ fail2(undefined);
1687
1688
  var try_ = (evaluate) => {
1688
1689
  if (isFunction(evaluate)) {
1689
1690
  try {
@@ -1841,6 +1842,17 @@ var contains = /* @__PURE__ */ containsWith(/* @__PURE__ */ asEquivalence());
1841
1842
  var empty2 = () => [];
1842
1843
  var of = (a) => [a];
1843
1844
  var map3 = /* @__PURE__ */ dual(2, (self, f) => self.map(f));
1845
+ var filterMap = /* @__PURE__ */ dual(2, (self, f) => {
1846
+ const as = fromIterable(self);
1847
+ const out = [];
1848
+ for (let i = 0;i < as.length; i++) {
1849
+ const result = f(as[i], i);
1850
+ if (isSuccess2(result)) {
1851
+ out.push(result.success);
1852
+ }
1853
+ }
1854
+ return out;
1855
+ });
1844
1856
  var filter = /* @__PURE__ */ dual(2, (self, predicate) => {
1845
1857
  const as = fromIterable(self);
1846
1858
  const out = [];
@@ -3475,6 +3487,7 @@ var flatMapEager = /* @__PURE__ */ dual(2, (self, f) => {
3475
3487
  }
3476
3488
  return flatMap2(self, f);
3477
3489
  });
3490
+ var flatten2 = (self) => flatMap2(self, identity);
3478
3491
  var map4 = /* @__PURE__ */ dual(2, (self, f) => new ContImpl(self, mapCont, f));
3479
3492
  var mapEager = /* @__PURE__ */ dual(2, (self, f) => effectIsExit(self) ? exitMap(self, f) : map4(self, f));
3480
3493
  var mapErrorEager = /* @__PURE__ */ dual(2, (self, f) => effectIsExit(self) ? exitMapError(self, f) : mapError(self, f));
@@ -5059,6 +5072,7 @@ var try_3 = try_2;
5059
5072
  var withFiber2 = withFiber;
5060
5073
  var fromResult2 = fromResult;
5061
5074
  var flatMap3 = flatMap2;
5075
+ var flatten3 = flatten2;
5062
5076
  var andThen2 = andThen;
5063
5077
  var tap2 = tap;
5064
5078
  var exit2 = exit;
@@ -12485,6 +12499,45 @@ function isBase64(annotations) {
12485
12499
  ...annotations
12486
12500
  });
12487
12501
  }
12502
+ function makeIsGreaterThan(options) {
12503
+ const gt = isGreaterThan(options.order);
12504
+ const formatter = options.formatter ?? format;
12505
+ return (exclusiveMinimum, annotations) => {
12506
+ return makeFilter2((input) => gt(input, exclusiveMinimum), {
12507
+ expected: `a value greater than ${formatter(exclusiveMinimum)}`,
12508
+ arbitraryConstraint: {
12509
+ order: options.order,
12510
+ minimum: exclusiveMinimum,
12511
+ exclusiveMinimum: true
12512
+ },
12513
+ ...options.annotate?.(exclusiveMinimum),
12514
+ ...annotations
12515
+ });
12516
+ };
12517
+ }
12518
+ function encodeNumberPayload(number) {
12519
+ if (!globalThis.Number.isFinite(number)) {
12520
+ throw new globalThis.RangeError(`Expected a finite number, got ${format(number)}`);
12521
+ }
12522
+ return number;
12523
+ }
12524
+ var isGreaterThan2 = /* @__PURE__ */ makeIsGreaterThan({
12525
+ order: Number2,
12526
+ annotate: (exclusiveMinimum) => ({
12527
+ representation: {
12528
+ id: "effect/schema/isGreaterThan",
12529
+ payload: {
12530
+ exclusiveMinimum: encodeNumberPayload(exclusiveMinimum)
12531
+ }
12532
+ },
12533
+ toJsonSchema: () => ({
12534
+ exclusiveMinimum
12535
+ }),
12536
+ toCode: () => ({
12537
+ runtime: `Schema.isGreaterThan(${format(exclusiveMinimum)})`
12538
+ })
12539
+ })
12540
+ });
12488
12541
  function isInt(annotations) {
12489
12542
  return makeFilter2((n) => globalThis.Number.isSafeInteger(n), {
12490
12543
  expected: "an integer",
@@ -12614,6 +12667,7 @@ var URL2 = /* @__PURE__ */ instanceOf(globalThis.URL, {
12614
12667
  expected: "URL",
12615
12668
  toCodecJson: () => link3()(URLString, urlFromString)
12616
12669
  });
12670
+ var URLFromString = /* @__PURE__ */ URLString.pipe(/* @__PURE__ */ decodeTo2(URL2, urlFromString));
12617
12671
  var File = /* @__PURE__ */ instanceOf(globalThis.File, {
12618
12672
  representation: {
12619
12673
  id: "effect/schema/File",
@@ -13649,7 +13703,7 @@ var Custom = (initialState, ...args) => {
13649
13703
  op.events = events;
13650
13704
  return op;
13651
13705
  };
13652
- var Date4 = (options) => {
13706
+ var Date5 = (options) => {
13653
13707
  const opts = {
13654
13708
  initial: new globalThis.Date,
13655
13709
  dateMask: "YYYY-MM-DD HH:mm:ss",
@@ -18141,7 +18195,7 @@ var promptSingle = (single) => {
18141
18195
  })), commandLineArg);
18142
18196
  }
18143
18197
  case "Date":
18144
- return map6(run3(Date4({
18198
+ return map6(run3(Date5({
18145
18199
  message
18146
18200
  })), (date) => commandLineArg(date.toISOString()));
18147
18201
  case "Finite":
@@ -18519,7 +18573,7 @@ var runWith2 = (command, config) => {
18519
18573
  }) => andThen2(showUserError(error, userMessage), fail7(error))), catchFilter2((e) => isQuitError(e) ? succeed2(e) : fail2(e), (_) => interrupt2));
18520
18574
  };
18521
18575
  // package.json
18522
- var version = "0.0.3";
18576
+ var version = "0.0.4";
18523
18577
  // node_modules/effect/dist/http/HttpBody.js
18524
18578
  var TypeId35 = "~effect/http/HttpBody";
18525
18579
  var HttpBodyErrorTypeId = "~effect/http/HttpBody/HttpBodyError";
@@ -18804,6 +18858,7 @@ var make32 = (method) => (url, options) => modify(empty8, {
18804
18858
  url,
18805
18859
  ...options ?? undefined
18806
18860
  });
18861
+ var get4 = /* @__PURE__ */ make32("GET");
18807
18862
  var post = /* @__PURE__ */ make32("POST");
18808
18863
  var modify = /* @__PURE__ */ dual(2, (self, options) => {
18809
18864
  let result = self;
@@ -19645,9 +19700,111 @@ var setup = fn2("Setup.run")(function* setup() {
19645
19700
  return yield* log("Local Convex setup complete.");
19646
19701
  });
19647
19702
 
19703
+ // src/sync-dev.ts
19704
+ var failure = (cause) => new UserError({ cause });
19705
+ var reserved = /^(?:CONVEX_|DOTENV_|NEXT_PUBLIC_|PUBLIC_|VITE_|REACT_APP_|EXPO_PUBLIC_|VERCEL_|GITHUB_|NODE_|BUN_|AFFECT_|CI$|PATH$|HOME$|PWD$|SHELL$|ENV$|TMPDIR$|(?:HTTPS?|ALL|NO)_PROXY$)/u;
19706
+ var SettingName = String5.check(makeFilter2((name) => !reserved.test(name)));
19707
+ var Declaration2 = Record(String5, Struct({
19708
+ isConvexValidator: Literal2(true),
19709
+ isOptional: Literals(["required", "optional"])
19710
+ }));
19711
+ var Origin = URLFromString.check(makeFilter2((url) => url.protocol === "https:" && url.username === "" && url.password === "" && url.pathname === "/" && url.search === "" && url.hash === ""));
19712
+ var Profile = Struct({
19713
+ CONVEX_DEPLOY_KEY: String5.check(isPattern2(/^dev:[a-z0-9-]+\|.+$/u)),
19714
+ NEXT_PUBLIC_CONVEX_SITE_URL: Origin,
19715
+ NEXT_PUBLIC_CONVEX_URL: Origin
19716
+ });
19717
+ var Slug = String5.check(isPattern2(/^[a-z0-9][a-z0-9_-]*$/u));
19718
+ var Id = Int.check(isGreaterThan2(0));
19719
+ var Identity = Struct({ project: Slug, projectId: Id, team: Slug, teamId: Id });
19720
+ var Deployment = Struct({
19721
+ deploymentType: Literal2("dev"),
19722
+ id: Id,
19723
+ kind: Literal2("cloud"),
19724
+ projectId: Id,
19725
+ teamId: Id
19726
+ });
19727
+ var CanonicalUrls = Struct({ convexCloudUrl: Origin, convexSiteUrl: Origin });
19728
+ var Existing = Struct({
19729
+ environmentVariables: Record(String5, String5)
19730
+ });
19731
+ var request = fn2("DevelopmentEnv.request")(function* request(url, authorization, body) {
19732
+ const client = yield* HttpClient;
19733
+ const base = setHeader(body === undefined ? get4(url) : post(url), "Authorization", authorization);
19734
+ const outgoing = body === undefined ? base : yield* bodyJson(base, body);
19735
+ const response = yield* client.execute(outgoing);
19736
+ if (response.status !== 200) {
19737
+ return yield* failure("Development environment request failed.");
19738
+ }
19739
+ return response;
19740
+ }, mapError2(() => failure("Development environment request failed.")));
19741
+ var json2 = (schema, response) => response.json.pipe(flatMap3(decodeUnknownEffect2(schema)), mapError2(() => failure("Invalid development deployment metadata.")));
19742
+ var syncDev = fn2("DevelopmentEnv.sync")(function* syncDev(force) {
19743
+ const path = yield* Path;
19744
+ const fs = yield* FileSystem;
19745
+ const file = path.resolve("env", "development", ".env");
19746
+ if (!(yield* fs.exists(file))) {
19747
+ return yield* failure("Missing env/development/.env.");
19748
+ }
19749
+ yield* NonEmptyString2("DOTENV_PRIVATE_KEY").pipe(mapError2(() => failure("Set the matching development DOTENV_PRIVATE_KEY.")));
19750
+ const values = yield* read2(file);
19751
+ const profile = yield* decodeUnknownEffect2(Profile)(values).pipe(mapError2(() => failure("Development profile requires a dev-scoped key and HTTPS Convex origins.")));
19752
+ const name = profile.CONVEX_DEPLOY_KEY.split("|")[0]?.slice(4);
19753
+ if ((values["CONVEX_DEPLOYMENT"] ?? "") !== "" && values["CONVEX_DEPLOYMENT"] !== `dev:${name}`) {
19754
+ return yield* failure("CONVEX_DEPLOYMENT does not match the development key.");
19755
+ }
19756
+ const declarationModule = yield* try_3({
19757
+ catch: () => failure("Cannot load the declaration-only convex/env.config.ts."),
19758
+ try: () => decodeUnknownEffect2(Struct({ default: Declaration2 }))(import.meta.require(path.resolve("convex", "env.config.ts")))
19759
+ }).pipe(flatten3, mapError2(() => failure("Cannot load a valid declaration-only convex/env.config.ts with Convex validators.")));
19760
+ const declaration = declarationModule.default;
19761
+ yield* decodeEffect2(ArraySchema(SettingName))(keys(declaration)).pipe(mapError2(() => failure("Server declaration contains a credential, control or client-public name.")));
19762
+ const entries = toEntries(declaration);
19763
+ const missing = filter(entries, ([key, validator]) => validator.isOptional === "required" && !has(values, key));
19764
+ if (missing.length > 0) {
19765
+ return yield* failure(`Missing required server settings: ${join(map3(missing, ([key]) => key), ", ")}.`);
19766
+ }
19767
+ const selected = filterMap(entries, ([key]) => {
19768
+ const value = has(values, key) ? values[key] : undefined;
19769
+ return value === undefined ? failVoid : succeed2({ name: key, value });
19770
+ });
19771
+ const key = profile.CONVEX_DEPLOY_KEY;
19772
+ const bearer = `Bearer ${key}`;
19773
+ const lookup = { deployKey: key };
19774
+ const cloud = yield* json2(Origin, yield* request("https://api.convex.dev/api/deployment/url_for_key", bearer, lookup));
19775
+ if (cloud.origin !== profile.NEXT_PUBLIC_CONVEX_URL.origin) {
19776
+ return yield* failure("Development key and public Convex URL do not match.");
19777
+ }
19778
+ const identity = yield* json2(Identity, yield* request("https://api.convex.dev/api/deployment/team_and_project_for_key", bearer, lookup));
19779
+ const authorization = `Convex ${key}`;
19780
+ const deployment = yield* json2(Deployment, yield* request(`${cloud.origin}/api/v1/deployment_info`, authorization));
19781
+ if (deployment.teamId !== identity.teamId || deployment.projectId !== identity.projectId) {
19782
+ return yield* failure("Development deployment identity does not match the key.");
19783
+ }
19784
+ const urls = yield* json2(CanonicalUrls, yield* request(`${cloud.origin}/api/v1/get_canonical_urls`, authorization));
19785
+ if (urls.convexCloudUrl.origin !== cloud.origin || urls.convexSiteUrl.origin !== profile.NEXT_PUBLIC_CONVEX_SITE_URL.origin) {
19786
+ return yield* failure("Development canonical Convex URLs do not match the profile.");
19787
+ }
19788
+ yield* error(`Convex target: ${identity.team}/${identity.project}; development ${name} (ID ${deployment.id}).`);
19789
+ const existing = yield* json2(Existing, yield* request(`${cloud.origin}/api/v1/list_environment_variables`, authorization));
19790
+ const changed = filter(selected, (entry) => !has(existing.environmentVariables, entry.name) || existing.environmentVariables[entry.name] !== entry.value);
19791
+ const conflicts = filter(changed, (entry) => has(existing.environmentVariables, entry.name));
19792
+ if (!force && conflicts.length > 0) {
19793
+ return yield* failure(`Existing settings differ: ${join(map3(conflicts, (entry) => entry.name), ", ")}. Use --force to overwrite them.`);
19794
+ }
19795
+ if (changed.length > 0) {
19796
+ yield* request(`${cloud.origin}/api/v1/update_environment_variables`, authorization, {
19797
+ changes: changed
19798
+ });
19799
+ }
19800
+ return yield* log(`Development settings: ${changed.length - conflicts.length} added, ${conflicts.length} updated, ${selected.length - changed.length} unchanged.`);
19801
+ }, provide3(layer11), provideService2(RequestInit, { redirect: "error" }));
19802
+
19648
19803
  // src/index.ts
19649
19804
  var program = make30("affect").pipe(withDescription3("Environment and deployment commands for Affect applications."), withSubcommands([
19650
- make30("env", {}, () => exports().pipe(flatMap3(log))).pipe(withDescription3("Print shell exports for the selected encrypted environment.")),
19805
+ make30("env", {}, () => exports().pipe(flatMap3(log))).pipe(withDescription3("Print shell exports for the selected encrypted environment."), withSubcommands([
19806
+ make30("sync-dev", { force: Boolean6("force").pipe(withDefault5(false)) }, ({ force }) => syncDev(force)).pipe(withDescription3("Sync declared server settings to the verified hosted development backend; --force overwrites conflicts. No deployment."))
19807
+ ])),
19651
19808
  make30("run", {
19652
19809
  command: String10("command"),
19653
19810
  params: String10("args").pipe(variadic2())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "affect-cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Affect command-line interface",
5
5
  "homepage": "https://github.com/ludicroushq/affect-cli#readme",
6
6
  "bugs": {