bazis 0.126.0 → 0.128.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.
Files changed (41) hide show
  1. package/cli/generateModule.ts +5 -5
  2. package/cli/generateProcess.ts +56 -0
  3. package/cli/main.ts +35 -1
  4. package/cli/moduleRegistration.ts +23 -10
  5. package/cli/parseCli.ts +45 -3
  6. package/cli/templates/process.ts +28 -0
  7. package/cli/workflow.ts +66 -0
  8. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  9. package/core/workflow/Workflow.module.ts +4 -2
  10. package/core/workflow/contracts/WorkflowClock.ts +21 -0
  11. package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +14 -6
  12. package/core/workflow/definitions_module/code/CodeProcessCatalog.ts +19 -0
  13. package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +44 -0
  14. package/core/workflow/definitions_module/code/codeProcesses.ts +85 -0
  15. package/core/workflow/definitions_module/code/defineProcess.ts +40 -0
  16. package/core/workflow/definitions_module/code/inspectProcesses.ts +74 -0
  17. package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +11 -2
  18. package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +106 -45
  19. package/core/workflow/index.ts +21 -0
  20. package/core/workflow/internal/http.ts +6 -0
  21. package/core/workflow/runtime_module/WorkflowRuntime.module.ts +2 -0
  22. package/core/workflow/runtime_module/background/WorkflowRetention.ts +9 -3
  23. package/core/workflow/runtime_module/background/WorkflowWorker.ts +11 -88
  24. package/core/workflow/runtime_module/events/Runtime.events.ts +6 -0
  25. package/core/workflow/runtime_module/internal/Schedules.ts +4 -2
  26. package/core/workflow/runtime_module/internal/ToolStep.ts +3 -3
  27. package/core/workflow/runtime_module/internal/WorkflowEngine.ts +18 -11
  28. package/core/workflow/runtime_module/internal/WorkflowJobs.ts +100 -0
  29. package/core/workflow/tasks_module/WorkflowTasks.module.ts +31 -0
  30. package/core/workflow/tasks_module/events/TaskEvents.ts +23 -0
  31. package/core/workflow/tasks_module/http/Tasks.controller.ts +67 -0
  32. package/core/workflow/tasks_module/model/Tasks.dbContext.ts +14 -0
  33. package/core/workflow/tasks_module/model/WfTask.model.ts +69 -0
  34. package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +52 -0
  35. package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +169 -0
  36. package/core/workflow/testing/index.ts +162 -0
  37. package/library/terminal/messages.ts +28 -0
  38. package/library/terminal/screen.ts +1 -0
  39. package/library/workflow/index.ts +1 -0
  40. package/library/workflow/mermaid.ts +87 -0
  41. package/package.json +2 -1
@@ -39,7 +39,7 @@ export interface GenerateModuleResult {
39
39
  readonly warnings: readonly string[];
40
40
  }
41
41
 
