ignotum 0.0.18 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +73 -62
  2. package/dist/cli/Undici-C6Asbalt.mjs +30 -0
  3. package/dist/cli/Undici-C6Asbalt.mjs.map +1 -0
  4. package/dist/cli/bin.mjs +732 -24022
  5. package/dist/cli/bin.mjs.map +1 -1
  6. package/dist/platform/_headers +1 -1
  7. package/dist/platform/_ignotum/v1.1/client.js +12 -0
  8. package/dist/runtime/{api-BnUKx9cj-D49Tf0rX.d.ts → api-BnUKx9cj-K-F-SOLs.d.ts} +4 -4
  9. package/dist/runtime/{api-C-tltz6s.js → api-CrrSjYL1.js} +2 -2
  10. package/dist/runtime/{api-C-tltz6s.js.map → api-CrrSjYL1.js.map} +1 -1
  11. package/dist/runtime/{bootstrap-DlNIfH-r.js → bootstrap-CLVnD5vS.js} +4 -4
  12. package/dist/runtime/bootstrap-CLVnD5vS.js.map +1 -0
  13. package/dist/runtime/{bootstrap-DjTwGJao-DBx5zdFI.d.ts → bootstrap-DjTwGJao-DPi768T4.d.ts} +3 -3
  14. package/dist/runtime/client.d.ts +9 -9
  15. package/dist/runtime/client.js +4 -4
  16. package/dist/runtime/id-BPFV3D3V.d.ts +1 -0
  17. package/dist/runtime/{id-7an3nxTu-DZJSM5xh.d.ts → id-D87q-n2D-CDMwTSUR.d.ts} +2 -2
  18. package/dist/runtime/{id-BrEnxY-r.js → id-rzjaCxeF.js} +11 -2
  19. package/dist/runtime/{id-BrEnxY-r.js.map → id-rzjaCxeF.js.map} +1 -1
  20. package/dist/runtime/{index-Biaw5vdF.d.ts → index-BWHInzfU.d.ts} +3 -3
  21. package/dist/runtime/internal/api.d.ts +1 -1
  22. package/dist/runtime/internal/api.js +1 -1
  23. package/dist/runtime/internal/bootstrap.d.ts +1 -1
  24. package/dist/runtime/internal/bootstrap.js +1 -1
  25. package/dist/runtime/internal/client.d.ts +1 -1
  26. package/dist/runtime/internal/client.js +4 -4
  27. package/dist/runtime/internal/client.js.map +1 -1
  28. package/dist/runtime/internal/host.d.ts +6 -6
  29. package/dist/runtime/internal/host.js +3 -3
  30. package/dist/runtime/internal/routes.d.ts +1 -1
  31. package/dist/runtime/internal/server.d.ts +1 -1
  32. package/dist/runtime/internal/server.js +1 -1
  33. package/dist/runtime/internal/types.d.ts +2 -2
  34. package/dist/runtime/internal/types.js +1 -1
  35. package/dist/runtime/pagination-BPFV3D3V.d.ts +1 -0
  36. package/dist/runtime/{pagination--f4kMKf6.js → pagination-CfEleffG.js} +3 -2
  37. package/dist/runtime/pagination-CfEleffG.js.map +1 -0
  38. package/dist/runtime/result-Bq32FA7g.d.ts +1 -0
  39. package/dist/runtime/{router-C1eC9yy5.js → router-CfNPtoVO.js} +109 -60
  40. package/dist/runtime/router-CfNPtoVO.js.map +1 -0
  41. package/dist/runtime/{schema-BSjepoNF.js → schema-JtFb4BbG.js} +3 -3
  42. package/dist/runtime/{schema-BSjepoNF.js.map → schema-JtFb4BbG.js.map} +1 -1
  43. package/dist/runtime/server.d.ts +5 -5
  44. package/dist/runtime/server.js +2 -2
  45. package/dist/runtime/{sync-By2hX5ev.d.ts → sync-BMmV8oYt.d.ts} +2 -2
  46. package/dist/runtime/{sync-BwgS19Xy.js → sync-DUfp7S5R.js} +2 -2
  47. package/dist/runtime/{sync-BwgS19Xy.js.map → sync-DUfp7S5R.js.map} +1 -1
  48. package/dist/runtime/{types-DeCCyqLU-CHx7KFtx.d.ts → types-BeT6F0Pg-DMvRDN0c.d.ts} +5 -5
  49. package/package.json +9 -8
  50. package/src/cli/app-configuration.ts +44 -5
  51. package/src/cli/auth-client.ts +3 -3
  52. package/src/cli/build/managed-server.ts +3 -2
  53. package/src/cli/build/server.ts +2 -2
  54. package/src/cli/command.ts +18 -11
  55. package/src/cli/control-client.ts +2 -2
  56. package/src/cli/deploy.ts +27 -2
  57. package/src/cli/dev.ts +1 -1
  58. package/src/cli/managed-dev-platform.ts +13 -4
  59. package/src/cli/module-boundaries.ts +1 -1
  60. package/src/cli/new-app.ts +35 -6
  61. package/src/cli/package-manager.ts +1 -1
  62. package/src/cli/route-static.ts +7 -2
  63. package/src/cli/typecheck.ts +66 -0
  64. package/src/cli/upload-journal.ts +3 -3
  65. package/src/dev-runtime/database.ts +1 -1
  66. package/src/dev-runtime/files.ts +1 -1
  67. package/src/dev-runtime/managed-functions.ts +3 -2
  68. package/src/dev-runtime/managed-sync.ts +114 -105
  69. package/src/dev-runtime/managed-websocket.ts +11 -7
  70. package/src/dev-runtime/migrations.ts +2 -2
  71. package/src/dev-runtime/sync.ts +3 -3
  72. package/dist/platform/_ignotum/v1.0/client.js +0 -12
  73. package/dist/runtime/bootstrap-DlNIfH-r.js.map +0 -1
  74. package/dist/runtime/id-BI08JkmR.d.ts +0 -1
  75. package/dist/runtime/pagination--f4kMKf6.js.map +0 -1
  76. package/dist/runtime/pagination-BI08JkmR.d.ts +0 -1
  77. package/dist/runtime/result-BkziOG1L.d.ts +0 -1
  78. package/dist/runtime/router-C1eC9yy5.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { NodeHttpClient, NodeRuntime, NodeServices } from "@effect/platform-node";
