@formigio/fazemos-cli 0.5.1 → 0.6.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/dist/index.js +250 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { config, getEnv, getToken, getActiveOrgId, setActiveOrgId, addEnvironmen
|
|
|
5
5
|
import { login, signup, confirmSignup, adminLogin } from './auth.js';
|
|
6
6
|
import { api, ApiError } from './api.js';
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
|
-
import { readFileSync, readdirSync } from 'fs';
|
|
8
|
+
import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync, statSync } from 'fs';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import { dirname, resolve, basename } from 'path';
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -25,6 +25,58 @@ function parseJson(val, flag) {
|
|
|
25
25
|
process.exit(1);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
/** Resolve a value that may be `@filepath` (reads file contents) or inline text. */
|
|
29
|
+
function resolveFileOrInline(val) {
|
|
30
|
+
if (val.startsWith('@')) {
|
|
31
|
+
const filePath = resolve(val.slice(1));
|
|
32
|
+
try {
|
|
33
|
+
return readFileSync(filePath, 'utf-8');
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
throw new Error(`Cannot read --sections file "${filePath}": ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return val;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Make a string safe to use as a filename: strip path separators and other
|
|
43
|
+
* shell-hostile characters, reject `..` traversal. Used by `agents export` so
|
|
44
|
+
* an agent display_name like `foo/bar` or `..` cannot escape the output dir.
|
|
45
|
+
*/
|
|
46
|
+
function safeFileName(name) {
|
|
47
|
+
const cleaned = name.replace(/[^\w.\- ]+/g, '_').trim();
|
|
48
|
+
if (!cleaned || cleaned === '.' || cleaned === '..') {
|
|
49
|
+
throw new Error(`Cannot derive safe filename from "${name}"`);
|
|
50
|
+
}
|
|
51
|
+
return cleaned;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the markdown content for an exported agent. With `includeFrontmatter`,
|
|
55
|
+
* prepends YAML frontmatter reconstructed from `agent_config`. Note: only
|
|
56
|
+
* `name` round-trips through `upload-all` today — the other fields are
|
|
57
|
+
* informational on import. String values are JSON-stringified to keep YAML
|
|
58
|
+
* valid even if a model name or role contains a colon.
|
|
59
|
+
*/
|
|
60
|
+
function buildAgentFileContent(agent, includeFrontmatter) {
|
|
61
|
+
const prompt = agent.agent_config?.systemPrompt || '';
|
|
62
|
+
if (!includeFrontmatter)
|
|
63
|
+
return prompt + '\n';
|
|
64
|
+
const ac = agent.agent_config || {};
|
|
65
|
+
const fm = ['---'];
|
|
66
|
+
fm.push(`name: ${JSON.stringify(agent.display_name)}`);
|
|
67
|
+
if (ac.model)
|
|
68
|
+
fm.push(`model: ${JSON.stringify(ac.model)}`);
|
|
69
|
+
if (ac.maxBudgetUsd != null)
|
|
70
|
+
fm.push(`maxBudgetUsd: ${ac.maxBudgetUsd}`);
|
|
71
|
+
if (ac.maxTurns != null)
|
|
72
|
+
fm.push(`maxTurns: ${ac.maxTurns}`);
|
|
73
|
+
if (ac.timeoutMs != null)
|
|
74
|
+
fm.push(`timeoutMs: ${ac.timeoutMs}`);
|
|
75
|
+
if (ac.roles?.length)
|
|
76
|
+
fm.push(`roles: ${JSON.stringify(ac.roles)}`);
|
|
77
|
+
fm.push('---');
|
|
78
|
+
return fm.join('\n') + '\n\n' + prompt + '\n';
|
|
79
|
+
}
|
|
28
80
|
const program = new Command();
|
|
29
81
|
program
|
|
30
82
|
.name('fazemos')
|
|
@@ -1870,6 +1922,16 @@ function allSteps(definition) {
|
|
|
1870
1922
|
function findStepById(definition, stepId) {
|
|
1871
1923
|
return allSteps(definition).find((s) => s.id === stepId);
|
|
1872
1924
|
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Pipeline-sourced step inputs went through a schema migration: the old
|
|
1927
|
+
* shape stored the pipeline input name under `pipeline_input`; the new shape
|
|
1928
|
+
* uses `source: "pipeline"` + `source_pipeline_input`. Read sites should call
|
|
1929
|
+
* this helper instead of touching either field directly so both shapes work.
|
|
1930
|
+
* Returns undefined for step-sourced inputs.
|
|
1931
|
+
*/
|
|
1932
|
+
function getPipelineInputName(input) {
|
|
1933
|
+
return input?.source_pipeline_input ?? input?.pipeline_input;
|
|
1934
|
+
}
|
|
1873
1935
|
const VALID_IO_TYPES = ['text', 'markdown', 'number', 'boolean', 'url', 'json', 'object', 'array'];
|
|
1874
1936
|
function requireDraftStatus(template) {
|
|
1875
1937
|
if (template.status !== 'draft') {
|
|
@@ -1893,6 +1955,7 @@ const templates = program.command('templates').alias('tpl').description('Pipelin
|
|
|
1893
1955
|
' 4. tpl add-output / add-input Wire I/O between steps\n' +
|
|
1894
1956
|
' 5. tpl validate <id> Check for errors\n' +
|
|
1895
1957
|
' 6. tpl activate <id> Make available for instances\n\n' +
|
|
1958
|
+
' Use "tpl phases <id>" to list phase IDs needed by --phase options.\n' +
|
|
1896
1959
|
' Use "tpl steps <id>" to list step IDs needed by --step options.\n' +
|
|
1897
1960
|
' Use "tpl show <id>" to see full structure with I/O declarations.');
|
|
1898
1961
|
templates
|
|
@@ -1920,7 +1983,7 @@ templates
|
|
|
1920
1983
|
});
|
|
1921
1984
|
templates
|
|
1922
1985
|
.command('show')
|
|
1923
|
-
.description('Show template detail including phases, steps, I/O declarations, and
|
|
1986
|
+
.description('Show template detail including phases, steps, I/O declarations, pipeline inputs, and revision number. Use this to inspect the full structure of a template and discover phase/step IDs needed by other commands.')
|
|
1924
1987
|
.argument('<id>', 'Template ID (use "tpl list" to find IDs)')
|
|
1925
1988
|
.action(async (id) => {
|
|
1926
1989
|
try {
|
|
@@ -1929,7 +1992,7 @@ templates
|
|
|
1929
1992
|
console.log(chalk.cyan(t.name));
|
|
1930
1993
|
console.log(` ID: ${t.id}`);
|
|
1931
1994
|
console.log(` Status: ${t.status}`);
|
|
1932
|
-
console.log(`
|
|
1995
|
+
console.log(` Revision: ${t.version}`);
|
|
1933
1996
|
// Pipeline-level inputs
|
|
1934
1997
|
if (t.definition?.inputs?.length) {
|
|
1935
1998
|
console.log(chalk.cyan('\n Pipeline Inputs:'));
|
|
@@ -1942,7 +2005,7 @@ templates
|
|
|
1942
2005
|
}
|
|
1943
2006
|
if (t.definition?.phases) {
|
|
1944
2007
|
for (const phase of t.definition.phases) {
|
|
1945
|
-
console.log(chalk.cyan(`\n Phase: ${phase.name}`));
|
|
2008
|
+
console.log(chalk.cyan(`\n Phase: ${phase.name}`) + chalk.dim(` ${phase.id}`));
|
|
1946
2009
|
for (const step of phase.steps || []) {
|
|
1947
2010
|
const reviewer = step.reviewer ? ` reviewer:${step.reviewer}` : '';
|
|
1948
2011
|
const cycles = step.reviewer && step.max_review_cycles ? ` max-cycles:${step.max_review_cycles}` : '';
|
|
@@ -1958,8 +2021,9 @@ templates
|
|
|
1958
2021
|
if (step.inputs?.length) {
|
|
1959
2022
|
console.log(' Inputs:');
|
|
1960
2023
|
for (const inp of step.inputs) {
|
|
1961
|
-
|
|
1962
|
-
|
|
2024
|
+
const pName = getPipelineInputName(inp);
|
|
2025
|
+
if (pName) {
|
|
2026
|
+
console.log(` ← ${inp.name} ← pipeline.${pName}`);
|
|
1963
2027
|
}
|
|
1964
2028
|
else {
|
|
1965
2029
|
const srcStep = findStepById(t.definition, inp.source_step_id);
|
|
@@ -2064,13 +2128,23 @@ templates
|
|
|
2064
2128
|
description: s.description || '',
|
|
2065
2129
|
step_type: s.executionMode === 'script' ? 'script' : (s.agent ? 'agent' : 'human'),
|
|
2066
2130
|
role: s.role || s.agent || 'unassigned',
|
|
2067
|
-
inputs: (s.inputs || []).map((inp) =>
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2131
|
+
inputs: (s.inputs || []).map((inp) => {
|
|
2132
|
+
const pipelineName = getPipelineInputName(inp);
|
|
2133
|
+
return {
|
|
2134
|
+
name: inp.name,
|
|
2135
|
+
...(inp.source_step_id ? {
|
|
2136
|
+
source: 'step',
|
|
2137
|
+
source_step_id: idMap.get(inp.source_step_id) || inp.source_step_id,
|
|
2138
|
+
source_output_name: inp.source_output_name,
|
|
2139
|
+
} : {}),
|
|
2140
|
+
...(pipelineName ? {
|
|
2141
|
+
source: 'pipeline',
|
|
2142
|
+
source_pipeline_input: pipelineName,
|
|
2143
|
+
} : {}),
|
|
2144
|
+
...(inp.description ? { description: inp.description } : {}),
|
|
2145
|
+
required: inp.required !== false,
|
|
2146
|
+
};
|
|
2147
|
+
}),
|
|
2074
2148
|
outputs: (s.outputs || []).map((o) => ({
|
|
2075
2149
|
name: o.name,
|
|
2076
2150
|
type: o.type || 'text',
|
|
@@ -2138,7 +2212,7 @@ templates
|
|
|
2138
2212
|
// ── Template structure commands ─────────────────────────────
|
|
2139
2213
|
templates
|
|
2140
2214
|
.command('update')
|
|
2141
|
-
.description('Update template name or description. Works on any status (draft, active, or archived). Does not modify the definition or bump
|
|
2215
|
+
.description('Update template name or description. Works on any status (draft, active, or archived). Does not modify the definition or bump the revision counter.')
|
|
2142
2216
|
.argument('<id>', 'Template ID')
|
|
2143
2217
|
.option('-n, --name <name>', 'New name')
|
|
2144
2218
|
.option('-d, --description <desc>', 'New description')
|
|
@@ -2225,9 +2299,9 @@ templates
|
|
|
2225
2299
|
});
|
|
2226
2300
|
templates
|
|
2227
2301
|
.command('remove-phase')
|
|
2228
|
-
.description('Remove a phase from a template. Blocked if the phase contains steps unless --force is used. Template must be in draft status. Use "tpl show" to find phase IDs.')
|
|
2302
|
+
.description('Remove a phase from a template. Blocked if the phase contains steps unless --force is used. Template must be in draft status. Use "tpl phases" or "tpl show" to find phase IDs.')
|
|
2229
2303
|
.argument('<templateId>', 'Template ID')
|
|
2230
|
-
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl show" output)')
|
|
2304
|
+
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl phases" or "tpl show" output)')
|
|
2231
2305
|
.option('--force', 'Remove even if phase contains steps')
|
|
2232
2306
|
.action(async (templateId, opts) => {
|
|
2233
2307
|
try {
|
|
@@ -2256,9 +2330,9 @@ templates
|
|
|
2256
2330
|
});
|
|
2257
2331
|
templates
|
|
2258
2332
|
.command('edit-phase')
|
|
2259
|
-
.description('Edit a phase name or description. Template must be in draft status. Provide at least one of --name or --description.')
|
|
2333
|
+
.description('Edit a phase name or description. Template must be in draft status. Provide at least one of --name or --description. Use "tpl phases" to find phase IDs.')
|
|
2260
2334
|
.argument('<templateId>', 'Template ID')
|
|
2261
|
-
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl show" output)')
|
|
2335
|
+
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl phases" or "tpl show" output)')
|
|
2262
2336
|
.option('--name <name>', 'New phase name (must be unique within the template)')
|
|
2263
2337
|
.option('--description <desc>', 'New phase description')
|
|
2264
2338
|
.action(async (templateId, opts) => {
|
|
@@ -2294,9 +2368,9 @@ templates
|
|
|
2294
2368
|
});
|
|
2295
2369
|
templates
|
|
2296
2370
|
.command('add-step')
|
|
2297
|
-
.description('Add a step to a phase. Template must be in draft status. Step names must be unique within their phase. Returns the generated step ID needed by I/O commands (add-output, add-input, etc.). Use "tpl show" or "tpl steps" to find existing step IDs.')
|
|
2371
|
+
.description('Add a step to a phase. Template must be in draft status. Step names must be unique within their phase. Returns the generated step ID needed by I/O commands (add-output, add-input, etc.). Use --sections to provide agent instructions (inline or @filepath). Use --after to control ordering. Use "tpl show" or "tpl steps" to find existing step IDs.')
|
|
2298
2372
|
.argument('<templateId>', 'Template ID')
|
|
2299
|
-
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl show" or "tpl add-phase" output)')
|
|
2373
|
+
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl phases", "tpl show", or "tpl add-phase" output)')
|
|
2300
2374
|
.requiredOption('--name <name>', 'Step name (must be unique within the phase)')
|
|
2301
2375
|
.option('--type <type>', 'Step type: human (manual task), agent (AI agent), script (automated), gate (approval checkpoint)', 'human')
|
|
2302
2376
|
.option('--role <role>', 'Role or agent name (e.g., "kate", "marco", "dev-team")')
|
|
@@ -2309,6 +2383,9 @@ templates
|
|
|
2309
2383
|
.option('--timeout <seconds>', 'Timeout in seconds for script steps', parseNumber)
|
|
2310
2384
|
.option('--working-dir <dir>', 'Working directory for script steps (absolute path)')
|
|
2311
2385
|
.option('--env <json>', 'Environment variables for script steps as JSON (e.g., \'{"KEY":"value"}\')')
|
|
2386
|
+
.option('--sections <text>', 'Agent instructions / step content. Use @filepath to load from a file (e.g., --sections @steps/review.md)')
|
|
2387
|
+
.option('--after <stepId>', 'Insert after this step within the same phase (sets sort_order to target + 1, shifts subsequent steps). Target must live in the phase named by --phase.')
|
|
2388
|
+
.option('--sort-order <n>', 'Set sort_order directly (lower-level escape hatch)', parseNumber)
|
|
2312
2389
|
.option('--agent-config <json>', 'Per-step agent config overrides as JSON (e.g., \'{"model":"opus","maxBudgetUsd":20}\'). Overrides agent member defaults for: model, maxBudgetUsd, maxTurns, timeoutMs, cwd, repos')
|
|
2313
2390
|
.action(async (templateId, opts) => {
|
|
2314
2391
|
try {
|
|
@@ -2349,6 +2426,9 @@ templates
|
|
|
2349
2426
|
execConfig.env = parseJson(opts.env, '--env');
|
|
2350
2427
|
}
|
|
2351
2428
|
}
|
|
2429
|
+
// Default sort_order: max existing + 1, so non-contiguous gaps don't
|
|
2430
|
+
// cause new steps to collide with existing ones (e.g. after a --after insert).
|
|
2431
|
+
const maxSortOrder = phase.steps.reduce((m, s) => Math.max(m, s.sort_order ?? 0), -1);
|
|
2352
2432
|
const step = {
|
|
2353
2433
|
id: crypto.randomUUID(),
|
|
2354
2434
|
name: opts.name,
|
|
@@ -2357,16 +2437,37 @@ templates
|
|
|
2357
2437
|
role: opts.role || 'unassigned',
|
|
2358
2438
|
inputs: [],
|
|
2359
2439
|
outputs: [],
|
|
2360
|
-
sections: '',
|
|
2440
|
+
sections: opts.sections ? resolveFileOrInline(opts.sections) : '',
|
|
2361
2441
|
reviewer: opts.reviewer || null,
|
|
2362
2442
|
max_review_cycles: parseInt(opts.maxReviewCycles) || 0,
|
|
2363
2443
|
execution_config: execConfig,
|
|
2364
2444
|
parallel_group: opts.parallelGroup || null,
|
|
2365
|
-
sort_order:
|
|
2445
|
+
sort_order: maxSortOrder + 1,
|
|
2366
2446
|
};
|
|
2367
2447
|
if (opts.agentConfig)
|
|
2368
2448
|
step.agent_config = parseJson(opts.agentConfig, '--agent-config');
|
|
2449
|
+
// Positioning: --after or --sort-order
|
|
2450
|
+
if (opts.after && opts.sortOrder !== undefined) {
|
|
2451
|
+
console.error(chalk.red('Cannot use both --after and --sort-order'));
|
|
2452
|
+
process.exit(1);
|
|
2453
|
+
}
|
|
2454
|
+
if (opts.after) {
|
|
2455
|
+
const target = phase.steps.find((s) => s.id === opts.after);
|
|
2456
|
+
if (!target) {
|
|
2457
|
+
console.error(chalk.red(`Step "${opts.after}" not found in phase "${phase.name}"`));
|
|
2458
|
+
process.exit(1);
|
|
2459
|
+
}
|
|
2460
|
+
step.sort_order = target.sort_order + 1;
|
|
2461
|
+
for (const s of phase.steps) {
|
|
2462
|
+
if (s.sort_order >= step.sort_order)
|
|
2463
|
+
s.sort_order++;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
else if (opts.sortOrder !== undefined) {
|
|
2467
|
+
step.sort_order = opts.sortOrder;
|
|
2468
|
+
}
|
|
2369
2469
|
phase.steps.push(step);
|
|
2470
|
+
phase.steps.sort((a, b) => a.sort_order - b.sort_order);
|
|
2370
2471
|
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
2371
2472
|
console.log(chalk.green(`Added step: ${opts.name} (${opts.type}) to phase ${phase.name}`));
|
|
2372
2473
|
console.log(` ID: ${step.id}`);
|
|
@@ -2421,7 +2522,7 @@ templates
|
|
|
2421
2522
|
});
|
|
2422
2523
|
templates
|
|
2423
2524
|
.command('edit-step')
|
|
2424
|
-
.description('Edit step properties. Template must be in draft status. Provide at least one field to update. Use "tpl steps" to find step IDs.')
|
|
2525
|
+
.description('Edit step properties including sections (agent instructions). Template must be in draft status. Provide at least one field to update. Use "tpl steps" to find step IDs.')
|
|
2425
2526
|
.argument('<templateId>', 'Template ID')
|
|
2426
2527
|
.requiredOption('--step <stepId>', 'Step ID (from "tpl steps" output)')
|
|
2427
2528
|
.option('--name <name>', 'New step name (must be unique within the phase)')
|
|
@@ -2436,13 +2537,14 @@ templates
|
|
|
2436
2537
|
.option('--timeout <seconds>', 'Timeout in seconds for script steps', parseNumber)
|
|
2437
2538
|
.option('--working-dir <dir>', 'Working directory for script steps (absolute path)')
|
|
2438
2539
|
.option('--env <json>', 'Environment variables for script steps as JSON')
|
|
2540
|
+
.option('--sections <text>', 'Agent instructions / step content. Use @filepath to load from a file (e.g., --sections @steps/review.md)')
|
|
2439
2541
|
.option('--agent-config <json>', 'Per-step agent config overrides as JSON (merges with existing)')
|
|
2440
2542
|
.action(async (templateId, opts) => {
|
|
2441
2543
|
try {
|
|
2442
2544
|
const hasUpdate = opts.name || opts.type || opts.role || opts.description != null
|
|
2443
2545
|
|| opts.reviewer != null || opts.maxReviewCycles != null || opts.parallelGroup != null
|
|
2444
2546
|
|| opts.image || opts.command || opts.timeout !== undefined || opts.workingDir || opts.env
|
|
2445
|
-
|| opts.agentConfig;
|
|
2547
|
+
|| opts.agentConfig || opts.sections != null;
|
|
2446
2548
|
if (!hasUpdate) {
|
|
2447
2549
|
console.error(chalk.red('Provide at least one field to update'));
|
|
2448
2550
|
process.exit(1);
|
|
@@ -2504,6 +2606,9 @@ templates
|
|
|
2504
2606
|
step.execution_config.env = parseJson(opts.env, '--env');
|
|
2505
2607
|
}
|
|
2506
2608
|
}
|
|
2609
|
+
// sections (agent instructions)
|
|
2610
|
+
if (opts.sections != null)
|
|
2611
|
+
step.sections = resolveFileOrInline(opts.sections);
|
|
2507
2612
|
// agent_config overrides (merge with existing)
|
|
2508
2613
|
if (opts.agentConfig) {
|
|
2509
2614
|
step.agent_config = { ...(step.agent_config || {}), ...parseJson(opts.agentConfig, '--agent-config') };
|
|
@@ -2517,6 +2622,28 @@ templates
|
|
|
2517
2622
|
}
|
|
2518
2623
|
});
|
|
2519
2624
|
// ── Template I/O commands ──────────────────────────────────
|
|
2625
|
+
templates
|
|
2626
|
+
.command('phases')
|
|
2627
|
+
.description('List phase IDs and names in a template. Use this to discover phase IDs needed by --phase options in add-step, edit-phase, remove-phase, etc.')
|
|
2628
|
+
.argument('<id>', 'Template ID')
|
|
2629
|
+
.action(async (id) => {
|
|
2630
|
+
try {
|
|
2631
|
+
const data = await api('GET', `/api/pipeline-templates/${id}`);
|
|
2632
|
+
const phases = data.template.definition?.phases || [];
|
|
2633
|
+
if (!phases.length) {
|
|
2634
|
+
console.log(chalk.yellow('No phases'));
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
for (const p of phases) {
|
|
2638
|
+
const stepCount = (p.steps || []).length;
|
|
2639
|
+
console.log(` ${chalk.dim(p.id)} ${p.name} ${chalk.dim(`(${stepCount} steps)`)}`);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
catch (err) {
|
|
2643
|
+
console.error(chalk.red(err.message));
|
|
2644
|
+
process.exit(1);
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2520
2647
|
templates
|
|
2521
2648
|
.command('steps')
|
|
2522
2649
|
.description('List step IDs and names in a template. Use this to discover step IDs needed by --step options in add-output, add-input, remove-step, edit-step, etc.')
|
|
@@ -2690,11 +2817,13 @@ templates
|
|
|
2690
2817
|
}
|
|
2691
2818
|
const input = { name: opts.name, required: !opts.optional };
|
|
2692
2819
|
if (opts.sourceStep) {
|
|
2820
|
+
input.source = 'step';
|
|
2693
2821
|
input.source_step_id = opts.sourceStep;
|
|
2694
2822
|
input.source_output_name = opts.sourceOutput;
|
|
2695
2823
|
}
|
|
2696
2824
|
else {
|
|
2697
|
-
input.
|
|
2825
|
+
input.source = 'pipeline';
|
|
2826
|
+
input.source_pipeline_input = opts.pipelineInput;
|
|
2698
2827
|
}
|
|
2699
2828
|
if (opts.description)
|
|
2700
2829
|
input.description = opts.description;
|
|
@@ -2799,7 +2928,7 @@ templates
|
|
|
2799
2928
|
process.exit(1);
|
|
2800
2929
|
}
|
|
2801
2930
|
// Block if steps reference it unless --force
|
|
2802
|
-
const refs = allSteps(t.definition).filter((s) => (s.inputs || []).some((inp) => inp
|
|
2931
|
+
const refs = allSteps(t.definition).filter((s) => (s.inputs || []).some((inp) => getPipelineInputName(inp) === opts.name));
|
|
2803
2932
|
if (refs.length && !opts.force) {
|
|
2804
2933
|
console.error(chalk.yellow(`Pipeline input "${opts.name}" is referenced by: ${refs.map((r) => r.name).join(', ')}`));
|
|
2805
2934
|
console.error(chalk.yellow('Use --force to remove anyway'));
|
|
@@ -2873,9 +3002,10 @@ templates
|
|
|
2873
3002
|
errors++;
|
|
2874
3003
|
}
|
|
2875
3004
|
}
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
3005
|
+
const pipelineName = getPipelineInputName(inp);
|
|
3006
|
+
if (pipelineName) {
|
|
3007
|
+
if (!pipelineInputs.find((p) => p.name === pipelineName)) {
|
|
3008
|
+
console.log(chalk.red(` ✗ Step "${step.name}" input "${inp.name}" references missing pipeline input "${pipelineName}"`));
|
|
2879
3009
|
hasUnresolved = true;
|
|
2880
3010
|
errors++;
|
|
2881
3011
|
}
|
|
@@ -4428,6 +4558,97 @@ agentsCmd
|
|
|
4428
4558
|
process.exit(1);
|
|
4429
4559
|
}
|
|
4430
4560
|
});
|
|
4561
|
+
agentsCmd
|
|
4562
|
+
.command('export')
|
|
4563
|
+
.description('Export all agent system prompts to a directory. Default: body only (matches what upload-all consumes). Use --include-frontmatter to also write readable YAML frontmatter — note that upload-all currently only reads the `name:` field, so the other frontmatter fields are informational.')
|
|
4564
|
+
.argument('<output-dir>', 'Directory to write agent .md files to (created if needed)')
|
|
4565
|
+
.option('--include-frontmatter', 'Include YAML frontmatter with agent config fields (name, model, maxBudgetUsd, maxTurns, timeoutMs, roles)')
|
|
4566
|
+
.action(async (outputDir, opts) => {
|
|
4567
|
+
try {
|
|
4568
|
+
const orgId = getActiveOrgId();
|
|
4569
|
+
if (!orgId) {
|
|
4570
|
+
console.error(chalk.red('No active org'));
|
|
4571
|
+
process.exit(1);
|
|
4572
|
+
}
|
|
4573
|
+
const resolvedDir = resolve(outputDir);
|
|
4574
|
+
if (existsSync(resolvedDir) && statSync(resolvedDir).isFile()) {
|
|
4575
|
+
console.error(chalk.red(`"${resolvedDir}" is a file, not a directory`));
|
|
4576
|
+
process.exit(1);
|
|
4577
|
+
}
|
|
4578
|
+
mkdirSync(resolvedDir, { recursive: true });
|
|
4579
|
+
const membersData = await api('GET', `/api/organizations/${orgId}/members?type=agent`);
|
|
4580
|
+
const agents = membersData.members.filter((m) => m.member_type === 'agent');
|
|
4581
|
+
let exported = 0;
|
|
4582
|
+
let skipped = 0;
|
|
4583
|
+
const writtenFiles = new Set();
|
|
4584
|
+
for (const agent of agents) {
|
|
4585
|
+
const prompt = agent.agent_config?.systemPrompt;
|
|
4586
|
+
if (!prompt) {
|
|
4587
|
+
console.log(chalk.yellow(` ⊘ ${agent.display_name} — no systemPrompt`));
|
|
4588
|
+
skipped++;
|
|
4589
|
+
continue;
|
|
4590
|
+
}
|
|
4591
|
+
let fileName;
|
|
4592
|
+
try {
|
|
4593
|
+
fileName = `${safeFileName(agent.display_name)}.md`;
|
|
4594
|
+
}
|
|
4595
|
+
catch (err) {
|
|
4596
|
+
console.log(chalk.yellow(` ⊘ ${agent.display_name} — ${err.message}`));
|
|
4597
|
+
skipped++;
|
|
4598
|
+
continue;
|
|
4599
|
+
}
|
|
4600
|
+
if (writtenFiles.has(fileName)) {
|
|
4601
|
+
console.log(chalk.yellow(` ⊘ ${agent.display_name} — filename "${fileName}" already written by another agent (display name collision)`));
|
|
4602
|
+
skipped++;
|
|
4603
|
+
continue;
|
|
4604
|
+
}
|
|
4605
|
+
const content = buildAgentFileContent(agent, !!opts.includeFrontmatter);
|
|
4606
|
+
writeFileSync(resolve(resolvedDir, fileName), content, 'utf-8');
|
|
4607
|
+
writtenFiles.add(fileName);
|
|
4608
|
+
console.log(chalk.green(` ✓ ${agent.display_name}`));
|
|
4609
|
+
exported++;
|
|
4610
|
+
}
|
|
4611
|
+
console.log(`\n${exported} agents exported to ${resolvedDir}${skipped ? `, ${skipped} skipped` : ''}`);
|
|
4612
|
+
}
|
|
4613
|
+
catch (err) {
|
|
4614
|
+
console.error(chalk.red(err.message));
|
|
4615
|
+
process.exit(1);
|
|
4616
|
+
}
|
|
4617
|
+
});
|
|
4618
|
+
agentsCmd
|
|
4619
|
+
.command('export-definition')
|
|
4620
|
+
.description('Export a single agent system prompt to a file. Matches agent by display name (case-insensitive). Default: body only. Use --include-frontmatter to also write readable YAML frontmatter — note that upload-definition only reads the body, so the frontmatter fields are informational.')
|
|
4621
|
+
.argument('<name>', 'Agent display name')
|
|
4622
|
+
.argument('<file>', 'Output file path')
|
|
4623
|
+
.option('--include-frontmatter', 'Include YAML frontmatter with agent config fields (name, model, maxBudgetUsd, maxTurns, timeoutMs, roles)')
|
|
4624
|
+
.action(async (name, file, opts) => {
|
|
4625
|
+
try {
|
|
4626
|
+
const orgId = getActiveOrgId();
|
|
4627
|
+
if (!orgId) {
|
|
4628
|
+
console.error(chalk.red('No active org'));
|
|
4629
|
+
process.exit(1);
|
|
4630
|
+
}
|
|
4631
|
+
const membersData = await api('GET', `/api/organizations/${orgId}/members?type=agent`);
|
|
4632
|
+
const norm = (s) => s.toLowerCase().replace(/[-_\s]+/g, ' ').trim();
|
|
4633
|
+
const agent = membersData.members.find((m) => norm(m.display_name) === norm(name));
|
|
4634
|
+
if (!agent) {
|
|
4635
|
+
console.error(chalk.red(`Agent "${name}" not found`));
|
|
4636
|
+
process.exit(1);
|
|
4637
|
+
}
|
|
4638
|
+
const prompt = agent.agent_config?.systemPrompt;
|
|
4639
|
+
if (!prompt) {
|
|
4640
|
+
console.error(chalk.red(`Agent "${agent.display_name}" has no systemPrompt`));
|
|
4641
|
+
process.exit(1);
|
|
4642
|
+
}
|
|
4643
|
+
const content = buildAgentFileContent(agent, !!opts.includeFrontmatter);
|
|
4644
|
+
writeFileSync(resolve(file), content, 'utf-8');
|
|
4645
|
+
console.log(chalk.green(`Exported ${agent.display_name} to ${resolve(file)} (${prompt.length} chars)`));
|
|
4646
|
+
}
|
|
4647
|
+
catch (err) {
|
|
4648
|
+
console.error(chalk.red(err.message));
|
|
4649
|
+
process.exit(1);
|
|
4650
|
+
}
|
|
4651
|
+
});
|
|
4431
4652
|
// ── Health ──────────────────────────────────────────────────
|
|
4432
4653
|
program
|
|
4433
4654
|
.command('health')
|