42
- interface PlannedWrite {
42
+ export interface PlannedWrite {
43
43
  readonly absolute: string;
44
44
  readonly content: string;
45
45
  readonly original: string | undefined;
@@ -82,7 +82,7 @@ export async function generateModulePack(options: GenerateModulePackOptions): Pr
82
82
  }
83
83
 
84
84
  /** The existing composite module `name` under `modulesRoot`: `Catalog` → `catalog_modules/Catalog.module.ts`. */
85
- async function findPack(modulesRoot: string, name: string): Promise<{ readonly dir: string; readonly entry: string; readonly moduleClass: string }> {
85
+ export async function findPack(modulesRoot: string, name: string): Promise<{ readonly dir: string; readonly entry: string; readonly moduleClass: string }> {
86
86
  const naming = parseModuleName(name);
87
87
  const dir = path.join(modulesRoot, `${naming.folder}_modules`);
88
88
  const entry = path.join(dir, `${naming.module}.module.ts`);
@@ -150,7 +150,7 @@ async function generateFiles(options: GenerateModuleOptions, naming: ModuleNamin
150
150
  };
151
151
  }
152
152
 
153
- async function fileInfo(absolute: string) {
153
+ export async function fileInfo(absolute: string) {
154
154
  try {
155
155
  return await lstat(absolute);
156
156
  } catch (error) {
@@ -161,7 +161,7 @@ async function fileInfo(absolute: string) {
161
161
  }
162
162
 
163
163
  /** Relative imports must survive runtime realpath resolution (e.g. /var -> /private/var). */
164
- async function canonicalDirectory(directory: string): Promise<string> {
164
+ export async function canonicalDirectory(directory: string): Promise<string> {
165
165
  try {
166
166
  return await realpath(directory);
167
167
  } catch (error) {
@@ -172,7 +172,7 @@ async function canonicalDirectory(directory: string): Promise<string> {
172
172
  }
173
173
  }
174
174
 
175
- async function commitWrites(writes: readonly PlannedWrite[]): Promise<void> {
175
+ export async function commitWrites(writes: readonly PlannedWrite[]): Promise<void> {
176
176
  const completed: PlannedWrite[] = [];
177
177
  const createdDirectories = new Set<string>();
178
178
  try {
@@ -0,0 +1,56 @@
1
+ import path from "node:path";
2
+ import { readFile } from "node:fs/promises";
3
+ import { canonicalDirectory, commitWrites, fileInfo, findPack, type GenerateModuleResult, type PlannedWrite } from "./generateModule";
4
+ import { registerInModuleField } from "./moduleRegistration";
5
+ import { parseModuleName } from "./naming";
6
+ import { packPartFolder } from "./templates/pack";
7
+ import { buildProcessTemplate } from "./templates/process";
8
+
9
+ export interface GenerateProcessOptions {
10
+ /** OrderApproval, order-approval. */
11
+ readonly name: string;
12
+ /** The module that owns the process: `Orders` → `{modulesRoot}/orders/Orders.module.ts`. */
13
+ readonly module: string;
14
+ /** The composite module the owner is a part of. */
15
+ readonly pack?: string;
16
+ /** Default: src/app/modules, relative to cwd. */
17
+ readonly modulesRoot?: string;
18
+ readonly force?: boolean;
19
+ readonly dryRun?: boolean;
20
+ }
21
+
22
+ /**
23
+ * Creates `processes/<Name>.process.ts` in a module and lists the process in
24
+ * the module's `@Module({ processes })` (WORKFLOW-001 §14).
25
+ */
26
+ export async function generateProcess(options: GenerateProcessOptions): Promise<GenerateModuleResult> {
27
+ const naming = parseModuleName(options.name);
28
+ const owner = parseModuleName(options.module);
29
+ const modulesRoot = await canonicalDirectory(path.resolve(options.modulesRoot ?? "src/app/modules"));
30
+ const pack = options.pack === undefined ? undefined : await findPack(modulesRoot, options.pack);
31
+ const moduleDir = pack ? path.join(pack.dir, packPartFolder(owner)) : path.join(modulesRoot, owner.folder);
32
+ const moduleFile = path.join(moduleDir, `${owner.module}.module.ts`);
33
+ const moduleInfo = await fileInfo(moduleFile);
34
+ if (moduleInfo?.isFile() !== true) {
35
+ throw new Error(`Module ${owner.moduleClass} not found: ${path.relative(process.cwd(), moduleFile)}.${pack ? "" : " For a part of a composite module add --pack <Name>."}`);
36
+ }
37
+ const processFile = path.join(moduleDir, "processes", `${naming.module}.process.ts`);
38
+ const existing = await fileInfo(processFile);
39
+ if (existing && !options.force) throw new Error(`File already exists: ${processFile}. Use --force to overwrite.`);
40
+ if (existing && !existing.isFile()) throw new Error(`Refusing to overwrite a non-regular file: ${processFile}`);
41
+ const moduleSource = await readFile(moduleFile, "utf8");
42
+ const writes: PlannedWrite[] = [
43
+ { absolute: processFile, content: buildProcessTemplate(naming, owner.moduleClass), original: existing ? await readFile(processFile, "utf8") : undefined },
44
+ { absolute: moduleFile, content: registerInModuleField(moduleSource, moduleFile, processFile, `${naming.module}Process`, { field: "processes", hostClass: owner.moduleClass }), original: moduleSource },
45
+ ];
46
+ const changed = writes.filter((write) => write.content !== write.original);
47
+ if (!options.dryRun) await commitWrites(changed);
48
+ return {
49
+ moduleDir,
50
+ files: [path.relative(process.cwd(), processFile)],
51
+ registered: true,
52
+ dryRun: options.dryRun === true,
53
+ warnings: [],
54
+ changes: changed.map((write) => ({ path: path.relative(process.cwd(), write.absolute), action: write.original === undefined ? "create" : "update" })),
55
+ };
56
+ }
package/cli/main.ts CHANGED
@@ -6,12 +6,18 @@ import { parseCliArgs, type ParsedGenerateArgs } from "./parseCli";
6
6
  import { runCodegen, type CodegenOptions } from "./codegen";
7
7
  import { generateProject } from "./generateProject";
8
8
  import { runBuild, runDev, runTest } from "./build";
9
+ import type { ParsedProcessArgs } from "./parseCli";
9
10
  import { bazisVersion, commandFailed, commandHeader, createScreens, lineOutput, requiredBun, type CliOutput } from "./ui";
10
11
 
11
12
  export const USAGE = `Usage:
12
13
  bazis new <Name> [options] Create a new application project
13
14
  bazis g module <Name> [options] Atomic module (alias: m)
14
15
  bazis g pack <Name> --parts <a,b> [options] Composite module (aliases: p, module-pack)
16
+ bazis g process <Name> --module <Name> Process in code: processes/<Name>.process.ts in the module,
17
+ listed in its @Module processes (alias: pr; --pack <Name>
18
+ for a part of a composite module)
19
+ bazis workflow check [--app-module <path>] Check the processes of the modules against their Tools
20
+ bazis workflow diagram <key> Print a process as a Mermaid flowchart
15
21
  bazis codegen [--target <name|all>] Generate DI wiring into src/generated/bazis
16
22
  bazis dev [--watch] Codegen, then run the app from source with
17
23
  BAZIS_ENV=development (unless set);
@@ -58,6 +64,9 @@ Examples (from the project root):
58
64
  bunx bazis g module Mailer --empty --no-register
59
65
  bunx bazis g pack DataManager --parts tables,fields,validators,records
60
66
  bunx bazis g module Prices --empty --pack Catalog
67
+ bunx bazis g process OrderApproval --module Orders
68
+ bunx bazis workflow check
69
+ bunx bazis workflow diagram order-approval > order-approval.mmd
61
70
  bunx bazis codegen --target production
62
71
  bunx bazis dev --watch
63
72
  bunx bazis test
@@ -134,15 +143,40 @@ export async function runCli(argv: readonly string[], runtime: CliRuntime = defa
134
143
  if (parsed.kind === "test") return await runTest(cwd, parsed.args, codegen, ui);
135
144
  if (parsed.kind === "build") return await runBuild(cwd, { bin: parsed.bin, outfile: parsed.outfile, dotenv: parsed.dotenv, platform: parsed.platform }, codegen, ui);
136
145
  if (parsed.kind === "new") return await runNew(parsed, ui);
146
+ if (parsed.kind === "workflow") {
147
+ const { runWorkflowCheck, runWorkflowDiagram } = await import("./workflow");
148
+ return parsed.action === "check" ? await runWorkflowCheck(cwd, parsed.appModule, ui) : await runWorkflowDiagram(cwd, parsed.key, parsed.appModule, runtime.log);
149
+ }
137
150
  // Module registration parses the host module with the TypeScript API: load it
138
151
  // only after switching TypeScript 7 to @typescript/typescript6.
139
152
  useTypeScriptCompilerApi(cwd);
153
+ if (parsed.kind === "process") return await runGenerateProcess(cwd, parsed.args, ui);
140
154
  return await runGenerate(cwd, parsed.args, codegen, ui);
141
155
  } catch (error) {
142
- return commandFailed(ui.err, ui.err.t.failure.command, [{ tag: "CLI", where: parsed.kind === "generate" ? `g ${parsed.args.generator}` : parsed.kind, cause: error instanceof Error ? error.message : String(error) }], 1);
156
+ return commandFailed(ui.err, ui.err.t.failure.command, [{ tag: "CLI", where: parsed.kind === "generate" ? `g ${parsed.args.generator}` : parsed.kind === "process" ? "g process" : parsed.kind === "workflow" ? `workflow ${parsed.action}` : parsed.kind, cause: error instanceof Error ? error.message : String(error) }], 1);
143
157
  }
144
158
  }
145
159
 
160
+ async function runGenerateProcess(cwd: string, args: ParsedProcessArgs, ui: CliOutput): Promise<number> {
161
+ const s = ui.out;
162
+ const t = s.t;
163
+ const { generateProcess } = await import("./generateProcess");
164
+ commandHeader(s, "gen", cwd, [
165
+ s.kv(t.workflow.processKind, s.strong(args.name)),
166
+ s.kv(t.noun("module", 1), s.text(args.pack === undefined ? args.module : `${args.pack} / ${args.module}`)),
167
+ ...(args.dryRun ? [s.fg(PALETTE.yellow, t.common.dryRun)] : []),
168
+ ]);
169
+ const result = await generateProcess(args);
170
+ s.section(t.section.files, s.muted(path.relative(cwd, result.moduleDir) || result.moduleDir));
171
+ s.step("ok", t.step.files, s.num(result.changes.length, t.noun("change", result.changes.length)));
172
+ s.tree(result.changes.map((change) => ({ text: `${change.action === "create" ? s.strong("+", PALETTE.lime) : s.strong("~", PALETTE.yellow)} ${s.text(change.path)}` })), s.style.color ? 9 : 4);
173
+ const host = result.changes.find((change) => change.action === "update")?.path;
174
+ if (host !== undefined) s.line(` ${s.strong("↗", PALETTE.export)} ${s.text(t.workflow.processRegistered(host))}`);
175
+ s.line();
176
+ s.banner("success", result.dryRun ? t.workflow.processPlanned(args.name) : t.workflow.processCreated(args.name));
177
+ return 0;
178
+ }
179
+
146
180
  async function runCodegenCommand(cwd: string, target: string | undefined, codegen: CliRuntime["codegen"], ui: CliOutput): Promise<number> {
147
181
  const s = ui.out;
148
182
  const t = s.t;
@@ -10,9 +10,19 @@ export function moduleImportPath(fromFile: string, moduleFile: string): string {
10
10
 
11
11
  /** Edits only the import and the selected @Module metadata, preserving other source text. */
12
12
  export function registerModuleInSource(content: string, appFile: string, moduleFile: string, moduleClass: string): string {
13
+ return registerInModuleField(content, appFile, moduleFile, moduleClass, { field: "imports", hostClass: "AppModule" });
14
+ }
15
+
16
+ /**
17
+ * Adds `moduleClass` (an export of `moduleFile`) to the array `field` of the
18
+ * `@Module` in `appFile` and imports it: `imports` for a module, `processes`
19
+ * for a process. The class named `hostClass` is chosen, or the only one.
20
+ */
21
+ export function registerInModuleField(content: string, appFile: string, moduleFile: string, moduleClass: string, options: { readonly field: string; readonly hostClass?: string }): string {
22
+ const { field } = options;
13
23
  const diagnostics = ts.transpileModule(content, { fileName: appFile, reportDiagnostics: true }).diagnostics ?? [];
14
24
  if (diagnostics.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)) {
15
- throw new Error(`Cannot register a module in invalid TypeScript: ${appFile}`);
25
+ throw new Error(`Cannot register in invalid TypeScript: ${appFile}`);
16
26
  }
17
27
  const source = ts.createSourceFile(appFile, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
18
28
  const importPath = moduleImportPath(appFile, moduleFile);
@@ -75,26 +85,26 @@ export function registerModuleInSource(content: string, appFile: string, moduleF
75
85
  candidates.push({ name: statement.name?.text, metadata });
76
86
  }
77
87
  }
78
- const candidate = candidates.find((item) => item.name === "AppModule") ?? (candidates.length === 1 ? candidates[0] : undefined);
79
- if (!candidate) throw new Error("Could not select a unique @Module class for registration in App.module.ts.");
88
+ const candidate = candidates.find((item) => item.name === options.hostClass) ?? (candidates.length === 1 ? candidates[0] : undefined);
89
+ if (!candidate) throw new Error(`Could not select a unique @Module class for registration in ${path.basename(appFile)}.`);
80
90
  const metadata = candidate.metadata;
81
91
  if (metadata.properties.some(ts.isSpreadAssignment)) throw new Error("Automatic registration does not support spread @Module metadata; register the module explicitly.");
82
92
  if (metadata.properties.some((property) => property.name && ts.isComputedPropertyName(property.name))) throw new Error("Automatic registration does not support computed @Module properties; register the module explicitly.");
83
- const importProperties = metadata.properties.filter((property) => property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && property.name.text === "imports");
84
- if (importProperties.length > 1) throw new Error("Duplicate imports properties in @Module metadata.");
93
+ const importProperties = metadata.properties.filter((property) => property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && property.name.text === field);
94
+ if (importProperties.length > 1) throw new Error(`Duplicate ${field} properties in @Module metadata.`);
85
95
  const property = importProperties[0];
86
96
  const edits: Edit[] = [];
87
97
  if (property) {
88
98
  if (!ts.isPropertyAssignment(property) || !ts.isArrayLiteralExpression(property.initializer)) {
89
- throw new Error("@Module imports must be an array literal for automatic registration.");
99
+ throw new Error(`@Module ${field} must be an array literal for automatic registration.`);
90
100
  }
91
101
  const array = property.initializer;
92
- if (array.elements.some(ts.isOmittedExpression)) throw new Error("@Module imports contains an empty element; fix the array before registration.");
102
+ if (array.elements.some(ts.isOmittedExpression)) throw new Error(`@Module ${field} contains an empty element; fix the array before registration.`);
93
103
  if (!array.elements.some((element) => ts.isIdentifier(element) && element.text === binding)) {
94
104
  appendMember(content, array, array.elements, binding, edits, eol);
95
105
  }
96
106
  } else {
97
- appendMember(content, metadata, metadata.properties, `imports: [${binding}]`, edits, eol);
107
+ appendMember(content, metadata, metadata.properties, `${field}: [${binding}]`, edits, eol);
98
108
  }
99
109
  if (!imported) {
100
110
  const lastImport = imports.at(-1);
@@ -110,12 +120,15 @@ export function registerModuleInSource(content: string, appFile: string, moduleF
110
120
  function appendMember(content: string, node: ts.Node, members: ts.NodeArray<ts.Node>, member: string, edits: Edit[], eol: string): void {
111
121
  const close = node.end - 1;
112
122
  const last = members.at(-1);
113
- if (last && !members.hasTrailingComma) edits.push({ at: last.end, text: "," });
114
123
  const inner = content.slice(node.getStart() + 1, close);
115
124
  if (!inner.includes("\n")) {
116
- edits.push({ at: close, text: `${last || inner.trim() ? " " : ""}${member}${members.hasTrailingComma ? "," : ""}` });
125
+ // One line: right after the last member (and its comma), `{ a, b }` → `{ a, b, c }`.
126
+ if (!last) edits.push({ at: close, text: inner.length > 0 ? `${member} ` : member });
127
+ else if (members.hasTrailingComma) edits.push({ at: content.indexOf(",", last.end) + 1, text: ` ${member},` });
128
+ else edits.push({ at: last.end, text: `, ${member}` });
117
129
  return;
118
130
  }
131
+ if (last && !members.hasTrailingComma) edits.push({ at: last.end, text: "," });
119
132
  const lineStart = content.lastIndexOf("\n", close) + 1;
120
133
  const closingIndent = content.slice(lineStart, close);
121
134
  if (/^\s*$/.test(closingIndent)) {
package/cli/parseCli.ts CHANGED
@@ -18,11 +18,24 @@ export interface ParsedGenerateArgs {
18
18
  readonly target?: string;
19
19
  }
20
20
 
21
+ export interface ParsedProcessArgs {
22
+ readonly name: string;
23
+ /** The module that owns the process. */
24
+ readonly module: string;
25
+ readonly pack?: string;
26
+ readonly modulesRoot?: string;
27
+ readonly force: boolean;
28
+ readonly dryRun: boolean;
29
+ }
30
+
21
31
  export type ParseCliResult =
22
32
  | { readonly kind: "help"; readonly help: boolean }
23
33
  | { readonly kind: "version" }
24
34
  | { readonly kind: "new"; readonly name: string; readonly outputPath?: string; readonly frameworkPath?: string; readonly linkFramework: boolean; readonly vendor: boolean; readonly dryRun: boolean }
25
35
  | { readonly kind: "generate"; readonly args: ParsedGenerateArgs }
36
+ | { readonly kind: "process"; readonly args: ParsedProcessArgs }
37
+ | { readonly kind: "workflow"; readonly action: "check"; readonly appModule?: string }
38
+ | { readonly kind: "workflow"; readonly action: "diagram"; readonly key: string; readonly appModule?: string }
26
39
  | { readonly kind: "codegen"; readonly target?: string }
27
40
  | { readonly kind: "dev"; readonly watch: boolean }
28
41
  | { readonly kind: "test"; readonly args: readonly string[] }
@@ -33,7 +46,7 @@ const GENERATOR_ALIASES = new Map<string, "module" | "pack">([
33
46
  ["module", "module"], ["m", "module"],
34
47
  ["pack", "pack"], ["p", "pack"], ["module-pack", "pack"],
35
48
  ]);
36
- const VALUE_OPTIONS = new Set(["--modules-root", "--app-module", "--pack", "--parts", "--target", "--path", "--framework", "--outfile", "--platform"]);
49
+ const VALUE_OPTIONS = new Set(["--modules-root", "--app-module", "--pack", "--module", "--parts", "--target", "--path", "--framework", "--outfile", "--platform"]);
37
50
  /** Platforms of `bun build --compile --target=bun-<platform>`. */
38
51
  export const BINARY_PLATFORM = /^(linux|darwin|windows)-(x64|arm64)(-musl)?(-baseline|-modern)?$/;
39
52
  const FLAG_OPTIONS = new Set(["--no-register", "--force", "--full", "--enterprise", "--minimal", "--empty", "--dry-run", "--no-codegen", "--link-framework", "--vendor", "--bin", "--watch", "--dotenv"]);
@@ -114,13 +127,42 @@ export function parseCliArgs(argv: readonly string[]): ParseCliResult {
114
127
  }
115
128
  return { kind: "codegen", target };
116
129
  }
130
+ if (command === "workflow") {
131
+ for (const option of options.keys()) {
132
+ if (option !== "--app-module") return error(`Option ${option} is not supported by workflow.`);
133
+ }
134
+ const appModule = options.get("--app-module");
135
+ if (generatorToken === "check" && positional.length === 2) return { kind: "workflow", action: "check", ...(appModule === undefined ? {} : { appModule }) };
136
+ if (generatorToken === "diagram" && positional.length === 3) return { kind: "workflow", action: "diagram", key: name!, ...(appModule === undefined ? {} : { appModule }) };
137
+ return error("Use: bazis workflow check [--app-module <path>] or bazis workflow diagram <key> [--app-module <path>]");
138
+ }
139
+ if (options.has("--module") && !((command === "g" || command === "generate") && (generatorToken === "process" || generatorToken === "pr"))) return error("--module is only supported by g process.");
140
+ if ((command === "g" || command === "generate") && (generatorToken === "process" || generatorToken === "pr")) {
141
+ for (const option of options.keys()) {
142
+ if (!["--module", "--pack", "--modules-root", "--force", "--dry-run"].includes(option)) return error(`Option ${option} is not supported by g process.`);
143
+ }
144
+ if (name === undefined || positional.length !== 3) return error("Use: bazis g process <Name> --module <Name> [--pack <Name>]");
145
+ const module = options.get("--module");
146
+ if (module === undefined) return error("g process needs the module that owns the process: --module <Name>.");
147
+ try {
148
+ parseModuleName(name);
149
+ parseModuleName(module);
150
+ if (options.has("--pack")) parseModuleName(options.get("--pack")!);
151
+ } catch (cause) {
152
+ return error(cause instanceof Error ? cause.message : String(cause));
153
+ }
154
+ return {
155
+ kind: "process",
156
+ args: { name, module, pack: options.get("--pack"), modulesRoot: options.get("--modules-root"), force: options.has("--force"), dryRun: options.has("--dry-run") },
157
+ };
158
+ }
117
159
  if (command !== "g" && command !== "generate") {
118
160
  return error(`Unknown command: ${command ?? "(missing)"}. Use: bazis --help`);
119
161
  }
120
- if (generatorToken === undefined) return error("Generator is required: module (m) or pack (p).");
162
+ if (generatorToken === undefined) return error("Generator is required: module (m), pack (p) or process (pr).");
121
163
  if (options.has("--path") || options.has("--framework") || options.has("--link-framework") || options.has("--vendor")) return error("--path, --framework, --link-framework and --vendor are only supported by new.");
122
164
  const generator = GENERATOR_ALIASES.get(generatorToken);
123
- if (generator === undefined) return error(`Unknown generator: ${generatorToken}. Supported: module (m), pack (p)`);
165
+ if (generator === undefined) return error(`Unknown generator: ${generatorToken}. Supported: module (m), pack (p), process (pr)`);
124
166
  if (name === undefined) return error("Module name is required.");
125
167
  if (positional.length !== 3) return error(`Unexpected argument: ${positional[3]}`);
126
168
 
@@ -0,0 +1,28 @@
1
+ import type { ModuleNaming } from "../naming";
2
+
3
+ /** `processes/<Name>.process.ts`: a manual start and an end, to grow into the real process. */
4
+ export function buildProcessTemplate(process: ModuleNaming, moduleClass: string): string {
5
+ const title = process.folder.split("-").map((word, index) => (index === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word)).join(" ");
6
+ return `import { defineProcess } from "bazis/core/workflow";
7
+
8
+ /**
9
+ * ${title}: a process of ${moduleClass}. It is published when the application
10
+ * starts and changes only here. Check it with \`bunx bazis workflow check\`,
11
+ * draw it with \`bunx bazis workflow diagram ${process.folder}\`.
12
+ */
13
+ export const ${process.module}Process = defineProcess({
14
+ key: "${process.folder}",
15
+ title: "${title}",
16
+ document: {
17
+ schemaVersion: "bazis.workflow/v1",
18
+ nodes: {
19
+ Start: { kind: "manualStart", title: "Start" },
20
+ End: { kind: "end", title: "End" },
21
+ },
22
+ flows: {
23
+ f1: { source: "Start", target: "End" },
24
+ },
25
+ },
26
+ });
27
+ `;
28
+ }
@@ -0,0 +1,66 @@
1
+ import path from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+ import { PALETTE } from "../library/terminal";
4
+ import { processToMermaid } from "../library/workflow";
5
+ import { isBazisModuleClass } from "../core/di/module/Module";
6
+ import type { BazisModuleRef } from "../core/di";
7
+ import { inspectProcesses, type ProcessInspection } from "../core/workflow/definitions_module/code/inspectProcesses";
8
+ import { commandFailed, commandHeader, type CliOutput } from "./ui";
9
+
10
+ /**
11
+ * The application's root module, imported without starting anything: the
12
+ * export `AppModule` of the app module file, or its only `@Module` class.
13
+ */
14
+ export async function loadAppModule(cwd: string, appModule = "src/app/modules/App.module.ts"): Promise<{ readonly file: string; readonly root: BazisModuleRef }> {
15
+ const file = path.resolve(cwd, appModule);
16
+ if (!(await Bun.file(file).exists())) throw new Error(`App module not found: ${path.relative(cwd, file)}. Pass --app-module <path>.`);
17
+ const exports = await import(pathToFileURL(file).href) as Record<string, unknown>;
18
+ const modules = Object.entries(exports).filter(([, value]) => isBazisModuleClass(value));
19
+ const chosen = modules.find(([name]) => name === "AppModule") ?? (modules.length === 1 ? modules[0] : undefined);
20
+ if (!chosen) throw new Error(`${path.relative(cwd, file)} exports ${modules.length === 0 ? "no @Module class" : "several @Module classes and none is AppModule"}.`);
21
+ return { file, root: chosen[1] as BazisModuleRef };
22
+ }
23
+
24
+ /** `bazis workflow check`: every process of the module tree against the Tools; exit code 1 on errors. */
25
+ export async function runWorkflowCheck(cwd: string, appModule: string | undefined, ui: CliOutput): Promise<number> {
26
+ const s = ui.out;
27
+ const t = s.t.workflow;
28
+ const { file, root } = await loadAppModule(cwd, appModule);
29
+ commandHeader(s, "workflow", cwd, [s.kv(t.app, s.text(path.relative(cwd, file)))]);
30
+ const inspection = inspectProcesses(root);
31
+ s.section(t.section, s.muted(`${inspection.processes.length} ${s.t.noun("process", inspection.processes.length)} · ${inspection.tools.length} ${s.t.noun("tool", inspection.tools.length)}`));
32
+ if (inspection.processes.length === 0) s.line(` ${s.muted(t.none)}`);
33
+ for (const { process, module, errors, warnings } of inspection.processes) {
34
+ const nodes = Object.keys(process.document.nodes ?? {}).length;
35
+ s.step(errors.length > 0 ? "fail" : warnings.length > 0 ? "warn" : "ok", process.key, s.muted(`${module} · ${nodes} ${s.t.noun("node", nodes)}`));
36
+ s.tree([...errors.map((text) => ({ text: s.fg(PALETTE.red, text) })), ...warnings.map((text) => ({ text: s.fg(PALETTE.yellow, text) }))], s.style.color ? 9 : 4);
37
+ }
38
+ const errors = errorCount(inspection);
39
+ s.line();
40
+ if (errors > 0) {
41
+ const problems = [
42
+ ...inspection.errors.map((cause) => ({ tag: "WORKFLOW", where: "processes", cause })),
43
+ ...inspection.processes.filter((entry) => entry.errors.length > 0).map((entry) => ({ tag: "WORKFLOW", where: `${entry.module} · ${entry.process.key}`, cause: entry.errors.join("\n") })),
44
+ ];
45
+ return commandFailed(ui.err, t.failed(errors), problems, 1, t.fix);
46
+ }
47
+ s.banner("success", t.passed(inspection.processes.length));
48
+ return 0;
49
+ }
50
+
51
+ /** `bazis workflow diagram <key>`: the process as Mermaid on stdout, nothing else, so it can be redirected. */
52
+ export async function runWorkflowDiagram(cwd: string, key: string, appModule: string | undefined, log: (text: string) => void): Promise<number> {
53
+ const { root } = await loadAppModule(cwd, appModule);
54
+ const inspection = inspectProcesses(root);
55
+ const entry = inspection.processes.find(({ process }) => process.key === key);
56
+ if (!entry) {
57
+ const keys = inspection.processes.map(({ process }) => process.key);
58
+ throw new Error(`No process "${key}" is declared in code. ${keys.length === 0 ? "The modules declare no processes." : `Declared: ${keys.join(", ")}.`}`);
59
+ }
60
+ log(processToMermaid(entry.process.document).trimEnd());
61
+ return 0;
62
+ }
63
+
64
+ function errorCount(inspection: ProcessInspection): number {
65
+ return inspection.errors.length + inspection.processes.reduce((sum, entry) => sum + entry.errors.length, 0);
66
+ }
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
232
232
  if (method === "account/updated") this.modelCache = undefined;
233
233
  }, () => { this.clearLogin(); this.modelCache = undefined; });
234
234
  const initialized = await client.request("initialize", {
235
- clientInfo: { name: "bazis", title: "Bazis", version: "0.126.0" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.128.0" }, capabilities: { experimentalApi: true },
236
236
  }, 15_000, signal);
237
237
  if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
238
238
  client.notify("initialized");
@@ -3,13 +3,15 @@ import { WorkflowDefinitionsModule } from "./definitions_module/WorkflowDefiniti
3
3
  import { IWorkflowDefinitions } from "./definitions_module/services/IWorkflowDefinitions.service";
4
4
  import { WorkflowRuntimeModule } from "./runtime_module/WorkflowRuntime.module";
5
5
  import { IWorkflowRuntime } from "./runtime_module/services/IWorkflowRuntime.service";
6
+ import { WorkflowTasksModule } from "./tasks_module/WorkflowTasks.module";
7
+ import { IWorkflowTasks } from "./tasks_module/services/IWorkflowTasks.service";
6
8
 
7
9
  /**
8
10
  * Business processes (WORKFLOW-001): the pack only assembles its parts
9
11
  * ([passport](MODULE.md)). An application imports it, or only the parts it needs.
10
12
  */
11
13
  @Module({
12
- imports: [WorkflowDefinitionsModule, WorkflowRuntimeModule],
13
- exports: [IWorkflowDefinitions, IWorkflowRuntime],
14
+ imports: [WorkflowDefinitionsModule, WorkflowRuntimeModule, WorkflowTasksModule],
15
+ exports: [IWorkflowDefinitions, IWorkflowRuntime, IWorkflowTasks],
14
16
  })
15
17
  export class WorkflowModule {}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Where the engine takes the time from (WORKFLOW-001 §14): due times of timers,
3
+ * retries, scheduled starts and job delays, the retention cutoff and task marks.
4
+ * Record stamps (`createdAt`, history `at`) stay the database's time. The
5
+ * default is the system clock; tests replace it (`bazis/core/workflow/testing`).
6
+ */
7
+ export abstract class WorkflowClock {
8
+ abstract now(): Date;
9
+ /**
10
+ * Time moves only when a test moves it: the worker and retention then leave
11
+ * the jobs to the test's `drain`, so nothing runs behind its back.
12
+ */
13
+ readonly manual?: boolean;
14
+ }
15
+
16
+ /** The system clock. */
17
+ export class SystemWorkflowClock extends WorkflowClock {
18
+ now(): Date {
19
+ return new Date();
20
+ }
21
+ }
@@ -1,5 +1,9 @@
1
- import { DI, Module, SERVICE_PROVIDER, scoped } from "../../di";
1
+ import { DI, HOSTED_SERVICE, Module, SERVICE_PROVIDER, scoped } from "../../di";
2
+ import { SystemWorkflowClock, WorkflowClock } from "../contracts/WorkflowClock";
2
3
  import { WorkflowTools } from "../internal/WorkflowTools";
4
+ import { CodeProcessCatalog } from "./code/CodeProcessCatalog";
5
+ import { CodeProcessPublisher } from "./code/CodeProcessPublisher";
6
+ import { codeProcessesOf } from "./code/codeProcesses";
3
7
  import { ProcessesController } from "./http/Processes.controller";
4
8
  import { DefinitionsDbContext, definitionsStore } from "./model/Definitions.dbContext";
5
9
  import { WfProcess } from "./model/WfProcess.model";
@@ -8,17 +12,21 @@ import { IWorkflowDefinitions } from "./services/IWorkflowDefinitions.service";
8
12
  import { WorkflowDefinitions } from "./services/WorkflowDefinitions.service";
9
13
 
10
14
  /**
11
- * The process catalog: drafts and published versions ([passport](MODULE.md)).
12
- * Dependencies are explicit: the framework's classes are not covered by the
13
- * application's codegen.
15
+ * The process catalog: drafts and published versions, and the processes
16
+ * declared in code ([passport](MODULE.md)). Dependencies are explicit: the
17
+ * framework's classes are not covered by the application's codegen.
14
18
  */
15
19
  @Module({
16
20
  ormBazis: { context: DefinitionsDbContext, entities: [WfProcess, WfVersion], ownedStore: definitionsStore, registerRepositories: false },
17
21
  providers: [
22
+ DI.singleton(DI.factoryProvider(WorkflowClock, [], () => new SystemWorkflowClock())),
18
23
  DI.singleton(DI.factoryProviderWithResolver(WorkflowTools, [], (resolver) => new WorkflowTools(resolver.resolve(SERVICE_PROVIDER)))),
19
- scoped(IWorkflowDefinitions, WorkflowDefinitions, [DefinitionsDbContext, WorkflowTools] as const),
24
+ DI.singleton(DI.factoryProviderWithResolver(CodeProcessCatalog, [], (resolver) => new CodeProcessCatalog(codeProcessesOf(resolver.resolve(SERVICE_PROVIDER))))),
25
+ scoped(WorkflowDefinitions, WorkflowDefinitions, [DefinitionsDbContext, WorkflowTools, CodeProcessCatalog, WorkflowClock] as const),
26
+ DI.scoped(DI.factoryProviderWithResolver(IWorkflowDefinitions, [], (resolver) => resolver.resolve(WorkflowDefinitions))),
27
+ DI.singleton(DI.factoryProviderWithResolver(HOSTED_SERVICE, [], (resolver) => new CodeProcessPublisher(resolver.resolve(SERVICE_PROVIDER)))),
20
28
  ],
21
29
  controllers: [DI.bindDeps(ProcessesController, IWorkflowDefinitions)],
22
- exports: [IWorkflowDefinitions, WorkflowTools],
30
+ exports: [IWorkflowDefinitions, WorkflowTools, WorkflowClock],
23
31
  })
24
32
  export class WorkflowDefinitionsModule {}
@@ -0,0 +1,19 @@
1
+ import type { OwnedCodeProcess } from "./codeProcesses";
2
+
3
+ /** The processes declared in the application's modules, by key: which module owns a key. */
4
+ export class CodeProcessCatalog {
5
+ private readonly byKey: ReadonlyMap<string, OwnedCodeProcess>;
6
+
7
+ constructor(entries: readonly OwnedCodeProcess[]) {
8
+ this.byKey = new Map([...entries].sort((a, b) => a.process.key.localeCompare(b.process.key)).map((entry) => [entry.process.key, entry]));
9
+ }
10
+
11
+ /** The module whose `processes` declare `key`; `undefined` for a process of the catalog. */
12
+ moduleOf(key: string): string | undefined {
13
+ return this.byKey.get(key)?.module;
14
+ }
15
+
16
+ list(): readonly OwnedCodeProcess[] {
17
+ return [...this.byKey.values()];
18
+ }
19
+ }
@@ -0,0 +1,44 @@
1
+ import { markConfigurationError, type ServiceProvider } from "../../../di";
2
+ import type { HostedService } from "../../../di/extensions/hosted-service";
3
+ import { LOGGER } from "../../../kernel";
4
+ import { WorkflowTools } from "../../internal/WorkflowTools";
5
+ import { WorkflowDefinitions } from "../services/WorkflowDefinitions.service";
6
+ import { CodeProcessCatalog } from "./CodeProcessCatalog";
7
+ import { definitionProblems, WorkflowSetupError } from "./codeProcesses";
8
+
9
+ /**
10
+ * Publishes the processes of `@Module({ processes })` at start, after the
11
+ * tables exist (WORKFLOW-001 §14). A document with an unknown Tool stops the
12
+ * start with every finding of every process; the same document again gives no
13
+ * new version.
14
+ */
15
+ export class CodeProcessPublisher implements HostedService {
16
+ readonly diagnosticName = "workflow:processes";
17
+
18
+ constructor(private readonly services: ServiceProvider) {}
19
+
20
+ async start(): Promise<void> {
21
+ const entries = this.services.resolve(CodeProcessCatalog).list();
22
+ if (entries.length === 0) return;
23
+ const tools = this.services.resolve(WorkflowTools);
24
+ const problems = entries.flatMap(({ process, module }) => {
25
+ const found = definitionProblems(process, (name) => tools.has(name));
26
+ return found.length === 0 ? [] : [`${module}: process "${process.key}":\n${found.map((line) => ` - ${line}`).join("\n")}`];
27
+ });
28
+ if (problems.length > 0) {
29
+ throw markConfigurationError(new WorkflowSetupError(`Processes declared in code cannot be published:\n${problems.join("\n")}`));
30
+ }
31
+ const scope = this.services.createScope();
32
+ try {
33
+ const results = await scope.resolve(WorkflowDefinitions).publishCode(entries);
34
+ this.services.tryResolve(LOGGER)?.info("workflow processes from code", {
35
+ published: results.filter((result) => result.published).map((result) => `${result.key} v${result.version}`),
36
+ unchanged: results.filter((result) => !result.published).map((result) => result.key),
37
+ });
38
+ } finally {
39
+ await scope.dispose();
40
+ }
41
+ }
42
+
43
+ stop(): void {}
44
+ }