ignotum 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +23 -7
  2. package/dist/cli/bin.mjs +32313 -751
  3. package/dist/cli/bin.mjs.map +1 -1
  4. package/dist/runtime/{api-Cpx57mk3.d.ts → api-D2bsAz4T.d.ts} +3 -2
  5. package/dist/runtime/{api-BXXIZc_Q.js → api-DtX8qPrq.js} +58 -27
  6. package/dist/runtime/api-DtX8qPrq.js.map +1 -0
  7. package/dist/runtime/client.d.ts +11 -4
  8. package/dist/runtime/client.js +343 -79
  9. package/dist/runtime/client.js.map +1 -1
  10. package/dist/runtime/{result-B2W-z2wG.js → descriptor-t6BOEGw9-BTHR-ZMv.js} +119 -4
  11. package/dist/runtime/descriptor-t6BOEGw9-BTHR-ZMv.js.map +1 -0
  12. package/dist/runtime/id-Btwac71X-B9OeBzvq.d.ts +9 -0
  13. package/dist/runtime/id-D570vudg.js +26 -0
  14. package/dist/runtime/id-D570vudg.js.map +1 -0
  15. package/dist/runtime/{index-BgWROoyk.d.ts → index-Dl_VQGmA.d.ts} +114 -31
  16. package/dist/runtime/internal/api.d.ts +1 -1
  17. package/dist/runtime/internal/api.js +1 -1
  18. package/dist/runtime/internal/host.d.ts +44 -0
  19. package/dist/runtime/internal/host.js +213 -0
  20. package/dist/runtime/internal/host.js.map +1 -0
  21. package/dist/runtime/internal/server.d.ts +1 -1
  22. package/dist/runtime/internal/server.js +1 -1
  23. package/dist/runtime/internal/types.d.ts +1 -1
  24. package/dist/runtime/internal/types.js +1 -1
  25. package/dist/runtime/{result-C1ZdsM6Y.d.ts → result-BgcHn25t.d.ts} +21 -6
  26. package/dist/runtime/schema-B-jMZEbs.js +227 -0
  27. package/dist/runtime/schema-B-jMZEbs.js.map +1 -0
  28. package/dist/runtime/server.d.ts +2 -2
  29. package/dist/runtime/server.js +2 -2
  30. package/package.json +15 -5
  31. package/src/cli/agent-files.ts +6 -2
  32. package/src/cli/app-configuration.ts +135 -0
  33. package/src/cli/bin.ts +24 -1
  34. package/src/cli/build/artifact.ts +95 -0
  35. package/src/cli/build/client.ts +103 -0
  36. package/src/cli/build/server.ts +526 -0
  37. package/src/cli/client-config.ts +141 -0
  38. package/src/cli/client-entry.ts +152 -0
  39. package/src/cli/client-plugin.ts +86 -16
  40. package/src/cli/codegen.ts +5 -5
  41. package/src/cli/command.ts +41 -9
  42. package/src/cli/control-client.ts +343 -0
  43. package/src/cli/deploy.ts +217 -0
  44. package/src/cli/dev.ts +43 -108
  45. package/src/cli/module-boundaries.ts +183 -0
  46. package/src/cli/{new-project.ts → new-app.ts} +86 -86
  47. package/src/cli/package-manager.ts +9 -9
  48. package/src/client/app.ts +15 -0
  49. package/src/client/errors.ts +1 -17
  50. package/src/client/index.ts +2 -0
  51. package/src/client/sync.ts +172 -120
  52. package/src/dev-runtime/database.ts +253 -96
  53. package/src/dev-runtime/dev-database.ts +14 -14
  54. package/src/dev-runtime/functions.ts +57 -88
  55. package/src/dev-runtime/id.ts +2 -10
  56. package/src/dev-runtime/migrations.ts +68 -0
  57. package/src/dev-runtime/sync.ts +266 -83
  58. package/src/internal/function-definition.ts +77 -0
  59. package/src/internal/host.ts +1 -0
  60. package/src/internal/http-paths.ts +3 -1
  61. package/dist/runtime/api-BXXIZc_Q.js.map +0 -1
  62. package/dist/runtime/result-B2W-z2wG.js.map +0 -1
  63. package/dist/runtime/schema-CNEVLF7D.js +0 -116
  64. package/dist/runtime/schema-CNEVLF7D.js.map +0 -1
