@sapiom/orchestration-core 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,29 +1,29 @@
1
- export { OrchestrationError } from './errors.js';
2
- export type { StructuredError } from './errors.js';
3
- export { GatewayClient, createClient, DEFAULT_WORKFLOWS_HOST } from './client.js';
4
- export type { ClientOptions, GatewayErrorBody } from './client.js';
5
- export { readConfig, requireConfig, writeConfig, CONFIG_FILE } from './config.js';
6
- export type { SapiomConfig } from './config.js';
7
- export { scaffold, resolveVersions, resolveTemplate, listTemplates, DEFAULT_TEMPLATE } from './scaffold.js';
8
- export type { ScaffoldOptions, ScaffoldResult, ResolvedVersions } from './scaffold.js';
9
- export { check } from './check.js';
10
- export type { CheckOptions, CheckResult } from './check.js';
11
- export { link } from './link.js';
12
- export type { LinkOptions, LinkResult, DefinitionSummary } from './link.js';
13
- export { deploy } from './deploy.js';
14
- export type { DeployOptions, DeployResult } from './deploy.js';
15
- export { run, parseJsonInput } from './run.js';
16
- export type { RunOptions, RunResult } from './run.js';
17
- export { inspect, listExecutions, inspectBuild } from './inspect.js';
18
- export type { InspectOptions, InspectResult, ListExecutionsResult, InspectBuildOptions, InspectBuildResult, ExecutionDetail, StepRecord, BuildDetail, } from './inspect.js';
19
- export { signal, parseSignalPayload } from './signal.js';
20
- export type { SignalOptions, SignalResult } from './signal.js';
21
- export { assertDeployable, pushHead } from './git.js';
22
- export { parseStubFile, STUB_FILE_VERSION } from './local/stubs.js';
23
- export type { StubFile, StepStubs, StubResponse } from './local/stubs.js';
24
- export { runLocal, runLocalFromDir, STUBS_FILE } from './local/run-local.js';
25
- export type { RunLocalOptions, LocalRunResult, LocalRunOutcome } from './local/run-local.js';
26
- export { loadDefinition } from './local/load.js';
27
- export type { LoadedDefinition } from './local/load.js';
28
- export { LocalStubDispatcher } from './local/dispatcher.js';
29
- export type { LocalStepTrace, LogEntry } from './local/dispatcher.js';
1
+ export { OrchestrationError } from "./errors.js";
2
+ export type { StructuredError } from "./errors.js";
3
+ export { GatewayClient, createClient, DEFAULT_WORKFLOWS_HOST, } from "./client.js";
4
+ export type { ClientOptions, GatewayErrorBody } from "./client.js";
5
+ export { readConfig, requireConfig, writeConfig, CONFIG_FILE, } from "./config.js";
6
+ export type { SapiomConfig } from "./config.js";
7
+ export { scaffold, resolveVersions, resolveTemplate, listTemplates, DEFAULT_TEMPLATE, } from "./scaffold.js";
8
+ export type { ScaffoldOptions, ScaffoldResult, ResolvedVersions, } from "./scaffold.js";
9
+ export { check } from "./check.js";
10
+ export type { CheckOptions, CheckResult } from "./check.js";
11
+ export { link } from "./link.js";
12
+ export type { LinkOptions, LinkResult, DefinitionSummary } from "./link.js";
13
+ export { deploy } from "./deploy.js";
14
+ export type { DeployOptions, DeployResult } from "./deploy.js";
15
+ export { run, parseJsonInput } from "./run.js";
16
+ export type { RunOptions, RunResult } from "./run.js";
17
+ export { inspect, listExecutions, inspectBuild, waitForExecution, isExecutionTerminal, } from "./inspect.js";
18
+ export type { InspectOptions, InspectResult, ListExecutionsResult, InspectBuildOptions, InspectBuildResult, ExecutionDetail, StepRecord, BuildDetail, WaitForExecutionOptions, WaitForExecutionResult, WaitStopReason, } from "./inspect.js";
19
+ export { signal, parseSignalPayload } from "./signal.js";
20
+ export type { SignalOptions, SignalResult } from "./signal.js";
21
+ export { assertDeployable, pushHead } from "./git.js";
22
+ export { parseStubFile, STUB_FILE_VERSION } from "./local/stubs.js";
23
+ export type { StubFile, StepStubs, StubResponse } from "./local/stubs.js";
24
+ export { runLocal, runLocalFromDir, STUBS_FILE } from "./local/run-local.js";
25
+ export type { RunLocalOptions, LocalRunResult, LocalRunOutcome, } from "./local/run-local.js";
26
+ export { loadDefinition } from "./local/load.js";
27
+ export type { LoadedDefinition } from "./local/load.js";
28
+ export { LocalStubDispatcher } from "./local/dispatcher.js";
29
+ export type { LocalStepTrace, LogEntry } from "./local/dispatcher.js";
package/dist/esm/index.js CHANGED
@@ -1,15 +1,15 @@
1
- export { OrchestrationError } from './errors.js';
2
- export { GatewayClient, createClient, DEFAULT_WORKFLOWS_HOST } from './client.js';
3
- export { readConfig, requireConfig, writeConfig, CONFIG_FILE } from './config.js';
4
- export { scaffold, resolveVersions, resolveTemplate, listTemplates, DEFAULT_TEMPLATE } from './scaffold.js';
5
- export { check } from './check.js';
6
- export { link } from './link.js';
7
- export { deploy } from './deploy.js';
8
- export { run, parseJsonInput } from './run.js';
9
- export { inspect, listExecutions, inspectBuild } from './inspect.js';
10
- export { signal, parseSignalPayload } from './signal.js';
11
- export { assertDeployable, pushHead } from './git.js';
12
- export { parseStubFile, STUB_FILE_VERSION } from './local/stubs.js';
13
- export { runLocal, runLocalFromDir, STUBS_FILE } from './local/run-local.js';
14
- export { loadDefinition } from './local/load.js';
15
- export { LocalStubDispatcher } from './local/dispatcher.js';
1
+ export { OrchestrationError } from "./errors.js";
2
+ export { GatewayClient, createClient, DEFAULT_WORKFLOWS_HOST, } from "./client.js";
3
+ export { readConfig, requireConfig, writeConfig, CONFIG_FILE, } from "./config.js";
4
+ export { scaffold, resolveVersions, resolveTemplate, listTemplates, DEFAULT_TEMPLATE, } from "./scaffold.js";
5
+ export { check } from "./check.js";
6
+ export { link } from "./link.js";
7
+ export { deploy } from "./deploy.js";
8
+ export { run, parseJsonInput } from "./run.js";
9
+ export { inspect, listExecutions, inspectBuild, waitForExecution, isExecutionTerminal, } from "./inspect.js";
10
+ export { signal, parseSignalPayload } from "./signal.js";
11
+ export { assertDeployable, pushHead } from "./git.js";
12
+ export { parseStubFile, STUB_FILE_VERSION } from "./local/stubs.js";
13
+ export { runLocal, runLocalFromDir, STUBS_FILE } from "./local/run-local.js";
14
+ export { loadDefinition } from "./local/load.js";
15
+ export { LocalStubDispatcher } from "./local/dispatcher.js";
@@ -1,4 +1,4 @@
1
- import { GatewayClient } from './client.js';
1
+ import { GatewayClient } from "./client.js";
2
2
  export interface StepRecord {
3
3
  stepName: string;
4
4
  attempt: number;
@@ -12,6 +12,7 @@ export interface ExecutionDetail {
12
12
  id: string;
13
13
  status: string;
14
14
  currentStep?: string | null;
15
+ pausedSignalName?: string | null;
15
16
  error?: unknown;
16
17
  steps?: StepRecord[];
17
18
  }
@@ -27,6 +28,22 @@ export interface InspectResult {
27
28
  execution: ExecutionDetail;
28
29
  }
29
30
  export declare function inspect(opts: InspectOptions, client: GatewayClient): Promise<InspectResult>;
31
+ export declare function isExecutionTerminal(status: string): boolean;
32
+ export type WaitStopReason = "terminal" | "needs-signal" | "timeout";
33
+ export interface WaitForExecutionOptions {
34
+ executionId: string;
35
+ maxWaitMs?: number;
36
+ pollMs?: number;
37
+ autoResumeSignals?: string[];
38
+ sleep?: (ms: number) => Promise<void>;
39
+ now?: () => number;
40
+ }
41
+ export interface WaitForExecutionResult {
42
+ execution: ExecutionDetail;
43
+ reason: WaitStopReason;
44
+ done: boolean;
45
+ }
46
+ export declare function waitForExecution(opts: WaitForExecutionOptions, client: GatewayClient): Promise<WaitForExecutionResult>;
30
47
  export interface ListExecutionsResult {
31
48
  executions: ExecutionDetail[];
32
49
  }
@@ -2,8 +2,44 @@ export async function inspect(opts, client) {
2
2
  const execution = await client.get(`/executions/${opts.executionId}`);
3
3
  return { execution };
4
4
  }
5
+ const TERMINAL_STATUSES = new Set([
6
+ "completed",
7
+ "failed",
8
+ "cancelled",
9
+ "canceled",
10
+ ]);
11
+ export function isExecutionTerminal(status) {
12
+ return TERMINAL_STATUSES.has(status);
13
+ }
14
+ const AUTO_RESUME_PAUSE_SIGNALS = ["agent.coding.result"];
15
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
+ export async function waitForExecution(opts, client) {
17
+ const maxWaitMs = opts.maxWaitMs ?? 45000;
18
+ const autoResume = opts.autoResumeSignals ?? AUTO_RESUME_PAUSE_SIGNALS;
19
+ const sleep = opts.sleep ?? defaultSleep;
20
+ const now = opts.now ?? Date.now;
21
+ const deadline = now() + maxWaitMs;
22
+ let interval = opts.pollMs ?? 1000;
23
+ for (;;) {
24
+ const { execution } = await inspect({ executionId: opts.executionId }, client);
25
+ if (isExecutionTerminal(execution.status)) {
26
+ return { execution, reason: "terminal", done: true };
27
+ }
28
+ if (execution.status === "paused") {
29
+ const signal = execution.pausedSignalName ?? null;
30
+ if (!signal || !autoResume.includes(signal)) {
31
+ return { execution, reason: "needs-signal", done: false };
32
+ }
33
+ }
34
+ const remaining = deadline - now();
35
+ if (remaining <= 0)
36
+ return { execution, reason: "timeout", done: false };
37
+ await sleep(Math.min(interval, remaining));
38
+ interval = Math.min(interval * 1.5, 5000);
39
+ }
40
+ }
5
41
  export async function listExecutions(client) {
6
- const executions = await client.get('/executions');
42
+ const executions = await client.get("/executions");
7
43
  return { executions };
8
44
  }
9
45
  export async function inspectBuild(opts, client) {
@@ -1,8 +1,8 @@
1
- import { type NextStepDirective, type OrchestrationDefinition } from '@sapiom/orchestration';
2
- import { type StepDispatcher, type StepDispatchRequest, type WorkflowRunnerCore } from '@sapiom/orchestration-runtime';
3
- import type { StubFile } from './stubs.js';
1
+ import { type NextStepDirective, type OrchestrationDefinition } from "@sapiom/orchestration";
2
+ import { type StepDispatcher, type StepDispatchRequest, type WorkflowRunnerCore } from "@sapiom/orchestration-runtime";
3
+ import type { StubFile } from "./stubs.js";
4
4
  export interface LogEntry {
5
- level: 'info' | 'warn' | 'error' | 'debug';
5
+ level: "info" | "warn" | "error" | "debug";
6
6
  msg: string;
7
7
  meta?: Record<string, unknown>;
8
8
  }
@@ -10,7 +10,7 @@ export interface LocalStepTrace {
10
10
  step: string;
11
11
  attempt: number;
12
12
  input: unknown;
13
- status: 'succeeded' | 'threw';
13
+ status: "succeeded" | "threw";
14
14
  output?: unknown;
15
15
  directive?: NextStepDirective;
16
16
  error?: {
@@ -27,6 +27,8 @@ export declare class LocalStubDispatcher implements StepDispatcher {
27
27
  private maxAttemptsPerStep;
28
28
  private signals;
29
29
  readonly trace: LocalStepTrace[];
30
+ readonly usedKeysByStep: Map<string, Set<string>>;
31
+ readonly stubWarnings: Set<string>;
30
32
  constructor(definition: OrchestrationDefinition, stubs: StubFile);
31
33
  setCore(core: WorkflowRunnerCore): void;
32
34
  setMaxAttempts(max: number): void;
@@ -1,6 +1,6 @@
1
- import { InMemoryContextStore, } from '@sapiom/orchestration';
2
- import { parseCorrelationId, STEP_COMPLETION_OUTCOME, } from '@sapiom/orchestration-runtime';
3
- import { createStubClient } from '@sapiom/tools/stub';
1
+ import { InMemoryContextStore, } from "@sapiom/orchestration";
2
+ import { parseCorrelationId, STEP_COMPLETION_OUTCOME, } from "@sapiom/orchestration-runtime";
3
+ import { createStubClient } from "@sapiom/tools/stub";
4
4
  export class LocalStubDispatcher {
5
5
  constructor(definition, stubs) {
6
6
  this.definition = definition;
@@ -8,6 +8,8 @@ export class LocalStubDispatcher {
8
8
  this.core = null;
9
9
  this.signals = new Map();
10
10
  this.trace = [];
11
+ this.usedKeysByStep = new Map();
12
+ this.stubWarnings = new Set();
11
13
  }
12
14
  setCore(core) {
13
15
  this.core = core;
@@ -20,7 +22,7 @@ export class LocalStubDispatcher {
20
22
  }
21
23
  async dispatch(request) {
22
24
  if (!this.core)
23
- throw new Error('LocalStubDispatcher: setCore() was not called');
25
+ throw new Error("LocalStubDispatcher: setCore() was not called");
24
26
  const step = this.definition.steps[request.stepName];
25
27
  if (!step)
26
28
  throw new Error(`LocalStubDispatcher: no step '${request.stepName}' in the definition`);
@@ -30,7 +32,17 @@ export class LocalStubDispatcher {
30
32
  const logs = [];
31
33
  const sharedStore = new InMemoryContextStore(request.shared);
32
34
  const overrides = (this.stubs.steps[request.stepName] ?? {});
33
- const sapiom = createStubClient({ overrides, signals: this.signals });
35
+ let usedKeys = this.usedKeysByStep.get(request.stepName);
36
+ if (!usedKeys) {
37
+ usedKeys = new Set();
38
+ this.usedKeysByStep.set(request.stepName, usedKeys);
39
+ }
40
+ const sapiom = createStubClient({
41
+ overrides,
42
+ signals: this.signals,
43
+ usedKeys,
44
+ warnings: this.stubWarnings,
45
+ });
34
46
  const ctx = {
35
47
  executionId: request.executionId,
36
48
  workflowName: request.workflowName,
@@ -53,7 +65,7 @@ export class LocalStubDispatcher {
53
65
  step: request.stepName,
54
66
  attempt: request.attempt,
55
67
  input: request.input,
56
- status: 'threw',
68
+ status: "threw",
57
69
  error: { name: e.name, message: e.message, stack: e.stack },
58
70
  logs,
59
71
  });
@@ -71,7 +83,7 @@ export class LocalStubDispatcher {
71
83
  step: request.stepName,
72
84
  attempt: request.attempt,
73
85
  input: request.input,
74
- status: 'succeeded',
86
+ status: "succeeded",
75
87
  output,
76
88
  directive,
77
89
  logs,
@@ -90,22 +102,44 @@ function makeLogger(sink) {
90
102
  const at = (level) => (msg, meta) => {
91
103
  sink.push({ level, msg, ...(meta ? { meta } : {}) });
92
104
  };
93
- return { info: at('info'), warn: at('warn'), error: at('error'), debug: at('debug') };
105
+ return {
106
+ info: at("info"),
107
+ warn: at("warn"),
108
+ error: at("error"),
109
+ debug: at("debug"),
110
+ };
94
111
  }
95
112
  function splitDirective(d) {
96
113
  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':
114
+ case "continue":
115
+ return {
116
+ output: d.input,
117
+ wire: { kind: "continue", stepName: d.stepName, input: d.input },
118
+ };
119
+ case "terminate":
104
120
  return {
105
121
  output: d.output,
106
- wire: { kind: 'pause_until_signal', signal: d.signal, timeoutMs: d.timeoutMs, resumeStep: d.resumeStep },
122
+ wire: { kind: "terminate", reason: d.reason },
123
+ };
124
+ case "fail":
125
+ return {
126
+ output: d.output,
127
+ wire: { kind: "fail", reason: d.reason },
128
+ };
129
+ case "pause_until_signal":
130
+ return {
131
+ output: d.output,
132
+ wire: {
133
+ kind: "pause_until_signal",
134
+ signal: d.signal,
135
+ timeoutMs: d.timeoutMs,
136
+ resumeStep: d.resumeStep,
137
+ },
138
+ };
139
+ case "retry":
140
+ return {
141
+ output: undefined,
142
+ wire: { kind: "retry", delayMs: d.delayMs, reason: d.reason },
107
143
  };
108
- case 'retry':
109
- return { output: undefined, wire: { kind: 'retry', delayMs: d.delayMs, reason: d.reason } };
110
144
  }
111
145
  }
@@ -1,6 +1,6 @@
1
- import type { OrchestrationDefinition, WorkflowManifest } from '@sapiom/orchestration';
2
- import { type LocalStepTrace } from './dispatcher.js';
3
- import { type StubFile } from './stubs.js';
1
+ import type { OrchestrationDefinition, WorkflowManifest } from "@sapiom/orchestration";
2
+ import { type LocalStepTrace } from "./dispatcher.js";
3
+ import { type StubFile } from "./stubs.js";
4
4
  export declare const STUBS_FILE: string;
5
5
  export interface RunLocalOptions {
6
6
  definition: OrchestrationDefinition;
@@ -9,13 +9,19 @@ export interface RunLocalOptions {
9
9
  stubs?: StubFile;
10
10
  maxAttemptsPerStep?: number;
11
11
  }
12
- export type LocalRunOutcome = 'completed' | 'failed' | 'paused' | 'running';
12
+ export type LocalRunOutcome = "completed" | "failed" | "paused" | "running";
13
+ export interface UnusedStub {
14
+ step: string;
15
+ key: string;
16
+ }
13
17
  export interface LocalRunResult {
14
18
  outcome: LocalRunOutcome;
15
19
  executionId: string;
16
20
  output?: unknown;
17
21
  error?: unknown;
18
22
  steps: LocalStepTrace[];
23
+ unusedStubs: UnusedStub[];
24
+ stubWarnings: string[];
19
25
  }
20
26
  export declare function runLocal(opts: RunLocalOptions): Promise<LocalRunResult>;
21
27
  export declare function runLocalFromDir(opts: {
@@ -1,22 +1,22 @@
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');
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
9
  function loadStubsFile(sourceDir) {
10
10
  const file = path.join(sourceDir, STUBS_FILE);
11
11
  if (!existsSync(file))
12
12
  return undefined;
13
13
  let raw;
14
14
  try {
15
- raw = JSON.parse(readFileSync(file, 'utf8'));
15
+ raw = JSON.parse(readFileSync(file, "utf8"));
16
16
  }
17
17
  catch (err) {
18
18
  throw new OrchestrationError({
19
- code: 'STUBS_INVALID',
19
+ code: "STUBS_INVALID",
20
20
  message: `${STUBS_FILE} is not valid JSON.`,
21
21
  hint: err instanceof Error ? err.message : String(err),
22
22
  });
@@ -31,7 +31,11 @@ export async function runLocal(opts) {
31
31
  const dispatcher = new LocalStubDispatcher(opts.definition, stubs);
32
32
  const signals = new Map();
33
33
  dispatcher.setSignals(signals);
34
- const core = new WorkflowRunnerCore({ store, dispatcher, observer: NOOP_OBSERVER });
34
+ const core = new WorkflowRunnerCore({
35
+ store,
36
+ dispatcher,
37
+ observer: NOOP_OBSERVER,
38
+ });
35
39
  dispatcher.setCore(core);
36
40
  dispatcher.setMaxAttempts(max);
37
41
  const executionId = await core.createExecution(opts.definition.name, opts.definition.entry, opts.input, {
@@ -41,25 +45,43 @@ export async function runLocal(opts) {
41
45
  while (guard++ < MAX_ADVANCES) {
42
46
  await core.advance(executionId, max);
43
47
  const row = await store.loadExecution(executionId);
44
- if (!row || row.status === 'completed' || row.status === 'failed' || row.status === 'cancelled')
48
+ if (!row ||
49
+ row.status === "completed" ||
50
+ row.status === "failed" ||
51
+ row.status === "cancelled")
45
52
  break;
46
- if (row.status === 'paused') {
47
- const payload = signals.get(row.pausedSignalCorrelationId ?? '') ?? {};
53
+ if (row.status === "paused") {
54
+ const payload = signals.get(row.pausedSignalCorrelationId ?? "") ?? {};
48
55
  await core.resetForResume(executionId, { fromStepInput: payload });
49
56
  }
50
57
  }
51
58
  const final = await store.loadExecution(executionId);
52
- const outcome = final?.status === 'cancelled' ? 'failed' : (final?.status ?? 'running');
59
+ const outcome = final?.status === "cancelled" ? "failed" : (final?.status ?? "running");
60
+ const unusedStubs = [];
61
+ for (const [step, used] of dispatcher.usedKeysByStep) {
62
+ for (const key of Object.keys(stubs.steps[step] ?? {})) {
63
+ if (!used.has(key))
64
+ unusedStubs.push({ step, key });
65
+ }
66
+ }
53
67
  return {
54
68
  outcome,
55
69
  executionId,
56
70
  output: final?.output,
57
71
  error: final?.error,
58
72
  steps: dispatcher.trace,
73
+ unusedStubs,
74
+ stubWarnings: [...dispatcher.stubWarnings],
59
75
  };
60
76
  }
61
77
  export async function runLocalFromDir(opts) {
62
78
  const { definition, manifest } = await loadDefinition(opts.sourceDir);
63
79
  const stubs = opts.stubs ?? loadStubsFile(opts.sourceDir);
64
- return runLocal({ definition, manifest, input: opts.input, stubs, maxAttemptsPerStep: opts.maxAttemptsPerStep });
80
+ return runLocal({
81
+ definition,
82
+ manifest,
83
+ input: opts.input,
84
+ stubs,
85
+ maxAttemptsPerStep: opts.maxAttemptsPerStep,
86
+ });
65
87
  }
@@ -18,5 +18,6 @@ export interface ScaffoldResult {
18
18
  targetDir: string;
19
19
  template: string;
20
20
  projectName: string;
21
+ gitInitialized: boolean;
21
22
  }
22
23
  export declare function scaffold(opts: ScaffoldOptions): Promise<ScaffoldResult>;
@@ -1,31 +1,34 @@
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';
1
+ import { execFileSync } from "node:child_process";
2
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { OrchestrationError } from "./errors.js";
5
6
  function resolveModuleDir() {
6
- if (typeof __dirname !== 'undefined')
7
+ if (typeof __dirname !== "undefined")
7
8
  return __dirname;
8
9
  try {
9
- const metaUrl = eval('import.meta.url');
10
- if (typeof metaUrl === 'string')
10
+ const metaUrl = eval("import.meta.url");
11
+ if (typeof metaUrl === "string")
11
12
  return path.dirname(fileURLToPath(metaUrl));
12
13
  }
13
14
  catch {
14
15
  }
15
- return '';
16
+ return "";
16
17
  }
17
18
  const moduleDir = resolveModuleDir();
18
19
  function getTemplatesDir(override) {
19
- return override ?? process.env.SAPIOM_TEMPLATES_DIR ?? path.resolve(moduleDir, '..', '..', 'templates');
20
+ return (override ??
21
+ process.env.SAPIOM_TEMPLATES_DIR ??
22
+ path.resolve(moduleDir, "..", "..", "templates"));
20
23
  }
21
- export const DEFAULT_TEMPLATE = 'default';
22
- const DOTFILE_NAMES = new Set(['_gitignore', '_npmrc']);
23
- const REGISTRY = 'https://registry.npmjs.org';
24
+ export const DEFAULT_TEMPLATE = "default";
25
+ const DOTFILE_NAMES = new Set(["_gitignore", "_npmrc"]);
26
+ const REGISTRY = "https://registry.npmjs.org";
24
27
  const VERSION_FALLBACK = {
25
- orchestration: '0.1.1',
26
- tools: '0.1.1',
28
+ orchestration: "0.1.1",
29
+ tools: "0.1.1",
27
30
  };
28
- const ZOD_VERSION = '3.25.76';
31
+ const ZOD_VERSION = "3.25.76";
29
32
  async function latestNpmVersion(pkg) {
30
33
  try {
31
34
  const res = await fetch(`${REGISTRY}/${encodeURIComponent(pkg)}/latest`, {
@@ -34,7 +37,7 @@ async function latestNpmVersion(pkg) {
34
37
  if (!res.ok)
35
38
  return null;
36
39
  const json = (await res.json());
37
- return typeof json.version === 'string' ? json.version : null;
40
+ return typeof json.version === "string" ? json.version : null;
38
41
  }
39
42
  catch {
40
43
  return null;
@@ -42,8 +45,8 @@ async function latestNpmVersion(pkg) {
42
45
  }
43
46
  export async function resolveVersions() {
44
47
  const [orchestration, tools] = await Promise.all([
45
- latestNpmVersion('@sapiom/orchestration'),
46
- latestNpmVersion('@sapiom/tools'),
48
+ latestNpmVersion("@sapiom/orchestration"),
49
+ latestNpmVersion("@sapiom/tools"),
47
50
  ]);
48
51
  return {
49
52
  orchestration: orchestration ?? VERSION_FALLBACK.orchestration,
@@ -62,9 +65,11 @@ export function resolveTemplate(name, templatesDir) {
62
65
  if (!existsSync(dir) || !statSync(dir).isDirectory()) {
63
66
  const available = listTemplates(templatesDir);
64
67
  throw new OrchestrationError({
65
- code: 'UNKNOWN_TEMPLATE',
68
+ code: "UNKNOWN_TEMPLATE",
66
69
  message: `Unknown template '${name}'.` +
67
- (available.length ? ` Available: ${available.join(', ')}.` : ' No templates are bundled.'),
70
+ (available.length
71
+ ? ` Available: ${available.join(", ")}.`
72
+ : " No templates are bundled."),
68
73
  });
69
74
  }
70
75
  return dir;
@@ -72,7 +77,7 @@ export function resolveTemplate(name, templatesDir) {
72
77
  function applyReplacements(file, replacements) {
73
78
  let content;
74
79
  try {
75
- content = readFileSync(file, 'utf8');
80
+ content = readFileSync(file, "utf8");
76
81
  }
77
82
  catch {
78
83
  return;
@@ -101,7 +106,7 @@ function copyTemplate(templateDir, targetDir, replacements) {
101
106
  walk(targetDir, (file) => {
102
107
  const base = path.basename(file);
103
108
  if (DOTFILE_NAMES.has(base)) {
104
- const dotted = path.join(path.dirname(file), '.' + base.slice(1));
109
+ const dotted = path.join(path.dirname(file), "." + base.slice(1));
105
110
  renameSync(file, dotted);
106
111
  applyReplacements(dotted, replacements);
107
112
  return;
@@ -109,13 +114,37 @@ function copyTemplate(templateDir, targetDir, replacements) {
109
114
  applyReplacements(file, replacements);
110
115
  });
111
116
  }
117
+ function initGitRepo(dir) {
118
+ const tryGit = (args) => {
119
+ try {
120
+ execFileSync("git", args, { cwd: dir, stdio: "ignore" });
121
+ return true;
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ };
127
+ if (!tryGit(["init"]))
128
+ return false;
129
+ tryGit(["add", "-A"]);
130
+ return (tryGit(["commit", "-m", "Initial commit"]) ||
131
+ tryGit([
132
+ "-c",
133
+ "user.name=Sapiom",
134
+ "-c",
135
+ "user.email=noreply@sapiom.ai",
136
+ "commit",
137
+ "-m",
138
+ "Initial commit",
139
+ ]));
140
+ }
112
141
  export async function scaffold(opts) {
113
142
  const { targetDir } = opts;
114
143
  const template = opts.template ?? DEFAULT_TEMPLATE;
115
144
  const projectName = opts.projectName ?? path.basename(targetDir);
116
145
  if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
117
146
  throw new OrchestrationError({
118
- code: 'DIR_NOT_EMPTY',
147
+ code: "DIR_NOT_EMPTY",
119
148
  message: `Target directory '${targetDir}' already exists and is not empty.`,
120
149
  });
121
150
  }
@@ -128,8 +157,9 @@ export async function scaffold(opts) {
128
157
  __TOOLS_VERSION__: versions.tools,
129
158
  __ZOD_VERSION__: versions.zod,
130
159
  });
131
- const devDir = path.join(targetDir, '.sapiom-dev');
160
+ const devDir = path.join(targetDir, ".sapiom-dev");
132
161
  mkdirSync(devDir, { recursive: true });
133
- writeFileSync(path.join(devDir, 'stubs.json'), JSON.stringify({ version: 1, steps: {} }, null, 2) + '\n');
134
- return { targetDir, template, projectName };
162
+ writeFileSync(path.join(devDir, "stubs.json"), JSON.stringify({ version: 1, steps: {} }, null, 2) + "\n");
163
+ const gitInitialized = initGitRepo(targetDir);
164
+ return { targetDir, template, projectName, gitInitialized };
135
165
  }