eraspace 0.2.0 → 0.3.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)),
@@ -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.3.0",
38943
38998
  private: true,
38944
38999
  license: "MIT",
38945
39000
  type: "module",
@@ -39266,6 +39321,48 @@ function formatLog(l) {
39266
39321
  const data = l.data === void 0 || l.data === null ? "" : " " + c.gray(JSON.stringify(l.data));
39267
39322
  return `${c.gray(time3)} ${color(level.padEnd(5))} ${fn3}${l.msg ?? ""}${data}`;
39268
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");
39365
+ }
39269
39366
 
39270
39367
  // cli/src/api.ts
39271
39368
  var Timestamp = Schema_exports.NullOr(Schema_exports.Union([Schema_exports.Number, Schema_exports.String]));
@@ -39280,10 +39377,34 @@ 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)
39287
39408
  });
39288
39409
  var AppList = Schema_exports.Struct({ apps: Schema_exports.Array(App) });
39289
39410
  var StateChange = Schema_exports.Struct({ state: Schema_exports.String });
@@ -39448,15 +39569,20 @@ var login = Effect_exports.fnUntraced(function* (config2, browser) {
39448
39569
  }
39449
39570
  return yield* fail12("The sign-in code expired. Run eraspace auth login again.");
39450
39571
  });
39572
+ var LogoutResult = Schema_exports.Struct({ ok: Schema_exports.Literal(true) });
39451
39573
  var logout = Effect_exports.fnUntraced(function* (config2) {
39452
39574
  const session = yield* sessionFor(config2.origin);
39453
39575
  if (!session) {
39454
39576
  yield* Console_exports.log(`Not signed in to ${config2.origin}.`);
39455
39577
  return;
39456
39578
  }
39457
- yield* request3(config2, "POST", "/_api/auth/logout", { bearer: Redacted_exports.make(session.token) }).pipe(Effect_exports.timeout("10 seconds"), Effect_exports.ignore);
39579
+ const response = yield* api(config2, "POST", "/_api/auth/logout", { bearer: Redacted_exports.make(session.token), timeout: "10 seconds" });
39580
+ if (response.status !== 401) {
39581
+ if (response.status >= 400 || response.json.ok === false) return yield* Effect_exports.fail(yield* failure(response));
39582
+ yield* decode5(LogoutResult, response.json, "sign-out");
39583
+ }
39458
39584
  yield* removeSession(config2.origin);
39459
- yield* Console_exports.log(`Signed out of ${config2.origin}.`);
39585
+ yield* Console_exports.log(response.status === 401 ? `Removed invalid session for ${config2.origin}.` : `Signed out of ${config2.origin}.`);
39460
39586
  });
39461
39587
  var okWhoami = ok(Whoami);
