lilflow 0.1.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.
@@ -0,0 +1,91 @@
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+
5
+ /**
6
+ * Absolute path to the session store file for a given run.
7
+ *
8
+ * @param {string} cwd - Workflow working directory.
9
+ * @param {string} runId - Workflow run identifier.
10
+ * @returns {string} Session store path.
11
+ */
12
+ export function getSessionStorePath(cwd, runId) {
13
+ return path.join(cwd, ".flow", "sessions", `${runId}.json`);
14
+ }
15
+
16
+ /**
17
+ * Load the session store for a run. Returns an empty store if the file is absent.
18
+ *
19
+ * @param {string} cwd - Workflow working directory.
20
+ * @param {string} runId - Workflow run identifier.
21
+ * @returns {Promise<{sessions: Record<string, {provider: string, sessionId: string, updatedAt: string}>}>} Parsed store.
22
+ */
23
+ export async function loadSessionStore(cwd, runId) {
24
+ const storePath = getSessionStorePath(cwd, runId);
25
+
26
+ try {
27
+ const raw = await readFile(storePath, "utf8");
28
+ const parsed = JSON.parse(raw);
29
+
30
+ if (parsed && typeof parsed === "object" && parsed.sessions && typeof parsed.sessions === "object") {
31
+ return { sessions: parsed.sessions };
32
+ }
33
+
34
+ return { sessions: {} };
35
+ } catch (error) {
36
+ if (error.code === "ENOENT") {
37
+ return { sessions: {} };
38
+ }
39
+
40
+ throw error;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Look up the prior session ID for a step within a run.
46
+ *
47
+ * @param {{sessions: Record<string, {provider: string, sessionId: string, updatedAt: string}>}} store - Loaded store.
48
+ * @param {string} stepName - Step name used as the session key.
49
+ * @returns {string | null} Session ID or null when absent.
50
+ */
51
+ export function getSessionId(store, stepName) {
52
+ const entry = store.sessions[stepName];
53
+
54
+ if (!entry || typeof entry.sessionId !== "string" || entry.sessionId === "") {
55
+ return null;
56
+ }
57
+
58
+ return entry.sessionId;
59
+ }
60
+
61
+ /**
62
+ * Persist a session ID captured from an agent result event.
63
+ *
64
+ * Writes atomically via a uuid-suffixed temp file + rename to avoid torn writes
65
+ * when the workflow crashes mid-save.
66
+ *
67
+ * @param {string} cwd - Workflow working directory.
68
+ * @param {string} runId - Workflow run identifier.
69
+ * @param {string} stepName - Step name used as the session key.
70
+ * @param {string} provider - Provider name (e.g. `opencode`, `claude-code`).
71
+ * @param {string} sessionId - Session identifier returned by the agent.
72
+ * @returns {Promise<void>} Resolves when the store is durable on disk.
73
+ */
74
+ export async function saveSessionId(cwd, runId, stepName, provider, sessionId) {
75
+ const store = await loadSessionStore(cwd, runId);
76
+
77
+ store.sessions[stepName] = {
78
+ provider,
79
+ sessionId,
80
+ updatedAt: new Date().toISOString()
81
+ };
82
+
83
+ const storePath = getSessionStorePath(cwd, runId);
84
+ const storeDir = path.dirname(storePath);
85
+
86
+ await mkdir(storeDir, { recursive: true });
87
+
88
+ const tempPath = `${storePath}.${randomUUID()}.tmp`;
89
+ await writeFile(tempPath, `${JSON.stringify(store, null, 2)}\n`, "utf8");
90
+ await rename(tempPath, storePath);
91
+ }
package/src/cli.js ADDED
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { getInitHelpText, runInitCommand } from "./init-project.js";
4
+ import {
5
+ ConfigError,
6
+ DEFAULT_CONFIG,
7
+ ensureConfigFile,
8
+ getConfigHelpText,
9
+ getGlobalConfigPath,
10
+ getProjectConfigPath,
11
+ loadConfig,
12
+ parseConfigCommandArgs,
13
+ renderConfig,
14
+ renderConfigWithSources
15
+ } from "./config.js";
16
+ import {
17
+ getListHelpText,
18
+ getLogsHelpText,
19
+ getResumeHelpText,
20
+ getRunHelpText,
21
+ getSetStepHelpText,
22
+ getSignalHelpText,
23
+ runWorkflowListCommand,
24
+ runWorkflowLogsCommand,
25
+ getStatusHelpText,
26
+ runWorkflowCommand,
27
+ runWorkflowResumeCommand,
28
+ runWorkflowSetStepCommand,
29
+ runWorkflowSignalCommand,
30
+ runWorkflowStatusCommand
31
+ } from "./run-workflow.js";
32
+
33
+ /**
34
+ * Return the CLI help text for `flow`.
35
+ *
36
+ * @returns {string} Help text shown to users.
37
+ */
38
+ export function getHelpText() {
39
+ return `flow
40
+
41
+ Usage:
42
+ flow init [--template <name>]
43
+ flow config
44
+ flow run
45
+ flow resume
46
+ flow set-step
47
+ flow signal <run-id> <step-name> [--data '{}']
48
+ flow status
49
+ flow list
50
+ flow logs
51
+ flow --help
52
+
53
+ Commands:
54
+ init Initialize flow in the current repository or from a reusable template
55
+ config Show or initialize flow configuration
56
+ run Execute a workflow from YAML
57
+ resume Resume a failed persisted workflow run
58
+ set-step Manually set which persisted workflow step to resume from
59
+ signal Deliver a signal to a waiting workflow step
60
+ status Show the status of a persisted workflow run
61
+ list List persisted workflow runs
62
+ logs Show persisted logs for a workflow run`;
63
+ }
64
+
65
+ /**
66
+ * Execute the `flow` command line interface.
67
+ *
68
+ * @param {string[]} argv - Process arguments excluding the Node executable.
69
+ * @param {(message: string) => void} [stdout=console.log] - Standard output writer.
70
+ * @param {(message: string) => void} [stderr=console.error] - Standard error writer.
71
+ * @param {object} [options={}] - Runtime overrides used by tests.
72
+ * @param {string} [options.cwd=process.cwd()] - Working directory override.
73
+ * @param {object} [options.env=process.env] - Environment override.
74
+ * @param {string} [options.homeDir] - Home directory override.
75
+ * @returns {Promise<number>} Process exit code.
76
+ */
77
+ export async function runCli(argv, stdout = console.log, stderr = console.error, options = {}) {
78
+ const [command, ...commandArgs] = argv;
79
+ const {
80
+ cwd = process.cwd(),
81
+ env = process.env,
82
+ homeDir
83
+ } = options;
84
+
85
+ if (!command || command === "--help" || command === "-h") {
86
+ stdout(getHelpText());
87
+ return 0;
88
+ }
89
+
90
+ if (command === "config") {
91
+ return runConfigCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
92
+ }
93
+
94
+ if (command === "init") {
95
+ return runInitCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
96
+ }
97
+
98
+ if (command === "run") {
99
+ return runWorkflowCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
100
+ }
101
+
102
+ if (command === "resume") {
103
+ return runWorkflowResumeCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
104
+ }
105
+
106
+ if (command === "set-step") {
107
+ return runWorkflowSetStepCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
108
+ }
109
+
110
+ if (command === "status") {
111
+ return runWorkflowStatusCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
112
+ }
113
+
114
+ if (command === "list") {
115
+ return runWorkflowListCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
116
+ }
117
+
118
+ if (command === "logs") {
119
+ return runWorkflowLogsCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
120
+ }
121
+
122
+ if (command === "signal") {
123
+ return runWorkflowSignalCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
124
+ }
125
+
126
+ stderr(`Unknown command: ${command}`);
127
+ stderr("Run 'flow --help' to see available commands.");
128
+ return 1;
129
+ }
130
+
131
+ /**
132
+ * Execute `flow config`.
133
+ *
134
+ * @param {string[]} args - CLI args after `config`.
135
+ * @param {(message: string) => void} stdout - Standard output writer.
136
+ * @param {(message: string) => void} stderr - Standard error writer.
137
+ * @param {object} options - Runtime overrides used by tests.
138
+ * @param {string} options.cwd - Working directory override.
139
+ * @param {object} options.env - Environment override.
140
+ * @param {string} [options.homeDir] - Home directory override.
141
+ * @returns {Promise<number>} Process exit code.
142
+ */
143
+ async function runConfigCommand(args, stdout, stderr, options) {
144
+ try {
145
+ const { commandFlags, overrideFlags } = parseConfigCommandArgs(args);
146
+
147
+ if (commandFlags.help) {
148
+ stdout(getConfigHelpText());
149
+ return 0;
150
+ }
151
+
152
+ if (commandFlags.defaults) {
153
+ stdout(renderConfig(DEFAULT_CONFIG));
154
+ return 0;
155
+ }
156
+
157
+ if (commandFlags.global) {
158
+ const globalConfigPath = getGlobalConfigPath(options.homeDir);
159
+ const created = await ensureConfigFile(globalConfigPath);
160
+ stdout(`${created ? "Created" : "Global config already exists at"} ${globalConfigPath}`);
161
+ return 0;
162
+ }
163
+
164
+ if (commandFlags.project) {
165
+ const projectConfigPath = getProjectConfigPath(options.cwd);
166
+ const created = await ensureConfigFile(projectConfigPath);
167
+ stdout(`${created ? "Created" : "Project config already exists at"} ${projectConfigPath}`);
168
+ return 0;
169
+ }
170
+
171
+ const { config, sources } = await loadConfig({
172
+ cwd: options.cwd,
173
+ env: options.env,
174
+ flags: overrideFlags,
175
+ homeDir: options.homeDir
176
+ });
177
+
178
+ stdout(commandFlags.show ? renderConfigWithSources(config, sources) : renderConfig(config));
179
+ return 0;
180
+ } catch (error) {
181
+ if (error instanceof ConfigError) {
182
+ stderr(`Error: ${error.message}`);
183
+ return 1;
184
+ }
185
+
186
+ throw error;
187
+ }
188
+ }
189
+
190
+ export {
191
+ getInitHelpText,
192
+ getListHelpText,
193
+ getLogsHelpText,
194
+ getResumeHelpText,
195
+ getRunHelpText,
196
+ getSetStepHelpText,
197
+ getSignalHelpText,
198
+ getStatusHelpText
199
+ };
200
+
201
+ if (import.meta.url === `file://${process.argv[1]}`) {
202
+ const exitCode = await runCli(process.argv.slice(2));
203
+ process.exit(exitCode);
204
+ }
@@ -0,0 +1,23 @@
1
+ # src/config
2
+
3
+ ## Purpose
4
+
5
+ Contains flow configuration loading, validation, rendering, and file initialization.
6
+
7
+ ## Modules
8
+
9
+ - `../config.js`: config defaults, parsing, merge precedence, and `flow config` helpers
10
+
11
+ ## Conventions
12
+
13
+ - Keep the precedence order exact: defaults, global, project, workflow, workflow-local, env, flag
14
+ - Validate both keys and values before merging user-provided config
15
+ - Keep YAML output stable for tests and proof artifacts
16
+ - Prefer explicit supported-key handling over generic deep merges
17
+
18
+ ## Verification
19
+
20
+ - Run `npm test`
21
+ - Run `npm run coverage`
22
+ - Run `npx eslint src/ --fix`
23
+ - Run `npx eslint src/`