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.
- package/README.md +16 -1
- package/dist/index.js +161 -4
- 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
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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())
|