39462
39588
  var whoami = Effect_exports.fnUntraced(function* (config2) {
@@ -39491,6 +39617,7 @@ function scanImports(file2, source) {
39491
39617
  }
39492
39618
  return { imports: imports.sort((a, b) => (a.at ?? 0) - (b.at ?? 0)) };
39493
39619
  }
39620
+ var MAX_MODULE_BYTES = 256 * 1024;
39494
39621
  var EXTENSIONS = [".ts", ".mts", ".js", ".mjs", ".tsx"];
39495
39622
  function importCandidates(base) {
39496
39623
  const stem = /\.m?js$/.test(base) ? base.replace(/\.m?js$/, "") : void 0;
@@ -39526,6 +39653,10 @@ var collectServerFiles = Effect_exports.fnUntraced(function* (root) {
39526
39653
  const rel = toPosix(relative2(root, abs2));
39527
39654
  const source = yield* fs.readFileString(abs2);
39528
39655
  files[rel] = source;
39656
+ const bytes2 = Buffer.byteLength(source, "utf8");
39657
+ if (bytes2 > MAX_MODULE_BYTES) {
39658
+ 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` });
39659
+ }
39529
39660
  const { imports, error: error2 } = scanImports(abs2, source);
39530
39661
  if (error2) {
39531
39662
  errors.push({ file: rel, ...lineCol(source, error2.at), message: error2.message });
@@ -39804,7 +39935,6 @@ var withoutEnv = Effect_exports.fnUntraced(function* (payload) {
39804
39935
  }
39805
39936
  return { ...payload, env: {} };
39806
39937
  });
39807
- var okDeployed = ok(Deployed);
39808
39938
  var requestedName = (config2, opts) => opts.name !== void 0 ? { name: opts.name } : { name: config2.name ?? toAppName(basename(config2.root)), uniqueName: true };
39809
39939
  var createApp = Effect_exports.fnUntraced(function* (config2, payload, extra, opts, signedIn) {
39810
39940
  const naming = requestedName(config2, opts);
@@ -39837,23 +39967,27 @@ var deploy = Effect_exports.fnUntraced(function* (config2, opts = {}) {
39837
39967
  const deleteTables = yield* confirmedDeletes(opts.deleteTables);
39838
39968
  const extra = {
39839
39969
  ...opts.publicInspect ? { publicInspection: true } : {},
39840
- ...Object.keys(deleteTables).length ? { deleteTables } : {}
39970
+ ...Object.keys(deleteTables).length ? { deleteTables } : {},
39971
+ ...opts.shadow ? { shadow: true } : {},
39972
+ ...opts.failOnQueryErrors ? { failOnQueryErrors: true } : {}
39841
39973
  };
39842
39974
  const signedIn = (yield* bearerFor(config2.origin)) !== void 0;
39843
39975
  const t = yield* target(config2);
39844
39976
  let result4;
39845
39977
  if (t.ref) {
39846
39978
  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
- );
39979
+ const path = `/_api/apps/${enc(t.ref)}/deploy`;
39980
+ const response = yield* api(config2, "POST", path, {
39981
+ body: { ...payload, ...extra },
39982
+ claim: t.claim,
39983
+ timeout: DEPLOY_TIMEOUT
39984
+ });
39985
+ if (response.status >= 400 || response.json.ok === false) {
39986
+ const refused = Schema_exports.decodeUnknownOption(ShadowReport)(response.json.shadow);
39987
+ if (Option_exports.isSome(refused)) yield* Console_exports.log(formatShadow(refused.value));
39988
+ return yield* Effect_exports.fail(yield* failure(response, config2.root));
39989
+ }
39990
+ result4 = yield* decode5(Deployed, response.json, `POST ${path}`);
39857
39991
  if (t.claim && result4.claimed)
39858
39992
  yield* removeLocalDeploy(config2.root, config2.origin);
39859
39993
  } else if (signedIn) {
@@ -39878,6 +40012,7 @@ var deploy = Effect_exports.fnUntraced(function* (config2, opts = {}) {
39878
40012
  expiresAt: Number.isFinite(expiresAt) ? expiresAt : null
39879
40013
  });
39880
40014
  }
40015
+ if (result4.shadow) yield* Console_exports.log(formatShadow(result4.shadow));
39881
40016
  const label = result4.names?.[0] ?? result4.id;
39882
40017
  yield* Console_exports.log(
39883
40018
  `${label}${result4.version === void 0 ? "" : ` v${result4.version}`}: ${result4.url ?? config2.url}`
@@ -39964,7 +40099,10 @@ var dev = Effect_exports.fnUntraced(function* (config2, opts) {
39964
40099
  });
39965
40100
 
39966
40101
  // cli/src/logs.ts
39967
- var IDLE = Duration_exports.seconds(75);
40102
+ var LogTimeouts = Context_exports.Reference(
40103
+ "eraspace/cli/LogTimeouts",
40104
+ { defaultValue: () => ({ idle: Duration_exports.seconds(75), stalled: Duration_exports.seconds(30), errorBody: Duration_exports.seconds(10) }) }
40105
+ );
39968
40106
  var Printed = class {
39969
40107
  lines = [];
39970
40108
  limit;
@@ -40005,12 +40143,14 @@ var readOnce = Effect_exports.fnUntraced(function* (config2, path, claim2, follo
40005
40143
  const response = yield* request3(config2, "GET", path, { claim: claim2 }).pipe(
40006
40144
  Effect_exports.timeoutOrElse({ duration: "30 seconds", orElse: () => Effect_exports.fail(new Transient({ message: `The platform at ${config2.origin} did not answer within 30s.` })) })
40007
40145
  );
40146
+ const timeouts = yield* LogTimeouts;
40147
+ const errorText = response.text.pipe(Effect_exports.timeout(timeouts.errorBody), Effect_exports.orElseSucceed(() => ""));
40008
40148
  if (isGatewayError(response.status)) {
40009
- yield* response.text.pipe(Effect_exports.ignore);
40149
+ yield* errorText;
40010
40150
  return yield* new Transient({ message: `The platform at ${config2.origin} is unavailable (HTTP ${response.status}).` });
40011
40151
  }
40012
40152
  if (response.status >= 400) {
40013
- const text2 = yield* response.text.pipe(Effect_exports.orElseSucceed(() => ""));
40153
+ const text2 = yield* errorText;
40014
40154
  let json2 = { ok: false, error: text2.trim() || `HTTP ${response.status}` };
40015
40155
  try {
40016
40156
  json2 = JSON.parse(text2);
@@ -40018,12 +40158,15 @@ var readOnce = Effect_exports.fnUntraced(function* (config2, path, claim2, follo
40018
40158
  }
40019
40159
  return yield* Effect_exports.fail(yield* failure({ status: response.status, json: json2 }, config2.root));
40020
40160
  }
40021
- let lines2 = response.stream.pipe(
40161
+ const lines2 = response.stream.pipe(
40162
+ Stream_exports.mapError((error2) => new Transient({ message: `The log stream broke: ${describe(error2.cause ?? error2)}` })),
40163
+ Stream_exports.timeoutOrElse({
40164
+ duration: follow ? timeouts.idle : timeouts.stalled,
40165
+ orElse: () => Stream_exports.fail(new Transient({ message: follow ? "The log stream went quiet." : `The platform at ${config2.origin} stopped sending logs.` }))
40166
+ }),
40022
40167
  Stream_exports.decodeText(),
40023
- Stream_exports.splitLines,
40024
- Stream_exports.mapError((error2) => new Transient({ message: `The log stream broke: ${describe(error2.cause ?? error2)}` }))
40168
+ Stream_exports.splitLines
40025
40169
  );
40026
- if (follow) lines2 = lines2.pipe(Stream_exports.timeoutOrElse({ duration: IDLE, orElse: () => Stream_exports.fail(new Transient({ message: "The log stream went quiet." })) }));
40027
40170
  const fresh2 = printed.connection();
40028
40171
  yield* lines2.pipe(
40029
40172
  Stream_exports.filter((line) => line.trim() !== ""),
@@ -40090,6 +40233,68 @@ var lifecycle = Effect_exports.fnUntraced(function* (config2, action, explicit,
40090
40233
  const result4 = yield* okState(config2, "POST", `/_api/apps/${enc(t.ref)}/${action}`, { claim: t.claim });
40091
40234
  yield* Console_exports.log(`${t.ref}: ${result4.state}`);
40092
40235
  });
40236
+ var explain = Effect_exports.fnUntraced(function* (config2, fn3, inputs, json2) {
40237
+ const args2 = yield* parseCallArgs(inputs);
40238
+ const t = yield* requireTarget(config2);
40239
+ const report = yield* okJson(config2, "POST", `/_api/apps/${enc(t.ref)}/explain`, { body: { fn: fn3, args: args2 }, claim: t.claim });
40240
+ if (json2) return yield* Console_exports.log(JSON.stringify(report, null, 2));
40241
+ const scans = report.scans ?? [];
40242
+ 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)}`);
40243
+ 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}`);
40244
+ const suggestions = report.suggestions ?? [];
40245
+ if (!suggestions.length) yield* Console_exports.log(c.dim("No index suggestions."));
40246
+ for (const s of suggestions) yield* Console_exports.log(`${c.yellow("suggest")} ${s.message}`);
40247
+ });
40248
+ var profile = Effect_exports.fnUntraced(function* (config2, explicit, seconds2, out) {
40249
+ const t = yield* requireTarget(config2, explicit);
40250
+ yield* Console_exports.error(c.dim(`Profiling ${t.ref} for ${seconds2}s...`));
40251
+ const result4 = yield* okJson(config2, "POST", `/_api/apps/${enc(t.ref)}/profile`, { body: { seconds: seconds2 }, claim: t.claim, timeout: Duration_exports.seconds(seconds2 + 30) });
40252
+ const file2 = out ?? `${t.ref}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.cpuprofile`;
40253
+ const fs = yield* FileSystem_exports.FileSystem;
40254
+ yield* fs.writeFileString(file2, JSON.stringify(result4));
40255
+ const nodes = new Map((result4.nodes ?? []).map((n) => [n.id, n]));
40256
+ const counts = /* @__PURE__ */ new Map();
40257
+ let busy = 0;
40258
+ for (const id2 of result4.samples ?? []) {
40259
+ const f = nodes.get(id2)?.callFrame;
40260
+ if (!f || f.functionName === "(idle)" || f.functionName === "(root)") continue;
40261
+ busy++;
40262
+ const at = f.url ? ` ${f.url}:${f.lineNumber + 1}` : "";
40263
+ const name = `${f.functionName || "(anonymous)"}${at}`;
40264
+ counts.set(name, (counts.get(name) ?? 0) + 1);
40265
+ }
40266
+ const samples = (result4.samples ?? []).length;
40267
+ yield* Console_exports.log(`${t.ref}: busy in ${busy} of ${samples} samples. Open ${file2} in Chrome DevTools (Performance > Load profile).`);
40268
+ for (const [name, n] of [...counts].sort((a, b) => b[1] - a[1]).slice(0, 10)) {
40269
+ yield* Console_exports.log(` ${(100 * n / Math.max(busy, 1)).toFixed(1).padStart(5)}% ${name}`);
40270
+ }
40271
+ });
40272
+ var changes2 = Effect_exports.fnUntraced(function* (config2, since, limit) {
40273
+ const t = yield* requireTarget(config2);
40274
+ const response = yield* request3(config2, "GET", `/_api/apps/${enc(t.ref)}/changes?since=${since}&limit=${limit}`, { claim: t.claim });
40275
+ const text2 = yield* response.text.pipe(Effect_exports.orElseSucceed(() => ""));
40276
+ if (response.status >= 400) {
40277
+ let json2 = { ok: false, error: text2.trim() || `HTTP ${response.status}` };
40278
+ try {
40279
+ json2 = JSON.parse(text2);
40280
+ } catch {
40281
+ }
40282
+ return yield* Effect_exports.fail(yield* failure({ status: response.status, json: json2 }));
40283
+ }
40284
+ const lines2 = text2.split("\n").filter(Boolean);
40285
+ for (const line of lines2) yield* Console_exports.log(line);
40286
+ const head5 = Number(response.headers["x-eraspace-commit-seq"] ?? 0);
40287
+ const last = lines2.length ? JSON.parse(lines2.at(-1)).seq : since;
40288
+ const more = response.headers["x-eraspace-more"] === "1";
40289
+ yield* Console_exports.error(c.dim(more ? `Latest commit ${head5}; continue with --since ${last}` : `Up to date at commit ${head5}`));
40290
+ });
40291
+ var undo = Effect_exports.fnUntraced(function* (config2, to, yes) {
40292
+ const t = yield* requireTarget(config2);
40293
+ 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.`);
40294
+ const result4 = yield* okJson(config2, "POST", `/_api/apps/${enc(t.ref)}/undo`, { body: { toSeq: to }, claim: t.claim });
40295
+ const undone = Number(result4.undone ?? 0);
40296
+ 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}`);
40297
+ });
40093
40298
  var listTokens = Effect_exports.fnUntraced(function* (config2) {
40094
40299
  const { tokens } = yield* ok(TokenList)(config2, "GET", "/_api/tokens");
40095
40300
  if (!tokens.length) yield* Console_exports.log("No tokens.");
@@ -40122,7 +40327,7 @@ Pass each argument as JSON, for example eraspace call send '"general"' '{"text":
40122
40327
  }
40123
40328
  return Effect_exports.succeed(args2);
40124
40329
  }
40125
- var call = Effect_exports.fnUntraced(function* (config2, fn3, inputs) {
40330
+ var call2 = Effect_exports.fnUntraced(function* (config2, fn3, inputs) {
40126
40331
  const args2 = yield* parseCallArgs(inputs);
40127
40332
  const t = yield* requireTarget(config2);
40128
40333
  const { status, text: text2 } = yield* Effect_exports.gen(function* () {
@@ -40222,6 +40427,8 @@ export default capsule({
40222
40427
  \`paginate({ cursor, numItems })\`. Every table has an implicit \`by_creation\` index. Await every db call.
40223
40428
  - Queries and actions have a read-only db. Write in mutations or writable endpoints. Actions can call
40224
40429
  \`ctx.runQuery\`, \`ctx.runMutation\` and global \`fetch\`.
40430
+ - Mutations schedule later work with \`ctx.scheduler.runAfter(ms, "mutationName", ...args)\`, which returns a job id.
40431
+ The job commits with the mutation and its run commits once; only mutations can be scheduled.
40225
40432
  - Handler arguments are positional and not validated. Check them in the handler.
40226
40433
  - \`ctx.auth.userId\` is a per-connection guest id that changes on reload. It is not a login: never use it for
40227
40434
  ownership or access control. Every app is public.
@@ -40230,7 +40437,9 @@ export default capsule({
40230
40437
  \`json(value)\`, \`text(string)\`, \`empty()\`, \`redirect(url)\` or a plain value (sent as JSON).
40231
40438
  - Removing a populated table needs its row count confirmed: \`${cli} deploy --delete-table <table>=<rows>\` (the
40232
40439
  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.
40440
+ - Changing a field's type or making it required fails the deploy while stored rows do not match. Declare a
40441
+ migration instead: \`table({ ... }).migrate(1, row => ({ ...row, done: false }))\` upgrades old rows when read
40442
+ and rewrites them in the background. Number migrations 1, 2, 3, ... and keep each one until its rows are done.
40234
40443
 
40235
40444
  ## Client API (\`eraspace/client\`)
40236
40445
 
@@ -40239,7 +40448,7 @@ import { createClient } from "eraspace/client";
40239
40448
  import { useState } from "preact/hooks";
40240
40449
  import type app from "../server/index";
40241
40450
 
40242
- const client = createClient<typeof app>({ reloadOnDeploy: true });
40451
+ const client = createClient<typeof app>();
40243
40452
 
40244
40453
  export default function App() {
40245
40454
  const items = client.useQuery("items", "some-list"); // undefined while loading, then live
@@ -40266,6 +40475,8 @@ export default function App() {
40266
40475
  cleanup). \`client.usePresence(name, meta)\` returns just the members.
40267
40476
  - Files: see "File uploads" below.
40268
40477
  - Pages: \`Router\`, \`Route\`, \`Link\`, \`useLocation\`, \`useNavigate\`, \`useParams\` from \`eraspace/client\`.
40478
+ - Open tabs reload by themselves after each deploy. Pass \`createClient({ reloadOnDeploy: false })\` to keep the
40479
+ page and react to \`client.onDeployed(version => ...)\` instead.
40269
40480
  - Client imports are limited to \`eraspace/client\`, \`preact\`, \`preact/hooks\`, \`@preact/signals\` and relative files.
40270
40481
 
40271
40482
  ## File uploads
@@ -40292,10 +40503,11 @@ const addPhoto = client.useMutation("addPhoto");
40292
40503
  children for the label. \`onUpload(file)\` runs once per stored file; if it throws, the file is deleted again.
40293
40504
  - \`<FilePreview file={fileRef or key}>\` shows images, video and audio inline, plain text in a frame, and other
40294
40505
  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)\`.
40506
+ Pass the saved \`FileRef\`: a bare key costs a metadata request per preview. Images, video and audio uploaded
40507
+ from the same page show at once from the browser's copy; \`<FileUpload>\` shows an image while it uploads.
40508
+ - Custom UI: \`const { upload, uploading, progress, error, file } = client.useUpload({ accept, maxBytes, onUpload })\`,
40509
+ then \`upload(input.files)\`; \`file\` is the Blob uploading now. Lower level: \`client.storage.upload(blob, { onProgress })\`,
40510
+ \`.url(key)\`, \`.localUrl(key)\`, \`.info(key)\` and \`.delete(key)\`.
40299
40511
  - \`accept\` and \`maxBytes\` are checked in the browser only; the server stores any type up to its limit.
40300
40512
  - Up to 5 MiB per file and 100 MiB per app. Anyone with a key can read or delete that file; never upload secrets.
40301
40513
 
@@ -40307,8 +40519,9 @@ Values crossing the database or RPC boundary must be JSON-compatible.
40307
40519
 
40308
40520
  - Imports support relative deployed files and \`eraspace/server\`. npm packages, Node built-ins and dynamic
40309
40521
  \`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\`.
40522
+ - The host supplies \`fetch\`, \`URL\`, \`URLPattern\`, \`Headers\`, \`Request\`, \`Response\`, Web Streams,
40523
+ \`Blob\`, \`File\`, \`FormData\`, \`crypto\` (including \`crypto.subtle\`), \`TextEncoder\`, \`TextDecoder\`,
40524
+ \`CompressionStream\`, timers and \`console\`. Bodies arrive and leave whole rather than incrementally.
40312
40525
  - Persist state in the database. Module state is not durable and resets when an app unloads or a worker restarts.
40313
40526
  - Await database calls. Mutations run through one writer per app and reply after their Postgres commit.
40314
40527
  - Handlers have CPU, wall-clock and heap limits. Keep loops and reads bounded.
@@ -40456,6 +40669,16 @@ var deleteTableFlag = {
40456
40669
  Flag_exports.optional
40457
40670
  )
40458
40671
  };
40672
+ var shadowFlags = {
40673
+ shadow: Flag_exports.Boolean("shadow").pipe(
40674
+ Flag_exports.withDefault(false),
40675
+ Flag_exports.withDescription("Before installing, rerun the app's live queries on the new code and report which results change or start throwing")
40676
+ ),
40677
+ failOnQueryErrors: Flag_exports.Boolean("fail-on-query-errors").pipe(
40678
+ Flag_exports.withDefault(false),
40679
+ Flag_exports.withDescription("Like --shadow, but refuse the deploy if a live query that returns a value now would throw")
40680
+ )
40681
+ };
40459
40682
  var browserFlag = {
40460
40683
  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
40684
  };
@@ -40479,11 +40702,17 @@ var typecheckCommand = Command_exports.make("typecheck", {}, () => Effect_export
40479
40702
  );
40480
40703
  var deployCommand = Command_exports.make(
40481
40704
  "deploy",
40482
- { ...deployFlags, ...deleteTableFlag },
40483
- (input) => Effect_exports.flatMap(config, (c2) => deploy(c2, { ...deployOptions(input), deleteTables: value3(input.deleteTable) }))
40705
+ { ...deployFlags, ...deleteTableFlag, ...shadowFlags },
40706
+ (input) => Effect_exports.flatMap(
40707
+ config,
40708
+ (c2) => deploy(c2, { ...deployOptions(input), deleteTables: value3(input.deleteTable), shadow: input.shadow, failOnQueryErrors: input.failOnQueryErrors })
40709
+ )
40484
40710
  ).pipe(
40485
40711
  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" }])
40712
+ Command_exports.withExamples([
40713
+ { command: "eraspace deploy --delete-table drafts=12", description: "Deploy a schema without drafts, deleting its 12 rows" },
40714
+ { command: "eraspace deploy --fail-on-query-errors", description: "Deploy only if no live query starts throwing on the new code" }
40715
+ ])
40487
40716
  );
40488
40717
  var devCommand = Command_exports.make("dev", deployFlags, (input) => Effect_exports.flatMap(config, (c2) => dev(c2, deployOptions(input)))).pipe(
40489
40718
  Command_exports.withDescription("Deploy, then redeploy whenever server/, client/, shared/ or .env.eraspace.server change")
@@ -40491,14 +40720,38 @@ var devCommand = Command_exports.make("dev", deployFlags, (input) => Effect_expo
40491
40720
  var callCommand = Command_exports.make(
40492
40721
  "call",
40493
40722
  {
40494
- fn: Argument_exports.String("fn").pipe(Argument_exports.withDescription("Query or mutation name")),
40723
+ fn: Argument_exports.String("fn").pipe(Argument_exports.withDescription("Query, mutation or action name")),
40495
40724
  args: Argument_exports.String("json").pipe(Argument_exports.withDescription("Arguments, each one a JSON value"), Argument_exports.variadic())
40496
40725
  },
40497
- ({ fn: fn3, args: args2 }) => Effect_exports.flatMap(config, (c2) => call(c2, fn3, args2))
40726
+ ({ fn: fn3, args: args2 }) => Effect_exports.flatMap(config, (c2) => call2(c2, fn3, args2))
40498
40727
  ).pipe(
40499
40728
  Command_exports.withDescription("Call a handler with positional JSON arguments"),
40500
40729
  Command_exports.withExamples([{ command: `eraspace call messages '"general"'`, description: "Strings are JSON too, so quote them" }])
40501
40730
  );
40731
+ var profileCommand = Command_exports.make(
40732
+ "profile",
40733
+ {
40734
+ app: optionalArg("app", "App name or id (default: this checkout's app)"),
40735
+ 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")),
40736
+ out: optionalString("out", "Where to write the .cpuprofile (default: <app>-<time>.cpuprofile)")
40737
+ },
40738
+ ({ app, seconds: seconds2, out }) => Effect_exports.flatMap(config, (c2) => profile(c2, value3(app), seconds2, value3(out)))
40739
+ ).pipe(
40740
+ Command_exports.withDescription("Sample the app's server CPU time and write a .cpuprofile that opens in Chrome DevTools against the TypeScript sources"),
40741
+ Command_exports.withExamples([{ command: "eraspace profile my-app --seconds 10", description: "Profile while you exercise the app" }])
40742
+ );
40743
+ var explainCommand = Command_exports.make(
40744
+ "explain",
40745
+ {
40746
+ fn: Argument_exports.String("fn").pipe(Argument_exports.withDescription("Query name")),
40747
+ args: Argument_exports.String("json").pipe(Argument_exports.withDescription("Arguments, each one a JSON value"), Argument_exports.variadic()),
40748
+ json: Flag_exports.Boolean("json").pipe(Flag_exports.withDefault(false), Flag_exports.withDescription("Print the full report as JSON"))
40749
+ },
40750
+ ({ fn: fn3, args: args2, json: json2 }) => Effect_exports.flatMap(config, (c2) => explain(c2, fn3, args2, json2))
40751
+ ).pipe(
40752
+ Command_exports.withDescription("Run a query once and suggest indexes for rows it filters in JavaScript"),
40753
+ Command_exports.withExamples([{ command: `eraspace explain todosByStatus '"done"'`, description: "Strings are JSON too, so quote them" }])
40754
+ );
40502
40755
  var logsCommand = Command_exports.make(
40503
40756
  "logs",
40504
40757
  {
@@ -40543,15 +40796,40 @@ var inspectionCommand = Command_exports.make(
40543
40796
  },
40544
40797
  ({ visibility, app }) => Effect_exports.flatMap(config, (c2) => inspection(c2, visibility, value3(app)))
40545
40798
  ).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,
40799
+ var lifecycleApp = optionalArg("app", "App name or id (default: this checkout's app)");
40800
+ var lifecycleCommand = (action, description) => Command_exports.make(action, { app: lifecycleApp }, ({ app }) => Effect_exports.flatMap(config, (c2) => lifecycle(c2, action, value3(app), false))).pipe(
40801
+ Command_exports.withDescription(description)
40802
+ );
40803
+ var changesCommand = Command_exports.make(
40804
+ "changes",
40549
40805
  {
40550
- app: optionalArg("app", "App name or id (default: this checkout's app)"),
40551
- yes: yesFlag
40806
+ 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")),
40807
+ 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)"))
40808
+ },
40809
+ ({ since, limit }) => Effect_exports.flatMap(config, (c2) => changes2(c2, since, limit))
40810
+ ).pipe(
40811
+ Command_exports.withDescription("Print recent committed row changes as NDJSON (the change log keeps 24 hours by default)"),
40812
+ Command_exports.withExamples([{ command: "eraspace changes --since 120 | jq .", description: "Changes committed after commit 120" }])
40813
+ );
40814
+ var undoCommand = Command_exports.make(
40815
+ "undo",
40816
+ {
40817
+ to: Argument_exports.Int("commit").pipe(Argument_exports.withDescription("Commit number to restore rows to")),
40818
+ yes: Flag_exports.Boolean("yes").pipe(Flag_exports.withAlias("y"), Flag_exports.withDefault(false), Flag_exports.withDescription("Confirm the undo"))
40819
+ },
40820
+ ({ to, yes }) => Effect_exports.flatMap(config, (c2) => undo(c2, to, yes))
40821
+ ).pipe(
40822
+ 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"),
40823
+ Command_exports.withExamples([{ command: "eraspace undo 120 --yes", description: "Revert everything committed after commit 120" }])
40824
+ );
40825
+ var terminateCommand = Command_exports.make(
40826
+ "terminate",
40827
+ {
40828
+ app: lifecycleApp,
40829
+ yes: Flag_exports.Boolean("yes").pipe(Flag_exports.withAlias("y"), Flag_exports.withDefault(false), Flag_exports.withDescription("Confirm that the app stops permanently"))
40552
40830
  },
40553
- ({ app, yes }) => Effect_exports.flatMap(config, (c2) => lifecycle(c2, action, value3(app), yes))
40554
- ).pipe(Command_exports.withDescription(description));
40831
+ ({ app, yes }) => Effect_exports.flatMap(config, (c2) => lifecycle(c2, "terminate", value3(app), yes))
40832
+ ).pipe(Command_exports.withDescription("Stop permanently (data kept, no more deploys); needs --yes"));
40555
40833
  var tokenCommand = Command_exports.make("token").pipe(
40556
40834
  Command_exports.withDescription("Create, list and revoke API tokens"),
40557
40835
  Command_exports.withSubcommands([
@@ -40585,6 +40863,8 @@ var command = eraspace.pipe(
40585
40863
  deployCommand,
40586
40864
  devCommand,
40587
40865
  callCommand,
40866
+ explainCommand,
40867
+ profileCommand,
40588
40868
  logsCommand,
40589
40869
  inspectCommand,
40590
40870
  authCommand,
@@ -40594,7 +40874,9 @@ var command = eraspace.pipe(
40594
40874
  inspectionCommand,
40595
40875
  lifecycleCommand("archive", "Stop serving and keep the data"),
40596
40876
  lifecycleCommand("restore", "Serve an archived app again"),
40597
- lifecycleCommand("terminate", "Stop permanently (data kept, no more deploys); needs --yes"),
40877
+ terminateCommand,
40878
+ changesCommand,
40879
+ undoCommand,
40598
40880
  tokenCommand,
40599
40881
  ...hidden
40600
40882
  ])