eraspace 0.2.0 → 0.4.0

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/bin.js CHANGED
@@ -241,6 +241,34 @@ __export(Effect_exports, {
241
241
  zipWith: () => zipWith3
242
242
  });
243
243
 
244
+ // node_modules/.pnpm/effect@4.0.5/node_modules/effect/dist/Context.js
245
+ var Context_exports = {};
246
+ __export(Context_exports, {
247
+ Reference: () => Reference,
248
+ Service: () => Service,
249
+ ServiceTypeId: () => ServiceTypeId,
250
+ add: () => add,
251
+ addOrOmit: () => addOrOmit,
252
+ addUnsafe: () => addUnsafe,
253
+ empty: () => empty,
254
+ get: () => get,
255
+ getOption: () => getOption,
256
+ getOrElse: () => getOrElse2,
257
+ getOrUndefined: () => getOrUndefined2,
258
+ getOrUndefinedUnsafe: () => getOrUndefinedUnsafe,
259
+ getUnsafe: () => getUnsafe,
260
+ hasSameCache: () => hasSameCache,
261
+ isContext: () => isContext,
262
+ isKey: () => isKey,
263
+ isReference: () => isReference,
264
+ make: () => make5,
265
+ makeUnsafe: () => makeUnsafe,
266
+ merge: () => merge,
267
+ mergeAll: () => mergeAll,
268
+ omit: () => omit,
269
+ pick: () => pick
270
+ });
271
+
244
272
  // node_modules/.pnpm/effect@4.0.5/node_modules/effect/dist/Pipeable.js
245
273
  var pipeArguments = (self, args2) => {
246
274
  switch (args2.length) {
@@ -2273,6 +2301,7 @@ var Proto = {
2273
2301
  };
2274
2302
  var hasSameCache = (self, that) => self.cacheRoot === that.cacheRoot;
2275
2303
  var isContext = (u) => hasProperty(u, TypeId3);
2304
+ var isKey = (u) => hasProperty(u, ServiceTypeId);
2276
2305
  var isReference = (u) => !!u[ReferenceTypeId];
2277
2306
  var empty = () => emptyContext2;
2278
2307
  var emptyContext2 = /* @__PURE__ */ makeUnsafe(/* @__PURE__ */ new Map());
@@ -2293,6 +2322,12 @@ var addUnsafe = (self, key, service4) => {
2293
2322
  parent: impl.overlay
2294
2323
  }, impl.depth + 1);
2295
2324
  };
2325
+ var addOrOmit = /* @__PURE__ */ dual(3, (self, key, service4) => service4._tag === "None" ? omit(key)(self) : add(self, key, service4.value));
2326
+ var getOrElse2 = /* @__PURE__ */ dual(3, (self, key, orElse6) => {
2327
+ const value4 = lookup(self, key.key);
2328
+ if (value4 !== notFound) return value4;
2329
+ return isReference(key) ? getDefaultValue(key) : orElse6();
2330
+ });
2296
2331
  var getOrUndefined2 = /* @__PURE__ */ dual(2, (self, key) => getOrUndefinedUnsafe(self, key.key));
2297
2332
  var getOrUndefinedUnsafe = (self, key) => {
2298
2333
  const value4 = lookup(self, key);
@@ -2342,6 +2377,26 @@ var mergeAll = (...ctxs) => {
2342
2377
  }
2343
2378
  return makeUnsafe(map18);
2344
2379
  };
2380
+ var pick = (...services) => (self) => {
2381
+ if (services.length <= 1) {
2382
+ const map18 = /* @__PURE__ */ new Map();
2383
+ if (services.length === 1) {
2384
+ const key = services[0].key;
2385
+ const value4 = lookup(self, key);
2386
+ if (value4 !== notFound) map18.set(key, value4);
2387
+ }
2388
+ return makeUnsafe(map18);
2389
+ }
2390
+ const keep = new Set(services.map((key) => key.key));
2391
+ return withFlat(self, (map18) => map18.forEach((_, key) => {
2392
+ if (!keep.has(key)) map18.delete(key);
2393
+ }));
2394
+ };
2395
+ var omit = (...keys4) => (self) => withFlat(self, (map18) => {
2396
+ for (let i = 0; i < keys4.length; i++) {
2397
+ map18.delete(keys4[i].key);
2398
+ }
2399
+ });
2345
2400
  var Reference = Service;
2346
2401
 
2347
2402
  // node_modules/.pnpm/effect@4.0.5/node_modules/effect/dist/Duration.js
@@ -2988,7 +3043,7 @@ var merge2 = /* @__PURE__ */ match3({
2988
3043
  onFailure: identity,
2989
3044
  onSuccess: identity
2990
3045
  });
