@mhd-ghaith-abtah/flow 0.7.2-beta.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/CHANGELOG.md +87 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/adapters/e2e/_interface.md +42 -0
- package/adapters/e2e/none.md +15 -0
- package/adapters/e2e/playwright-mcp.md +86 -0
- package/adapters/issue-tracker/_interface.md +46 -0
- package/adapters/issue-tracker/github-issues.md +103 -0
- package/adapters/issue-tracker/linear.md +65 -0
- package/adapters/issue-tracker/none.md +26 -0
- package/adapters/pr/_interface.md +29 -0
- package/adapters/pr/github.md +61 -0
- package/adapters/pr/none.md +32 -0
- package/adapters/verify/_interface.md +26 -0
- package/adapters/verify/custom.md +27 -0
- package/adapters/verify/make.md +30 -0
- package/adapters/verify/pnpm.md +27 -0
- package/bin/flow.js +129 -0
- package/catalog.yaml +364 -0
- package/docs/adapters.md +99 -0
- package/docs/migrate-from-bmad.md +95 -0
- package/docs/profiles.md +81 -0
- package/docs/quickstart.md +82 -0
- package/lib/catalog.js +164 -0
- package/lib/commands/add.js +147 -0
- package/lib/commands/doctor.js +392 -0
- package/lib/commands/init.js +86 -0
- package/lib/commands/install.js +181 -0
- package/lib/commands/plan.js +108 -0
- package/lib/commands/remove.js +87 -0
- package/lib/commands/uninstall.js +157 -0
- package/lib/repo-root.js +53 -0
- package/package.json +62 -0
- package/schemas/catalog.schema.json +155 -0
- package/schemas/flow-config.schema.json +85 -0
- package/schemas/install-state.schema.json +79 -0
- package/skills/flow-doctor/SKILL.md +15 -0
- package/skills/flow-doctor/workflow.md +157 -0
- package/skills/flow-init/SKILL.md +10 -0
- package/skills/flow-init/workflow.md +420 -0
- package/skills/flow-sprint/SKILL.md +10 -0
- package/skills/flow-sprint/workflow.md +394 -0
- package/skills/flow-story/SKILL.md +12 -0
- package/skills/flow-story/workflow.md +531 -0
- package/templates/claude-md-section.md.tmpl +55 -0
- package/templates/flow-readme.md.tmpl +34 -0
- package/templates/flow.config.yaml.tmpl +94 -0
- package/templates/pr.md.tmpl +40 -0
- package/templates/retro.md.tmpl +58 -0
- package/templates/sprint.yaml.tmpl +18 -0
- package/templates/story.md.tmpl +35 -0
- package/tools/fix-caveman-shrink.sh +68 -0
- package/tools/lint-changelog.js +98 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Quickstart
|
|
2
|
+
|
|
3
|
+
A 10-minute path from zero to first shipped story.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- [Claude Code](https://claude.com/claude-code) installed (`claude` on PATH)
|
|
8
|
+
- Node 20+ (for the `flow` CLI; not strictly required if you only use slash commands)
|
|
9
|
+
- Git repo, with `origin` set if you want PRs
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Inside Claude Code, in your project root:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
/flow-init
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This launches an interactive installer. It will:
|
|
20
|
+
|
|
21
|
+
1. **Detect project shape** — package manager, framework, existing BMad/ECC install, CLAUDE.md presence
|
|
22
|
+
2. **Ask ~8 questions** — profile (mini/standard/team), adapters (issue tracker, PR platform, E2E, verify), upstream subsets (BMad, ECC, Caveman)
|
|
23
|
+
3. **Delegate to upstream installers** — `npx bmad-method install --modules <curated>`, `ECC ./install.sh --profile <curated>`, plus Caveman
|
|
24
|
+
4. **Set up MCP servers** — context7, playwright, linear (if selected)
|
|
25
|
+
5. **Scaffold docs/flow/ + flow.config.yaml** — sprint.yaml, stories/, journeys/, retros/, deferred.md
|
|
26
|
+
6. **Optionally migrate** an existing BMad `sprint-status.yaml`
|
|
27
|
+
|
|
28
|
+
Re-running is idempotent — `/flow-init --update` will diff against the recorded state and apply only deltas.
|
|
29
|
+
|
|
30
|
+
## First story
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
/flow-sprint add "Wire up Stripe webhook handler" --epic E1 --tags payments,auth
|
|
34
|
+
/flow-sprint next
|
|
35
|
+
/flow-story
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`/flow-story` auto-detects the current phase (plan / implement / review / verify / e2e / docs / commit / pr) from `sprint.yaml` + git branch + commit state, and invokes the right ECC primitive at each step. It chains phases automatically until it hits a destructive boundary (commit, PR) or a CRITICAL/HIGH review finding, then pauses for your CONFIRM.
|
|
39
|
+
|
|
40
|
+
**Useful flags:**
|
|
41
|
+
|
|
42
|
+
| Flag | Effect |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `--auto` | No CONFIRM gates. Use for low-risk stories. |
|
|
45
|
+
| `--auto-merge` | After PR opens, poll `gh pr merge --auto` until CI passes (15-min cap). |
|
|
46
|
+
| `--hard-review` | Force adversarial + edge-case reviewers regardless of tags. |
|
|
47
|
+
| `--no-review` | Skip code review. Risky. Use for trivial config tweaks. |
|
|
48
|
+
| `--no-verify` | Skip the verify command. Risky. |
|
|
49
|
+
| `--no-e2e` | Skip the E2E adapter. |
|
|
50
|
+
| `--strict-plan` | Block on plan CONFIRM even with `--auto`. |
|
|
51
|
+
| `--skip-plan` | Treat the story as pre-planned. |
|
|
52
|
+
| `--advise-only` | Print phase + suggested next action; don't execute. |
|
|
53
|
+
|
|
54
|
+
## Closing out a story
|
|
55
|
+
|
|
56
|
+
After a PR merges, `/flow-story` auto-detects the `merge-done` phase and asks for CONFIRM to flip the story to `done`. Or you can do it manually:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
/flow-sprint done E1-001
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## End-of-sprint
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
/flow-sprint retro
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Generates `docs/flow/retros/<date>.md` from your last sprint's stories: what shipped, what got deferred, review-finding rollup, verify failure rate.
|
|
69
|
+
|
|
70
|
+
## Health check
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
/flow-doctor
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Verifies catalog / state / adapter files / MCP registration / required CLIs / upstream installations. Probes for known bugs (caveman-shrink mis-registered, severity-label stripping, loose marker matches). Add `--fix` for safe auto-repairs (prints the commands; doesn't auto-run anything destructive).
|
|
77
|
+
|
|
78
|
+
## Next
|
|
79
|
+
|
|
80
|
+
- [profiles.md](profiles.md) — choosing mini vs standard vs team
|
|
81
|
+
- [adapters.md](adapters.md) — picking + swapping integrations
|
|
82
|
+
- [migrate-from-bmad.md](migrate-from-bmad.md) — porting an existing BMad project
|
package/lib/catalog.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// lib/catalog.js — load and resolve flow's catalog.yaml.
|
|
2
|
+
//
|
|
3
|
+
// Minimal v0.7 implementation: parse YAML, resolve profile inheritance, expose
|
|
4
|
+
// helpers for `flow plan` and `flow init`. JSON-schema validation lands when
|
|
5
|
+
// schemas/catalog.schema.json is finalized (tracked in task #9).
|
|
6
|
+
|
|
7
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
import { parse as parseYaml } from 'yaml';
|
|
10
|
+
import Ajv from 'ajv';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} Profile
|
|
14
|
+
* @property {string} description
|
|
15
|
+
* @property {string[]} flow_components
|
|
16
|
+
* @property {string[]} adapters
|
|
17
|
+
* @property {string[]} mcps
|
|
18
|
+
* @property {string} bmad_subset
|
|
19
|
+
* @property {string} ecc_subset
|
|
20
|
+
* @property {string} caveman_subset
|
|
21
|
+
* @property {Object} [features]
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} Catalog
|
|
26
|
+
* @property {number} version
|
|
27
|
+
* @property {string} flow_version_compat
|
|
28
|
+
* @property {Array<Object>} flow_components
|
|
29
|
+
* @property {Array<Object>} adapters
|
|
30
|
+
* @property {Object} [mcps]
|
|
31
|
+
* @property {Object} [upstreams]
|
|
32
|
+
* @property {Record<string, RawProfile>} profiles
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {Object} RawProfile
|
|
37
|
+
* @property {string} description
|
|
38
|
+
* @property {string} [extends]
|
|
39
|
+
* @property {string[]} [flow_components]
|
|
40
|
+
* @property {string[]} [adapters]
|
|
41
|
+
* @property {string[]} [mcps]
|
|
42
|
+
* @property {string} [bmad_subset]
|
|
43
|
+
* @property {string} [ecc_subset]
|
|
44
|
+
* @property {string} [caveman_subset]
|
|
45
|
+
* @property {Object} [features]
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Load and parse catalog.yaml from the given repo root. Validates against
|
|
50
|
+
* schemas/catalog.schema.json if present (issue #11).
|
|
51
|
+
* @param {string} repoRoot
|
|
52
|
+
* @param {Object} [opts]
|
|
53
|
+
* @param {boolean} [opts.validate=true] - run ajv validation if schema present
|
|
54
|
+
* @returns {Catalog}
|
|
55
|
+
*/
|
|
56
|
+
export function loadCatalog(repoRoot, opts = {}) {
|
|
57
|
+
const { validate = true } = opts;
|
|
58
|
+
const catalogPath = resolve(repoRoot, 'catalog.yaml');
|
|
59
|
+
const raw = readFileSync(catalogPath, 'utf-8');
|
|
60
|
+
const catalog = parseYaml(raw);
|
|
61
|
+
|
|
62
|
+
if (!catalog || typeof catalog !== 'object') {
|
|
63
|
+
throw new Error(`catalog.yaml at ${catalogPath} did not parse to an object`);
|
|
64
|
+
}
|
|
65
|
+
if (!catalog.profiles || typeof catalog.profiles !== 'object') {
|
|
66
|
+
throw new Error(`catalog.yaml has no 'profiles' section`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (validate) {
|
|
70
|
+
const schemaPath = resolve(repoRoot, 'schemas', 'catalog.schema.json');
|
|
71
|
+
if (existsSync(schemaPath)) {
|
|
72
|
+
const schema = JSON.parse(readFileSync(schemaPath, 'utf-8'));
|
|
73
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
74
|
+
const check = ajv.compile(schema);
|
|
75
|
+
if (!check(catalog)) {
|
|
76
|
+
const errs = (check.errors || []).map(e => ` ${e.instancePath || '/'}: ${e.message}`).join('\n');
|
|
77
|
+
throw new Error(`catalog.yaml failed schema validation:\n${errs}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return catalog;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve a profile name to its fully-merged spec (walks `extends:` chain).
|
|
87
|
+
* Adapter lists override family-by-family (later profile wins per family).
|
|
88
|
+
* @param {Catalog} catalog
|
|
89
|
+
* @param {string} profileName
|
|
90
|
+
* @returns {Profile}
|
|
91
|
+
*/
|
|
92
|
+
export function resolveProfile(catalog, profileName) {
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
const chain = [];
|
|
95
|
+
let cursor = profileName;
|
|
96
|
+
while (cursor) {
|
|
97
|
+
if (seen.has(cursor)) {
|
|
98
|
+
throw new Error(`Profile inheritance cycle detected at '${cursor}'`);
|
|
99
|
+
}
|
|
100
|
+
seen.add(cursor);
|
|
101
|
+
const profile = catalog.profiles[cursor];
|
|
102
|
+
if (!profile) {
|
|
103
|
+
throw new Error(`Profile '${cursor}' not found in catalog. Available: ${Object.keys(catalog.profiles).join(', ')}`);
|
|
104
|
+
}
|
|
105
|
+
chain.unshift(profile);
|
|
106
|
+
cursor = profile.extends;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Merge: later entries in the chain override earlier ones. For adapters,
|
|
110
|
+
// override by family (one adapter per family — later wins).
|
|
111
|
+
const merged = {
|
|
112
|
+
description: '',
|
|
113
|
+
flow_components: [],
|
|
114
|
+
adapters: [],
|
|
115
|
+
mcps: [],
|
|
116
|
+
bmad_subset: 'none',
|
|
117
|
+
ecc_subset: 'none',
|
|
118
|
+
caveman_subset: 'full',
|
|
119
|
+
features: {}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
for (const layer of chain) {
|
|
123
|
+
if (layer.description) merged.description = layer.description;
|
|
124
|
+
if (layer.flow_components) merged.flow_components = uniq([...merged.flow_components, ...layer.flow_components]);
|
|
125
|
+
if (layer.adapters) merged.adapters = mergeAdapters(merged.adapters, layer.adapters, catalog);
|
|
126
|
+
if (layer.mcps) merged.mcps = uniq([...merged.mcps, ...layer.mcps]);
|
|
127
|
+
if (layer.bmad_subset) merged.bmad_subset = layer.bmad_subset;
|
|
128
|
+
if (layer.ecc_subset) merged.ecc_subset = layer.ecc_subset;
|
|
129
|
+
if (layer.caveman_subset) merged.caveman_subset = layer.caveman_subset;
|
|
130
|
+
if (layer.features) merged.features = { ...merged.features, ...layer.features };
|
|
131
|
+
}
|
|
132
|
+
return merged;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Adapters are keyed by family — replacing one adapter for a family overrides
|
|
137
|
+
* any previously-merged adapter for the same family.
|
|
138
|
+
* @param {string[]} current
|
|
139
|
+
* @param {string[]} incoming
|
|
140
|
+
* @param {Catalog} catalog
|
|
141
|
+
* @returns {string[]}
|
|
142
|
+
*/
|
|
143
|
+
function mergeAdapters(current, incoming, catalog) {
|
|
144
|
+
const byFamily = new Map();
|
|
145
|
+
for (const id of [...current, ...incoming]) {
|
|
146
|
+
const adapter = catalog.adapters.find(a => a.id === id);
|
|
147
|
+
const family = adapter ? adapter.family : id.split(':')[1]?.split('-')[0] ?? id;
|
|
148
|
+
byFamily.set(family, id);
|
|
149
|
+
}
|
|
150
|
+
return [...byFamily.values()];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function uniq(arr) {
|
|
154
|
+
return [...new Set(arr)];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* List all available profile names.
|
|
159
|
+
* @param {Catalog} catalog
|
|
160
|
+
* @returns {string[]}
|
|
161
|
+
*/
|
|
162
|
+
export function listProfiles(catalog) {
|
|
163
|
+
return Object.keys(catalog.profiles);
|
|
164
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// lib/commands/add.js — `flow add <component-id>` adds a single component.
|
|
2
|
+
//
|
|
3
|
+
// Most useful for adapter swaps: `flow add adapter:e2e-playwright-mcp` to
|
|
4
|
+
// install the Playwright E2E adapter alongside the existing setup. Updates
|
|
5
|
+
// flow.config.yaml's adapters block too — picking the new adapter as active
|
|
6
|
+
// for its family (replacing the previous one for that family).
|
|
7
|
+
//
|
|
8
|
+
// Refuses without --yes. Refuses if catalog has no such component.
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, statSync, readdirSync } from 'node:fs';
|
|
11
|
+
import { resolve, join, dirname } from 'node:path';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
14
|
+
import { loadCatalog } from '../catalog.js';
|
|
15
|
+
import { resolveRepoRoot } from '../repo-root.js';
|
|
16
|
+
|
|
17
|
+
export default async function add(args, ctx) {
|
|
18
|
+
const repoRoot = ctx.repoRoot ?? resolveRepoRoot(import.meta.url);
|
|
19
|
+
const catalog = loadCatalog(repoRoot);
|
|
20
|
+
const yes = Boolean(args.yes);
|
|
21
|
+
const dryRun = Boolean(args['dry-run']);
|
|
22
|
+
|
|
23
|
+
// The component id is the first positional. bin/flow.js strips `add` from
|
|
24
|
+
// argv before passing to yargs-parser, so args._[0] is the component id.
|
|
25
|
+
const componentId = args._?.[0];
|
|
26
|
+
if (!componentId) {
|
|
27
|
+
console.error(chalk.red('✗ Missing component id. Example: flow add adapter:e2e-playwright-mcp'));
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Find it in the catalog. Could be in flow_components or adapters.
|
|
32
|
+
const adapter = catalog.adapters?.find((a) => a.id === componentId);
|
|
33
|
+
const component = catalog.flow_components?.find((c) => c.id === componentId);
|
|
34
|
+
const item = adapter || component;
|
|
35
|
+
if (!item) {
|
|
36
|
+
console.error(chalk.red(`✗ Unknown component: ${componentId}`));
|
|
37
|
+
console.error(` Available adapters: ${catalog.adapters?.map((a) => a.id).join(', ') || '(none)'}`);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const homeRoot = ctx.home || process.env.HOME;
|
|
42
|
+
const home = (p) => resolve(p.replace(/\$HOME/g, homeRoot));
|
|
43
|
+
|
|
44
|
+
console.log(chalk.bold(`━━━ flow add ${componentId} ━━━`));
|
|
45
|
+
console.log();
|
|
46
|
+
console.log(`Description: ${item.description}`);
|
|
47
|
+
if (adapter) {
|
|
48
|
+
console.log(`Family: ${adapter.family}`);
|
|
49
|
+
if (adapter.needs_mcp?.length) console.log(`Needs MCPs: ${adapter.needs_mcp.join(', ')}`);
|
|
50
|
+
if (adapter.needs_cli?.length) console.log(`Needs CLIs: ${adapter.needs_cli.join(', ')}`);
|
|
51
|
+
}
|
|
52
|
+
console.log();
|
|
53
|
+
|
|
54
|
+
const operations = [];
|
|
55
|
+
for (const op of item.operations || []) {
|
|
56
|
+
if (op.copy) {
|
|
57
|
+
operations.push({
|
|
58
|
+
kind: 'copy',
|
|
59
|
+
from: join(repoRoot, op.copy.from),
|
|
60
|
+
to: home(op.copy.to),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(chalk.bold('Operations:'));
|
|
66
|
+
for (const op of operations) {
|
|
67
|
+
console.log(` ${chalk.green('+')} ${op.kind} ${chalk.dim(`${op.from} → ${op.to}`)}`);
|
|
68
|
+
}
|
|
69
|
+
console.log();
|
|
70
|
+
|
|
71
|
+
if (dryRun) {
|
|
72
|
+
console.log(chalk.dim('--dry-run: stopping before execution.'));
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!yes) {
|
|
77
|
+
console.log(chalk.yellow('?'), 'Re-run with --yes to execute.');
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Execute file ops.
|
|
82
|
+
let executed = 0;
|
|
83
|
+
for (const op of operations) {
|
|
84
|
+
try {
|
|
85
|
+
if (!existsSync(op.from)) {
|
|
86
|
+
throw new Error(`source missing: ${op.from}`);
|
|
87
|
+
}
|
|
88
|
+
const isDir = statSync(op.from).isDirectory();
|
|
89
|
+
if (isDir) {
|
|
90
|
+
copyDirRecursive(op.from, op.to);
|
|
91
|
+
} else {
|
|
92
|
+
mkdirSync(dirname(op.to), { recursive: true });
|
|
93
|
+
copyFileSync(op.from, op.to);
|
|
94
|
+
}
|
|
95
|
+
console.log(` ${chalk.green('✓')} copy ${op.to}`);
|
|
96
|
+
executed++;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(` ${chalk.red('✗')} ${op.kind} failed: ${err.message}`);
|
|
99
|
+
return 2;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// If it's an adapter, update flow.config.yaml's adapters block.
|
|
104
|
+
if (adapter) {
|
|
105
|
+
const configPath = join(ctx.cwd, 'flow.config.yaml');
|
|
106
|
+
if (existsSync(configPath)) {
|
|
107
|
+
const config = parseYaml(readFileSync(configPath, 'utf-8')) || {};
|
|
108
|
+
config.adapters = config.adapters || {};
|
|
109
|
+
// The family in catalog is hyphenated (e.g. "issue-tracker"); the config
|
|
110
|
+
// key is snake_case (e.g. "issue_tracker"). Normalize.
|
|
111
|
+
const configKey = adapter.family.replace(/-/g, '_');
|
|
112
|
+
// Strip "adapter:" prefix and the family prefix to get the short id.
|
|
113
|
+
// e.g. "adapter:issue-tracker-linear" → "linear"
|
|
114
|
+
const shortId = adapter.id.replace(`adapter:${adapter.family}-`, '');
|
|
115
|
+
const previous = config.adapters[configKey];
|
|
116
|
+
config.adapters[configKey] = shortId;
|
|
117
|
+
writeFileSync(configPath, stringifyYaml(config));
|
|
118
|
+
console.log();
|
|
119
|
+
if (previous && previous !== shortId) {
|
|
120
|
+
console.log(` ${chalk.cyan('↻')} flow.config.yaml: adapters.${configKey} ${previous} → ${shortId}`);
|
|
121
|
+
} else {
|
|
122
|
+
console.log(` ${chalk.cyan('+')} flow.config.yaml: adapters.${configKey} = ${shortId}`);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
console.log();
|
|
126
|
+
console.log(chalk.yellow('⚠'), `No flow.config.yaml in ${ctx.cwd}. Add this manually:`);
|
|
127
|
+
console.log(` adapters.${adapter.family.replace(/-/g, '_')}: ${adapter.id.replace(`adapter:${adapter.family}-`, '')}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log();
|
|
132
|
+
console.log(chalk.bold(`Done: ${executed} operations executed.`));
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function copyDirRecursive(src, dest) {
|
|
137
|
+
mkdirSync(dest, { recursive: true });
|
|
138
|
+
for (const entry of readdirSync(src)) {
|
|
139
|
+
const srcPath = join(src, entry);
|
|
140
|
+
const destPath = join(dest, entry);
|
|
141
|
+
if (statSync(srcPath).isDirectory()) {
|
|
142
|
+
copyDirRecursive(srcPath, destPath);
|
|
143
|
+
} else {
|
|
144
|
+
copyFileSync(srcPath, destPath);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|