@@ -0,0 +1,141 @@
1
+ import { createRequire } from "node:module";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import { Effect, FileSystem, Path, Schema } from "effect";
5
+ import { defaultClientConditions, normalizePath } from "vite";
6
+ import type { Alias } from "vite";
7
+
8
+ const clientEntryFileName = "index.tsx";
9
+ const clientIconFileName = "icon.svg";
10
+ const preactSpecifiers = [
11
+ "preact/jsx-dev-runtime",
12
+ "preact/jsx-runtime",
13
+ "preact/devtools",
14
+ "preact/debug",
15
+ "preact/hooks",
16
+ "preact",
17
+ ] as const;
18
+ const prefreshSpecifiers = ["@prefresh/core", "@prefresh/utils"] as const;
19
+
20
+ export class ClientFileNotFound extends Schema.TaggedError<ClientFileNotFound>()(
21
+ "ClientFileNotFound",
22
+ {
23
+ message: Schema.String,
24
+ path: Schema.String,
25
+ },
26
+ ) {}
27
+
28
+ export class InvalidClientFile extends Schema.TaggedError<InvalidClientFile>()(
29
+ "InvalidClientFile",
30
+ {
31
+ message: Schema.String,
32
+ path: Schema.String,
33
+ },
34
+ ) {}
35
+
36
+ export class ClientRuntimeResolutionFailed extends Schema.TaggedError<ClientRuntimeResolutionFailed>()(
37
+ "ClientRuntimeResolutionFailed",
38
+ {
39
+ cause: Schema.Defect(),
40
+ message: Schema.String,
41
+ },
42
+ ) {}
43
+
44
+ export interface ClientBuildConfig {
45
+ readonly aliases: ReadonlyArray<Alias>;
46
+ readonly conditions: ReadonlyArray<string>;
47
+ }
48
+
49
+ export interface ClientFiles {
50
+ readonly clientDirectory: string;
51
+ readonly entryPath: string;
52
+ readonly iconPath: string | undefined;
53
+ }
54
+
55
+ export const validateClientFiles = Effect.fn("ClientConfig.validateFiles")(function* (
56
+ appDirectory: string,
57
+ ) {
58
+ const fileSystem = yield* FileSystem.FileSystem;
59
+ const path = yield* Path.Path;
60
+ const clientDirectory = path.join(appDirectory, "client");
61
+ const entryPath = path.join(clientDirectory, clientEntryFileName);
62
+ if (!(yield* fileSystem.exists(entryPath))) {
63
+ return yield* ClientFileNotFound.make({
64
+ message: `Required client file not found: ${entryPath}`,
65
+ path: entryPath,
66
+ });
67
+ }
68
+ const entryInfo = yield* fileSystem.stat(entryPath);
69
+ if (entryInfo.type !== "File") {
70
+ return yield* InvalidClientFile.make({
71
+ message: `Required client entry is not a regular file: ${entryPath}`,
72
+ path: entryPath,
73
+ });
74
+ }
75
+
76
+ const iconPath = path.join(clientDirectory, clientIconFileName);
77
+ const hasIcon = yield* fileSystem.exists(iconPath);
78
+ if (hasIcon) {
79
+ const iconInfo = yield* fileSystem.stat(iconPath);
80
+ if (iconInfo.type !== "File") {
81
+ return yield* InvalidClientFile.make({
82
+ message: `Optional client icon is not a regular file: ${iconPath}`,
83
+ path: iconPath,
84
+ });
85
+ }
86
+ }
87
+
88
+ return {
89
+ clientDirectory,
90
+ entryPath,
91
+ iconPath: hasIcon ? iconPath : undefined,
92
+ } satisfies ClientFiles;
93
+ });
94
+
95
+ export const resolveClientBuildConfig = Effect.fn("ClientConfig.resolve")(function* (
96
+ includePrefresh: boolean,
97
+ ) {
98
+ const fileSystem = yield* FileSystem.FileSystem;
99
+ const sourceClientEntry = fileURLToPath(new URL("../../src/client/index.ts", import.meta.url));
100
+ const sourceContractsEntry = fileURLToPath(
101
+ new URL("../../../contracts/src/schema/index.ts", import.meta.url),
102
+ );
103
+ const hasSourceClient = yield* fileSystem.exists(sourceClientEntry);
104
+ const hasSourceContracts = yield* fileSystem.exists(sourceContractsEntry);
105
+ const hasWorkspaceSourceExports = hasSourceClient && hasSourceContracts;
106
+
107
+ return yield* Effect.try({
108
+ try: (): ClientBuildConfig => {
109
+ const prefreshRequire = createRequire(import.meta.resolve("@prefresh/vite"));
110
+ const aliases: ReadonlyArray<Alias> = [
111
+ {
112
+ find: /^tailwindcss$/,
113
+ replacement: normalizePath(fileURLToPath(import.meta.resolve("tailwindcss/index.css"))),
114
+ },
115
+ ...preactSpecifiers.map((specifier) => ({
116
+ find: specifier,
117
+ replacement: normalizePath(fileURLToPath(import.meta.resolve(specifier))),
118
+ })),
119
+ ...(includePrefresh
120
+ ? prefreshSpecifiers.map((specifier) => ({
121
+ find: specifier,
122
+ replacement: normalizePath(prefreshRequire.resolve(specifier)),
123
+ }))
124
+ : []),
125
+ ];
126
+
127
+ return {
128
+ aliases,
129
+ conditions: [
130
+ ...(hasWorkspaceSourceExports ? ["@ignotum/source"] : []),
131
+ ...defaultClientConditions,
132
+ ],
133
+ };
134
+ },
135
+ catch: (cause) =>
136
+ ClientRuntimeResolutionFailed.make({
137
+ cause,
138
+ message: "Ignotum could not resolve its client runtime.",
139
+ }),
140
+ });
141
+ });
@@ -0,0 +1,152 @@
1
+ import { Effect, FileSystem, Predicate, Result, Schema } from "effect";
2
+ import { parseSync, type Node, type ObjectProperty, type ObjectPropertyKind } from "oxc-parser";
3
+
4
+ export class InvalidClientEntry extends Schema.TaggedError<InvalidClientEntry>()(
5
+ "InvalidClientEntry",
6
+ {
7
+ column: Schema.Int,
8
+ line: Schema.Int,
9
+ message: Schema.String,
10
+ path: Schema.String,
11
+ },
12
+ ) {}
13
+
14
+ export interface ClientEntrySource {
15
+ readonly path: string;
16
+ readonly source: string;
17
+ }
18
+
19
+ const requiredForm = 'export default app({ title: "My App", component: App });';
20
+
21
+ const location = (source: string, offset: number) => {
22
+ const prefix = source.slice(0, offset);
23
+ const line = prefix.split("\n").length;
24
+ const lastNewline = prefix.lastIndexOf("\n");
25
+ return { column: offset - lastNewline, line };
26
+ };
27
+
28
+ const invalid = (entry: ClientEntrySource, node: Pick<Node, "start">, detail: string) => {
29
+ const position = location(entry.source, node.start);
30
+ return InvalidClientEntry.make({
31
+ ...position,
32
+ message: `${entry.path}:${position.line}:${position.column}: ${detail} Required form: ${requiredForm}`,
33
+ path: entry.path,
34
+ });
35
+ };
36
+
37
+ const isNamedProperty = (property: ObjectProperty, name: string): boolean =>
38
+ property.kind === "init" &&
39
+ !property.computed &&
40
+ !property.method &&
41
+ !property.shorthand &&
42
+ property.key.type === "Identifier" &&
43
+ property.key.name === name;
44
+
45
+ const isObjectProperty = (property: ObjectPropertyKind): property is ObjectProperty =>
46
+ property.type === "Property";
47
+
48
+ export const extractClientEntryTitle = (
49
+ entry: ClientEntrySource,
50
+ ): Result.Result<string, InvalidClientEntry> => {
51
+ const parsed = parseSync(entry.path, entry.source, {
52
+ lang: "tsx",
53
+ preserveParens: true,
54
+ showSemanticErrors: true,
55
+ sourceType: "module",
56
+ });
57
+ const parserError = parsed.errors[0];
58
+ if (parserError !== undefined) {
59
+ const label = parserError.labels[0];
60
+ return Result.fail(
61
+ invalid(
62
+ entry,
63
+ { start: label?.start ?? 0 },
64
+ `The client entry is invalid: ${parserError.message}.`,
65
+ ),
66
+ );
67
+ }
68
+
69
+ const importsApp = parsed.program.body.some(
70
+ (statement) =>
71
+ statement.type === "ImportDeclaration" &&
72
+ statement.source.value === "ignotum/client" &&
73
+ statement.importKind !== "type" &&
74
+ statement.specifiers.some(
75
+ (specifier) =>
76
+ specifier.type === "ImportSpecifier" &&
77
+ specifier.importKind !== "type" &&
78
+ specifier.imported.type === "Identifier" &&
79
+ specifier.imported.name === "app" &&
80
+ specifier.local.name === "app",
81
+ ),
82
+ );
83
+ if (!importsApp) {
84
+ return Result.fail(
85
+ invalid(entry, parsed.program, 'Import { app } from "ignotum/client" without an alias.'),
86
+ );
87
+ }
88
+
89
+ const defaultExports = parsed.program.body.filter(
90
+ (statement) => statement.type === "ExportDefaultDeclaration",
91
+ );
92
+ const defaultExport = defaultExports[0];
93
+ if (defaultExports.length !== 1 || defaultExport === undefined) {
94
+ return Result.fail(
95
+ invalid(entry, parsed.program, "The client entry needs one default export."),
96
+ );
97
+ }
98
+ const call = defaultExport.declaration;
99
+ if (
100
+ call.type !== "CallExpression" ||
101
+ call.optional ||
102
+ call.typeArguments != null ||
103
+ call.callee.type !== "Identifier" ||
104
+ call.callee.name !== "app" ||
105
+ call.arguments.length !== 1
106
+ ) {
107
+ return Result.fail(invalid(entry, defaultExport, "The default export must be an app call."));
108
+ }
109
+ const definition = call.arguments[0];
110
+ if (definition?.type !== "ObjectExpression" || definition.properties.length !== 2) {
111
+ return Result.fail(
112
+ invalid(entry, call, "The app call needs exactly title and component properties."),
113
+ );
114
+ }
115
+ const properties = definition.properties;
116
+ if (!properties.every(isObjectProperty)) {
117
+ return Result.fail(invalid(entry, definition, "Spread properties are not supported."));
118
+ }
119
+ const titleProperties = properties.filter((property) => isNamedProperty(property, "title"));
120
+ const componentProperties = properties.filter((property) =>
121
+ isNamedProperty(property, "component"),
122
+ );
123
+ if (titleProperties.length !== 1 || componentProperties.length !== 1) {
124
+ return Result.fail(
125
+ invalid(entry, definition, "Use unquoted title and component properties with no extras."),
126
+ );
127
+ }
128
+ const titleProperty = titleProperties[0];
129
+ if (titleProperty === undefined) {
130
+ return Result.fail(invalid(entry, definition, "The title property is missing."));
131
+ }
132
+ const title = titleProperty.value;
133
+ if (
134
+ title.type !== "Literal" ||
135
+ !Predicate.isString(title.value) ||
136
+ (title.raw?.startsWith('"') !== true && title.raw?.startsWith("'") !== true)
137
+ ) {
138
+ return Result.fail(invalid(entry, title, "The title must be a quoted string literal."));
139
+ }
140
+ if (title.value.trim().length === 0) {
141
+ return Result.fail(invalid(entry, title, "The title must contain a non-whitespace character."));
142
+ }
143
+ return Result.succeed(title.value);
144
+ };
145
+
146
+ export const loadClientEntryTitle = Effect.fn("ClientEntry.loadTitle")(function* (path: string) {
147
+ const fileSystem = yield* FileSystem.FileSystem;
148
+ const source = yield* fileSystem.readFileString(path);
149
+ const extracted = extractClientEntryTitle({ path, source });
150
+ if (Result.isFailure(extracted)) return yield* extracted.failure;
151
+ return extracted.success;
152
+ });
@@ -1,29 +1,70 @@
1
1
  import type { Plugin } from "vite";