2991
- var getOrElse2 = /* @__PURE__ */ dual(2, (self, onFailure) => isFailure2(self) ? onFailure(self.failure) : self.success);
3046
+ var getOrElse3 = /* @__PURE__ */ dual(2, (self, onFailure) => isFailure2(self) ? onFailure(self.failure) : self.success);
2992
3047
  var getOrThrowWith2 = /* @__PURE__ */ dual(2, (self, onFailure) => {
2993
3048
  if (isSuccess2(self)) {
2994
3049
  return self.success;
@@ -16361,7 +16416,7 @@ function transformOptionalEffect(f) {
16361
16416
  transform: f
16362
16417
  });
16363
16418
  }
16364
- function omit() {
16419
+ function omit2() {
16365
16420
  return transformOptional(() => none2());
16366
16421
  }
16367
16422
  function withDefault(defaultValue) {
@@ -18565,9 +18620,9 @@ function isPattern(regExp, annotations) {
18565
18620
  });
18566
18621
  }
18567
18622
  var bodyOwners = /* @__PURE__ */ new WeakMap();
18568
- function copy2(ast, changes2) {
18569
- const out = Object.assign(Object.create(Object.getPrototypeOf(ast)), ast, changes2);
18570
- if (Reflect.ownKeys(changes2).every((key) => key === "context" || key === "encoding")) {
18623
+ function copy2(ast, changes3) {
18624
+ const out = Object.assign(Object.create(Object.getPrototypeOf(ast)), ast, changes3);
18625
+ if (Reflect.ownKeys(changes3).every((key) => key === "context" || key === "encoding")) {
18571
18626
  bodyOwners.set(out, getContextOwner(ast));
18572
18627
  }
18573
18628
  return out;
@@ -24947,10 +25002,10 @@ function toFormatter(ast, options3) {
24947
25002
  }
24948
25003
 
24949
25004
  // node_modules/.pnpm/effect@4.0.5/node_modules/effect/dist/Struct.js
24950
- var pick = /* @__PURE__ */ dual(2, (self, keys4) => {
25005
+ var pick2 = /* @__PURE__ */ dual(2, (self, keys4) => {
24951
25006
  return buildStruct(self, (k, v) => hasPropertyKey(keys4, k) ? [k, v] : void 0);
24952
25007
  });
24953
- var omit2 = /* @__PURE__ */ dual(2, (self, keys4) => {
25008
+ var omit3 = /* @__PURE__ */ dual(2, (self, keys4) => {
24954
25009
  return buildStruct(self, (k, v) => !hasPropertyKey(keys4, k) ? [k, v] : void 0);
24955
25010
  });
24956
25011
  var assign = /* @__PURE__ */ dual(2, (self, that) => {
@@ -25070,10 +25125,10 @@ var OptionalImpl = class {
25070
25125
  this.replaceResult = replaceResult;
25071
25126
  }
25072
25127
  replace(a, s) {
25073
- return getOrElse2(this.replaceResult(a, s), () => s);
25128
+ return getOrElse3(this.replaceResult(a, s), () => s);
25074
25129
  }
25075
25130
  modify(f) {
25076
- return (s) => getOrElse2(flatMap2(this.getResult(s), (a) => this.replaceResult(f(a), s)), () => s);
25131
+ return (s) => getOrElse3(flatMap2(this.getResult(s), (a) => this.replaceResult(f(a), s)), () => s);
25077
25132
  }
25078
25133
  compose(that) {
25079
25134
  return make36(compose3(this.node, that.node));
@@ -25121,14 +25176,14 @@ var OptionalImpl = class {
25121
25176
  })));
25122
25177
  }
25123
25178
  pick(keys4) {
25124
- return this.compose(makeLens(pick(keys4), (p, a) => ({
25125
- ...omit2(a, keys4),
25179
+ return this.compose(makeLens(pick2(keys4), (p, a) => ({
25180
+ ...omit3(a, keys4),
25126
25181
  ...p
25127
25182
  })));
25128
25183
  }
25129
25184
  omit(keys4) {
25130
- return this.compose(makeLens(omit2(keys4), (o, a) => ({
25131
- ...pick(a, keys4),
25185
+ return this.compose(makeLens(omit3(keys4), (o, a) => ({
25186
+ ...pick2(a, keys4),
25132
25187
  ...o
25133
25188
  })));
25134
25189
  }
@@ -25174,7 +25229,7 @@ var OptionalImpl = class {
25174
25229
  );
25175
25230
  }
25176
25231
  modifyAll(f) {
25177
- return (s) => getOrElse2(flatMap2(this.getResult(s), (as5) => this.replaceResult(as5.map(f), s)), () => s);
25232
+ return (s) => getOrElse3(flatMap2(this.getResult(s), (as5) => this.replaceResult(as5.map(f), s)), () => s);
25178
25233
  }
25179
25234
  };
25180
25235
  var IsoImpl = class extends OptionalImpl {
@@ -25213,7 +25268,7 @@ var PrismImpl = class extends OptionalImpl {
25213
25268
  return this.set(a);
25214
25269
  }
25215
25270
  modify(f) {
25216
- return (s) => getOrElse2(map2(this.getResult(s), (a) => this.set(f(a))), () => s);
25271
+ return (s) => getOrElse3(map2(this.getResult(s), (a) => this.set(f(a))), () => s);
25217
25272
  }
25218
25273
  };
25219
25274
  function make36(node) {
@@ -26001,7 +26056,7 @@ function toIssueEffect(self) {
26001
26056
  return catchCause3(self, (cause) => failCauseSync2(() => map6(cause, (error2) => error2.issue)));
26002
26057
  }
26003
26058
  function withDecodingDefaultKey(defaultValue, options3) {
26004
- const encode5 = options3?.encodingStrategy === "omit" ? omit() : passthrough2();
26059
+ const encode5 = options3?.encodingStrategy === "omit" ? omit2() : passthrough2();
26005
26060
  return (self) => {
26006
26061
  return optionalKey2(toEncoded2(self)).pipe(decodeTo2(self, {
26007
26062
  decode: withDefault(toIssueEffect(defaultValue)),
@@ -26015,7 +26070,7 @@ function withDecodingDefaultTypeKey(defaultValue, options3) {
26015
26070
  };
26016
26071
  }
26017
26072
  function withDecodingDefault(defaultValue, options3) {
26018
- const encode5 = options3?.encodingStrategy === "omit" ? omit() : passthrough2();
26073
+ const encode5 = options3?.encodingStrategy === "omit" ? omit2() : passthrough2();
26019
26074
  return (self) => {
26020
26075
  return optional2(toEncoded2(self)).pipe(decodeTo2(self, {
26021
26076
  decode: withDefault(toIssueEffect(defaultValue)),
@@ -32824,15 +32879,15 @@ var TomlParser = class {
32824
32879
  }
32825
32880
  parseDate(token) {
32826
32881
  const date = "\\d{4}-\\d{2}-\\d{2}";
32827
- const time3 = "\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?";
32828
- if (new RegExp(`^${date}[Tt]${time3}(?:[Zz]|[+-]\\d{2}:\\d{2})$`).test(token)) {
32882
+ const time4 = "\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?";
32883
+ if (new RegExp(`^${date}[Tt]${time4}(?:[Zz]|[+-]\\d{2}:\\d{2})$`).test(token)) {
32829
32884
  const value4 = new Date(token.replace("t", "T").replace("z", "Z"));
32830
32885
  if (Number.isNaN(value4.getTime())) {
32831
32886
  this.fail(`Invalid date-time '${token}'`);
32832
32887
  }
32833
32888
  return value4;
32834
32889
  }
32835
- if (new RegExp(`^${date}[Tt]${time3}$`).test(token) || new RegExp(`^${date}$`).test(token) || new RegExp(`^${time3}$`).test(token)) {
32890
+ if (new RegExp(`^${date}[Tt]${time4}$`).test(token) || new RegExp(`^${date}$`).test(token) || new RegExp(`^${time4}$`).test(token)) {
32836
32891
  return token.replace("t", "T");
32837
32892
  }
32838
32893
  return void 0;
@@ -38939,7 +38994,7 @@ var runWith2 = (command2, config2) => {
38939
38994
  // cli/package.json
38940
38995
  var package_default = {
38941
38996
  name: "eraspace-cli",
38942
- version: "0.2.0",
38997
+ version: "0.4.0",
38943
38998
  private: true,
38944
38999
  license: "MIT",
38945
39000
  type: "module",
@@ -39259,12 +39314,54 @@ function findSpecifier(source, spec) {
39259
39314
  }
39260
39315
  function formatLog(l) {
39261
39316
  const d = l.ts === void 0 ? /* @__PURE__ */ new Date() : new Date(typeof l.ts === "number" ? l.ts : Date.parse(l.ts));
39262
- const time3 = isNaN(d.getTime()) ? String(l.ts) : d.toISOString().slice(11, 23);
39317
+ const time4 = isNaN(d.getTime()) ? String(l.ts) : d.toISOString().slice(11, 23);
39263
39318
  const level = (l.level ?? "info").toLowerCase();
39264
39319
  const color = level === "error" ? c.red : level === "warn" ? c.yellow : level === "debug" ? c.gray : c.blue;
39265
39320
  const fn3 = l.fn ? c.magenta(l.fn) + " " : "";
39266
39321
  const data = l.data === void 0 || l.data === null ? "" : " " + c.gray(JSON.stringify(l.data));
39267
- return `${c.gray(time3)} ${color(level.padEnd(5))} ${fn3}${l.msg ?? ""}${data}`;
39322
+ return `${c.gray(time4)} ${color(level.padEnd(5))} ${fn3}${l.msg ?? ""}${data}`;
39323
+ }
39324
+ var SKIPPED = {
39325
+ fetch: "uses fetch",
39326
+ identity: "reads the caller's identity",
39327
+ running: "was running",
39328
+ recovering: "cache recovering",
39329
+ "new index": "reads an index this deploy adds",
39330
+ committed: "a commit changed it during the check",
39331
+ budget: "out of time",
39332
+ limit: "too many entries",
39333
+ interrupted: "interrupted"
39334
+ };
39335
+ var call = (fn3, args2) => {
39336
+ const text2 = Array.isArray(args2) ? args2.map((a) => JSON.stringify(a)).join(", ") : JSON.stringify(args2);
39337
+ return `${fn3}(${text2 && text2.length > 80 ? `${text2.slice(0, 77)}...` : text2 ?? ""})`;
39338
+ };
39339
+ function formatShadow(report) {
39340
+ if (report.note) return c.dim(`Live queries: none checked (${report.note})`);
39341
+ const total = report.total ?? 0;
39342
+ if (total === 0) return c.dim("Live queries: none cached, nothing to check");
39343
+ const errors = report.errors ?? 0;
39344
+ const changed = report.changed ?? 0;
39345
+ const parts2 = [
39346
+ `${report.unchanged ?? 0} unchanged`,
39347
+ changed ? c.yellow(`${changed} change value`) : "0 change value",
39348
+ errors ? c.red(`${errors} now throw`) : "0 now throw",
39349
+ ...report.fixed ? [c.green(`${report.fixed} fixed`)] : [],
39350
+ ...report.skipped ? [c.dim(`${report.skipped} skipped`)] : []
39351
+ ];
39352
+ const at = report.commitSeq ? ` at commit ${report.commitSeq}` : "";
39353
+ const lines2 = [`Live queries: ${report.checked ?? 0} of ${total} checked against the new code${at}: ${parts2.join(", ")}`];
39354
+ for (const e of report.entries ?? []) {
39355
+ const what = call(e.fn, e.args);
39356
+ if (e.outcome === "error") lines2.push(` ${c.red("throws ")} ${what}: ${e.error ? `${e.error.name}: ${e.error.message}` : ""}`);
39357
+ else if (e.outcome === "changed") lines2.push(` ${c.yellow("changes")} ${what}${e.error ? `: ${e.error.name}: ${e.error.message}` : ""}`);
39358
+ else if (e.outcome === "fixed") lines2.push(` ${c.green("fixed ")} ${what}`);
39359
+ else lines2.push(c.dim(` skipped ${what}: ${SKIPPED[e.reason ?? ""] ?? e.reason ?? ""}`));
39360
+ }
39361
+ const listed = report.entries?.length ?? 0;
39362
+ const notable = errors + changed + (report.fixed ?? 0) + (report.skipped ?? 0);
39363
+ if (notable > listed) lines2.push(c.dim(` ... and ${notable - listed} more`));
39364
+ return lines2.join("\n");
39268
39365
  }
39269
39366
 
39270
39367
  // cli/src/api.ts
@@ -39280,10 +39377,40 @@ var App = Schema_exports.Struct({
39280
39377
  expiresAt: Schema_exports.optional(Timestamp),
39281
39378
  publicInspection: Schema_exports.optional(Schema_exports.Boolean)
39282
39379
  });
39380
+ var Count = Schema_exports.optional(Schema_exports.Number);
39381
+ var ShadowReport = Schema_exports.Struct({
39382
+ total: Count,
39383
+ checked: Count,
39384
+ unchanged: Count,
39385
+ changed: Count,
39386
+ errors: Count,
39387
+ fixed: Count,
39388
+ skipped: Count,
39389
+ commitSeq: Count,
39390
+ note: Schema_exports.optional(Schema_exports.String),
39391
+ entries: Schema_exports.optional(
39392
+ Schema_exports.Array(
39393
+ Schema_exports.Struct({
39394
+ fn: Schema_exports.String,
39395
+ args: Schema_exports.optional(Schema_exports.Unknown),
39396
+ outcome: Schema_exports.String,
39397
+ reason: Schema_exports.optional(Schema_exports.String),
39398
+ error: Schema_exports.optional(Schema_exports.Struct({ name: Schema_exports.String, message: Schema_exports.String }))
39399
+ })
39400
+ )
39401
+ )
39402
+ });
39283
39403
  var Deployed = Schema_exports.Struct({
39284
39404
  ...App.fields,
39285
39405
  version: Schema_exports.optional(Schema_exports.Number),
39286
- claimToken: Schema_exports.optional(Schema_exports.String)
39406
+ claimToken: Schema_exports.optional(Schema_exports.String),
39407
+ shadow: Schema_exports.optional(ShadowReport),
39408
+ /** For a rollback: the kept version whose files were deployed again. */
39409
+ rolledBackTo: Schema_exports.optional(Schema_exports.Number)
39410
+ });
39411
+ var DeployHistory = Schema_exports.Struct({
39412
+ version: Schema_exports.optional(Schema_exports.Number),
39413
+ deploys: Schema_exports.optional(Schema_exports.Array(Schema_exports.Struct({ version: Schema_exports.Number, createdAt: Schema_exports.optional(Schema_exports.Number) })))
39287
39414
  });
39288
39415
  var AppList = Schema_exports.Struct({ apps: Schema_exports.Array(App) });
39289
39416
  var StateChange = Schema_exports.Struct({ state: Schema_exports.String });
@@ -39300,7 +39427,7 @@ var Token2 = Schema_exports.Struct({
39300
39427
  lastUsedAt: Schema_exports.optional(Timestamp)
39301
39428
  });
39302
39429
  var TokenList = Schema_exports.Struct({ tokens: Schema_exports.Array(Token2) });
39303
- var CreatedToken = Schema_exports.Struct({ token: Schema_exports.String });
39430
+ var CreatedToken = Schema_exports.Struct({ token: Schema_exports.String, id: Schema_exports.String });
39304
39431
  var DeviceStart = Schema_exports.Struct({
39305
39432
  deviceCode: Schema_exports.String,
39306
39433
  userCode: Schema_exports.String,
@@ -39448,15 +39575,20 @@ var login = Effect_exports.fnUntraced(function* (config2, browser) {
39448
39575
  }
39449
39576
  return yield* fail12("The sign-in code expired. Run eraspace auth login again.");
39450
39577
  });
39578
+ var LogoutResult = Schema_exports.Struct({ ok: Schema_exports.Literal(true) });
39451
39579
  var logout = Effect_exports.fnUntraced(function* (config2) {
39452
39580
  const session = yield* sessionFor(config2.origin);
39453
39581
  if (!session) {
39454
39582
  yield* Console_exports.log(`Not signed in to ${config2.origin}.`);
39455
39583
  return;
39456
39584
  }
39457
- yield* request3(config2, "POST", "/_api/auth/logout", { bearer: Redacted_exports.make(session.token) }).pipe(Effect_exports.timeout("10 seconds"), Effect_exports.ignore);
39585
+ const response = yield* api(config2, "POST", "/_api/auth/logout", { bearer: Redacted_exports.make(session.token), timeout: "10 seconds" });
39586
+ if (response.status !== 401) {
39587
+ if (response.status >= 400 || response.json.ok === false) return yield* Effect_exports.fail(yield* failure(response));
39588
+ yield* decode5(LogoutResult, response.json, "sign-out");
39589
+ }
39458
39590
  yield* removeSession(config2.origin);
39459
- yield* Console_exports.log(`Signed out of ${config2.origin}.`);
39591
+ yield* Console_exports.log(response.status === 401 ? `Removed invalid session for ${config2.origin}.` : `Signed out of ${config2.origin}.`);
39460
39592
  });
39461
39593
  var okWhoami = ok(Whoami);
39462
39594
  var whoami = Effect_exports.fnUntraced(function* (config2) {
@@ -39491,6 +39623,7 @@ function scanImports(file2, source) {
39491
39623
  }
39492
39624
  return { imports: imports.sort((a, b) => (a.at ?? 0) - (b.at ?? 0)) };
39493
39625
  }
39626
+ var MAX_MODULE_BYTES = 256 * 1024;
39494
39627
  var EXTENSIONS = [".ts", ".mts", ".js", ".mjs", ".tsx"];
39495
39628
  function importCandidates(base) {
39496
39629
  const stem = /\.m?js$/.test(base) ? base.replace(/\.m?js$/, "") : void 0;
@@ -39526,6 +39659,10 @@ var collectServerFiles = Effect_exports.fnUntraced(function* (root) {
39526
39659
  const rel = toPosix(relative2(root, abs2));
39527
39660
  const source = yield* fs.readFileString(abs2);
39528
39661
  files[rel] = source;
39662
+ const bytes2 = Buffer.byteLength(source, "utf8");
39663
+ if (bytes2 > MAX_MODULE_BYTES) {
39664
+ errors.push({ file: rel, line: 0, column: 0, message: `module is ${bytes2} bytes, over the server's ${MAX_MODULE_BYTES} byte (256 KiB) limit; split it into smaller modules` });
39665
+ }
39529
39666
  const { imports, error: error2 } = scanImports(abs2, source);
39530
39667
  if (error2) {
39531
39668
  errors.push({ file: rel, ...lineCol(source, error2.at), message: error2.message });
@@ -39804,7 +39941,6 @@ var withoutEnv = Effect_exports.fnUntraced(function* (payload) {
39804
39941
  }
39805
39942
  return { ...payload, env: {} };
39806
39943
  });
39807
- var okDeployed = ok(Deployed);
39808
39944
  var requestedName = (config2, opts) => opts.name !== void 0 ? { name: opts.name } : { name: config2.name ?? toAppName(basename(config2.root)), uniqueName: true };
39809
39945
  var createApp = Effect_exports.fnUntraced(function* (config2, payload, extra, opts, signedIn) {
39810
39946
  const naming = requestedName(config2, opts);
@@ -39831,29 +39967,37 @@ var createApp = Effect_exports.fnUntraced(function* (config2, payload, extra, op
39831
39967
  return { result: result4, name: naming.name };
39832
39968
  });
39833
39969
  var deploy = Effect_exports.fnUntraced(function* (config2, opts = {}) {
39970
+ const t = yield* target(config2);
39971
+ if (t.ref && opts.name !== void 0)
39972
+ return yield* fail12(
39973
+ t.claim ? `--name only names a new app, and this checkout already deploys to ${t.ref}. Name it when you claim it: eraspace claim --name ${opts.name}` : `--name only names a new app, and this checkout already deploys to ${t.ref}. Add the name with eraspace names add ${opts.name}`
39974
+ );
39834
39975
  const build3 = yield* buildCapsule(config2.root);
39835
39976
  if (!build3.ok || !build3.payload)
39836
39977
  return yield* fail12(yield* formatErrors(build3.errors, config2.root));
39837
39978
  const deleteTables = yield* confirmedDeletes(opts.deleteTables);
39838
39979
  const extra = {
39839
39980
  ...opts.publicInspect ? { publicInspection: true } : {},
39840
- ...Object.keys(deleteTables).length ? { deleteTables } : {}
39981
+ ...Object.keys(deleteTables).length ? { deleteTables } : {},
39982
+ ...opts.shadow ? { shadow: true } : {},
39983
+ ...opts.failOnQueryErrors ? { failOnQueryErrors: true } : {}
39841
39984
  };
39842
39985
  const signedIn = (yield* bearerFor(config2.origin)) !== void 0;
39843
- const t = yield* target(config2);
39844
39986
  let result4;
39845
39987
  if (t.ref) {
39846
39988
  const payload = t.claim ? yield* withoutEnv(build3.payload) : build3.payload;
39847
- result4 = yield* okDeployed(
39848
- config2,
39849
- "POST",
39850
- `/_api/apps/${enc(t.ref)}/deploy`,
39851
- {
39852
- body: { ...payload, ...extra },
39853
- claim: t.claim,
39854
- timeout: DEPLOY_TIMEOUT
39855
- }
39856
- );
39989
+ const path = `/_api/apps/${enc(t.ref)}/deploy`;
39990
+ const response = yield* api(config2, "POST", path, {
39991
+ body: { ...payload, ...extra },
39992
+ claim: t.claim,
39993
+ timeout: DEPLOY_TIMEOUT
39994
+ });
39995
+ if (response.status >= 400 || response.json.ok === false) {
39996
+ const refused = Schema_exports.decodeUnknownOption(ShadowReport)(response.json.shadow);
39997
+ if (Option_exports.isSome(refused)) yield* Console_exports.log(formatShadow(refused.value));
39998
+ return yield* Effect_exports.fail(yield* failure(response, config2.root));
39999
+ }
40000
+ result4 = yield* decode5(Deployed, response.json, `POST ${path}`);
39857
40001
  if (t.claim && result4.claimed)
39858
40002
  yield* removeLocalDeploy(config2.root, config2.origin);
39859
40003
  } else if (signedIn) {
@@ -39878,6 +40022,7 @@ var deploy = Effect_exports.fnUntraced(function* (config2, opts = {}) {
39878
40022
  expiresAt: Number.isFinite(expiresAt) ? expiresAt : null
39879
40023
  });
39880
40024
  }
40025
+ if (result4.shadow) yield* Console_exports.log(formatShadow(result4.shadow));
39881
40026
  const label = result4.names?.[0] ?? result4.id;
39882
40027
  yield* Console_exports.log(
39883
40028
  `${label}${result4.version === void 0 ? "" : ` v${result4.version}`}: ${result4.url ?? config2.url}`
@@ -39900,6 +40045,40 @@ var deploy = Effect_exports.fnUntraced(function* (config2, opts = {}) {
39900
40045
  }
39901
40046
  return result4;
39902
40047
  });
40048
+ var time2 = (ms) => ms === void 0 ? "" : new Date(ms).toISOString();
40049
+ var deploys = Effect_exports.fnUntraced(function* (config2, explicit) {
40050
+ const t = yield* requireTarget(config2, explicit);
40051
+ const history = yield* ok(DeployHistory)(config2, "GET", `/_api/apps/${enc(t.ref)}`, { claim: t.claim });
40052
+ const kept = history.deploys ?? [];
40053
+ if (!kept.length) return yield* Console_exports.log(`${t.ref} has no deploys.`);
40054
+ for (const d of kept) {
40055
+ const serving = d.version === history.version ? ` ${c.green("serving")}` : "";
40056
+ yield* Console_exports.log(`v${d.version} ${c.dim(time2(d.createdAt))}${serving}`);
40057
+ }
40058
+ if (kept.length >= 50) yield* Console_exports.log(c.dim(`Showing the latest ${kept.length}; any older kept versions still roll back by number: eraspace rollback <version>`));
40059
+ });
40060
+ var rollback = Effect_exports.fnUntraced(function* (config2, version, opts = {}) {
40061
+ const t = yield* requireTarget(config2);
40062
+ const deleteTables = yield* confirmedDeletes(opts.deleteTables);
40063
+ const body = {
40064
+ ...version === void 0 ? {} : { version },
40065
+ ...Object.keys(deleteTables).length ? { deleteTables } : {},
40066
+ ...opts.shadow ? { shadow: true } : {},
40067
+ ...opts.failOnQueryErrors ? { failOnQueryErrors: true } : {}
40068
+ };
40069
+ const path = `/_api/apps/${enc(t.ref)}/rollback`;
40070
+ const response = yield* api(config2, "POST", path, { body, claim: t.claim, timeout: DEPLOY_TIMEOUT });
40071
+ if (response.status >= 400 || response.json.ok === false) {
40072
+ const refused = Schema_exports.decodeUnknownOption(ShadowReport)(response.json.shadow);
40073
+ if (Option_exports.isSome(refused)) yield* Console_exports.log(formatShadow(refused.value));
40074
+ return yield* Effect_exports.fail(yield* failure(response, config2.root));
40075
+ }
40076
+ const result4 = yield* decode5(Deployed, response.json, `POST ${path}`);
40077
+ if (result4.shadow) yield* Console_exports.log(formatShadow(result4.shadow));
40078
+ const label = result4.names?.[0] ?? result4.id;
40079
+ yield* Console_exports.log(`${label} v${String(result4.version)}: rolled back to v${String(result4.rolledBackTo)}: ${result4.url ?? config2.url}`);
40080
+ return result4;
40081
+ });
39903
40082
  var claimRequest = (config2, path, body, claim2) => api(config2, "POST", path, { body, claim: claim2 });
39904
40083
  var claim = Effect_exports.fnUntraced(function* (config2, name) {
39905
40084
  const local = yield* localDeploy(config2.root, config2.origin);
@@ -39947,10 +40126,14 @@ var IGNORED = /(^|[/\\])(node_modules|\.git|\.eraspace)([/\\]|$)/;
39947
40126
  var isWatched = (file2) => !IGNORED.test(file2) && WATCHED.test(file2);
39948
40127
  var dev = Effect_exports.fnUntraced(function* (config2, opts) {
39949
40128
  const fs = yield* FileSystem_exports.FileSystem;
40129
+ let current = opts;
39950
40130
  const redeploy = Effect_exports.suspend(
39951
40131
  () => loadConfig({ dir: config2.root, app: config2.app, url: config2.url })
39952
40132
  ).pipe(
39953
- Effect_exports.flatMap((current) => deploy(current, opts)),
40133
+ Effect_exports.flatMap((loaded) => deploy(loaded, current)),
40134
+ Effect_exports.tap(() => Effect_exports.sync(() => {
40135
+ current = { ...current, name: void 0 };
40136
+ })),
39954
40137
  Effect_exports.catch((error2) => Console_exports.error(messageOf(error2)))
39955
40138
  );
39956
40139
  yield* redeploy;
@@ -39964,7 +40147,10 @@ var dev = Effect_exports.fnUntraced(function* (config2, opts) {
39964
40147
  });
39965
40148
 
39966
40149
  // cli/src/logs.ts
39967
- var IDLE = Duration_exports.seconds(75);
40150
+ var LogTimeouts = Context_exports.Reference(
40151
+ "eraspace/cli/LogTimeouts",
40152
+ { defaultValue: () => ({ idle: Duration_exports.seconds(75), stalled: Duration_exports.seconds(30), errorBody: Duration_exports.seconds(10) }) }
40153
+ );
39968
40154
  var Printed = class {
39969
40155
  lines = [];
39970
40156
  limit;
@@ -40005,12 +40191,14 @@ var readOnce = Effect_exports.fnUntraced(function* (config2, path, claim2, follo
40005
40191
  const response = yield* request3(config2, "GET", path, { claim: claim2 }).pipe(
40006
40192
  Effect_exports.timeoutOrElse({ duration: "30 seconds", orElse: () => Effect_exports.fail(new Transient({ message: `The platform at ${config2.origin} did not answer within 30s.` })) })
40007
40193
  );
40194
+ const timeouts = yield* LogTimeouts;
40195
+ const errorText = response.text.pipe(Effect_exports.timeout(timeouts.errorBody), Effect_exports.orElseSucceed(() => ""));
40008
40196
  if (isGatewayError(response.status)) {
40009
- yield* response.text.pipe(Effect_exports.ignore);
40197
+ yield* errorText;
40010
40198
  return yield* new Transient({ message: `The platform at ${config2.origin} is unavailable (HTTP ${response.status}).` });
40011
40199
  }
40012
40200
  if (response.status >= 400) {
40013
- const text2 = yield* response.text.pipe(Effect_exports.orElseSucceed(() => ""));
40201
+ const text2 = yield* errorText;
40014
40202
  let json2 = { ok: false, error: text2.trim() || `HTTP ${response.status}` };
40015
40203
  try {
40016
40204
  json2 = JSON.parse(text2);
@@ -40018,12 +40206,15 @@ var readOnce = Effect_exports.fnUntraced(function* (config2, path, claim2, follo
40018
40206
  }
40019
40207
  return yield* Effect_exports.fail(yield* failure({ status: response.status, json: json2 }, config2.root));
40020
40208
  }
40021
- let lines2 = response.stream.pipe(
40209
+ const lines2 = response.stream.pipe(
40210
+ Stream_exports.mapError((error2) => new Transient({ message: `The log stream broke: ${describe(error2.cause ?? error2)}` })),
40211
+ Stream_exports.timeoutOrElse({
40212
+ duration: follow ? timeouts.idle : timeouts.stalled,
40213
+ orElse: () => Stream_exports.fail(new Transient({ message: follow ? "The log stream went quiet." : `The platform at ${config2.origin} stopped sending logs.` }))
40214
+ }),
40022
40215
  Stream_exports.decodeText(),
40023
- Stream_exports.splitLines,
40024
- Stream_exports.mapError((error2) => new Transient({ message: `The log stream broke: ${describe(error2.cause ?? error2)}` }))
40216
+ Stream_exports.splitLines
40025
40217
  );
40026
- if (follow) lines2 = lines2.pipe(Stream_exports.timeoutOrElse({ duration: IDLE, orElse: () => Stream_exports.fail(new Transient({ message: "The log stream went quiet." })) }));
40027
40218
  const fresh2 = printed.connection();
40028
40219
  yield* lines2.pipe(
40029
40220
  Stream_exports.filter((line) => line.trim() !== ""),
@@ -40050,7 +40241,7 @@ var logs = Effect_exports.fnUntraced(function* (config2, explicit, follow, limit
40050
40241
  var okApp = ok(App);
40051
40242
  var okJson = ok(JsonObject2);
40052
40243
  var okState = ok(StateChange);
40053
- var time2 = (value4) => {
40244
+ var time3 = (value4) => {
40054
40245
  if (value4 === null || value4 === void 0) return void 0;
40055
40246
  const date = new Date(typeof value4 === "number" ? value4 : Date.parse(value4));
40056
40247
  return Number.isNaN(date.getTime()) ? String(value4) : date.toISOString();
@@ -40090,12 +40281,74 @@ var lifecycle = Effect_exports.fnUntraced(function* (config2, action, explicit,
40090
40281
  const result4 = yield* okState(config2, "POST", `/_api/apps/${enc(t.ref)}/${action}`, { claim: t.claim });
40091
40282
  yield* Console_exports.log(`${t.ref}: ${result4.state}`);
40092
40283
  });
40284
+ var explain = Effect_exports.fnUntraced(function* (config2, fn3, inputs, json2) {
40285
+ const args2 = yield* parseCallArgs(inputs);
40286
+ const t = yield* requireTarget(config2);
40287
+ const report = yield* okJson(config2, "POST", `/_api/apps/${enc(t.ref)}/explain`, { body: { fn: fn3, args: args2 }, claim: t.claim });
40288
+ if (json2) return yield* Console_exports.log(JSON.stringify(report, null, 2));
40289
+ const scans = report.scans ?? [];
40290
+ yield* Console_exports.log(`${fn3}: read ${String(report.rowsRead)} row${report.rowsRead === 1 ? "" : "s"} in ${scans.length} scan${scans.length === 1 ? "" : "s"} and returned ${String(report.returned)}`);
40291
+ for (const s of scans) yield* Console_exports.log(` ${s.table} via ${s.index} (${s.mode}): read ${s.rowsRead}, kept ${s.kept} of ${s.rows}`);
40292
+ const suggestions = report.suggestions ?? [];
40293
+ if (!suggestions.length) yield* Console_exports.log(c.dim("No index suggestions."));
40294
+ for (const s of suggestions) yield* Console_exports.log(`${c.yellow("suggest")} ${s.message}`);
40295
+ });
40296
+ var profile = Effect_exports.fnUntraced(function* (config2, explicit, seconds2, out) {
40297
+ const t = yield* requireTarget(config2, explicit);
40298
+ yield* Console_exports.error(c.dim(`Profiling ${t.ref} for ${seconds2}s...`));
40299
+ const result4 = yield* okJson(config2, "POST", `/_api/apps/${enc(t.ref)}/profile`, { body: { seconds: seconds2 }, claim: t.claim, timeout: Duration_exports.seconds(seconds2 + 30) });
40300
+ const file2 = out ?? `${t.ref}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.cpuprofile`;
40301
+ const fs = yield* FileSystem_exports.FileSystem;
40302
+ yield* fs.writeFileString(file2, JSON.stringify(result4));
40303
+ const nodes = new Map((result4.nodes ?? []).map((n) => [n.id, n]));
40304
+ const counts = /* @__PURE__ */ new Map();
40305
+ let busy = 0;
40306
+ for (const id2 of result4.samples ?? []) {
40307
+ const f = nodes.get(id2)?.callFrame;
40308
+ if (!f || f.functionName === "(idle)" || f.functionName === "(root)") continue;
40309
+ busy++;
40310
+ const at = f.url ? ` ${f.url}:${f.lineNumber + 1}` : "";
40311
+ const name = `${f.functionName || "(anonymous)"}${at}`;
40312
+ counts.set(name, (counts.get(name) ?? 0) + 1);
40313
+ }
40314
+ const samples = (result4.samples ?? []).length;
40315
+ yield* Console_exports.log(`${t.ref}: busy in ${busy} of ${samples} samples. Open ${file2} in Chrome DevTools (Performance > Load profile).`);
40316
+ for (const [name, n] of [...counts].sort((a, b) => b[1] - a[1]).slice(0, 10)) {
40317
+ yield* Console_exports.log(` ${(100 * n / Math.max(busy, 1)).toFixed(1).padStart(5)}% ${name}`);
40318
+ }
40319
+ });
40320
+ var changes2 = Effect_exports.fnUntraced(function* (config2, since, limit) {
40321
+ const t = yield* requireTarget(config2);
40322
+ const response = yield* request3(config2, "GET", `/_api/apps/${enc(t.ref)}/changes?since=${since}&limit=${limit}`, { claim: t.claim });
40323
+ const text2 = yield* response.text.pipe(Effect_exports.orElseSucceed(() => ""));
40324
+ if (response.status >= 400) {
40325
+ let json2 = { ok: false, error: text2.trim() || `HTTP ${response.status}` };
40326
+ try {
40327
+ json2 = JSON.parse(text2);
40328
+ } catch {
40329
+ }
40330
+ return yield* Effect_exports.fail(yield* failure({ status: response.status, json: json2 }));
40331
+ }
40332
+ const lines2 = text2.split("\n").filter(Boolean);
40333
+ for (const line of lines2) yield* Console_exports.log(line);
40334
+ const head5 = Number(response.headers["x-eraspace-commit-seq"] ?? 0);
40335
+ const last = lines2.length ? JSON.parse(lines2.at(-1)).seq : since;
40336
+ const more = response.headers["x-eraspace-more"] === "1";
40337
+ yield* Console_exports.error(c.dim(more ? `Latest commit ${head5}; continue with --since ${last}` : `Up to date at commit ${head5}`));
40338
+ });
40339
+ var undo = Effect_exports.fnUntraced(function* (config2, to, yes) {
40340
+ const t = yield* requireTarget(config2);
40341
+ if (!yes) return yield* fail12(`eraspace undo ${to} puts every row of ${t.ref} changed after commit ${to} back as it was. Re-run with --yes.`);
40342
+ const result4 = yield* okJson(config2, "POST", `/_api/apps/${enc(t.ref)}/undo`, { body: { toSeq: to }, claim: t.claim });
40343
+ const undone = Number(result4.undone ?? 0);
40344
+ yield* Console_exports.log(undone ? `${t.ref}: restored ${undone} row${undone === 1 ? "" : "s"} to commit ${to} as commit ${String(result4.commitSeq)}` : `${t.ref}: nothing changed after commit ${to}`);
40345
+ });
40093
40346
  var listTokens = Effect_exports.fnUntraced(function* (config2) {
40094
40347
  const { tokens } = yield* ok(TokenList)(config2, "GET", "/_api/tokens");
40095
40348
  if (!tokens.length) yield* Console_exports.log("No tokens.");
40096
40349
  for (const t of tokens) {
40097
40350
  const scope3 = t.scope === "personal" ? "personal (all your apps)" : `${t.app ?? "?"} (${t.access ?? "?"})`;
40098
- const used = time2(t.lastUsedAt);
40351
+ const used = time3(t.lastUsedAt);
40099
40352
  yield* Console_exports.log(`${t.id} ${t.name} ${scope3} ${c.dim(used ? `last used ${used}` : "never used")}`);
40100
40353
  }
40101
40354
  });
@@ -40106,9 +40359,10 @@ var revokeToken = Effect_exports.fnUntraced(function* (config2, id2) {
40106
40359
  var createToken = Effect_exports.fnUntraced(function* (config2, name, opts) {
40107
40360
  if (opts.personal && opts.read) return yield* fail12("--read only applies to app tokens; personal tokens have full access to your apps.");
40108
40361
  const spec = opts.personal ? { name, personal: true } : { name, app: (yield* requireTarget(config2)).ref, access: opts.read ? "read" : "deploy" };
40109
- const { token } = yield* ok(CreatedToken)(config2, "POST", "/_api/tokens", { body: spec });
40362
+ const { token, id: id2 } = yield* ok(CreatedToken)(config2, "POST", "/_api/tokens", { body: spec });
40110
40363
  yield* Console_exports.log(token);
40111
40364
  yield* Console_exports.error(c.dim(`Shown once. Use it as ERASPACE_TOKEN with ERASPACE_TOKEN_API=${config2.origin}.`));
40365
+ yield* Console_exports.error(c.dim(`Token id ${id2}; revoke it with eraspace token revoke ${id2}.`));
40112
40366
  });
40113
40367
  function parseCallArgs(inputs) {
40114
40368
  const args2 = [];
@@ -40122,7 +40376,7 @@ Pass each argument as JSON, for example eraspace call send '"general"' '{"text":
40122
40376
  }
40123
40377
  return Effect_exports.succeed(args2);
40124
40378
  }
40125
- var call = Effect_exports.fnUntraced(function* (config2, fn3, inputs) {
40379
+ var call2 = Effect_exports.fnUntraced(function* (config2, fn3, inputs) {
40126
40380
  const args2 = yield* parseCallArgs(inputs);
40127
40381
  const t = yield* requireTarget(config2);
40128
40382
  const { status, text: text2 } = yield* Effect_exports.gen(function* () {
@@ -40145,132 +40399,74 @@ import { basename as basename2, join as join7, relative as relative3, resolve as
40145
40399
  var TEMPLATES = ["chat", "todo", "poll"];
40146
40400
  var agents = (cli) => `# eraspace capsule: agent instructions
40147
40401
 
40148
- This directory is one eraspace app ("capsule"): a TypeScript server running in V8, a Preact client, a
40149
- Postgres-backed database with live queries, and hosting, all through the \`eraspace\` CLI. Treat this directory as the
40150
- whole app. Build the requested app by editing the starter files, deploy it, verify it, and report the URL.
40151
-
40152
- ## Commands
40153
-
40154
- Run these from this directory. There is nothing to install: the CLI bundles the SDK, Preact and TypeScript.
40155
-
40156
- - \`${cli} typecheck\`: tsc over \`server/\`, \`client/\` and \`shared/\`. Deploy does not typecheck.
40157
- - \`${cli} deploy\`: build the client, compile the server and print the app URL.
40158
- - \`${cli} call <fn> '<json arg>'...\`: call a query, mutation or action, e.g. \`call add '"milk"'\`.
40159
- - \`${cli} logs\`: handler logs, \`ctx.log\` and \`console.log\` output.
40160
- - \`${cli} inspect\`: deployed functions, schema and metadata.
40161
- - \`${cli} dev\`: redeploy on every save (long-running; keep it open).
40162
-
40163
- The platform URL comes from \`ERASPACE_URL\`, then \`eraspace.json\`, then \`${DEFAULT_URL}\`.
40402
+ This directory is an eraspace app ("capsule"): a TypeScript server in V8 and a Preact client over a database with
40403
+ live queries, hosted and driven by \`${cli}\`, which supplies TypeScript, Preact and \`eraspace/*\`, so there is
40404
+ nothing to install.
40164
40405
 
40165
40406
  ## Workflow
40166
40407
 
40167
- 1. Read \`server/index.ts\` and \`client/index.tsx\`, then replace them with the requested app.
40168
- 2. Run \`${cli} typecheck\` and fix every error.
40169
- 3. Run \`${cli} deploy\`. Fix any compile errors it prints (they include file:line:column).
40170
- 4. Exercise every query and mutation with \`${cli} call ...\`, including one invalid input. Check
40171
- \`${cli} logs\` for errors.
40172
- 5. Report the URL printed by deploy. Open it in two tabs to see live updates.
40173
-
40174
- ## Files
40175
-
40176
- - \`server/index.ts\` default-exports \`capsule({...})\` from \`eraspace/server\`. Other server files go in \`server/\`.
40177
- - \`client/index.tsx\` default-exports \`function App()\`. The CLI mounts it. Other client files go in \`client/\`.
40178
- - \`shared/\` holds pure TypeScript used by both sides (no DOM, no env, no eraspace runtime imports).
40179
- - \`client/styles.css\` plus Tailwind utility classes in JSX. Write complete class names.
40180
- - \`.env.eraspace.server\` holds server secrets, read as \`ctx.env.KEY\`. Never import it from client code.
40181
- - There is no package.json, tsconfig.json or node_modules, and none is needed. Do not add them.
40182
-
40183
- ## Server API (\`eraspace/server\`)
40184
-
40185
- \`\`\`ts
40186
- import { capsule, table, query, mutation, action, endpoint, string, number, boolean, id, array, object, any, json, text } from "eraspace/server";
40187
-
40188
- export default capsule({
40189
- schema: {
40190
- items: table({ title: string(), done: boolean().default(false), listId: id("lists"), tags: array(string()).optional() })
40191
- .index("by_list", ["listId"]),
40192
- lists: table({ name: string() }),
40193
- },
40194
- queries: {
40195
- items: query(async (ctx, listId: string) =>
40196
- ctx.db.items.withIndex("by_list", (q) => q.eq("listId", listId)).order("desc").take(100)),
40197
- },
40198
- mutations: {
40199
- add: mutation(async (ctx, listId: string, title: string) => {
40200
- const clean = title.trim();
40201
- if (!clean) throw new Error("Title is required."); // the message reaches the client
40202
- return ctx.db.items.insert({ listId, title: clean });
40203
- }),
40204
- },
40205
- actions: {
40206
- summary: action(async (ctx) => {
40207
- const rows = await ctx.runQuery("items", "some-list");
40208
- return rows.length;
40209
- }),
40210
- },
40211
- endpoints: {
40212
- health: endpoint({ method: "GET", path: "/health", readOnly: true }, async (ctx, req) => json({ ok: true })),
40213
- },
40214
- });
40215
- \`\`\`
40216
-
40217
- - Field validators: \`string() number() boolean() id("table") userId() any() array(v) object({...})\`, each with
40218
- \`.optional()\` or \`.default(value)\`. Rows always have \`id\` (string) and \`createdAt\` (epoch ms).
40219
- - \`ctx.db.<table>\`: \`insert(doc)\` returns the id, \`get(id)\` returns the row or null, \`update(id, partial)\`
40220
- (\`null\` removes a field), \`replace(id, doc)\`, \`delete(id)\`. Reads chain \`withIndex(name, q => q.eq(f, v).gt(...))\`,
40221
- \`order("asc" | "desc")\`, then \`collect()\`, \`take(n)\`, \`first()\`, \`unique()\`, \`count()\` or
40222
- \`paginate({ cursor, numItems })\`. Every table has an implicit \`by_creation\` index. Await every db call.
40223
- - Queries and actions have a read-only db. Write in mutations or writable endpoints. Actions can call
40224
- \`ctx.runQuery\`, \`ctx.runMutation\` and global \`fetch\`.
40225
- - Handler arguments are positional and not validated. Check them in the handler.
40226
- - \`ctx.auth.userId\` is a per-connection guest id that changes on reload. It is not a login: never use it for
40227
- ownership or access control. Every app is public.
40228
- - \`ctx.log.info(msg, data?)\` (also debug/warn/error) writes to \`eraspace logs\`.
40229
- - Endpoints receive \`req\` with \`method, path, url, headers.get(), query.get(), json(), text()\` and return
40230
- \`json(value)\`, \`text(string)\`, \`empty()\`, \`redirect(url)\` or a plain value (sent as JSON).
40231
- - Removing a populated table needs its row count confirmed: \`${cli} deploy --delete-table <table>=<rows>\` (the
40232
- refused deploy prints the exact flag). Deletion runs in background batches.
40233
- - Changing a field's type or making it required fails the deploy while stored rows do not match; migrate them first.
40234
-
40235
- ## Client API (\`eraspace/client\`)
40236
-
40237
- \`\`\`tsx
40238
- import { createClient } from "eraspace/client";
40239
- import { useState } from "preact/hooks";
40240
- import type app from "../server/index";
40241
-
40242
- const client = createClient<typeof app>({ reloadOnDeploy: true });
40243
-
40244
- export default function App() {
40245
- const items = client.useQuery("items", "some-list"); // undefined while loading, then live
40246
- const add = client.useMutation("add");
40247
- const [title, setTitle] = useState("");
40248
- if (items === undefined) return <p>Loading\u2026</p>;
40249
- return <main class="p-6">
40250
- <form onSubmit={async (e) => { e.preventDefault(); await add("some-list", title); setTitle(""); }}>
40251
- <input value={title} onInput={(e) => setTitle(e.currentTarget.value)} />
40252
- </form>
40253
- <ul>{items.map((item) => <li key={item.id}>{item.title}</li>)}</ul>
40254
- </main>;
40255
- }
40256
- \`\`\`
40257
-
40258
- - Query, mutation and action names, arguments and results are typed from the server. Import the server with
40259
- \`import type\` only.
40260
- - Optimistic updates: \`client.useMutation("vote", { optimistic(store, id) { store.update("results", [],
40261
- (rows) => rows.map((r) => (r.id === id ? { ...r, votes: r.votes + 1 } : r))) } })\`. The second argument of
40262
- \`store.update\` is the query's argument list.
40263
- - \`useAction(name)\`, \`usePaginatedQuery(name, args, { initialNumItems })\`, \`client.query/mutation/action(...)\`.
40264
- - Realtime rooms, not stored: \`const room = client.useRoom(name, meta)\` gives \`room.presence\`,
40265
- \`room.broadcast(event, payload)\` and \`room.on(event, cb)\` (call \`on\` inside \`useEffect\` and return its
40266
- cleanup). \`client.usePresence(name, meta)\` returns just the members.
40267
- - Files: see "File uploads" below.
40268
- - Pages: \`Router\`, \`Route\`, \`Link\`, \`useLocation\`, \`useNavigate\`, \`useParams\` from \`eraspace/client\`.
40269
- - Client imports are limited to \`eraspace/client\`, \`preact\`, \`preact/hooks\`, \`@preact/signals\` and relative files.
40408
+ 1. Read \`CODING_STANDARDS.md\`, \`server/index.ts\` and \`client/index.tsx\`.
40409
+ 2. Write the requested app or change.
40410
+ 3. Run \`${cli} typecheck\` until it prints "No type errors."
40411
+ 4. Run \`${cli} deploy\` until it prints the app URL. Pass \`--delete-table\` only for rows the user agreed to lose.
40412
+ 5. Run \`${cli} call <fn> '<json arg>'...\` on every query, mutation and action, and on an invalid input for each
40413
+ mutation that takes arguments, until each returns what the code intends and each invalid input is refused with
40414
+ its message; then \`${cli} logs\` shows no unexpected error.
40415
+ 6. Once steps 3 to 5 pass after your last edit, report the URL. For an unclaimed app, also report the expiry deploy
40416
+ printed and that \`${cli} auth login\` then \`${cli} claim\` keeps the app.
40417
+ `;
40418
+ var CODING_STANDARDS = `# eraspace capsule: coding standards
40419
+
40420
+ Before using one of these features, read its section in \`FEATURES.md\`: file uploads, rooms and presence, routing,
40421
+ optimistic updates, pagination, scheduled mutations, HTTP endpoints, reload on deploy.
40422
+
40423
+ ## Security
40424
+
40425
+ - Every caller is an anonymous guest and every app is public. \`ctx.auth.userId\` is a guest id that changes with
40426
+ each connection, so it never grants ownership or access.
40427
+ - Keep secrets in \`.env.eraspace.server\` as \`KEY=value\` lines and read them in server code as \`ctx.env.KEY\`:
40428
+ \`client/\` and \`shared/\` ship to the browser.
40429
+
40430
+ ## Server (\`eraspace/server\`)
40431
+
40432
+ - Fields: \`string() number() boolean() id("table") userId() any() array(field) object({...})\`, each with
40433
+ \`.optional()\` or \`.default(value)\`. Rows also have \`id\` and \`createdAt\` (epoch ms).
40434
+ - \`ctx.db.<table>\` has \`insert\`, \`get\`, \`update(id, partial)\` (\`null\` removes a field), \`replace\` and
40435
+ \`delete\`. Reads chain \`withIndex\`, \`order("asc" | "desc")\`, then \`collect()\`, \`take(n)\`, \`first()\`,
40436
+ \`unique()\`, \`count()\` or \`paginate(...)\`.
40437
+ - Declare an index with \`table({...}).index("by_list", ["listId"])\` and read it with
40438
+ \`ctx.db.items.withIndex("by_list", (q) => q.eq("listId", id))\`, then \`.gt(...)\` or \`.lt(...)\` on the next
40439
+ field. \`by_creation\` indexes every table by \`createdAt\`.
40440
+ - Validate every argument in the handler: arguments are positional, come from anyone and are not checked against
40441
+ their TypeScript types.
40442
+ - A query is a live query: its cached result is reused until rows it read change, then rerun and pushed to
40443
+ subscribers. Make it depend only on its arguments, its caller and the rows it reads: pass a time it needs as an
40444
+ argument, rounded so it stays the same across renders, and draw random values in mutations.
40445
+ - Mutations run one at a time per app, so a read-then-write in one mutation never races another.
40446
+ - Actions call outbound \`fetch\`, \`ctx.runQuery\`, \`ctx.runMutation\` and \`ctx.runAction\`: fetch in an action
40447
+ and save the result with \`ctx.runMutation\`.
40448
+ - Rows and results are JSON: keep times as epoch-ms numbers, as \`createdAt\` is.
40449
+ - Keep state in the database: module variables reset whenever the app unloads.
40450
+ - Handlers have CPU, wall-clock and heap limits: keep loops and reads bounded.
40451
+ - A \`.migrate\` upgrade is a pure function of the stored row.
40452
+
40453
+ ## Client (\`eraspace/client\`)
40454
+
40455
+ - \`client.useQuery(name, ...args)\` subscribes to a live query: \`undefined\` while loading, then every new result,
40456
+ so mutations need no refetch.
40457
+ - \`client.useAction(name)\` returns a function, as \`useMutation\` does. Outside components, call \`client.query\`,
40458
+ \`client.mutation\` or \`client.action(name, ...args)\`.
40459
+ - Style with Tailwind utility classes, each written as a complete literal, and \`client/styles.css\`.
40460
+
40461
+ ## Shared code
40462
+
40463
+ - Keep \`shared/\` to types, constants and pure functions, which both the browser and the server can run.
40464
+ `;
40465
+ var FEATURES = `# eraspace capsule: features
40270
40466
 
40271
40467
  ## File uploads
40272
40468
 
40273
- Files go to the app's bucket, not the database. Save the returned \`FileRef\` in a row, then preview it.
40469
+ Files live in app storage, not the database: a row saves the \`FileRef\` an upload returns.
40274
40470
 
40275
40471
  \`\`\`tsx
40276
40472
  // server/index.ts: declare the file in the schema and copy its fields in the mutation (extra fields are refused).
@@ -40290,34 +40486,93 @@ const addPhoto = client.useMutation("addPhoto");
40290
40486
  - \`<FileUpload>\` is a drop zone and file picker with progress and error text. Props: \`accept\`, \`multiple\`,
40291
40487
  \`maxBytes\`, \`disabled\`, \`class\` (replaces its default style; \`data-dragging\` is set during a drag) and
40292
40488
  children for the label. \`onUpload(file)\` runs once per stored file; if it throws, the file is deleted again.
40293
- - \`<FilePreview file={fileRef or key}>\` shows images, video and audio inline, plain text in a frame, and other
40294
- types (PDF, zip...) as a download card. \`caption={false}\` hides the name/size line; \`class\` replaces its style.
40295
- Pass the saved \`FileRef\`: a bare key costs a metadata request per preview.
40296
- - Custom UI: \`const { upload, uploading, progress, error } = client.useUpload({ accept, maxBytes, onUpload })\`,
40297
- then \`upload(input.files)\`. Lower level: \`client.storage.upload(blob, { onProgress })\`, \`.url(key)\`,
40298
- \`.info(key)\` and \`.delete(key)\`.
40489
+ - \`<FilePreview file={fileRef}>\` shows images, video and audio inline, plain text in a frame, and other types as a
40490
+ download card. Pass it the saved \`FileRef\`: a bare key costs a metadata request per preview. \`caption={false}\`
40491
+ hides the name/size line; \`class\` replaces its style.
40492
+ - Custom UI: \`const { upload, uploading, progress, error, file } = client.useUpload({ accept, maxBytes, onUpload })\`,
40493
+ then \`upload(input.files)\`; \`file\` is the Blob uploading now. Lower level: \`client.storage.upload(blob, { onProgress })\`,
40494
+ \`.url(key)\`, \`.localUrl(key)\`, \`.info(key)\` and \`.delete(key)\`.
40495
+ - Up to 5 MiB per file and 100 MiB per app.
40299
40496
  - \`accept\` and \`maxBytes\` are checked in the browser only; the server stores any type up to its limit.
40300
- - Up to 5 MiB per file and 100 MiB per app. Anyone with a key can read or delete that file; never upload secrets.
40497
+ - Anyone with a file's key can read that file, so upload only what may be public. The \`deleteToken\` that
40498
+ \`client.storage.upload\` returns lets its holder delete the file: keep it out of rows.
40499
+
40500
+ ## Rooms and presence
40501
+
40502
+ Rooms are realtime and never stored. \`const room = client.useRoom(name, meta)\` gives:
40503
+
40504
+ - \`room.presence\`, the members with their \`meta\`, and \`room.self\`, this connection's id.
40505
+ - \`room.broadcast(event, payload)\` and \`room.on(event, (payload, from) => ...)\`. Call \`on\` inside \`useEffect\`
40506
+ and return its cleanup.
40507
+ - \`room.state\`, shared by the members and set with \`room.setState(key, value)\`. It vanishes when the room
40508
+ empties: save what must last with a mutation.
40301
40509
 
40302
- ## Server execution
40510
+ \`client.usePresence(name, meta)\` returns just the members.
40303
40511
 
40304
- Server TypeScript is transformed with oxc and runs as ES modules in V8. JavaScript language features such as
40305
- classes, closures, async/await, generators, getters, private fields, Proxy, Reflect and BigInt use V8 semantics.
40306
- Values crossing the database or RPC boundary must be JSON-compatible.
40512
+ ## Routing
40307
40513
 
40308
- - Imports support relative deployed files and \`eraspace/server\`. npm packages, Node built-ins and dynamic
40309
- \`import()\` are unavailable. Apps need no installed server dependencies.
40310
- - The host supplies \`fetch\`, \`URL\`, \`Headers\`, \`Request\`, \`Response\`, \`crypto.randomUUID\`,
40311
- \`TextEncoder\`, \`TextDecoder\`, timers and \`console\`.
40312
- - Persist state in the database. Module state is not durable and resets when an app unloads or a worker restarts.
40313
- - Await database calls. Mutations run through one writer per app and reply after their Postgres commit.
40314
- - Handlers have CPU, wall-clock and heap limits. Keep loops and reads bounded.
40315
- - Outbound fetch is available to claimed apps in actions and read-only endpoints. Private addresses are blocked.
40514
+ \`\`\`tsx
40515
+ import { Router, Route, Link, useParams, useNavigate } from "eraspace/client";
40516
+
40517
+ <Router>
40518
+ <Route path="/" component={Home} />
40519
+ <Route path="/items/:id" component={Item} /> {/* Item reads useParams().id */}
40520
+ <Route default component={NotFound} />
40521
+ </Router>
40522
+ \`\`\`
40523
+
40524
+ Paths are app-relative: the app is served under \`/<app>/\`, and every path below it loads the app. Link with
40525
+ \`<Link to="/items/1">\` and navigate with \`useNavigate()("/items/1")\`, which add that base. \`useLocation()\`
40526
+ gives the current \`path\` and \`url\`.
40527
+
40528
+ ## Optimistic updates
40316
40529
 
40317
- ## Deploys without an account
40530
+ \`client.useMutation("vote", { optimistic(store, id) { store.update("results", [], (rows) => rows.map((r) =>
40531
+ (r.id === id ? { ...r, votes: r.votes + 1 } : r))) } })\`. The second argument of \`store.update\` is the query's
40532
+ argument list.
40318
40533
 
40319
- A deploy without sign-in creates a temporary app and saves its claim token in \`.eraspace/\` (git-ignored). It has no
40320
- server env or outbound fetch and expires after about a week. \`eraspace auth login\` then \`eraspace claim\` keeps it.
40534
+ ## Pagination
40535
+
40536
+ \`\`\`ts
40537
+ // server: the query takes PaginationOptions last and returns paginate(); cap numItems, which the client chooses.
40538
+ feed: query(async (ctx, listId: string, page: PaginationOptions) =>
40539
+ ctx.db.items.withIndex("by_list", (q) => q.eq("listId", listId)).order("desc")
40540
+ .paginate({ cursor: page.cursor, numItems: Math.min(page.numItems, 50) })),
40541
+
40542
+ // client
40543
+ const { results, status, loadMore } = client.usePaginatedQuery("feed", ["some-list"], { initialNumItems: 20 });
40544
+ \`\`\`
40545
+
40546
+ \`PaginationOptions\` comes from \`eraspace/server\`. \`status\` is \`"LoadingFirstPage"\`, \`"CanLoadMore"\`,
40547
+ \`"LoadingMore"\` or \`"Exhausted"\`; \`loadMore(n?)\` fetches the next page.
40548
+
40549
+ ## Scheduled mutations
40550
+
40551
+ Mutations and writable endpoints run a mutation later with \`ctx.scheduler.runAfter(ms, "mutationName", ...args)\`,
40552
+ which returns a job id. The job commits with the mutation that schedules it, and its run commits at most once. For
40553
+ recurring work, the scheduled mutation schedules its next run.
40554
+
40555
+ ## HTTP endpoints
40556
+
40557
+ \`\`\`ts
40558
+ import { endpoint, json } from "eraspace/server";
40559
+
40560
+ endpoints: {
40561
+ health: endpoint({ method: "GET", path: "/health", readOnly: true }, async (ctx, req) => json({ ok: true })),
40562
+ },
40563
+ \`\`\`
40564
+
40565
+ - An endpoint is served at \`/<app>/<path>\`. \`readOnly: true\` gives it a read-only db; otherwise it writes like a
40566
+ mutation.
40567
+ - \`req\` has \`method, path, url, headers.get(), query.get(), json(), text(), bytes(), formData()\`. Return
40568
+ \`json(value)\`, \`text(string)\`, \`empty()\`, \`redirect(url)\` (each takes \`{ status, headers }\` last) or a
40569
+ plain value (sent as JSON).
40570
+ - Endpoints are public: authenticate a webhook by checking its signature with \`crypto.subtle\`.
40571
+
40572
+ ## Reload on deploy
40573
+
40574
+ Open tabs reload by themselves after each deploy. Pass \`createClient({ reloadOnDeploy: false })\` to keep the page
40575
+ and react to \`client.onDeployed(version => ...)\` instead.
40321
40576
  `;
40322
40577
  var GITIGNORE = [".eraspace/", ".env.eraspace.server"];
40323
40578
  var keepsOnInit = (name) => name.startsWith(".") || /^(README|LICENSE)(\.|$)/i.test(name);
@@ -40347,6 +40602,8 @@ var createCapsule = Effect_exports.fnUntraced(function* (directory, template, mo
40347
40602
  const files = {
40348
40603
  "eraspace.json": JSON.stringify({ name: app }, null, 2) + "\n",
40349
40604
  "AGENTS.md": agents(command2),
40605
+ "CODING_STANDARDS.md": CODING_STANDARDS,
40606
+ "FEATURES.md": FEATURES,
40350
40607
  "CLAUDE.md": "@AGENTS.md\n"
40351
40608
  };
40352
40609
  const gitignore = join7(destination, ".gitignore");
@@ -40446,7 +40703,7 @@ var config = Effect_exports.gen(function* () {
40446
40703
  return yield* loadConfig({ dir: value3(flags.dir), app: value3(flags.app), url: value3(flags.url) });
40447
40704
  });
40448
40705
  var deployFlags = {
40449
- name: optionalString("name", "Exact name for a new app, failing if it is taken (default: the eraspace.json or directory name, or a free variant of it)"),
40706
+ name: optionalString("name", "Exact name for a new app, failing if it is taken (default: the eraspace.json or directory name, or a free variant of it). A redeploy refuses it; use eraspace names add"),
40450
40707
  publicInspect: Flag_exports.Boolean("public-inspect").pipe(Flag_exports.withDefault(false), Flag_exports.withDescription("Let anyone read this app's inspection data and logs"))
40451
40708
  };
40452
40709
  var deployOptions = (input) => ({ name: value3(input.name), publicInspect: input.publicInspect });
@@ -40456,6 +40713,16 @@ var deleteTableFlag = {
40456
40713
  Flag_exports.optional
40457
40714
  )
40458
40715
  };
40716
+ var shadowFlags = {
40717
+ shadow: Flag_exports.Boolean("shadow").pipe(
40718
+ Flag_exports.withDefault(false),
40719
+ Flag_exports.withDescription("Before installing, rerun the app's live queries on the new code and report which results change or start throwing")
40720
+ ),
40721
+ failOnQueryErrors: Flag_exports.Boolean("fail-on-query-errors").pipe(
40722
+ Flag_exports.withDefault(false),
40723
+ Flag_exports.withDescription("Like --shadow, but refuse the deploy if a live query that returns a value now would throw")
40724
+ )
40725
+ };
40459
40726
  var browserFlag = {
40460
40727
  browser: Flag_exports.Boolean("browser").pipe(Flag_exports.withDefault(true), Flag_exports.withDescription("Open the sign-in page in a browser; --no-browser only prints the URL"))
40461
40728
  };
@@ -40479,11 +40746,43 @@ var typecheckCommand = Command_exports.make("typecheck", {}, () => Effect_export
40479
40746
  );
40480
40747
  var deployCommand = Command_exports.make(
40481
40748
  "deploy",
40482
- { ...deployFlags, ...deleteTableFlag },
40483
- (input) => Effect_exports.flatMap(config, (c2) => deploy(c2, { ...deployOptions(input), deleteTables: value3(input.deleteTable) }))
40749
+ { ...deployFlags, ...deleteTableFlag, ...shadowFlags },
40750
+ (input) => Effect_exports.flatMap(
40751
+ config,
40752
+ (c2) => deploy(c2, { ...deployOptions(input), deleteTables: value3(input.deleteTable), shadow: input.shadow, failOnQueryErrors: input.failOnQueryErrors })
40753
+ )
40484
40754
  ).pipe(
40485
40755
  Command_exports.withDescription("Build and deploy. Without a login this creates a temporary unclaimed app."),
40486
- Command_exports.withExamples([{ command: "eraspace deploy --delete-table drafts=12", description: "Deploy a schema without drafts, deleting its 12 rows" }])
40756
+ Command_exports.withExamples([
40757
+ { command: "eraspace deploy --delete-table drafts=12", description: "Deploy a schema without drafts, deleting its 12 rows" },
40758
+ { command: "eraspace deploy --fail-on-query-errors", description: "Deploy only if no live query starts throwing on the new code" }
40759
+ ])
40760
+ );
40761
+ var deploysCommand = Command_exports.make(
40762
+ "deploys",
40763
+ { app: optionalArg("app", "App name or id (default: this checkout's app)") },
40764
+ ({ app }) => Effect_exports.flatMap(config, (c2) => deploys(c2, value3(app)))
40765
+ ).pipe(Command_exports.withDescription("List the latest 50 kept versions you can roll back to, newest first"));
40766
+ var rollbackCommand = Command_exports.make(
40767
+ "rollback",
40768
+ {
40769
+ version: Argument_exports.Int("version").pipe(
40770
+ Argument_exports.withDescription("Version to roll back to (default: the newest kept version below the one serving)"),
40771
+ Argument_exports.optional
40772
+ ),
40773
+ ...deleteTableFlag,
40774
+ ...shadowFlags
40775
+ },
40776
+ (input) => Effect_exports.flatMap(
40777
+ config,
40778
+ (c2) => rollback(c2, value3(input.version), { deleteTables: value3(input.deleteTable), shadow: input.shadow, failOnQueryErrors: input.failOnQueryErrors })
40779
+ )
40780
+ ).pipe(
40781
+ Command_exports.withDescription("Deploy an earlier version's server code, client files and server env again, as a new version"),
40782
+ Command_exports.withExamples([
40783
+ { command: "eraspace rollback", description: "Go back to the version before the one serving" },
40784
+ { command: "eraspace rollback 3", description: "Serve version 3's code again" }
40785
+ ])
40487
40786
  );
40488
40787
  var devCommand = Command_exports.make("dev", deployFlags, (input) => Effect_exports.flatMap(config, (c2) => dev(c2, deployOptions(input)))).pipe(
40489
40788
  Command_exports.withDescription("Deploy, then redeploy whenever server/, client/, shared/ or .env.eraspace.server change")
@@ -40491,14 +40790,38 @@ var devCommand = Command_exports.make("dev", deployFlags, (input) => Effect_expo
40491
40790
  var callCommand = Command_exports.make(
40492
40791
  "call",
40493
40792
  {
40494
- fn: Argument_exports.String("fn").pipe(Argument_exports.withDescription("Query or mutation name")),
40793
+ fn: Argument_exports.String("fn").pipe(Argument_exports.withDescription("Query, mutation or action name")),
40495
40794
  args: Argument_exports.String("json").pipe(Argument_exports.withDescription("Arguments, each one a JSON value"), Argument_exports.variadic())
40496
40795
  },
40497
- ({ fn: fn3, args: args2 }) => Effect_exports.flatMap(config, (c2) => call(c2, fn3, args2))
40796
+ ({ fn: fn3, args: args2 }) => Effect_exports.flatMap(config, (c2) => call2(c2, fn3, args2))
40498
40797
  ).pipe(
40499
40798
  Command_exports.withDescription("Call a handler with positional JSON arguments"),
40500
40799
  Command_exports.withExamples([{ command: `eraspace call messages '"general"'`, description: "Strings are JSON too, so quote them" }])
40501
40800
  );
40801
+ var profileCommand = Command_exports.make(
40802
+ "profile",
40803
+ {
40804
+ app: optionalArg("app", "App name or id (default: this checkout's app)"),
40805
+ seconds: Flag_exports.Int("seconds").pipe(Flag_exports.filter((n) => n >= 1 && n <= 60, (n) => `--seconds must be between 1 and 60, got ${n}`), Flag_exports.withDefault(10), Flag_exports.withDescription("How long to sample")),
40806
+ out: optionalString("out", "Where to write the .cpuprofile (default: <app>-<time>.cpuprofile)")
40807
+ },
40808
+ ({ app, seconds: seconds2, out }) => Effect_exports.flatMap(config, (c2) => profile(c2, value3(app), seconds2, value3(out)))
40809
+ ).pipe(
40810
+ Command_exports.withDescription("Sample the app's server CPU time and write a .cpuprofile that opens in Chrome DevTools against the TypeScript sources"),
40811
+ Command_exports.withExamples([{ command: "eraspace profile my-app --seconds 10", description: "Profile while you exercise the app" }])
40812
+ );
40813
+ var explainCommand = Command_exports.make(
40814
+ "explain",
40815
+ {
40816
+ fn: Argument_exports.String("fn").pipe(Argument_exports.withDescription("Query name")),
40817
+ args: Argument_exports.String("json").pipe(Argument_exports.withDescription("Arguments, each one a JSON value"), Argument_exports.variadic()),
40818
+ json: Flag_exports.Boolean("json").pipe(Flag_exports.withDefault(false), Flag_exports.withDescription("Print the full report as JSON"))
40819
+ },
40820
+ ({ fn: fn3, args: args2, json: json2 }) => Effect_exports.flatMap(config, (c2) => explain(c2, fn3, args2, json2))
40821
+ ).pipe(
40822
+ Command_exports.withDescription("Run a query once and suggest indexes for rows it filters in JavaScript"),
40823
+ Command_exports.withExamples([{ command: `eraspace explain todosByStatus '"done"'`, description: "Strings are JSON too, so quote them" }])
40824
+ );
40502
40825
  var logsCommand = Command_exports.make(
40503
40826
  "logs",
40504
40827
  {
@@ -40543,15 +40866,40 @@ var inspectionCommand = Command_exports.make(
40543
40866
  },
40544
40867
  ({ visibility, app }) => Effect_exports.flatMap(config, (c2) => inspection(c2, visibility, value3(app)))
40545
40868
  ).pipe(Command_exports.withDescription("Open or close inspection and logs to everyone"));
40546
- var yesFlag = Flag_exports.Boolean("yes").pipe(Flag_exports.withAlias("y"), Flag_exports.withDefault(false), Flag_exports.withDescription("Confirm"));
40547
- var lifecycleCommand = (action, description) => Command_exports.make(
40548
- action,
40869
+ var lifecycleApp = optionalArg("app", "App name or id (default: this checkout's app)");
40870
+ var lifecycleCommand = (action, description) => Command_exports.make(action, { app: lifecycleApp }, ({ app }) => Effect_exports.flatMap(config, (c2) => lifecycle(c2, action, value3(app), false))).pipe(
40871
+ Command_exports.withDescription(description)
40872
+ );
40873
+ var changesCommand = Command_exports.make(
40874
+ "changes",
40549
40875
  {
40550
- app: optionalArg("app", "App name or id (default: this checkout's app)"),
40551
- yes: yesFlag
40876
+ since: Flag_exports.Int("since").pipe(Flag_exports.filter((n) => n >= 0, (n) => `--since must be a commit number, got ${n}`), Flag_exports.withDefault(0), Flag_exports.withDescription("Print changes committed after this commit")),
40877
+ limit: Flag_exports.Int("limit").pipe(Flag_exports.filter((n) => n >= 1 && n <= 1e4, (n) => `--limit must be between 1 and 10000, got ${n}`), Flag_exports.withDefault(1e3), Flag_exports.withDescription("Rows to print at most (whole commits)"))
40878
+ },
40879
+ ({ since, limit }) => Effect_exports.flatMap(config, (c2) => changes2(c2, since, limit))
40880
+ ).pipe(
40881
+ Command_exports.withDescription("Print recent committed row changes as NDJSON (the change log keeps 24 hours by default)"),
40882
+ Command_exports.withExamples([{ command: "eraspace changes --since 120 | jq .", description: "Changes committed after commit 120" }])
40883
+ );
40884
+ var undoCommand = Command_exports.make(
40885
+ "undo",
40886
+ {
40887
+ to: Argument_exports.Int("commit").pipe(Argument_exports.withDescription("Commit number to restore rows to")),
40888
+ yes: Flag_exports.Boolean("yes").pipe(Flag_exports.withAlias("y"), Flag_exports.withDefault(false), Flag_exports.withDescription("Confirm the undo"))
40889
+ },
40890
+ ({ to, yes }) => Effect_exports.flatMap(config, (c2) => undo(c2, to, yes))
40891
+ ).pipe(
40892
+ Command_exports.withDescription("Put every row changed after a commit back as it was; needs --yes. The undo is a commit too, so it can be undone"),
40893
+ Command_exports.withExamples([{ command: "eraspace undo 120 --yes", description: "Revert everything committed after commit 120" }])
40894
+ );
40895
+ var terminateCommand = Command_exports.make(
40896
+ "terminate",
40897
+ {
40898
+ app: lifecycleApp,
40899
+ yes: Flag_exports.Boolean("yes").pipe(Flag_exports.withAlias("y"), Flag_exports.withDefault(false), Flag_exports.withDescription("Confirm that the app stops permanently"))
40552
40900
  },
40553
- ({ app, yes }) => Effect_exports.flatMap(config, (c2) => lifecycle(c2, action, value3(app), yes))
40554
- ).pipe(Command_exports.withDescription(description));
40901
+ ({ app, yes }) => Effect_exports.flatMap(config, (c2) => lifecycle(c2, "terminate", value3(app), yes))
40902
+ ).pipe(Command_exports.withDescription("Stop permanently (data kept, no more deploys); needs --yes"));
40555
40903
  var tokenCommand = Command_exports.make("token").pipe(
40556
40904
  Command_exports.withDescription("Create, list and revoke API tokens"),
40557
40905
  Command_exports.withSubcommands([
@@ -40583,8 +40931,12 @@ var command = eraspace.pipe(
40583
40931
  initCommand,
40584
40932
  typecheckCommand,
40585
40933
  deployCommand,
40934
+ deploysCommand,
40935
+ rollbackCommand,
40586
40936
  devCommand,
40587
40937
  callCommand,
40938
+ explainCommand,
40939
+ profileCommand,
40588
40940
  logsCommand,
40589
40941
  inspectCommand,
40590
40942
  authCommand,
@@ -40594,7 +40946,9 @@ var command = eraspace.pipe(
40594
40946
  inspectionCommand,
40595
40947
  lifecycleCommand("archive", "Stop serving and keep the data"),
40596
40948
  lifecycleCommand("restore", "Serve an archived app again"),
40597
- lifecycleCommand("terminate", "Stop permanently (data kept, no more deploys); needs --yes"),
40949
+ terminateCommand,
40950
+ changesCommand,
40951
+ undoCommand,
40598
40952
  tokenCommand,
40599
40953
  ...hidden
40600
40954
  ])