@sapiom/orchestration-core 0.1.1

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 (77) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/dist/cjs/check.d.ts +10 -0
  4. package/dist/cjs/check.js +154 -0
  5. package/dist/cjs/client.d.ts +17 -0
  6. package/dist/cjs/client.js +70 -0
  7. package/dist/cjs/config.d.ts +9 -0
  8. package/dist/cjs/config.js +38 -0
  9. package/dist/cjs/deploy.d.ts +12 -0
  10. package/dist/cjs/deploy.js +51 -0
  11. package/dist/cjs/errors.d.ts +15 -0
  12. package/dist/cjs/errors.js +23 -0
  13. package/dist/cjs/git.d.ts +2 -0
  14. package/dist/cjs/git.js +44 -0
  15. package/dist/cjs/index.d.ts +29 -0
  16. package/dist/cjs/index.js +50 -0
  17. package/dist/cjs/inspect.d.ts +41 -0
  18. package/dist/cjs/inspect.js +17 -0
  19. package/dist/cjs/link.d.ts +15 -0
  20. package/dist/cjs/link.js +19 -0
  21. package/dist/cjs/local/dispatcher.d.ts +35 -0
  22. package/dist/cjs/local/dispatcher.js +115 -0
  23. package/dist/cjs/local/load.d.ts +6 -0
  24. package/dist/cjs/local/load.js +102 -0
  25. package/dist/cjs/local/run-local.d.ts +26 -0
  26. package/dist/cjs/local/run-local.js +73 -0
  27. package/dist/cjs/local/stubs.d.ts +8 -0
  28. package/dist/cjs/local/stubs.js +35 -0
  29. package/dist/cjs/run.d.ts +11 -0
  30. package/dist/cjs/run.js +29 -0
  31. package/dist/cjs/scaffold.d.ts +22 -0
  32. package/dist/cjs/scaffold.js +145 -0
  33. package/dist/cjs/signal.d.ts +12 -0
  34. package/dist/cjs/signal.js +24 -0
  35. package/dist/esm/check.d.ts +10 -0
  36. package/dist/esm/check.js +115 -0
  37. package/dist/esm/client.d.ts +17 -0
  38. package/dist/esm/client.js +65 -0
  39. package/dist/esm/config.d.ts +9 -0
  40. package/dist/esm/config.js +29 -0
  41. package/dist/esm/deploy.d.ts +12 -0
  42. package/dist/esm/deploy.js +48 -0
  43. package/dist/esm/errors.d.ts +15 -0
  44. package/dist/esm/errors.js +19 -0
  45. package/dist/esm/git.d.ts +2 -0
  46. package/dist/esm/git.js +40 -0
  47. package/dist/esm/index.d.ts +29 -0
  48. package/dist/esm/index.js +15 -0
  49. package/dist/esm/inspect.d.ts +41 -0
  50. package/dist/esm/inspect.js +12 -0
  51. package/dist/esm/link.d.ts +15 -0
  52. package/dist/esm/link.js +16 -0
  53. package/dist/esm/local/dispatcher.d.ts +35 -0
  54. package/dist/esm/local/dispatcher.js +111 -0
  55. package/dist/esm/local/load.d.ts +6 -0
  56. package/dist/esm/local/load.js +63 -0
  57. package/dist/esm/local/run-local.d.ts +26 -0
  58. package/dist/esm/local/run-local.js +65 -0
  59. package/dist/esm/local/stubs.d.ts +8 -0
  60. package/dist/esm/local/stubs.js +31 -0
  61. package/dist/esm/package.json +1 -0
  62. package/dist/esm/run.d.ts +11 -0
  63. package/dist/esm/run.js +25 -0
  64. package/dist/esm/scaffold.d.ts +22 -0
  65. package/dist/esm/scaffold.js +135 -0
  66. package/dist/esm/signal.d.ts +12 -0
  67. package/dist/esm/signal.js +20 -0
  68. package/dist/tsconfig.cjs.tsbuildinfo +1 -0
  69. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  70. package/package.json +73 -0
  71. package/templates/default/AGENTS.md +29 -0
  72. package/templates/default/CLAUDE.md +7 -0
  73. package/templates/default/README.md +30 -0
  74. package/templates/default/_gitignore +3 -0
  75. package/templates/default/index.ts +26 -0
  76. package/templates/default/package.json +21 -0
  77. package/templates/default/tsconfig.json +12 -0