2
2
 
3
- const clientEntryId = "virtual:ignotum/client-entry";
3
+ export const clientEntryId = "virtual:ignotum/client-entry";
4
4
  const resolvedClientEntryId = `\0${clientEntryId}`;
5
+ export const clientStylesId = "virtual:ignotum/_styles.css";
6
+ const resolvedClientStylesId = `\0${clientStylesId}`;
7
+ export const clientStylesSource = '@import "tailwindcss";\n';
5
8
 
6
- const document = `<!doctype html>
9
+ interface ExternalClientScript {
10
+ readonly source: string;
11
+ readonly type: "External";
12
+ }
13
+
14
+ interface InlineClientScript {
15
+ readonly source: string;
16
+ readonly type: "Inline";
17
+ }
18
+
19
+ interface ClientDocumentOptions {
20
+ readonly icon: string | undefined;
21
+ readonly scripts: ReadonlyArray<ExternalClientScript | InlineClientScript>;
22
+ readonly styles: ReadonlyArray<string>;
23
+ readonly title: string;
24
+ }
25
+
26
+ export interface ClientPluginOptions {
27
+ readonly development: boolean;
28
+ readonly icon: string | undefined;
29
+ readonly loadTitle: (() => Promise<string>) | undefined;
30
+ }
31
+
32
+ const escapeHtml = (value: string): string =>
33
+ value
34
+ .replaceAll("&", "&amp;")
35
+ .replaceAll("<", "&lt;")
36
+ .replaceAll(">", "&gt;")
37
+ .replaceAll('"', "&quot;")
38
+ .replaceAll("'", "&#39;");
39
+
40
+ export const renderClientDocument = (options: ClientDocumentOptions): string => `<!doctype html>
7
41
  <html lang="en">