2
- import { Effect, Layer, Option, Path, Schema, Terminal } from "effect";
3
- import { Argument, CliError, Command, Flag } from "effect/unstable/cli";
2
+ import { Clock, Effect, Layer, Option, Path, Schema, Terminal } from "effect";
3
+ import { Argument, CliError, Command, Flag } from "effect/cli";
4
4
  import { AppSlug } from "@ignotum/contracts/control";
5
5
  import { IdGenerator } from "@ignotum/shared/id";
6
6
 
@@ -13,7 +13,7 @@ import {
13
13
  status as readAuthStatus,
14
14
  } from "./auth-client.js";
15
15
  import { ControlClient } from "./control-client.js";
16
- import { deploy as runDeploy, resumeVersion } from "./deploy.js";
16
+ import { DeployProgress, deploy as runDeploy, resumeVersion } from "./deploy.js";
17
17
  import { resetDevDatabase } from "../dev-runtime/dev-database.js";
18
18
  import { dev as runDev } from "./dev.js";
19
19
  import { createApp } from "./new-app.js";
@@ -118,9 +118,9 @@ const devDatabase = Command.make("db").pipe(
118
118
  const dev = Command.make(
119
119
  "dev",
120
120
  {
121
- host: Flag.string("host").pipe(Flag.withDefault("127.0.0.1")),
122
- open: Flag.boolean("open").pipe(Flag.withDefault(false)),
123
- port: Flag.integer("port").pipe(Flag.withSchema(DevPort), Flag.withDefault(3210)),
121
+ host: Flag.String("host").pipe(Flag.withDefault("127.0.0.1")),
122
+ open: Flag.Boolean("open").pipe(Flag.withDefault(false)),
123
+ port: Flag.Int("port").pipe(Flag.withSchema(DevPort), Flag.withDefault(3210)),
124
124
  },
125
125
  Effect.fn("dev")(function* ({ host, open, port }) {
126
126
  const path = yield* Path.Path;
@@ -144,11 +144,11 @@ const dev = Command.make(
144
144
  const deploy = Command.make(
145
145
  "deploy",
146
146
  {
147
- resume: Flag.boolean("resume").pipe(
147
+ resume: Flag.Boolean("resume").pipe(
148
148
  Flag.withDescription("Resume the saved upload or activation."),
149
149
  Flag.withDefault(false),
150
150
  ),
151
- app: Flag.string("app").pipe(
151
+ app: Flag.String("app").pipe(
152
152
  Flag.withDescription("Create or link this app slug on the first deploy."),
153
153
  Flag.withSchema(AppSlug),
154
154
  Flag.optional,
@@ -157,8 +157,14 @@ const deploy = Command.make(
157
157
  Effect.fn("deploy")(function* ({ app, resume }) {
158
158
  const path = yield* Path.Path;
159
159
  const terminal = yield* Terminal.Terminal;
160
+ const startedAt = yield* Clock.currentTimeMillis;
161
+ const progress = Effect.fn("deploy.progress")(function* (message: string) {
162
+ const elapsed = ((yield* Clock.currentTimeMillis) - startedAt) / 1000;
163
+ yield* terminal.display(`${message} (${elapsed.toFixed(1)}s)\n`).pipe(Effect.orDie);
164
+ });
160
165
  if (resume) {
161
166
  const result = yield* resumeVersion(path.resolve("."), Option.getOrUndefined(app)).pipe(
167
+ Effect.provideService(DeployProgress, progress),
162
168
  // @effect-diagnostics-next-line strictEffectProvide:off This command owns the authenticated control client.
163
169
  Effect.provide(ControlClient.layer),
164
170
  Effect.mapError((error) =>
@@ -171,6 +177,7 @@ const deploy = Command.make(
171
177
  return;
172
178
  }
173
179
  const result = yield* runDeploy(path.resolve("."), Option.getOrUndefined(app)).pipe(
180
+ Effect.provideService(DeployProgress, progress),
174
181
  // @effect-diagnostics-next-line strictEffectProvide:off The deploy subcommand owns this client layer for its full lifetime.
175
182
  Effect.provide(ControlClient.layer),
176
183
  Effect.mapError((error) =>
@@ -201,14 +208,14 @@ const install = Command.make(
201
208
  const newApp = Command.make(
202
209
  "new",
203
210
  {
204
- directory: Argument.string("directory").pipe(
211
+ directory: Argument.String("directory").pipe(
205
212
  Argument.withDescription('Directory to create. Use "." for the current directory.'),
206
213
  ),
207
- git: Flag.boolean("git").pipe(
214
+ git: Flag.Boolean("git").pipe(
208
215
  Flag.withDescription("Initialize a Git repository. Use --no-git to skip it."),
209
216
  Flag.withDefault(true),
210
217
  ),
211
- install: Flag.boolean("install").pipe(
218
+ install: Flag.Boolean("install").pipe(
212
219
  Flag.withDescription("Install dependencies. Use --no-install to skip it."),
213
220
  Flag.withDefault(true),
214
221
  ),
@@ -36,8 +36,8 @@ import {
36
36
  Schedule,
37
37
  Schema,
38
38
  } from "effect";
39
- import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
40
- import type { HttpClientRequest as HttpRequest } from "effect/unstable/http/HttpClientRequest";
39
+ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
40
+ import type { HttpClientRequest as HttpRequest } from "effect/http/HttpClientRequest";
41
41
  import { AuthCommandError, AuthCredentialStore } from "./auth-client.js";
42
42
 
43
43
  export interface HostedControlConfig {
package/src/cli/deploy.ts CHANGED
@@ -1,9 +1,11 @@
1
- import { Effect, Path, Schema } from "effect";
1
+ import { Context, Effect, Path, Schema } from "effect";
2
2
  import type { AppSlug, ControlVersion } from "@ignotum/contracts/control";
3
3
  import { VersionId, VersionNumber } from "@ignotum/contracts/runtime/identity";
4
4
  import { IdGenerator } from "@ignotum/shared/id";
5
5
  import { appUrl, configureApp, type AppConfiguration } from "./app-configuration.js";
6
6
  import { buildManagedVersion, type ManagedVersionBuildResult } from "./build/managed.js";
7
+ import { generateClientRoutes } from "./route-codegen.js";
8
+ import { checkAppTypes } from "./typecheck.js";
7
9
  import {
8
10
  ControlClient,
9
11
  ControlClientUnavailable,
@@ -13,6 +15,11 @@ import {
13
15
 
14
16
  import { saveUpload, loadUpload, restoreUploadBuild, clearUpload } from "./upload-journal.js";
15
17
 
18
+ export const DeployProgress = Context.Reference<(message: string) => Effect.Effect<void>>(
19
+ "ignotum/cli/deploy/DeployProgress",
20
+ { defaultValue: () => () => Effect.void },
21
+ );
22
+
16
23
  export class VersionUploadInterrupted extends Schema.TaggedError<VersionUploadInterrupted>()(
17
24
  "VersionUploadInterrupted",
18
25
  { cause: Schema.Defect(), versionId: VersionId, message: Schema.String },
@@ -65,7 +72,9 @@ export const uploadVersion = Effect.fn("Deploy.upload")(function* (
65
72
  savedVersionId?: VersionId,
66
73
  ) {
67
74
  const control = yield* ControlClient;
75
+ const progress = yield* DeployProgress;
68
76
  const versionId = savedVersionId ?? (yield* (yield* IdGenerator).generate(VersionId));
77
+ yield* progress("Preparing upload");
69
78
  const admission = yield* control
70
79
  .beginVersion(configuration.appId, versionId, build.declaration)
71
80
  .pipe(
@@ -112,9 +121,13 @@ export const uploadVersion = Effect.fn("Deploy.upload")(function* (
112
121
  }
113
122
  return Effect.succeed({ ...target, bytes } satisfies VersionUploadFile);
114
123
  });
124
+ yield* progress(`Uploading 0/${files.length} files`);
115
125
  yield* Effect.forEach(
116
126
  files,
117
- (file) => control.uploadVersionFile(configuration.appId, versionId, file),
127
+ (file, index) =>
128
+ control
129
+ .uploadVersionFile(configuration.appId, versionId, file)
130
+ .pipe(Effect.andThen(() => progress(`Uploading ${index + 1}/${files.length} files`))),
118
131
  // The hosted control plane runs within Cloudflare's per-request CPU budget.
119
132
  // Serialize uploads so several validation-heavy requests cannot contend in
120
133
  // the same isolate and exhaust that budget together.
@@ -129,6 +142,7 @@ export const uploadVersion = Effect.fn("Deploy.upload")(function* (
129
142
  }),
130
143
  }),
131
144
  );
145
+ yield* progress("Validating version");
132
146
  const version = yield* control.finalizeVersion(configuration.appId, versionId).pipe(
133
147
  Effect.catchTags({
134
148
  ControlClientUnavailable: (cause) =>
@@ -140,6 +154,7 @@ export const uploadVersion = Effect.fn("Deploy.upload")(function* (
140
154
  }),
141
155
  Effect.flatMap(readyVersion),
142
156
  );
157
+ yield* progress(`Activating Version ${version.number}`);
143
158
  const selection = yield* control
144
159
  .activateVersion(configuration.appId, version.number)
145
160
  .pipe(Effect.mapError((cause) => activationError(version, cause)));
@@ -159,7 +174,14 @@ export const deploy = Effect.fn("Deploy.run")(function* (
159
174
  appDirectory: string,
160
175
  requestedSlug: AppSlug | undefined,
161
176
  ) {
177
+ const progress = yield* DeployProgress;
178
+ yield* progress("Generating bindings");
179
+ yield* generateClientRoutes(appDirectory);
180
+ yield* progress("Checking TypeScript");
181
+ yield* checkAppTypes(appDirectory);
182
+ yield* progress("Building app");
162
183
  const build = yield* buildVersionArtifact(appDirectory);
184
+ yield* progress("Linking app");
163
185
  const configuration = yield* configureApp(appDirectory, requestedSlug);
164
186
  const versionId = yield* (yield* IdGenerator).generate(VersionId);
165
187
  yield* saveUpload(appDirectory, configuration, versionId, build);
@@ -172,6 +194,8 @@ export const resumeVersion = Effect.fn("Deploy.resume")(function* (
172
194
  appDirectory: string,
173
195
  requestedSlug?: AppSlug,
174
196
  ) {
197
+ const progress = yield* DeployProgress;
198
+ yield* progress("Restoring saved deployment");
175
199
  const configuration = yield* configureApp(appDirectory, requestedSlug);
176
200
  const control = yield* ControlClient;
177
201
  const journal = yield* loadUpload(appDirectory, configuration);
@@ -189,6 +213,7 @@ export const resumeVersion = Effect.fn("Deploy.resume")(function* (
189
213
  return { ...activated, appUrl: appUrl(configuration.slug) };
190
214
  }
191
215
  const version = yield* readyVersion(existing);
216
+ yield* progress(`Activating Version ${version.number}`);
192
217
  const selection = yield* control
193
218
  .activateVersion(configuration.appId, version.number)
194
219
  .pipe(Effect.mapError((cause) => activationError(version, cause)));
package/src/cli/dev.ts CHANGED
@@ -3,7 +3,7 @@ import prefresh from "@prefresh/vite";
3
3
  import tailwindcss from "@tailwindcss/vite";
4
4
  import { Crypto, Effect, FileSystem, Option, Path, Schema, Scope } from "effect";
5
5
  import { createServer, normalizePath, type Plugin, type ViteDevServer } from "vite";
6
- import { ChildProcessSpawner } from "effect/unstable/process";
6
+ import { ChildProcessSpawner } from "effect/process";
7
7
  import {
8
8
  PlatformRequirement,
9
9
  revisionPrefix,
@@ -1,12 +1,17 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { Effect, FileSystem, Path, Schema } from "effect";
4
- import { ServingSelection, type PlatformRevision } from "@ignotum/contracts/deployment";
4
+ import {
5
+ revisionPrefix,
6
+ ServingSelection,
7
+ type PlatformRevision,
8
+ } from "@ignotum/contracts/deployment";
5
9
  import { VersionId } from "@ignotum/contracts/runtime/identity";
6
10
  import { bundledGuests } from "@ignotum/platform/guests";
7
11
  import type { BundledGuest } from "@ignotum/platform/artifacts";
8
12
  import { compilerExports } from "@ignotum/platform/compiler-exports";
9
13
  import { platformImportsPlugin } from "@ignotum/platform/compiler";
14
+ import { revisions } from "@ignotum/platform/registry";
10
15
  import { runViteBuild } from "./build/artifact.js";
11
16
 
12
17
  export interface LocalPlatform {
@@ -15,9 +20,11 @@ export interface LocalPlatform {
15
20
  readonly guest: BundledGuest;
16
21
  }
17
22
 
23
+ const localPlatformRevision = revisions[0];
24
+
18
25
  export const loadLocalPlatform = Effect.fn("ManagedDev.loadPlatform")(function* () {
19
26
  const fs = yield* FileSystem.FileSystem;
20
- const revision = { major: 1, minor: 0 } as const;
27
+ const revision = localPlatformRevision;
21
28
  const guest = bundledGuests.find(
22
29
  (candidate) =>
23
30
  candidate.manifest.revision.major === revision.major &&
@@ -25,7 +32,9 @@ export const loadLocalPlatform = Effect.fn("ManagedDev.loadPlatform")(function*
25
32
  );
26
33
  if (guest === undefined) return yield* Effect.die("The local platform guest is missing.");
27
34
  const browser = yield* fs.readFile(
28
- fileURLToPath(new URL("../../dist/platform/_ignotum/v1.0/client.js", import.meta.url)),
35
+ fileURLToPath(
36
+ new URL(`../../dist/platform${revisionPrefix(revision)}/client.js`, import.meta.url),
37
+ ),
29
38
  );
30
39
  return { revision, browser, guest } satisfies LocalPlatform;
31
40
  });
@@ -45,7 +54,7 @@ export const localServingSelection = (
45
54
  formatVersion: 1,
46
55
  appId: `app_${digest}`,
47
56
  versionNumber: 1,
48
- platform: { major: 1, minor: 0 },
57
+ platform: localPlatformRevision,
49
58
  activationGeneration,
50
59
  deploymentEpoch,
51
60
  dataHistoryId: `hist_${digest}`,
@@ -71,7 +71,7 @@ export const moduleBoundariesPlugin = Effect.fn("Deploy.moduleBoundariesPlugin")
71
71
  const fileSystem = yield* FileSystem.FileSystem;
72
72
  const runPromise = Effect.runPromiseWith(yield* Effect.context<never>());
73
73
  const isolatedDiscovery =
74
- (yield* Config.string("IGNOTUM_ISOLATED_DISCOVERY").pipe(Config.withDefault("0"))) === "1";
74
+ (yield* Config.String("IGNOTUM_ISOLATED_DISCOVERY").pipe(Config.withDefault("0"))) === "1";
75
75
  const realAppDirectory = isolatedDiscovery
76
76
  ? normalizePath(cleanModuleId(appDirectory))
77
77
  : normalizePath(yield* fileSystem.realPath(cleanModuleId(appDirectory)));
@@ -1,5 +1,5 @@
1
1
  import { Effect, FileSystem, Path, Schema, String } from "effect";
2
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
3
3
  import cliPackageJson from "../../package.json" with { type: "json" };
4
4
 
5
5
  import { agentAppFiles, type AppFile } from "./agent-files.js";
@@ -145,7 +145,7 @@ const icon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
145
145
  const utils = `export const counterIncrement = 1;
146
146
  `;
147
147
 
148
- const home = `import { Result, useMutation, useQuery } from "ignotum/client";
148
+ const home = `import { Result, useMutation, useQuery, useState } from "ignotum/client";
149
149
  import { api } from "@/_generated/api.js";
150
150
  import { route } from "@/_generated/client.js";
151
151
  import { counterIncrement } from "@/shared/utils.js";
@@ -155,6 +155,24 @@ export default route({
155
155
  component: function Counter() {
156
156
  const count = useQuery(api.counter.get);
157
157
  const increment = useMutation(api.counter.increment);
158
+ const [busy, setBusy] = useState(false);
159
+ const [error, setError] = useState("");
160
+ const incrementCounter = async () => {
161
+ if (busy) return;
162
+ setBusy(true);
163
+ setError("");
164
+ try {
165
+ Result.match(await increment(), {
166
+ value: () => {},
167
+ error: (failure) => setError("Increment failed: " + failure._tag),
168
+ internalError: (failure) => setError("Increment failed. Request ID: " + failure.requestId),
169
+ });
170
+ } catch {
171
+ setError("The increment could not be confirmed. Check the current counter before trying again.");
172
+ } finally {
173
+ setBusy(false);
174
+ }
175
+ };
158
176
 
159
177
  return (
160
178
  <section class="text-center">
@@ -165,15 +183,20 @@ export default route({
165
183
  <>
166
184
  <p class="my-6 text-5xl tabular-nums">{value}</p>
167
185
  <button
168
- class="rounded bg-zinc-900 px-4 py-2 text-white"
186
+ class="rounded bg-zinc-900 px-4 py-2 text-white disabled:opacity-50"
169
187
  type="button"
170
- onClick={() => void increment()}
188
+ disabled={busy}
189
+ onClick={() => void incrementCounter()}
171
190
  >
172
- Increment by {counterIncrement}
191
+ {busy ? "Incrementing..." : "Increment by " + counterIncrement}
173
192
  </button>
174
193
  </>
175
194
  ),
195
+ internalError: (failure) => (
196
+ <p role="alert" class="mt-6">Counter could not be loaded. Request ID: {failure.requestId}</p>
197
+ ),
176
198
  })}
199
+ {error !== "" && <p role="alert" class="mt-4">{error}</p>}
177
200
  </section>
178
201
  );
179
202
  },
@@ -254,7 +277,9 @@ After generating bindings, run \`npx tsc --noEmit\` to check the app.
254
277
 
255
278
  ## Deploy
256
279
 
257
- Sign in with \`npx ignotum auth login\`, then run \`npx ignotum deploy --app my-app\`.
280
+ Sign in with \`npx ignotum auth login\`, then run \`npx ignotum deploy\`.
281
+ The first deploy tries the app name saved when this project was created. Use
282
+ \`npx ignotum deploy --app another-slug\` to choose a different slug.
258
283
  Later deployments use \`npx ignotum deploy\`. The command prints the hosted URL and confirms
259
284
  that the numbered App Version is live. If activation cannot be confirmed, follow the printed
260
285
  \`ignotum deploy --resume\` command to finish without creating another version.
@@ -278,6 +303,10 @@ export const appFiles = (appName: string): ReadonlyArray<AppFile> => [
278
303
  { content: schema, path: "server/schema.ts" },
279
304
  { content: utils, path: "shared/utils.ts" },
280
305
  { content: packageJson(appName), path: "package.json" },
306
+ {
307
+ content: `${JSON.stringify({ formatVersion: 1, suggestedSlug: appName }, null, 2)}\n`,
308
+ path: "ignotum.json",
309
+ },
281
310
  {
282
311
  content: `minimumReleaseAgeExclude:\n - ignotum@${cliPackageJson.version}\nallowBuilds:\n msgpackr-extract: false\n`,
283
312
  path: "pnpm-workspace.yaml",
@@ -1,5 +1,5 @@
1
1
  import { Effect, Schema } from "effect";
2
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
3
3
 
4
4
  export type PackageManager = "npm" | "pnpm";
5
5
 
@@ -589,7 +589,9 @@ export const createRouteExtractor = Effect.fn("Routes.createExtractor")(function
589
589
  `Unsupported URL validator '${method}' or argument count.`,
590
590
  );
591
591
  }
592
- return yield* Schema.decodeUnknownEffect(UrlValueDescriptor)(output).pipe(
592
+ return yield* Schema.decodeUnknownEffect(UrlValueDescriptor, {
593
+ onExcessProperty: "error",
594
+ })(output).pipe(
593
595
  Effect.mapError((error) =>
594
596
  sourceError(module, node, field, `Invalid ${method} URL descriptor: ${String(error)}`),
595
597
  ),
@@ -660,7 +662,9 @@ export const createRouteExtractor = Effect.fn("Routes.createExtractor")(function
660
662
  const extracted = yield* value(expression, name);
661
663
  const validate =
662
664
  name === "meta"
663
- ? Schema.decodeUnknownEffect(StaticMetadata.fields.meta)(extracted).pipe(Effect.asVoid)
665
+ ? Schema.decodeUnknownEffect(StaticMetadata.fields.meta, {
666
+ onExcessProperty: "error",
667
+ })(extracted).pipe(Effect.asVoid)
664
668
  : name === "titleTemplate"
665
669
  ? Schema.decodeUnknownEffect(LayoutMetadata.fields.titleTemplate)(extracted).pipe(
666
670
  Effect.asVoid,
@@ -677,6 +681,7 @@ export const createRouteExtractor = Effect.fn("Routes.createExtractor")(function
677
681
  }
678
682
  const decodedMetadata = yield* Schema.decodeUnknownEffect(
679
683
  kind === "route" ? StaticMetadata : LayoutMetadata,
684
+ { onExcessProperty: "error" },
680
685
  )(Object.fromEntries(metadata)).pipe(
681
686
  Effect.mapError((error) => sourceError(module, call, "metadata", String(error))),
682
687
  );
@@ -0,0 +1,66 @@
1
+ import { createRequire } from "node:module";
2
+ import process from "node:process";
3
+ import { Effect, FileSystem, Path, Schema } from "effect";
4
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
5
+
6
+ const CompilerPackage = Schema.Struct({ bin: Schema.Struct({ tsc: Schema.String }) });
7
+
8
+ export class AppTypecheckFailed extends Schema.TaggedError<AppTypecheckFailed>()(
9
+ "AppTypecheckFailed",
10
+ { message: Schema.String, cause: Schema.optional(Schema.Defect()) },
11
+ ) {}
12
+
13
+ export const checkAppTypes = Effect.fn("Typecheck.checkAppTypes")(function* (appDirectory: string) {
14
+ const fs = yield* FileSystem.FileSystem;
15
+ const path = yield* Path.Path;
16
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
17
+ const configuration = path.join(appDirectory, "tsconfig.json");
18
+ if (!(yield* fs.exists(configuration))) {
19
+ return yield* AppTypecheckFailed.make({
20
+ message: "Deployment requires a tsconfig.json in the app directory.",
21
+ });
22
+ }
23
+ const packagePath = yield* Effect.try({
24
+ try: () =>
25
+ createRequire(path.join(appDirectory, "package.json")).resolve("typescript/package.json"),
26
+ catch: (cause) =>
27
+ AppTypecheckFailed.make({
28
+ cause,
29
+ message:
30
+ "TypeScript is not installed for this app. Install TypeScript as a dev dependency, then deploy again.",
31
+ }),
32
+ });
33
+ const compiler = yield* fs.readFileString(packagePath).pipe(
34
+ Effect.flatMap(Schema.decodeEffect(Schema.fromJsonString(CompilerPackage))),
35
+ Effect.mapError((cause) =>
36
+ AppTypecheckFailed.make({
37
+ cause,
38
+ message: "Could not read the installed TypeScript compiler.",
39
+ }),
40
+ ),
41
+ );
42
+ const exitCode = yield* spawner
43
+ .exitCode(
44
+ ChildProcess.make(
45
+ process.execPath,
46
+ [
47
+ path.resolve(path.dirname(packagePath), compiler.bin.tsc),
48
+ "--project",
49
+ configuration,
50
+ "--noEmit",
51
+ ],
52
+ { cwd: appDirectory, stdin: "ignore", stdout: "inherit", stderr: "inherit" },
53
+ ),
54
+ )
55
+ .pipe(
56
+ Effect.mapError((cause) =>
57
+ AppTypecheckFailed.make({ cause, message: "Could not run the app's TypeScript compiler." }),
58
+ ),
59
+ );
60
+ if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
61
+ return yield* AppTypecheckFailed.make({
62
+ message:
63
+ "TypeScript checking failed. Fix the errors above before deploying. No version was uploaded.",
64
+ });
65
+ }
66
+ });
@@ -87,9 +87,9 @@ export const restoreUploadBuild = Effect.fn("UploadJournal.restoreBuild")(functi
87
87
  const outputDirectory = path.join(appDirectory, ".ignotum/build");
88
88
  const declarationBytes = yield* fs.readFile(path.join(outputDirectory, "declaration.json"));
89
89
  if ((yield* sha256(declarationBytes)) !== journal.declarationSha256) return yield* invalid();
90
- const declaration = yield* Schema.decodeEffect(Schema.fromJsonString(UploadDeclaration))(
91
- new TextDecoder().decode(declarationBytes),
92
- ).pipe(Effect.mapError(invalid));
90
+ const declaration = yield* Schema.decodeEffect(Schema.fromJsonString(UploadDeclaration), {
91
+ onExcessProperty: "error",
92
+ })(new TextDecoder().decode(declarationBytes)).pipe(Effect.mapError(invalid));
93
93
  const entries = yield* Effect.forEach(
94
94
  declaration.definition.artifacts,
95
95
  Effect.fn(function* (artifact) {
@@ -1,5 +1,5 @@
1
1
  import { Context, DateTime, Effect, Layer, Option, Schema } from "effect";
2
- import { SqlClient, SqlError, SqlSchema } from "effect/unstable/sql";
2
+ import { SqlClient, SqlError, SqlSchema } from "effect/sql";
3
3
  import {
4
4
  GeneratedId,
5
5
  TableId,
@@ -15,7 +15,7 @@ import { fileGrantUrlPrefix, fileUploadUrlPrefix } from "@ignotum/contracts/runt
15
15
  import type { InvocationId } from "@ignotum/contracts/runtime/identity";
16
16
  import { IdGenerator } from "@ignotum/shared/id";
17
17
  import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
18
- import { SqlClient } from "effect/unstable/sql";
18
+ import { SqlClient } from "effect/sql";
19
19
 
20
20
  interface Preparation {
21
21
  readonly function: FunctionAddress;
@@ -1,5 +1,6 @@
1
+ import { Base64 } from "effect/encoding";
1
2
  import { fileURLToPath, pathToFileURL } from "node:url";
2
- import { Crypto, Effect, Encoding, FileSystem, Path, Predicate, Schema } from "effect";
3
+ import { Crypto, Effect, FileSystem, Path, Predicate, Schema } from "effect";
3
4
  import type { ViteDevServer } from "vite";
4
5
  import { parseSync } from "oxc-parser";
5
6
  import { prepareManagedExecution, type ManagedExecution } from "@ignotum/runtime/quickjs";
@@ -165,7 +166,7 @@ export const createManagedFunctionLoader = Effect.fn("ManagedFunctions.createLoa
165
166
  }
166
167
  if (module.sourceMap !== undefined) {
167
168
  rewritten = rewritten.replace(/^\/\/# sourceMappingURL=.*$/gm, "");
168
- rewritten += `\n//# sourceMappingURL=data:application/json;base64,${Encoding.encodeBase64(module.sourceMap.bytes)}\n`;
169
+ rewritten += `\n//# sourceMappingURL=data:application/json;base64,${Base64.encode(module.sourceMap.bytes)}\n`;
169
170
  }
170
171
  yield* fs.writeFileString(fileURLToPath(urls.get(module.id)!), rewritten);
171
172
  }