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.
- package/AGENTS.md +140 -0
- package/README.md +112 -0
- package/package.json +50 -0
- package/src/AGENTS.md +27 -0
- package/src/agents/claude-code.js +352 -0
- package/src/agents/index.js +228 -0
- package/src/agents/ndjson.js +67 -0
- package/src/agents/opencode.js +290 -0
- package/src/agents/prompt.js +91 -0
- package/src/agents/session-store.js +91 -0
- package/src/cli.js +204 -0
- package/src/config/AGENTS.md +23 -0
- package/src/config.js +776 -0
- package/src/init-project.js +573 -0
- package/src/run-workflow.js +6274 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import { access, mkdir, readFile, readdir, rename, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { constants } from "node:fs";
|
|
5
|
+
import readline from "node:readline/promises";
|
|
6
|
+
import { ConfigError, getConfigTemplate, loadConfig } from "./config.js";
|
|
7
|
+
|
|
8
|
+
const WORKFLOW_TEMPLATE = `name: hello-world
|
|
9
|
+
version: "1.0"
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
- name: greet
|
|
13
|
+
run: echo "Hello, World!"
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const TEMPLATE_PLACEHOLDER_PATTERN =
|
|
17
|
+
/\{\{\s*(template|config)\.([A-Za-z_][A-Za-z0-9_.-]*)(?:\|([^}]+))?\s*\}\}/g;
|
|
18
|
+
const TEMPLATE_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Return the help text for `flow init`.
|
|
22
|
+
*
|
|
23
|
+
* @returns {string} Command help text.
|
|
24
|
+
*/
|
|
25
|
+
export function getInitHelpText() {
|
|
26
|
+
return `flow init
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
flow init
|
|
30
|
+
flow init --template <name>
|
|
31
|
+
flow init --list-templates
|
|
32
|
+
flow init --help
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--template <name> Initialize workflow.yaml from .flow/templates/<name>/workflow.yaml
|
|
36
|
+
--list-templates List available local workflow templates
|
|
37
|
+
--help Show this help text
|
|
38
|
+
|
|
39
|
+
Template Placeholders:
|
|
40
|
+
{{template.name}} Prompt for a value
|
|
41
|
+
{{template.name|default}} Prompt and fall back to the provided default
|
|
42
|
+
{{config.parallelism}} Fill from the resolved flow config
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
flow init
|
|
46
|
+
flow init --template ci-pipeline
|
|
47
|
+
flow init --list-templates`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse CLI args passed to `flow init`.
|
|
52
|
+
*
|
|
53
|
+
* @param {string[]} args - CLI args after `init`.
|
|
54
|
+
* @returns {{help: boolean, listTemplates: boolean, template: string | undefined}} Parsed args.
|
|
55
|
+
*/
|
|
56
|
+
export function parseInitCommandArgs(args) {
|
|
57
|
+
const parsed = {
|
|
58
|
+
help: false,
|
|
59
|
+
listTemplates: false,
|
|
60
|
+
template: undefined
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
64
|
+
const arg = args[index];
|
|
65
|
+
|
|
66
|
+
if (arg === "--help" || arg === "-h") {
|
|
67
|
+
parsed.help = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (arg === "--list-templates") {
|
|
72
|
+
parsed.listTemplates = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (arg === "--template") {
|
|
77
|
+
const templateName = args[index + 1];
|
|
78
|
+
|
|
79
|
+
if (!templateName || templateName.startsWith("--")) {
|
|
80
|
+
throw new ConfigError("flow init --template requires a template name.");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (parsed.template !== undefined) {
|
|
84
|
+
throw new ConfigError("flow init accepts at most one --template value.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
parsed.template = validateTemplateName(templateName);
|
|
88
|
+
index += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (arg.startsWith("--template=")) {
|
|
93
|
+
const templateName = arg.slice("--template=".length);
|
|
94
|
+
|
|
95
|
+
if (templateName.length === 0) {
|
|
96
|
+
throw new ConfigError("flow init --template requires a template name.");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (parsed.template !== undefined) {
|
|
100
|
+
throw new ConfigError("flow init accepts at most one --template value.");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
parsed.template = validateTemplateName(templateName);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
throw new ConfigError(`Unknown init flag: ${arg}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (parsed.help && (parsed.listTemplates || parsed.template !== undefined)) {
|
|
111
|
+
throw new ConfigError("flow init --help cannot be combined with other flags.");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (parsed.listTemplates && parsed.template !== undefined) {
|
|
115
|
+
throw new ConfigError("flow init cannot combine --list-templates with --template.");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return parsed;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check whether a directory looks like a git repository root.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} cwd - Directory to inspect.
|
|
125
|
+
* @returns {Promise<boolean>} True when `.git` exists.
|
|
126
|
+
*/
|
|
127
|
+
export async function isGitRepository(cwd) {
|
|
128
|
+
try {
|
|
129
|
+
const gitPath = path.join(cwd, ".git");
|
|
130
|
+
const gitStats = await stat(gitPath);
|
|
131
|
+
return gitStats.isDirectory() || gitStats.isFile();
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Write a file atomically only when it is currently missing.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} filePath - Target file path.
|
|
141
|
+
* @param {string} content - File content to write.
|
|
142
|
+
* @returns {Promise<boolean>} True when a new file was created.
|
|
143
|
+
*/
|
|
144
|
+
export async function writeFileIfMissing(filePath, content) {
|
|
145
|
+
try {
|
|
146
|
+
await access(filePath, constants.F_OK);
|
|
147
|
+
return false;
|
|
148
|
+
} catch {
|
|
149
|
+
const tempPath = `${filePath}.${randomUUID()}.tmp`;
|
|
150
|
+
await writeFile(tempPath, content, "utf8");
|
|
151
|
+
await rename(tempPath, filePath);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Return the local template directory path.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} cwd - Repository directory.
|
|
160
|
+
* @returns {string} Template directory path.
|
|
161
|
+
*/
|
|
162
|
+
export function getTemplateDirectory(cwd) {
|
|
163
|
+
return path.join(cwd, ".flow", "templates");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Validate and normalize a template name for filesystem use.
|
|
168
|
+
*
|
|
169
|
+
* @param {string} templateName - Template name provided by the user.
|
|
170
|
+
* @returns {string} Validated template name.
|
|
171
|
+
*/
|
|
172
|
+
export function validateTemplateName(templateName) {
|
|
173
|
+
if (!TEMPLATE_NAME_PATTERN.test(templateName)) {
|
|
174
|
+
throw new ConfigError(
|
|
175
|
+
"Template names may only contain letters, numbers, hyphens, and underscores."
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return templateName;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Return the workflow file path for a named template.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} cwd - Repository directory.
|
|
186
|
+
* @param {string} templateName - Template name.
|
|
187
|
+
* @returns {string} Absolute template workflow path.
|
|
188
|
+
*/
|
|
189
|
+
export function getTemplateWorkflowPath(cwd, templateName) {
|
|
190
|
+
return path.join(getTemplateDirectory(cwd), validateTemplateName(templateName), "workflow.yaml");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* List available local workflow templates.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} cwd - Repository directory.
|
|
197
|
+
* @returns {Promise<string[]>} Sorted template names.
|
|
198
|
+
*/
|
|
199
|
+
export async function listWorkflowTemplates(cwd) {
|
|
200
|
+
const templateDirectory = getTemplateDirectory(cwd);
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const entries = await readdir(templateDirectory, { withFileTypes: true });
|
|
204
|
+
const templateNames = [];
|
|
205
|
+
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
if (!entry.isDirectory()) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
await access(getTemplateWorkflowPath(cwd, entry.name), constants.F_OK);
|
|
213
|
+
templateNames.push(entry.name);
|
|
214
|
+
} catch {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return templateNames.sort((left, right) => left.localeCompare(right));
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error && error.code === "ENOENT") {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Resolve a dot-path inside an object.
|
|
231
|
+
*
|
|
232
|
+
* @param {object} source - Object to inspect.
|
|
233
|
+
* @param {string} dotPath - Dot-separated key path.
|
|
234
|
+
* @returns {unknown} Resolved value or undefined.
|
|
235
|
+
*/
|
|
236
|
+
function getValueAtPath(source, dotPath) {
|
|
237
|
+
return dotPath.split(".").reduce((value, segment) => {
|
|
238
|
+
if (value == null || typeof value !== "object" || !Object.hasOwn(value, segment)) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return value[segment];
|
|
243
|
+
}, source);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Convert a template placeholder value into a string.
|
|
248
|
+
*
|
|
249
|
+
* @param {unknown} value - Placeholder value.
|
|
250
|
+
* @returns {string} Serializable string value.
|
|
251
|
+
*/
|
|
252
|
+
function formatTemplateValue(value) {
|
|
253
|
+
if (typeof value === "string") {
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
258
|
+
return String(value);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
throw new ConfigError("Template placeholders must resolve to string, number, or boolean values.");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Build the default template prompt implementation.
|
|
266
|
+
*
|
|
267
|
+
* @param {object} options - Prompt wiring.
|
|
268
|
+
* @param {NodeJS.ReadableStream} options.stdin - Input stream.
|
|
269
|
+
* @param {NodeJS.WritableStream} options.stdoutStream - Output stream.
|
|
270
|
+
* @returns {(variableName: string, defaultValue?: string) => Promise<string>} Prompt function.
|
|
271
|
+
*/
|
|
272
|
+
function createTemplatePrompt({ stdin, stdoutStream }) {
|
|
273
|
+
return async (variableName, defaultValue) => {
|
|
274
|
+
const prompt =
|
|
275
|
+
defaultValue === undefined
|
|
276
|
+
? `Template value for '${variableName}': `
|
|
277
|
+
: `Template value for '${variableName}' [${defaultValue}]: `;
|
|
278
|
+
const interfaceHandle = readline.createInterface({
|
|
279
|
+
input: stdin,
|
|
280
|
+
output: stdoutStream
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const answer = (await interfaceHandle.question(prompt)).trim();
|
|
285
|
+
|
|
286
|
+
if (answer.length > 0) {
|
|
287
|
+
return answer;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (defaultValue !== undefined) {
|
|
291
|
+
return defaultValue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
throw new ConfigError(`Template variable '${variableName}' requires a value.`);
|
|
295
|
+
} finally {
|
|
296
|
+
interfaceHandle.close();
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Resolve template placeholders in template content.
|
|
303
|
+
*
|
|
304
|
+
* @param {string} content - Raw template content.
|
|
305
|
+
* @param {object} options - Resolution options.
|
|
306
|
+
* @param {object} options.config - Resolved flow config for config placeholders.
|
|
307
|
+
* @param {(variableName: string, defaultValue?: string) => Promise<string>} options.prompt - Prompt function.
|
|
308
|
+
* @param {Record<string, string | undefined>} [options.templateValues={}] - Pre-supplied template variable values.
|
|
309
|
+
* @returns {Promise<string>} Resolved template content.
|
|
310
|
+
*/
|
|
311
|
+
export async function resolveTemplatePlaceholders(content, options) {
|
|
312
|
+
const {
|
|
313
|
+
config,
|
|
314
|
+
prompt,
|
|
315
|
+
templateValues = {}
|
|
316
|
+
} = options;
|
|
317
|
+
const matches = Array.from(content.matchAll(TEMPLATE_PLACEHOLDER_PATTERN));
|
|
318
|
+
|
|
319
|
+
if (matches.length === 0) {
|
|
320
|
+
return content;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const resolvedTemplateValues = new Map();
|
|
324
|
+
let resolvedContent = "";
|
|
325
|
+
let lastIndex = 0;
|
|
326
|
+
|
|
327
|
+
for (const match of matches) {
|
|
328
|
+
const [placeholder, sourceType, variableName, defaultValue] = match;
|
|
329
|
+
resolvedContent += content.slice(lastIndex, match.index);
|
|
330
|
+
|
|
331
|
+
if (sourceType === "config") {
|
|
332
|
+
const configuredValue = getValueAtPath(config, variableName);
|
|
333
|
+
|
|
334
|
+
if (configuredValue === undefined) {
|
|
335
|
+
if (defaultValue !== undefined) {
|
|
336
|
+
resolvedContent += defaultValue.trim();
|
|
337
|
+
} else {
|
|
338
|
+
throw new ConfigError(`Template config value '${variableName}' is not defined.`);
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
resolvedContent += formatTemplateValue(configuredValue);
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
if (!resolvedTemplateValues.has(variableName)) {
|
|
345
|
+
const providedValue = templateValues[variableName];
|
|
346
|
+
resolvedTemplateValues.set(
|
|
347
|
+
variableName,
|
|
348
|
+
providedValue !== undefined ? providedValue : await prompt(variableName, defaultValue?.trim())
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
resolvedContent += resolvedTemplateValues.get(variableName);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
lastIndex = match.index + placeholder.length;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
resolvedContent += content.slice(lastIndex);
|
|
359
|
+
return resolvedContent;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Load and render a named workflow template.
|
|
364
|
+
*
|
|
365
|
+
* @param {object} options - Template load options.
|
|
366
|
+
* @param {string} options.cwd - Repository directory.
|
|
367
|
+
* @param {string} options.templateName - Template name.
|
|
368
|
+
* @param {object} options.config - Resolved flow config.
|
|
369
|
+
* @param {(variableName: string, defaultValue?: string) => Promise<string>} options.prompt - Prompt function.
|
|
370
|
+
* @param {Record<string, string | undefined>} [options.templateValues] - Pre-supplied template variable values.
|
|
371
|
+
* @returns {Promise<string>} Resolved workflow content.
|
|
372
|
+
*/
|
|
373
|
+
export async function loadWorkflowTemplate(options) {
|
|
374
|
+
const {
|
|
375
|
+
cwd,
|
|
376
|
+
templateName,
|
|
377
|
+
config,
|
|
378
|
+
prompt,
|
|
379
|
+
templateValues
|
|
380
|
+
} = options;
|
|
381
|
+
const templatePath = getTemplateWorkflowPath(cwd, templateName);
|
|
382
|
+
let content;
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
content = await readFile(templatePath, "utf8");
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (error && error.code === "ENOENT") {
|
|
388
|
+
throw new ConfigError(
|
|
389
|
+
`Template '${templateName}' was not found at ${templatePath}. Run 'flow init --list-templates' to inspect available templates.`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return resolveTemplatePlaceholders(content, {
|
|
397
|
+
config,
|
|
398
|
+
prompt,
|
|
399
|
+
templateValues
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Execute the `flow init` command.
|
|
405
|
+
*
|
|
406
|
+
* @param {string[]} args - CLI args after `init`.
|
|
407
|
+
* @param {(message: string) => void} [stdout=console.log] - Success output writer.
|
|
408
|
+
* @param {(message: string) => void} [stderr=console.error] - Error output writer.
|
|
409
|
+
* @param {object} [options={}] - Runtime overrides used by tests.
|
|
410
|
+
* @param {string} [options.cwd=process.cwd()] - Target working directory.
|
|
411
|
+
* @param {object} [options.env=process.env] - Environment override.
|
|
412
|
+
* @param {string} [options.homeDir] - Home directory override.
|
|
413
|
+
* @param {NodeJS.ReadableStream} [options.stdin=process.stdin] - Prompt input stream.
|
|
414
|
+
* @param {NodeJS.WritableStream} [options.stdoutStream=process.stdout] - Prompt output stream.
|
|
415
|
+
* @param {Record<string, string | undefined>} [options.templateValues] - Pre-supplied template values.
|
|
416
|
+
* @param {(variableName: string, defaultValue?: string) => Promise<string>} [options.prompt] - Prompt override.
|
|
417
|
+
* @returns {Promise<number>} Process exit code.
|
|
418
|
+
*/
|
|
419
|
+
export async function runInitCommand(args, stdout = console.log, stderr = console.error, options = {}) {
|
|
420
|
+
try {
|
|
421
|
+
const parsed = parseInitCommandArgs(args);
|
|
422
|
+
|
|
423
|
+
if (parsed.help) {
|
|
424
|
+
stdout(getInitHelpText());
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (parsed.listTemplates) {
|
|
429
|
+
const templateNames = await listWorkflowTemplates(options.cwd ?? process.cwd());
|
|
430
|
+
|
|
431
|
+
if (templateNames.length === 0) {
|
|
432
|
+
stdout("No workflow templates found in .flow/templates/.");
|
|
433
|
+
return 0;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
stdout("Available workflow templates:");
|
|
437
|
+
|
|
438
|
+
for (const templateName of templateNames) {
|
|
439
|
+
stdout(`- ${templateName}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return 0;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return initializeProject({
|
|
446
|
+
...options,
|
|
447
|
+
stdout,
|
|
448
|
+
stderr,
|
|
449
|
+
template: parsed.template
|
|
450
|
+
});
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (error instanceof ConfigError) {
|
|
453
|
+
stderr(`Error: ${error.message}`);
|
|
454
|
+
return 1;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Ensure flow-generated local state is present in `.gitignore` without duplicating entries.
|
|
463
|
+
*
|
|
464
|
+
* @param {string} cwd - Repository directory.
|
|
465
|
+
* @returns {Promise<boolean>} True when `.gitignore` was modified.
|
|
466
|
+
*/
|
|
467
|
+
export async function ensureGitignoreContainsFlow(cwd) {
|
|
468
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
469
|
+
let existing = "";
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
existing = await readFile(gitignorePath, "utf8");
|
|
473
|
+
} catch {
|
|
474
|
+
existing = "";
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const lines = existing.split(/\r?\n/).filter(Boolean);
|
|
478
|
+
const requiredEntries = [".flow/", "workflow.local.yaml"];
|
|
479
|
+
const missingEntries = requiredEntries.filter((entry) => !lines.includes(entry));
|
|
480
|
+
|
|
481
|
+
if (missingEntries.length === 0) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
486
|
+
await writeFile(gitignorePath, `${existing}${prefix}${missingEntries.join("\n")}\n`, "utf8");
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Initialize the repo-local flow project structure.
|
|
492
|
+
*
|
|
493
|
+
* @param {object} options - Initialization options.
|
|
494
|
+
* @param {string} [options.cwd=process.cwd()] - Target working directory.
|
|
495
|
+
* @param {(message: string) => void} [options.stdout=console.log] - Success output writer.
|
|
496
|
+
* @param {(message: string) => void} [options.stderr=console.error] - Error output writer.
|
|
497
|
+
* @returns {Promise<number>} Process exit code.
|
|
498
|
+
*/
|
|
499
|
+
export async function initializeProject(options = {}) {
|
|
500
|
+
const {
|
|
501
|
+
cwd = process.cwd(),
|
|
502
|
+
env = process.env,
|
|
503
|
+
homeDir,
|
|
504
|
+
stdin = process.stdin,
|
|
505
|
+
stdoutStream = process.stdout,
|
|
506
|
+
template,
|
|
507
|
+
templateValues,
|
|
508
|
+
prompt,
|
|
509
|
+
stdout = console.log,
|
|
510
|
+
stderr = console.error
|
|
511
|
+
} = options;
|
|
512
|
+
let config;
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
({ config } = await loadConfig({ cwd, env, homeDir }));
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (error instanceof ConfigError) {
|
|
518
|
+
stderr(`Error: ${error.message}`);
|
|
519
|
+
return 1;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (!(await isGitRepository(cwd))) {
|
|
526
|
+
stderr("Error: flow init must be run inside a git repository.");
|
|
527
|
+
return 1;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const flowDir = path.join(cwd, ".flow");
|
|
531
|
+
const runsDir = path.join(flowDir, "runs");
|
|
532
|
+
const configPath = path.join(flowDir, "config.yaml");
|
|
533
|
+
const workflowPath = path.join(cwd, "workflow.yaml");
|
|
534
|
+
const resolvedPrompt = prompt ?? createTemplatePrompt({ stdin, stdoutStream });
|
|
535
|
+
const workflowContent =
|
|
536
|
+
template === undefined
|
|
537
|
+
? WORKFLOW_TEMPLATE
|
|
538
|
+
: await loadWorkflowTemplate({
|
|
539
|
+
cwd,
|
|
540
|
+
templateName: template,
|
|
541
|
+
config,
|
|
542
|
+
prompt: resolvedPrompt,
|
|
543
|
+
templateValues
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
await mkdir(runsDir, { recursive: true });
|
|
547
|
+
|
|
548
|
+
const createdConfig = await writeFileIfMissing(configPath, getConfigTemplate());
|
|
549
|
+
const createdWorkflow = await writeFileIfMissing(workflowPath, workflowContent);
|
|
550
|
+
const updatedGitignore = await ensureGitignoreContainsFlow(cwd);
|
|
551
|
+
const alreadyInitialized = !createdConfig && !createdWorkflow && !updatedGitignore;
|
|
552
|
+
|
|
553
|
+
if (alreadyInitialized) {
|
|
554
|
+
stdout("Flow project already initialized.");
|
|
555
|
+
stdout(`Workflow file: ${workflowPath}`);
|
|
556
|
+
stdout("Run 'flow run' to execute your first workflow.");
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
stdout("✓ Initialized flow project in .flow/");
|
|
561
|
+
if (template !== undefined) {
|
|
562
|
+
stdout(`Template: ${template}`);
|
|
563
|
+
}
|
|
564
|
+
stdout(`Workflow file: ${workflowPath}`);
|
|
565
|
+
stdout(`Project config: ${configPath}`);
|
|
566
|
+
stdout("Run 'flow run' to execute your first workflow.");
|
|
567
|
+
return 0;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export const templates = {
|
|
571
|
+
workflow: WORKFLOW_TEMPLATE,
|
|
572
|
+
config: getConfigTemplate()
|
|
573
|
+
};
|