8
42
  <head>
9
43
  <meta charset="UTF-8" />
10
44
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
11
- <title>Ignotum</title>
45
+ <title>${escapeHtml(options.title)}</title>
46
+ ${options.icon === undefined ? "" : ` <link rel="icon" type="image/svg+xml" href="${escapeHtml(options.icon)}" />`}
47
+ ${options.styles.map((href) => ` <link rel="stylesheet" crossorigin href="/${escapeHtml(href)}" />`).join("\n")}
12
48
  </head>
13
49
  <body>
14
50
  <div id="app"></div>
15
- <script type="module">
16
- import "${clientEntryId}"
17
- </script>
51
+ ${options.scripts
52
+ .map((script) =>
53
+ script.type === "External"
54
+ ? ` <script type="module" crossorigin src="/${escapeHtml(script.source)}"></script>`
55
+ : ` <script type="module">\n ${script.source}\n </script>`,
56
+ )
57
+ .join("\n")}
18
58
  </body>
19
59
  </html>`;
20
60
 
21
- const clientEntry = `import "preact/debug"
22
- import { render } from "preact"
61
+ export const renderClientEntry = (
62
+ development: boolean,
63
+ ): string => `${development ? 'import "preact/debug"\n' : ""}import { render } from "preact"
23
64
  import { jsx } from "preact/jsx-runtime"
24
65
 
25
- import "/styles.css"
26
- import App from "/App.tsx"
66
+ import "${clientStylesId}"
67
+ import application from "/index.tsx"
27
68
 
28
69
  const root = document.getElementById("app")
29
70
 
@@ -31,10 +72,23 @@ if (root === null) {
31
72
  throw new Error("Ignotum mount element is missing.")
32
73
  }
33
74
 
34
- render(jsx(App, {}), root)
75
+ document.title = application.title
76
+ render(jsx(application.component, {}), root)
35
77
  `;