@@ -0,0 +1,111 @@
1
+ import { InMemoryContextStore, } from '@sapiom/orchestration';
2
+ import { parseCorrelationId, STEP_COMPLETION_OUTCOME, } from '@sapiom/orchestration-runtime';
3
+ import { createStubClient } from '@sapiom/tools/stub';
4
+ export class LocalStubDispatcher {
5
+ constructor(definition, stubs) {
6
+ this.definition = definition;
7
+ this.stubs = stubs;
8
+ this.core = null;
9
+ this.signals = new Map();
10
+ this.trace = [];
11
+ }
12
+ setCore(core) {
13
+ this.core = core;
14
+ }
15
+ setMaxAttempts(max) {
16
+ this.maxAttemptsPerStep = max;
17
+ }
18
+ setSignals(signals) {
19
+ this.signals = signals;
20
+ }
21
+ async dispatch(request) {
22
+ if (!this.core)
23
+ throw new Error('LocalStubDispatcher: setCore() was not called');
24
+ const step = this.definition.steps[request.stepName];
25
+ if (!step)
26
+ throw new Error(`LocalStubDispatcher: no step '${request.stepName}' in the definition`);
27
+ const parsed = parseCorrelationId(request.correlationId);
28
+ if (!parsed)
29
+ throw new Error(`LocalStubDispatcher: malformed correlationId '${request.correlationId}'`);
30
+ const logs = [];
31
+ const sharedStore = new InMemoryContextStore(request.shared);
32
+ const overrides = (this.stubs.steps[request.stepName] ?? {});
33
+ const sapiom = createStubClient({ overrides, signals: this.signals });
34
+ const ctx = {
35
+ executionId: request.executionId,
36
+ workflowName: request.workflowName,
37
+ organizationId: request.organizationId,
38
+ tenantId: request.tenantId,
39
+ input: request.workflowInput,
40
+ shared: sharedStore,
41
+ history: [],
42
+ attempts: request.attempt,
43
+ logger: makeLogger(logs),
44
+ sapiom,
45
+ };
46
+ let directive;
47
+ try {
48
+ directive = await step.run(request.input, ctx);
49
+ }
50
+ catch (err) {
51
+ const e = err instanceof Error ? err : new Error(String(err));
52
+ this.trace.push({
53
+ step: request.stepName,
54
+ attempt: request.attempt,
55
+ input: request.input,
56
+ status: 'threw',
57
+ error: { name: e.name, message: e.message, stack: e.stack },
58
+ logs,
59
+ });
60
+ await this.core.completeDispatchedStep({
61
+ protocol: 1,
62
+ correlationId: request.correlationId,
63
+ outcome: STEP_COMPLETION_OUTCOME.THREW,
64
+ error: { name: e.name, message: e.message, stack: e.stack },
65
+ shared: sharedStore.snapshot(),
66
+ }, parsed, this.maxAttemptsPerStep);
67
+ return;
68
+ }
69
+ const { output, wire } = splitDirective(directive);
70
+ this.trace.push({
71
+ step: request.stepName,
72
+ attempt: request.attempt,
73
+ input: request.input,
74
+ status: 'succeeded',
75
+ output,
76
+ directive,
77
+ logs,
78
+ });
79
+ const payload = {
80
+ protocol: 1,
81
+ correlationId: request.correlationId,
82
+ outcome: STEP_COMPLETION_OUTCOME.RESULT,
83
+ result: { output, directive: wire },
84
+ shared: sharedStore.snapshot(),
85
+ };
86
+ await this.core.completeDispatchedStep(payload, parsed, this.maxAttemptsPerStep);
87
+ }
88
+ }
89
+ function makeLogger(sink) {
90
+ const at = (level) => (msg, meta) => {
91
+ sink.push({ level, msg, ...(meta ? { meta } : {}) });
92
+ };
93
+ return { info: at('info'), warn: at('warn'), error: at('error'), debug: at('debug') };
94
+ }
95
+ function splitDirective(d) {
96
+ switch (d.kind) {
97
+ case 'continue':
98
+ return { output: d.input, wire: { kind: 'continue', stepName: d.stepName, input: d.input } };
99
+ case 'terminate':
100
+ return { output: d.output, wire: { kind: 'terminate', reason: d.reason } };
101
+ case 'fail':
102
+ return { output: d.output, wire: { kind: 'fail', reason: d.reason } };
103
+ case 'pause_until_signal':
104
+ return {
105
+ output: d.output,
106
+ wire: { kind: 'pause_until_signal', signal: d.signal, timeoutMs: d.timeoutMs, resumeStep: d.resumeStep },
107
+ };
108
+ case 'retry':
109
+ return { output: undefined, wire: { kind: 'retry', delayMs: d.delayMs, reason: d.reason } };
110
+ }
111
+ }
@@ -0,0 +1,6 @@
1
+ import { type OrchestrationDefinition, type WorkflowManifest } from '@sapiom/orchestration';
2
+ export interface LoadedDefinition {
3
+ definition: OrchestrationDefinition;
4
+ manifest: WorkflowManifest;
5
+ }
6
+ export declare function loadDefinition(sourceDir: string): Promise<LoadedDefinition>;
@@ -0,0 +1,63 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { buildManifest, isOrchestrationDefinition, workflowManifestSchema, } from '@sapiom/orchestration';
6
+ import * as esbuild from 'esbuild';
7
+ import { OrchestrationError } from '../errors.js';
8
+ const LOCAL_SDK_VERSION = '0.0.0-local';
9
+ export async function loadDefinition(sourceDir) {
10
+ const entryFile = path.join(sourceDir, 'index.ts');
11
+ if (!existsSync(entryFile)) {
12
+ throw new OrchestrationError({
13
+ code: 'NO_ENTRY',
14
+ message: `No index.ts found in ${sourceDir}.`,
15
+ hint: 'Run this from an orchestration project, or pass its directory.',
16
+ });
17
+ }
18
+ const tmp = mkdtempSync(path.join(tmpdir(), 'sapiom-run-'));
19
+ const bundlePath = path.join(tmp, 'definition.mjs');
20
+ try {
21
+ try {
22
+ await esbuild.build({
23
+ entryPoints: [entryFile],
24
+ outfile: bundlePath,
25
+ bundle: true,
26
+ platform: 'node',
27
+ target: 'node20',
28
+ format: 'esm',
29
+ logLevel: 'silent',
30
+ });
31
+ }
32
+ catch (err) {
33
+ throw new OrchestrationError({
34
+ code: 'BUNDLE_FAILED',
35
+ message: 'Failed to bundle the orchestration.',
36
+ hint: err instanceof Error ? err.message : String(err),
37
+ });
38
+ }
39
+ const mod = await import(`file://${bundlePath}?t=${Date.now()}`);
40
+ const defs = Object.values(mod).filter(isOrchestrationDefinition);
41
+ if (defs.length === 0) {
42
+ throw new OrchestrationError({
43
+ code: 'NO_DEFINITION',
44
+ message: 'No orchestration was exported from index.ts.',
45
+ hint: 'Export the result of defineOrchestration({ … }).',
46
+ });
47
+ }
48
+ if (defs.length > 1) {
49
+ throw new OrchestrationError({
50
+ code: 'MULTIPLE_DEFINITIONS',
51
+ message: 'index.ts exports more than one orchestration.',
52
+ hint: 'Export exactly one defineOrchestration({ … }) result.',
53
+ });
54
+ }
55
+ const definition = defs[0];
56
+ const sha256 = createHash('sha256').update(readFileSync(bundlePath)).digest('hex');
57
+ const manifest = workflowManifestSchema.parse(buildManifest(definition, { sdkVersion: LOCAL_SDK_VERSION, artifact: { sha256, entryFile: 'definition.mjs' } }));
58
+ return { definition, manifest };
59
+ }
60
+ finally {
61
+ rmSync(tmp, { recursive: true, force: true });
62
+ }
63
+ }
@@ -0,0 +1,26 @@
1
+ import type { OrchestrationDefinition, WorkflowManifest } from '@sapiom/orchestration';
2
+ import { type LocalStepTrace } from './dispatcher.js';
3
+ import { type StubFile } from './stubs.js';
4
+ export declare const STUBS_FILE: string;
5
+ export interface RunLocalOptions {
6
+ definition: OrchestrationDefinition;
7
+ manifest: WorkflowManifest;
8
+ input?: unknown;
9
+ stubs?: StubFile;
10
+ maxAttemptsPerStep?: number;
11
+ }
12
+ export type LocalRunOutcome = 'completed' | 'failed' | 'paused' | 'running';
13
+ export interface LocalRunResult {
14
+ outcome: LocalRunOutcome;
15
+ executionId: string;
16
+ output?: unknown;
17
+ error?: unknown;
18
+ steps: LocalStepTrace[];
19
+ }
20
+ export declare function runLocal(opts: RunLocalOptions): Promise<LocalRunResult>;
21
+ export declare function runLocalFromDir(opts: {
22
+ sourceDir: string;
23
+ input?: unknown;
24
+ stubs?: StubFile;
25
+ maxAttemptsPerStep?: number;
26
+ }): Promise<LocalRunResult>;
@@ -0,0 +1,65 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { DEFAULT_MAX_ATTEMPTS_PER_STEP, InMemoryExecutionStore, NOOP_OBSERVER, WorkflowRunnerCore, } from '@sapiom/orchestration-runtime';
4
+ import { OrchestrationError } from '../errors.js';
5
+ import { LocalStubDispatcher } from './dispatcher.js';
6
+ import { loadDefinition } from './load.js';
7
+ import { parseStubFile } from './stubs.js';
8
+ export const STUBS_FILE = path.join('.sapiom-dev', 'stubs.json');
9
+ function loadStubsFile(sourceDir) {
10
+ const file = path.join(sourceDir, STUBS_FILE);
11
+ if (!existsSync(file))
12
+ return undefined;
13
+ let raw;
14
+ try {
15
+ raw = JSON.parse(readFileSync(file, 'utf8'));
16
+ }
17
+ catch (err) {
18
+ throw new OrchestrationError({
19
+ code: 'STUBS_INVALID',
20
+ message: `${STUBS_FILE} is not valid JSON.`,
21
+ hint: err instanceof Error ? err.message : String(err),
22
+ });
23
+ }
24
+ return parseStubFile(raw);
25
+ }
26
+ const MAX_ADVANCES = 1000;
27
+ export async function runLocal(opts) {
28
+ const stubs = opts.stubs ?? { version: 1, steps: {} };
29
+ const max = opts.maxAttemptsPerStep ?? DEFAULT_MAX_ATTEMPTS_PER_STEP;
30
+ const store = new InMemoryExecutionStore();
31
+ const dispatcher = new LocalStubDispatcher(opts.definition, stubs);
32
+ const signals = new Map();
33
+ dispatcher.setSignals(signals);
34
+ const core = new WorkflowRunnerCore({ store, dispatcher, observer: NOOP_OBSERVER });
35
+ dispatcher.setCore(core);
36
+ dispatcher.setMaxAttempts(max);
37
+ const executionId = await core.createExecution(opts.definition.name, opts.definition.entry, opts.input, {
38
+ manifest: opts.manifest,
39
+ });
40
+ let guard = 0;
41
+ while (guard++ < MAX_ADVANCES) {
42
+ await core.advance(executionId, max);
43
+ const row = await store.loadExecution(executionId);
44
+ if (!row || row.status === 'completed' || row.status === 'failed' || row.status === 'cancelled')
45
+ break;
46
+ if (row.status === 'paused') {
47
+ const payload = signals.get(row.pausedSignalCorrelationId ?? '') ?? {};
48
+ await core.resetForResume(executionId, { fromStepInput: payload });
49
+ }
50
+ }
51
+ const final = await store.loadExecution(executionId);
52
+ const outcome = final?.status === 'cancelled' ? 'failed' : (final?.status ?? 'running');
53
+ return {
54
+ outcome,
55
+ executionId,
56
+ output: final?.output,
57
+ error: final?.error,
58
+ steps: dispatcher.trace,
59
+ };
60
+ }
61
+ export async function runLocalFromDir(opts) {
62
+ const { definition, manifest } = await loadDefinition(opts.sourceDir);
63
+ const stubs = opts.stubs ?? loadStubsFile(opts.sourceDir);
64
+ return runLocal({ definition, manifest, input: opts.input, stubs, maxAttemptsPerStep: opts.maxAttemptsPerStep });
65
+ }
@@ -0,0 +1,8 @@
1
+ export declare const STUB_FILE_VERSION = 1;
2
+ export type StubResponse = unknown;
3
+ export type StepStubs = Record<string, StubResponse | StubResponse[]>;
4
+ export interface StubFile {
5
+ version: number;
6
+ steps: Record<string, StepStubs>;
7
+ }
8
+ export declare function parseStubFile(raw: unknown): StubFile;
@@ -0,0 +1,31 @@
1
+ import { OrchestrationError } from '../errors.js';
2
+ export const STUB_FILE_VERSION = 1;
3
+ export function parseStubFile(raw) {
4
+ if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) {
5
+ throw invalid('the stub file must be a JSON object.');
6
+ }
7
+ const obj = raw;
8
+ const version = obj.version ?? STUB_FILE_VERSION;
9
+ if (typeof version !== 'number') {
10
+ throw invalid('`version` must be a number.');
11
+ }
12
+ const stepsRaw = obj.steps ?? {};
13
+ if (stepsRaw == null || typeof stepsRaw !== 'object' || Array.isArray(stepsRaw)) {
14
+ throw invalid('`steps` must be an object keyed by step name.');
15
+ }
16
+ const steps = {};
17
+ for (const [stepName, stepStubs] of Object.entries(stepsRaw)) {
18
+ if (stepStubs == null || typeof stepStubs !== 'object' || Array.isArray(stepStubs)) {
19
+ throw invalid(`steps.${stepName} must be an object keyed by capability path.`);
20
+ }
21
+ steps[stepName] = stepStubs;
22
+ }
23
+ return { version, steps };
24
+ }
25
+ function invalid(detail) {
26
+ return new OrchestrationError({
27
+ code: 'STUBS_INVALID',
28
+ message: `Invalid stub file: ${detail}`,
29
+ hint: 'Expected { "version": 1, "steps": { "<step>": { "<capability.path>": <response> | [<response>] } } }.',
30
+ });
31
+ }
@@ -0,0 +1 @@
1
+ {"type": "module"}
@@ -0,0 +1,11 @@
1
+ import { GatewayClient } from './client.js';
2
+ export interface RunOptions {
3
+ definitionId: string;
4
+ input?: unknown;
5
+ }
6
+ export interface RunResult {
7
+ executionId: string;
8
+ raw: Record<string, unknown>;
9
+ }
10
+ export declare function run(opts: RunOptions, client: GatewayClient): Promise<RunResult>;
11
+ export declare function parseJsonInput(raw: string): unknown;
@@ -0,0 +1,25 @@
1
+ import { OrchestrationError } from './errors.js';
2
+ export async function run(opts, client) {
3
+ const { definitionId, input = {} } = opts;
4
+ const res = await client.post('/executions', { definitionId, input });
5
+ const executionId = res.executionId ?? res.id;
6
+ if (!executionId) {
7
+ throw new OrchestrationError({
8
+ code: 'RUN_NO_ID',
9
+ message: 'The execution was started but no execution id was returned.',
10
+ });
11
+ }
12
+ return { executionId, raw: res };
13
+ }
14
+ export function parseJsonInput(raw) {
15
+ try {
16
+ return JSON.parse(raw);
17
+ }
18
+ catch {
19
+ throw new OrchestrationError({
20
+ code: 'BAD_INPUT',
21
+ message: 'Input is not valid JSON.',
22
+ hint: 'Pass a valid JSON string, e.g. \'{"key":"value"}\'',
23
+ });
24
+ }
25
+ }
@@ -0,0 +1,22 @@
1
+ export declare const DEFAULT_TEMPLATE = "default";
2
+ export interface ResolvedVersions {
3
+ orchestration: string;
4
+ tools: string;
5
+ zod: string;
6
+ }
7
+ export declare function resolveVersions(): Promise<ResolvedVersions>;
8
+ export declare function listTemplates(templatesDir?: string): string[];
9
+ export declare function resolveTemplate(name: string, templatesDir?: string): string;
10
+ export interface ScaffoldOptions {
11
+ targetDir: string;
12
+ template?: string;
13
+ projectName?: string;
14
+ templatesDir?: string;
15
+ versions?: ResolvedVersions;
16
+ }
17
+ export interface ScaffoldResult {
18
+ targetDir: string;
19
+ template: string;
20
+ projectName: string;
21
+ }
22
+ export declare function scaffold(opts: ScaffoldOptions): Promise<ScaffoldResult>;
@@ -0,0 +1,135 @@
1
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { OrchestrationError } from './errors.js';
5
+ function resolveModuleDir() {
6
+ if (typeof __dirname !== 'undefined')
7
+ return __dirname;
8
+ try {
9
+ const metaUrl = eval('import.meta.url');
10
+ if (typeof metaUrl === 'string')
11
+ return path.dirname(fileURLToPath(metaUrl));
12
+ }
13
+ catch {
14
+ }
15
+ return '';
16
+ }
17
+ const moduleDir = resolveModuleDir();
18
+ function getTemplatesDir(override) {
19
+ return override ?? process.env.SAPIOM_TEMPLATES_DIR ?? path.resolve(moduleDir, '..', '..', 'templates');
20
+ }
21
+ export const DEFAULT_TEMPLATE = 'default';
22
+ const DOTFILE_NAMES = new Set(['_gitignore', '_npmrc']);
23
+ const REGISTRY = 'https://registry.npmjs.org';
24
+ const VERSION_FALLBACK = {
25
+ orchestration: '0.1.1',
26
+ tools: '0.1.1',
27
+ };
28
+ const ZOD_VERSION = '3.25.76';
29
+ async function latestNpmVersion(pkg) {
30
+ try {
31
+ const res = await fetch(`${REGISTRY}/${encodeURIComponent(pkg)}/latest`, {
32
+ signal: AbortSignal.timeout(5000),
33
+ });
34
+ if (!res.ok)
35
+ return null;
36
+ const json = (await res.json());
37
+ return typeof json.version === 'string' ? json.version : null;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ export async function resolveVersions() {
44
+ const [orchestration, tools] = await Promise.all([
45
+ latestNpmVersion('@sapiom/orchestration'),
46
+ latestNpmVersion('@sapiom/tools'),
47
+ ]);
48
+ return {
49
+ orchestration: orchestration ?? VERSION_FALLBACK.orchestration,
50
+ tools: tools ?? VERSION_FALLBACK.tools,
51
+ zod: ZOD_VERSION,
52
+ };
53
+ }
54
+ export function listTemplates(templatesDir) {
55
+ const root = getTemplatesDir(templatesDir);
56
+ if (!existsSync(root))
57
+ return [];
58
+ return readdirSync(root).filter((name) => statSync(path.join(root, name)).isDirectory());
59
+ }
60
+ export function resolveTemplate(name, templatesDir) {
61
+ const dir = path.join(getTemplatesDir(templatesDir), name);
62
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) {
63
+ const available = listTemplates(templatesDir);
64
+ throw new OrchestrationError({
65
+ code: 'UNKNOWN_TEMPLATE',
66
+ message: `Unknown template '${name}'.` +
67
+ (available.length ? ` Available: ${available.join(', ')}.` : ' No templates are bundled.'),
68
+ });
69
+ }
70
+ return dir;
71
+ }
72
+ function applyReplacements(file, replacements) {
73
+ let content;
74
+ try {
75
+ content = readFileSync(file, 'utf8');
76
+ }
77
+ catch {
78
+ return;
79
+ }
80
+ let changed = false;
81
+ for (const [token, value] of Object.entries(replacements)) {
82
+ if (content.includes(token)) {
83
+ content = content.split(token).join(value);
84
+ changed = true;
85
+ }
86
+ }
87
+ if (changed)
88
+ writeFileSync(file, content);
89
+ }
90
+ function walk(dir, onFile) {
91
+ for (const entry of readdirSync(dir)) {
92
+ const full = path.join(dir, entry);
93
+ if (statSync(full).isDirectory())
94
+ walk(full, onFile);
95
+ else
96
+ onFile(full);
97
+ }
98
+ }
99
+ function copyTemplate(templateDir, targetDir, replacements) {
100
+ cpSync(templateDir, targetDir, { recursive: true });
101
+ walk(targetDir, (file) => {
102
+ const base = path.basename(file);
103
+ if (DOTFILE_NAMES.has(base)) {
104
+ const dotted = path.join(path.dirname(file), '.' + base.slice(1));
105
+ renameSync(file, dotted);
106
+ applyReplacements(dotted, replacements);
107
+ return;
108
+ }
109
+ applyReplacements(file, replacements);
110
+ });
111
+ }
112
+ export async function scaffold(opts) {
113
+ const { targetDir } = opts;
114
+ const template = opts.template ?? DEFAULT_TEMPLATE;
115
+ const projectName = opts.projectName ?? path.basename(targetDir);
116
+ if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
117
+ throw new OrchestrationError({
118
+ code: 'DIR_NOT_EMPTY',
119
+ message: `Target directory '${targetDir}' already exists and is not empty.`,
120
+ });
121
+ }
122
+ const templateDir = resolveTemplate(template, opts.templatesDir);
123
+ const versions = opts.versions ?? (await resolveVersions());
124
+ mkdirSync(targetDir, { recursive: true });
125
+ copyTemplate(templateDir, targetDir, {
126
+ __PROJECT_NAME__: projectName,
127
+ __ORCHESTRATION_VERSION__: versions.orchestration,
128
+ __TOOLS_VERSION__: versions.tools,
129
+ __ZOD_VERSION__: versions.zod,
130
+ });
131
+ const devDir = path.join(targetDir, '.sapiom-dev');
132
+ mkdirSync(devDir, { recursive: true });
133
+ writeFileSync(path.join(devDir, 'stubs.json'), JSON.stringify({ version: 1, steps: {} }, null, 2) + '\n');
134
+ return { targetDir, template, projectName };
135
+ }
@@ -0,0 +1,12 @@
1
+ import { GatewayClient } from './client.js';
2
+ export interface SignalOptions {
3
+ executionId: string;
4
+ name: string;
5
+ correlationId: string;
6
+ payload?: unknown;
7
+ }
8
+ export interface SignalResult {
9
+ matched: number;
10
+ }
11
+ export declare function signal(opts: SignalOptions, client: GatewayClient): Promise<SignalResult>;
12
+ export declare function parseSignalPayload(raw: string): unknown;
@@ -0,0 +1,20 @@
1
+ import { OrchestrationError } from './errors.js';
2
+ export async function signal(opts, client) {
3
+ const res = await client.post(`/executions/${opts.executionId}/signals`, {
4
+ name: opts.name,
5
+ correlationId: opts.correlationId,
6
+ payload: opts.payload,
7
+ });
8
+ return { matched: res.matched ?? 0 };
9
+ }
10
+ export function parseSignalPayload(raw) {
11
+ try {
12
+ return JSON.parse(raw);
13
+ }
14
+ catch {
15
+ throw new OrchestrationError({
16
+ code: 'BAD_PAYLOAD',
17
+ message: 'Signal payload is not valid JSON.',
18
+ });
19
+ }
20
+ }