36
78
 
37
- export const ignotumClientPlugin = (): Plugin => ({
79
+ const loadClientModule = (id: string, development: boolean): string | undefined => {
80
+ if (id === resolvedClientEntryId) return renderClientEntry(development);
81
+ if (id === resolvedClientStylesId) return clientStylesSource;
82
+ return undefined;
83
+ };
84
+
85
+ export const resolveClientModule = (id: string): string | undefined => {
86
+ if (id === clientEntryId) return resolvedClientEntryId;
87
+ if (id === clientStylesId) return resolvedClientStylesId;
88
+ return undefined;
89
+ };
90
+
91
+ export const ignotumClientPlugin = (options: ClientPluginOptions): Plugin => ({
38
92
  name: "ignotum:client",
39
93
 
40
94
  configureServer: (server) => {
@@ -49,8 +103,24 @@ export const ignotumClientPlugin = (): Plugin => ({
49
103
 
50
104
  const requestUrl = request.originalUrl ?? request.url ?? "/";
51
105
 
52
- void server
53
- .transformIndexHtml(requestUrl, document)
106
+ const loadTitle = options.loadTitle;
107
+ if (loadTitle === undefined) {
108
+ next(new Error("Ignotum's development title loader is missing."));
109
+ return;
110
+ }
111
+
112
+ void loadTitle()
113
+ .then((title) =>
114
+ server.transformIndexHtml(
115
+ requestUrl,
116
+ renderClientDocument({
117
+ icon: options.icon,
118
+ scripts: [{ source: `import "${clientEntryId}"`, type: "Inline" }],
119
+ styles: [],
120
+ title,
121
+ }),
122
+ ),
123
+ )
54
124
  .then((html) => {
55
125
  response.statusCode = 200;
56
126
  response.setHeader("Content-Type", "text/html; charset=utf-8");
@@ -60,7 +130,7 @@ export const ignotumClientPlugin = (): Plugin => ({
60
130
  });
61
131
  },
62
132
 
63
- load: (id) => (id === resolvedClientEntryId ? clientEntry : undefined),
133
+ load: (id) => loadClientModule(id, options.development),
64
134
 
65
- resolveId: (id) => (id === clientEntryId ? resolvedClientEntryId : undefined),
135
+ resolveId: resolveClientModule,
66
136
  });
@@ -152,10 +152,10 @@ const writeOutput = Effect.fn("Codegen.writeOutput")(function* (output: Generate
152
152
  );
153
153
  });
154
154
 
155
- export const generate = Effect.fn("Codegen.generate")(function* (projectDirectory: string) {
155
+ export const generate = Effect.fn("Codegen.generate")(function* (appDirectory: string) {
156
156
  const fileSystem = yield* FileSystem.FileSystem;
157
157
  const path = yield* Path.Path;
158
- const serverDirectory = path.join(projectDirectory, "server");
158
+ const serverDirectory = path.join(appDirectory, "server");
159
159
  const schemaPath = path.join(serverDirectory, "schema.ts");
160
160
  const schemaExists = yield* fileSystem.exists(schemaPath);
161
161
 
@@ -174,15 +174,15 @@ export const generate = Effect.fn("Codegen.generate")(function* (projectDirector
174
174
  const outputs: ReadonlyArray<GeneratedOutput> = [
175
175
  {
176
176
  content: renderServerBindings(),
177
- path: path.join(projectDirectory, "_generated", "server.ts"),
177
+ path: path.join(appDirectory, "_generated", "server.ts"),
178
178
  },
179
179
  {
180
180
  content: renderApi(functionModules),
181
- path: path.join(projectDirectory, "_generated", "api.ts"),
181
+ path: path.join(appDirectory, "_generated", "api.ts"),
182
182
  },
183
183
  {
184
184
  content: renderTypes(),
185
- path: path.join(projectDirectory, "_generated", "types.ts"),
185
+ path: path.join(appDirectory, "_generated", "types.ts"),
186
186
  },
187
187
  ];
188
188
  const inspections = yield* Effect.forEach(outputs, inspectOutput);
@@ -1,12 +1,16 @@
1
- import { NodeRuntime, NodeServices } from "@effect/platform-node";
2
- import { Effect, Path, Schema, Terminal } from "effect";
1
+ import { NodeHttpClient, NodeRuntime, NodeServices } from "@effect/platform-node";
2
+ import { Effect, Layer, Option, Path, Schema, Terminal } from "effect";
3
3
  import { Argument, CliError, Command, Flag } from "effect/unstable/cli";
4
+ import { AppSlug } from "@ignotum/contracts/control";
5
+ import { IdGenerator } from "@ignotum/shared/id";
4
6
 
5
7
  import packageJson from "../../package.json" with { type: "json" };
6
8
  import { generate } from "./codegen.js";
9
+ import { ControlClient } from "./control-client.js";
10
+ import { deploy as runDeploy } from "./deploy.js";
7
11
  import { resetDevDatabase } from "../dev-runtime/dev-database.js";
8
12
  import { dev as runDev } from "./dev.js";
9
- import { createProject } from "./new-project.js";
13
+ import { createApp } from "./new-app.js";
10
14
  import { installDependencies } from "./package-manager.js";
11
15
 
12
16
  const DevPort = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65_535 }));
@@ -67,7 +71,7 @@ const dev = Command.make(
67
71
  host,
68
72
  open,
69
73
  port,
70
- projectDirectory: path.resolve("."),
74
+ appDirectory: path.resolve("."),
71
75
  }).pipe(
72
76
  Effect.mapError((error) =>
73
77
  CliError.UserError.make({ cause: error, userMessage: error.message }),
@@ -79,6 +83,32 @@ const dev = Command.make(
79
83
  Command.withSubcommands([devDatabase]),
80
84
  );
81
85
 
86
+ const deploy = Command.make(
87
+ "deploy",
88
+ {
89
+ app: Flag.string("app").pipe(
90
+ Flag.withDescription("Create or link this app slug on the first deploy."),
91
+ Flag.withSchema(AppSlug),
92
+ Flag.optional,
93
+ ),
94
+ },
95
+ Effect.fn("deploy")(function* ({ app }) {
96
+ const path = yield* Path.Path;
97
+ const terminal = yield* Terminal.Terminal;
98
+ const result = yield* runDeploy(path.resolve("."), Option.getOrUndefined(app)).pipe(
99
+ // @effect-diagnostics-next-line strictEffectProvide:off The deploy subcommand owns this client layer for its full lifetime.
100
+ Effect.provide(ControlClient.layer),
101
+ Effect.mapError((error) =>
102
+ CliError.UserError.make({ cause: error, userMessage: error.message }),
103
+ ),
104
+ );
105
+
106
+ yield* terminal.display(
107
+ `Deployed ${result.clientAssets} client artifact${result.clientAssets === 1 ? "" : "s"} and ${result.serverFunctions} server function${result.serverFunctions === 1 ? "" : "s"}.\n\nApp: ${result.appUrl}\nDeployment: ${result.deploymentId}\n`,
108
+ );
109
+ }),
110
+ ).pipe(Command.withDescription("Build and deploy the application to Ignotum Cloud."));
111
+
82
112
  const install = Command.make(
83
113
  "install",
84
114
  {},
@@ -95,7 +125,7 @@ const install = Command.make(
95
125
  }),
96
126
  ).pipe(Command.withDescription("Install dependencies with pnpm, or npm when pnpm is unavailable."));
97
127
 
98
- const newProject = Command.make(
128
+ const newApp = Command.make(
99
129
  "new",
100
130
  {
101
131
  directory: Argument.string("directory").pipe(
@@ -114,7 +144,7 @@ const newProject = Command.make(
114
144
  const path = yield* Path.Path;
115
145
  const terminal = yield* Terminal.Terminal;
116
146
  const currentDirectory = path.resolve(".");
117
- const result = yield* createProject({ currentDirectory, directory, git, install }).pipe(
147
+ const result = yield* createApp({ currentDirectory, directory, git, install }).pipe(
118
148
  Effect.mapError((error) =>
119
149
  CliError.UserError.make({ cause: error, userMessage: error.message }),
120
150
  ),
@@ -124,7 +154,7 @@ const newProject = Command.make(
124
154
  const installCommand = result.packageManager === null ? " npx ignotum install\n" : "";
125
155
 
126
156
  yield* terminal.display(
127
- `Created ${result.projectName} in ${result.directory}.\n\nNext steps:\n${changeDirectory}${installCommand} npx ignotum dev\n`,
157
+ `Created ${result.appName} in ${result.directory}.\n\nNext steps:\n${changeDirectory}${installCommand} npx ignotum dev\n`,
128
158
  );
129
159
  }),
130
160
  ).pipe(
@@ -142,14 +172,16 @@ const newProject = Command.make(
142
172
  );
143
173
 
144
174
  const ignotum = Command.make("ignotum").pipe(
145
- Command.withSubcommands([newProject, install, codegen, dev]),
175
+ Command.withSubcommands([newApp, install, codegen, dev, deploy]),
146
176
  );
147
177
 
148
178
  export const main = (): void => {
149
179
  ignotum.pipe(
150
180
  Command.run({ version: packageJson.version }),
151
181
  // @effect-diagnostics-next-line strictEffectProvide:off
152
- Effect.provide(NodeServices.layer),
182
+ Effect.provide(
183
+ Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, IdGenerator.layer),
184
+ ),
153
185
  NodeRuntime.runMain,
154
186
  );
155
187
  };