@xdelivered/emberflow 0.2.0 → 0.5.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/bin/commands.ts +7 -3
- package/bin/init.test.ts +94 -0
- package/bin/init.ts +108 -2
- package/dist/bin/commands.d.ts +1 -0
- package/dist/bin/commands.js +6 -3
- package/dist/bin/init.d.ts +4 -0
- package/dist/bin/init.js +96 -2
- package/dist/server/agents/codexParse.js +31 -0
- package/dist/server/agents/detect.d.ts +18 -8
- package/dist/server/agents/detect.js +78 -13
- package/dist/server/agents/modelRejectionHint.js +1 -1
- package/dist/server/agents/prompt.d.ts +15 -1
- package/dist/server/agents/prompt.js +59 -37
- package/dist/server/agents/runManager.d.ts +23 -2
- package/dist/server/agents/runManager.js +36 -8
- package/dist/server/client.d.ts +1 -0
- package/dist/server/client.js +7 -2
- package/dist/server/index.js +208 -16
- package/dist/server/nodesPayload.d.ts +14 -1
- package/dist/server/nodesPayload.js +15 -2
- package/dist/server/projectMode.js +5 -2
- package/dist/server/runRegistry.d.ts +52 -1
- package/dist/server/runRegistry.js +170 -1
- package/dist/server/sourceNav.d.ts +77 -0
- package/dist/server/sourceNav.js +503 -0
- package/dist/server/subflowRunner.d.ts +48 -1
- package/dist/server/subflowRunner.js +34 -7
- package/dist/server/updateCheck.d.ts +68 -0
- package/dist/server/updateCheck.js +142 -0
- package/dist/src/engine/executor.js +4 -3
- package/dist/src/engine/registry.d.ts +27 -2
- package/dist/src/engine/registry.js +84 -2
- package/dist/src/nodes/index.d.ts +8 -2
- package/dist/src/nodes/index.js +7 -3
- package/dist/src/nodes/login.d.ts +3 -1
- package/dist/src/nodes/login.js +2 -2
- package/package.json +28 -7
- package/server/agentRoute.test.ts +20 -0
- package/server/agents/codexParse.test.ts +37 -0
- package/server/agents/codexParse.ts +34 -0
- package/server/agents/detect.test.ts +26 -7
- package/server/agents/detect.ts +82 -14
- package/server/agents/modelRejectionHint.ts +2 -1
- package/server/agents/prompt.test.ts +128 -0
- package/server/agents/prompt.ts +102 -3
- package/server/agents/runManager.test.ts +9 -0
- package/server/agents/runManager.ts +37 -7
- package/server/client.ts +10 -4
- package/server/index.ts +213 -15
- package/server/nodeRun.test.ts +189 -0
- package/server/nodesPayload.test.ts +38 -0
- package/server/nodesPayload.ts +28 -3
- package/server/projectMode.ts +5 -2
- package/server/runRegistry.ts +200 -3
- package/server/setupStatus.test.ts +3 -0
- package/server/sourceNav.test.ts +382 -0
- package/server/sourceNav.ts +644 -0
- package/server/sourceNavRoute.test.ts +163 -0
- package/server/stepDrill.test.ts +380 -0
- package/server/subflowRunner.ts +92 -11
- package/server/updateCheck.test.ts +209 -0
- package/server/updateCheck.ts +175 -0
- package/server/workflowTestRoute.test.ts +1 -1
- package/src/App.tsx +82 -2
- package/src/components/AgentConsole.tsx +7 -280
- package/src/components/AgentStream.test.tsx +88 -0
- package/src/components/AgentStream.tsx +303 -0
- package/src/components/CreateModal.tsx +43 -9
- package/src/components/Dock.tsx +1 -1
- package/src/components/EmptyState.test.tsx +49 -0
- package/src/components/EmptyState.tsx +61 -0
- package/src/components/EnvironmentDialog.tsx +4 -1
- package/src/components/EnvironmentPicker.tsx +8 -3
- package/src/components/EnvironmentsDialog.tsx +4 -1
- package/src/components/InfraPanel.tsx +30 -2
- package/src/components/InfrastructureDialog.test.tsx +97 -0
- package/src/components/InfrastructureDialog.tsx +111 -0
- package/src/components/Inspector.tsx +9 -26
- package/src/components/RunbookView.tsx +70 -6
- package/src/components/SettingsDialog.tsx +4 -59
- package/src/components/Sidebar.tsx +6 -26
- package/src/components/SourceNavigator.test.tsx +226 -0
- package/src/components/SourceNavigator.tsx +520 -0
- package/src/components/StatusBar.tsx +227 -26
- package/src/components/Toolbar.tsx +28 -16
- package/src/components/UpdateChip.test.tsx +31 -0
- package/src/components/WelcomeDialog.test.tsx +384 -13
- package/src/components/WelcomeDialog.tsx +996 -177
- package/src/engine/executor.ts +4 -3
- package/src/engine/registry.sourceRef.test.ts +112 -0
- package/src/engine/registry.ts +103 -2
- package/src/lib/guidedQuestions.test.ts +224 -0
- package/src/lib/guidedQuestions.ts +181 -0
- package/src/lib/runbookModel.ts +3 -3
- package/src/lib/runbookProjection.test.ts +43 -0
- package/src/lib/runbookProjection.ts +26 -6
- package/src/nodes/index.ts +10 -3
- package/src/nodes/login.ts +5 -2
- package/src/store/agentClient.ts +27 -8
- package/src/store/apiTree.ts +12 -0
- package/src/store/builderStore.test.ts +441 -99
- package/src/store/builderStore.ts +385 -314
- package/src/store/nodeMeta.ts +10 -2
- package/src/store/serverRunner.ts +56 -5
- package/src/store/setupClient.ts +7 -2
- package/src/store/sourceNavClient.ts +48 -0
- package/src/store/updateClient.ts +50 -0
- package/studio-dist/assets/index-BbzppDpt.js +65 -0
- package/studio-dist/assets/index-CLf6blcA.css +1 -0
- package/studio-dist/index.html +2 -2
- package/templates/skills/emberflow-basics/SKILL.md +29 -1
- package/templates/skills/emberflow-model-process/SKILL.md +92 -9
- package/templates/skills/emberflow-new-workflow/SKILL.md +8 -1
- package/templates/skills/emberflow-review-workflow/SKILL.md +64 -1
- package/studio-dist/assets/index-DNJwW-hM.css +0 -1
- package/studio-dist/assets/index-O26dKRjW.js +0 -65
package/bin/commands.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface ParsedArgs {
|
|
|
14
14
|
noSkills?: boolean;
|
|
15
15
|
scope?: 'global' | 'repo';
|
|
16
16
|
noLaunch?: boolean;
|
|
17
|
+
noGit?: boolean;
|
|
17
18
|
mock?: boolean;
|
|
18
19
|
js?: boolean;
|
|
19
20
|
ts?: boolean;
|
|
@@ -42,6 +43,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
42
43
|
else if (a === '--global') out.scope = 'global';
|
|
43
44
|
else if (a === '--local') out.scope = 'repo';
|
|
44
45
|
else if (a === '--no-launch') out.noLaunch = true;
|
|
46
|
+
else if (a === '--no-git') out.noGit = true;
|
|
45
47
|
else if (a === '--mock') out.mock = true;
|
|
46
48
|
else if (a === '--js') out.js = true;
|
|
47
49
|
else if (a === '--ts') out.ts = true;
|
|
@@ -243,17 +245,19 @@ export async function runCommand(p: ParsedArgs, ctx: RuntimeContext = defaultCtx
|
|
|
243
245
|
skills?: false | { scope: 'repo' | 'global'; home: string };
|
|
244
246
|
packageRoot?: string;
|
|
245
247
|
language?: 'javascript' | 'typescript';
|
|
248
|
+
git?: boolean;
|
|
246
249
|
}
|
|
247
250
|
) => Promise<number>;
|
|
248
251
|
tsxResolvable: (cwd: string) => boolean;
|
|
249
252
|
};
|
|
250
253
|
const scope = p.scope ?? (await promptScope());
|
|
251
254
|
const language = p.js ? 'javascript' : p.ts ? 'typescript' : await promptLanguage();
|
|
255
|
+
const git = !p.noGit;
|
|
252
256
|
const code = await mod.runInit(
|
|
253
257
|
process.cwd(),
|
|
254
258
|
p.noSkills
|
|
255
|
-
? { skills: false, packageRoot: pkgRoot, language }
|
|
256
|
-
: { skills: { scope, home: homedir() }, packageRoot: pkgRoot, language }
|
|
259
|
+
? { skills: false, packageRoot: pkgRoot, language, git }
|
|
260
|
+
: { skills: { scope, home: homedir() }, packageRoot: pkgRoot, language, git }
|
|
257
261
|
);
|
|
258
262
|
if (code !== 0) return code;
|
|
259
263
|
|
|
@@ -297,7 +301,7 @@ export async function runCommand(p: ParsedArgs, ctx: RuntimeContext = defaultCtx
|
|
|
297
301
|
}
|
|
298
302
|
default:
|
|
299
303
|
console.log(
|
|
300
|
-
'Usage: emberflow <dev|serve|mcp|init|run|test|doctor|list-nodes|node-schema|list-workflows|get-workflow|list-environments|login-environment|set-environment-auth|serving|validate|publish|save|create|delete|rename|samples> [--port N] [--project DIR] [--scenario NAME] [--no-skills] [--global|--local] [--no-launch] [--mock] [--js|--ts]\n' +
|
|
304
|
+
'Usage: emberflow <dev|serve|mcp|init|run|test|doctor|list-nodes|node-schema|list-workflows|get-workflow|list-environments|login-environment|set-environment-auth|serving|validate|publish|save|create|delete|rename|samples> [--port N] [--project DIR] [--scenario NAME] [--no-skills] [--global|--local] [--no-launch] [--no-git] [--mock] [--js|--ts]\n' +
|
|
301
305
|
' test [opId] [--environment NAME] [--json] Run scenario expectations in-process (no runner) — exit 0/1/2\n' +
|
|
302
306
|
' doctor [opId] [--fix] Diagnose operation(s) in-process (no runner); --fix seeds param defaults — exit 0/1/2'
|
|
303
307
|
);
|
package/bin/init.test.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
// bin/init.test.ts
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
2
3
|
import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
5
|
import { join } from 'node:path';
|
|
5
6
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
7
|
import { runInit, tsxResolvable } from './init';
|
|
7
8
|
|
|
9
|
+
/** Run git in `dir`, returning trimmed stdout. Pins an identity so commits work
|
|
10
|
+
* on a machine/CI with no global git config. */
|
|
11
|
+
function git(dir: string, args: string[]): string {
|
|
12
|
+
return execFileSync(
|
|
13
|
+
'git',
|
|
14
|
+
['-c', 'user.name=Test', '-c', 'user.email=test@example.com', ...args],
|
|
15
|
+
{ cwd: dir, encoding: 'utf8' },
|
|
16
|
+
).trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
const dirs: string[] = [];
|
|
9
20
|
function scratch(): string { const d = mkdtempSync(join(tmpdir(), 'ef-init-')); dirs.push(d); return d; }
|
|
10
21
|
afterEach(() => { for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }); });
|
|
@@ -152,6 +163,89 @@ describe('runInit', () => {
|
|
|
152
163
|
});
|
|
153
164
|
});
|
|
154
165
|
|
|
166
|
+
describe('runInit git bootstrap', () => {
|
|
167
|
+
it('git-inits the folder and makes the load-bearing initial commit', async () => {
|
|
168
|
+
const d = scratch();
|
|
169
|
+
await runInit(d, { skills: false });
|
|
170
|
+
expect(existsSync(join(d, '.git'))).toBe(true);
|
|
171
|
+
// Exactly one commit, the scaffold commit — the snapshot/diff/revert safety
|
|
172
|
+
// net needs a real HEAD to diff against (an unborn HEAD can't be reverted).
|
|
173
|
+
expect(git(d, ['rev-list', '--count', 'HEAD'])).toBe('1');
|
|
174
|
+
expect(git(d, ['log', '--oneline'])).toContain('chore: emberflow init');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('commits ONLY init-scaffolded files, never the user’s pre-existing code', async () => {
|
|
178
|
+
const d = scratch();
|
|
179
|
+
// A fresh dir may already hold the user's own uncommitted app code.
|
|
180
|
+
writeFileSync(join(d, 'app.js'), 'console.log("mine");\n');
|
|
181
|
+
await runInit(d, { skills: false });
|
|
182
|
+
|
|
183
|
+
const tracked = git(d, ['ls-tree', '-r', 'HEAD', '--name-only']).split('\n');
|
|
184
|
+
expect(tracked).toContain('emberflow.config.mjs');
|
|
185
|
+
expect(tracked).toContain('emberflow/apis/default/hello.json');
|
|
186
|
+
expect(tracked).toContain('.gitignore');
|
|
187
|
+
// The user's file was NOT swept into the commit (no `git add -A`).
|
|
188
|
+
expect(tracked).not.toContain('app.js');
|
|
189
|
+
// It's still there, just untracked.
|
|
190
|
+
expect(git(d, ['status', '--porcelain'])).toContain('?? app.js');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('--no-git (git: false) skips repo creation entirely', async () => {
|
|
194
|
+
const d = scratch();
|
|
195
|
+
await runInit(d, { skills: false, git: false });
|
|
196
|
+
expect(existsSync(join(d, '.git'))).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('leaves a pre-existing repo untouched — no nested repo, no extra commit', async () => {
|
|
200
|
+
const d = scratch();
|
|
201
|
+
git(d, ['init']);
|
|
202
|
+
writeFileSync(join(d, 'README.md'), '# mine\n');
|
|
203
|
+
git(d, ['add', '-A']);
|
|
204
|
+
git(d, ['commit', '-m', 'user commit']);
|
|
205
|
+
const before = git(d, ['rev-list', '--count', 'HEAD']);
|
|
206
|
+
|
|
207
|
+
await runInit(d, { skills: false });
|
|
208
|
+
|
|
209
|
+
// `d` is still the repo root — init did not nest a new repo under it
|
|
210
|
+
// (--show-cdup is empty only when cwd IS the toplevel).
|
|
211
|
+
expect(git(d, ['rev-parse', '--show-cdup'])).toBe('');
|
|
212
|
+
expect(existsSync(join(d, 'emberflow', 'apis', 'default'))).toBe(true);
|
|
213
|
+
// init added no commit of its own.
|
|
214
|
+
expect(git(d, ['rev-list', '--count', 'HEAD'])).toBe(before);
|
|
215
|
+
// The scaffold is present but uncommitted — the user commits on their terms.
|
|
216
|
+
expect(git(d, ['status', '--porcelain'])).toContain('emberflow.config.mjs');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('re-running init on an already-scaffolded, non-git dir makes an empty anchor commit (no unborn HEAD)', async () => {
|
|
220
|
+
const d = scratch();
|
|
221
|
+
// Pre-scaffold everything init would normally write, as if a prior
|
|
222
|
+
// `emberflow init --no-git` (or a non-interactive run) already wrote it
|
|
223
|
+
// and the directory is still not a git repo — exactly what the
|
|
224
|
+
// checklist's copyable skills command produces.
|
|
225
|
+
await runInit(d, { skills: false, git: false });
|
|
226
|
+
expect(existsSync(join(d, '.git'))).toBe(false);
|
|
227
|
+
|
|
228
|
+
await runInit(d, { skills: false });
|
|
229
|
+
|
|
230
|
+
expect(existsSync(join(d, '.git'))).toBe(true);
|
|
231
|
+
// HEAD must resolve — an unborn HEAD (git init with zero commits) makes
|
|
232
|
+
// snapshot.head null downstream, which the agent snapshot/diff/revert
|
|
233
|
+
// safety net treats as "unrevertable".
|
|
234
|
+
expect(() => git(d, ['rev-parse', 'HEAD'])).not.toThrow();
|
|
235
|
+
expect(git(d, ['rev-list', '--count', 'HEAD'])).toBe('1');
|
|
236
|
+
expect(git(d, ['log', '--oneline'])).toContain('chore: emberflow init (anchor)');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('folds repo-scope skill files into the same init commit as the rest of the scaffold', async () => {
|
|
240
|
+
const d = scratch();
|
|
241
|
+
await runInit(d, { skills: { scope: 'repo', home: join(d, '.home-unused') } });
|
|
242
|
+
|
|
243
|
+
const tracked = git(d, ['ls-tree', '-r', 'HEAD', '--name-only']).split('\n');
|
|
244
|
+
expect(tracked).toContain('emberflow.config.mjs');
|
|
245
|
+
expect(tracked).toContain('.claude/skills/emberflow-basics/SKILL.md');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
155
249
|
describe('tsxResolvable', () => {
|
|
156
250
|
it('returns true when tsx resolves from a project (this repo has it as a devDependency)', () => {
|
|
157
251
|
expect(tsxResolvable(process.cwd())).toBe(true);
|
package/bin/init.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
1
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, copyFileSync } from 'node:fs';
|
|
2
3
|
import { createRequire } from 'node:module';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
5
|
+
import { dirname, join, relative } from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { detectHarnesses, resolveSkillDirs } from './skillTargets';
|
|
7
8
|
|
|
@@ -148,6 +149,87 @@ function writeFileIfAbsent(path: string, content: string, log: string[]): void {
|
|
|
148
149
|
log.push(path);
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
/** Same notion as server/agents/gitScope.ts's isGitRepo: is `cwd` inside a git
|
|
153
|
+
* work tree? A plain rev-parse probe — we only need the yes/no. */
|
|
154
|
+
function isInsideGitRepo(cwd: string): boolean {
|
|
155
|
+
try {
|
|
156
|
+
return (
|
|
157
|
+
execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
158
|
+
cwd,
|
|
159
|
+
encoding: 'utf8',
|
|
160
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
161
|
+
}).trim() === 'true'
|
|
162
|
+
);
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Every agent feature is gated on git: AgentRunManager snapshots the tree with
|
|
170
|
+
* git so changes can be reviewed and reverted (server/agents/runManager.ts).
|
|
171
|
+
* Nothing else creates the repo, so a freshly-`init`ed folder that isn't already
|
|
172
|
+
* inside a repo gets one here, with an initial commit of ONLY the files init
|
|
173
|
+
* scaffolded — never `git add -A`, since a fresh dir may already hold the user's
|
|
174
|
+
* own uncommitted app code.
|
|
175
|
+
*
|
|
176
|
+
* The initial commit is load-bearing, not cosmetic: gitScope restores tracked
|
|
177
|
+
* files by checking them out of the snapshot commit, and an unborn HEAD (a repo
|
|
178
|
+
* with zero commits) leaves `snapshot.head` null — so agent edits to any
|
|
179
|
+
* pre-existing tracked file could not be reverted. The commit gives the safety
|
|
180
|
+
* net something to diff/revert against.
|
|
181
|
+
*
|
|
182
|
+
* Never nests a repo or commits when already inside one. Returns a one-line
|
|
183
|
+
* human summary of what happened, or null when nothing was done.
|
|
184
|
+
*/
|
|
185
|
+
function initGitRepo(cwd: string, scaffolded: string[]): string | null {
|
|
186
|
+
if (isInsideGitRepo(cwd)) return null; // pre-existing repo — never nest, never commit
|
|
187
|
+
|
|
188
|
+
const git = (args: string[]): void => {
|
|
189
|
+
execFileSync('git', args, { cwd, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
git(['init']);
|
|
193
|
+
|
|
194
|
+
// Commit ONLY init's own scaffold, by explicit relative path — not `-A`.
|
|
195
|
+
const paths = [
|
|
196
|
+
...new Set(
|
|
197
|
+
scaffolded
|
|
198
|
+
.map((p) => relative(cwd, p.replace(/\/+$/, '')))
|
|
199
|
+
.filter((p) => p.length > 0 && existsSync(join(cwd, p))),
|
|
200
|
+
),
|
|
201
|
+
];
|
|
202
|
+
if (paths.length === 0) {
|
|
203
|
+
// No scaffolded files to commit — most likely a re-run of init on a
|
|
204
|
+
// directory it already scaffolded (e.g. the checklist's copyable skills
|
|
205
|
+
// command, run non-interactively, against an existing project). `git init`
|
|
206
|
+
// alone leaves HEAD unborn, which breaks the agent snapshot safety net
|
|
207
|
+
// (gitScope reads snapshot.head as null and treats all later edits as
|
|
208
|
+
// unrevertable) while the git checklist row reports green. An empty anchor
|
|
209
|
+
// commit gives HEAD a real target with zero files tracked.
|
|
210
|
+
const commitEmpty = (extra: string[]): void =>
|
|
211
|
+
git([...extra, 'commit', '--allow-empty', '-m', 'chore: emberflow init (anchor)']);
|
|
212
|
+
try {
|
|
213
|
+
commitEmpty([]);
|
|
214
|
+
} catch {
|
|
215
|
+
commitEmpty(['-c', 'user.name=Emberflow', '-c', 'user.email=emberflow@localhost']);
|
|
216
|
+
}
|
|
217
|
+
return 'initialized a git repository with an empty anchor commit (no scaffolded files to commit)';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
git(['add', '--', ...paths]);
|
|
221
|
+
const commit = (extra: string[]): void => git([...extra, 'commit', '-m', 'chore: emberflow init']);
|
|
222
|
+
try {
|
|
223
|
+
commit([]);
|
|
224
|
+
} catch {
|
|
225
|
+
// No user.name/user.email configured (fresh machine, CI) — the commit is
|
|
226
|
+
// load-bearing for the agent snapshot safety net, so fall back to a one-off
|
|
227
|
+
// identity for THIS commit only rather than fail init.
|
|
228
|
+
commit(['-c', 'user.name=Emberflow', '-c', 'user.email=emberflow@localhost']);
|
|
229
|
+
}
|
|
230
|
+
return 'initialized a git repository and committed the scaffold (chore: emberflow init)';
|
|
231
|
+
}
|
|
232
|
+
|
|
151
233
|
export async function runInit(
|
|
152
234
|
cwd: string,
|
|
153
235
|
opts?: {
|
|
@@ -159,6 +241,10 @@ export async function runInit(
|
|
|
159
241
|
/** Which language to scaffold the config/nodes in. Defaults to javascript
|
|
160
242
|
* (matches the non-TTY prompt default in bin/commands.ts). */
|
|
161
243
|
language?: 'javascript' | 'typescript';
|
|
244
|
+
/** Create a git repo + initial commit when the target isn't already inside
|
|
245
|
+
* one (agent features need it — see initGitRepo). Defaults to true; the
|
|
246
|
+
* `--no-git` flag threads through as false. */
|
|
247
|
+
git?: boolean;
|
|
162
248
|
}
|
|
163
249
|
): Promise<number> {
|
|
164
250
|
const written: string[] = [];
|
|
@@ -265,6 +351,10 @@ export async function runInit(
|
|
|
265
351
|
console.log(`[emberflow] created ${path}`);
|
|
266
352
|
}
|
|
267
353
|
|
|
354
|
+
// Skills are copied BEFORE the git init/commit below so repo-scope skill
|
|
355
|
+
// files (e.g. .claude/skills/emberflow-basics/SKILL.md) land in the same
|
|
356
|
+
// scaffold commit instead of being left untracked.
|
|
357
|
+
let skillsWritten: string[] = [];
|
|
268
358
|
const skillsOpt = opts?.skills === undefined ? { scope: 'repo' as const, home: homedir() } : opts.skills;
|
|
269
359
|
if (skillsOpt !== false) {
|
|
270
360
|
const pkgRoot = opts?.packageRoot ?? join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -272,7 +362,6 @@ export async function runInit(
|
|
|
272
362
|
if (existsSync(skillsSrc)) {
|
|
273
363
|
const presence = detectHarnesses(cwd, skillsOpt.home);
|
|
274
364
|
const skillDirs = resolveSkillDirs(presence, skillsOpt.scope, cwd, skillsOpt.home);
|
|
275
|
-
const skillsWritten: string[] = [];
|
|
276
365
|
for (const dir of skillDirs) {
|
|
277
366
|
copySkillsRecursive(skillsSrc, dir, skillsWritten);
|
|
278
367
|
}
|
|
@@ -293,5 +382,22 @@ export async function runInit(
|
|
|
293
382
|
console.log(JSON.stringify(MCP_SNIPPET, null, 2));
|
|
294
383
|
}
|
|
295
384
|
|
|
385
|
+
// Agent features (snapshot/diff/revert) require git — create the repo + an
|
|
386
|
+
// initial commit of just the scaffold unless the user opted out or is already
|
|
387
|
+
// inside a repo. Best-effort: a git failure must never fail `init` itself.
|
|
388
|
+
if (opts?.git !== false) {
|
|
389
|
+
try {
|
|
390
|
+
// A `--global` skills install writes into the user's homedir, not the
|
|
391
|
+
// project — only fold in skill paths that actually live under cwd.
|
|
392
|
+
const repoScopeSkills = skillsWritten.filter((p) => !relative(cwd, p).startsWith('..'));
|
|
393
|
+
const summary = initGitRepo(cwd, [...written, ...repoScopeSkills]);
|
|
394
|
+
if (summary) console.log(`[emberflow] ${summary}`);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
console.warn(
|
|
397
|
+
`[emberflow] could not initialize git (agent features need a repo — run \`git init && git add -A && git commit -m "initial"\`): ${err instanceof Error ? err.message : String(err)}`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
296
402
|
return 0;
|
|
297
403
|
}
|
package/dist/bin/commands.d.ts
CHANGED
package/dist/bin/commands.js
CHANGED
|
@@ -24,6 +24,8 @@ export function parseArgs(argv) {
|
|
|
24
24
|
out.scope = 'repo';
|
|
25
25
|
else if (a === '--no-launch')
|
|
26
26
|
out.noLaunch = true;
|
|
27
|
+
else if (a === '--no-git')
|
|
28
|
+
out.noGit = true;
|
|
27
29
|
else if (a === '--mock')
|
|
28
30
|
out.mock = true;
|
|
29
31
|
else if (a === '--js')
|
|
@@ -203,9 +205,10 @@ export async function runCommand(p, ctx = defaultCtx) {
|
|
|
203
205
|
const mod = (await import('./init.js'));
|
|
204
206
|
const scope = p.scope ?? (await promptScope());
|
|
205
207
|
const language = p.js ? 'javascript' : p.ts ? 'typescript' : await promptLanguage();
|
|
208
|
+
const git = !p.noGit;
|
|
206
209
|
const code = await mod.runInit(process.cwd(), p.noSkills
|
|
207
|
-
? { skills: false, packageRoot: pkgRoot, language }
|
|
208
|
-
: { skills: { scope, home: homedir() }, packageRoot: pkgRoot, language });
|
|
210
|
+
? { skills: false, packageRoot: pkgRoot, language, git }
|
|
211
|
+
: { skills: { scope, home: homedir() }, packageRoot: pkgRoot, language, git });
|
|
209
212
|
if (code !== 0)
|
|
210
213
|
return code;
|
|
211
214
|
console.log('');
|
|
@@ -244,7 +247,7 @@ export async function runCommand(p, ctx = defaultCtx) {
|
|
|
244
247
|
}
|
|
245
248
|
}
|
|
246
249
|
default:
|
|
247
|
-
console.log('Usage: emberflow <dev|serve|mcp|init|run|test|doctor|list-nodes|node-schema|list-workflows|get-workflow|list-environments|login-environment|set-environment-auth|serving|validate|publish|save|create|delete|rename|samples> [--port N] [--project DIR] [--scenario NAME] [--no-skills] [--global|--local] [--no-launch] [--mock] [--js|--ts]\n' +
|
|
250
|
+
console.log('Usage: emberflow <dev|serve|mcp|init|run|test|doctor|list-nodes|node-schema|list-workflows|get-workflow|list-environments|login-environment|set-environment-auth|serving|validate|publish|save|create|delete|rename|samples> [--port N] [--project DIR] [--scenario NAME] [--no-skills] [--global|--local] [--no-launch] [--no-git] [--mock] [--js|--ts]\n' +
|
|
248
251
|
' test [opId] [--environment NAME] [--json] Run scenario expectations in-process (no runner) — exit 0/1/2\n' +
|
|
249
252
|
' doctor [opId] [--fix] Diagnose operation(s) in-process (no runner); --fix seeds param defaults — exit 0/1/2');
|
|
250
253
|
return 0;
|
package/dist/bin/init.d.ts
CHANGED
|
@@ -14,4 +14,8 @@ export declare function runInit(cwd: string, opts?: {
|
|
|
14
14
|
/** Which language to scaffold the config/nodes in. Defaults to javascript
|
|
15
15
|
* (matches the non-TTY prompt default in bin/commands.ts). */
|
|
16
16
|
language?: 'javascript' | 'typescript';
|
|
17
|
+
/** Create a git repo + initial commit when the target isn't already inside
|
|
18
|
+
* one (agent features need it — see initGitRepo). Defaults to true; the
|
|
19
|
+
* `--no-git` flag threads through as false. */
|
|
20
|
+
git?: boolean;
|
|
17
21
|
}): Promise<number>;
|
package/dist/bin/init.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
1
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, copyFileSync } from 'node:fs';
|
|
2
3
|
import { createRequire } from 'node:module';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
5
|
+
import { dirname, join, relative } from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { detectHarnesses, resolveSkillDirs } from './skillTargets.js';
|
|
7
8
|
const MCP_SNIPPET = {
|
|
@@ -133,6 +134,80 @@ function writeFileIfAbsent(path, content, log) {
|
|
|
133
134
|
writeFileSync(path, content);
|
|
134
135
|
log.push(path);
|
|
135
136
|
}
|
|
137
|
+
/** Same notion as server/agents/gitScope.ts's isGitRepo: is `cwd` inside a git
|
|
138
|
+
* work tree? A plain rev-parse probe — we only need the yes/no. */
|
|
139
|
+
function isInsideGitRepo(cwd) {
|
|
140
|
+
try {
|
|
141
|
+
return (execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
142
|
+
cwd,
|
|
143
|
+
encoding: 'utf8',
|
|
144
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
145
|
+
}).trim() === 'true');
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Every agent feature is gated on git: AgentRunManager snapshots the tree with
|
|
153
|
+
* git so changes can be reviewed and reverted (server/agents/runManager.ts).
|
|
154
|
+
* Nothing else creates the repo, so a freshly-`init`ed folder that isn't already
|
|
155
|
+
* inside a repo gets one here, with an initial commit of ONLY the files init
|
|
156
|
+
* scaffolded — never `git add -A`, since a fresh dir may already hold the user's
|
|
157
|
+
* own uncommitted app code.
|
|
158
|
+
*
|
|
159
|
+
* The initial commit is load-bearing, not cosmetic: gitScope restores tracked
|
|
160
|
+
* files by checking them out of the snapshot commit, and an unborn HEAD (a repo
|
|
161
|
+
* with zero commits) leaves `snapshot.head` null — so agent edits to any
|
|
162
|
+
* pre-existing tracked file could not be reverted. The commit gives the safety
|
|
163
|
+
* net something to diff/revert against.
|
|
164
|
+
*
|
|
165
|
+
* Never nests a repo or commits when already inside one. Returns a one-line
|
|
166
|
+
* human summary of what happened, or null when nothing was done.
|
|
167
|
+
*/
|
|
168
|
+
function initGitRepo(cwd, scaffolded) {
|
|
169
|
+
if (isInsideGitRepo(cwd))
|
|
170
|
+
return null; // pre-existing repo — never nest, never commit
|
|
171
|
+
const git = (args) => {
|
|
172
|
+
execFileSync('git', args, { cwd, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
173
|
+
};
|
|
174
|
+
git(['init']);
|
|
175
|
+
// Commit ONLY init's own scaffold, by explicit relative path — not `-A`.
|
|
176
|
+
const paths = [
|
|
177
|
+
...new Set(scaffolded
|
|
178
|
+
.map((p) => relative(cwd, p.replace(/\/+$/, '')))
|
|
179
|
+
.filter((p) => p.length > 0 && existsSync(join(cwd, p)))),
|
|
180
|
+
];
|
|
181
|
+
if (paths.length === 0) {
|
|
182
|
+
// No scaffolded files to commit — most likely a re-run of init on a
|
|
183
|
+
// directory it already scaffolded (e.g. the checklist's copyable skills
|
|
184
|
+
// command, run non-interactively, against an existing project). `git init`
|
|
185
|
+
// alone leaves HEAD unborn, which breaks the agent snapshot safety net
|
|
186
|
+
// (gitScope reads snapshot.head as null and treats all later edits as
|
|
187
|
+
// unrevertable) while the git checklist row reports green. An empty anchor
|
|
188
|
+
// commit gives HEAD a real target with zero files tracked.
|
|
189
|
+
const commitEmpty = (extra) => git([...extra, 'commit', '--allow-empty', '-m', 'chore: emberflow init (anchor)']);
|
|
190
|
+
try {
|
|
191
|
+
commitEmpty([]);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
commitEmpty(['-c', 'user.name=Emberflow', '-c', 'user.email=emberflow@localhost']);
|
|
195
|
+
}
|
|
196
|
+
return 'initialized a git repository with an empty anchor commit (no scaffolded files to commit)';
|
|
197
|
+
}
|
|
198
|
+
git(['add', '--', ...paths]);
|
|
199
|
+
const commit = (extra) => git([...extra, 'commit', '-m', 'chore: emberflow init']);
|
|
200
|
+
try {
|
|
201
|
+
commit([]);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// No user.name/user.email configured (fresh machine, CI) — the commit is
|
|
205
|
+
// load-bearing for the agent snapshot safety net, so fall back to a one-off
|
|
206
|
+
// identity for THIS commit only rather than fail init.
|
|
207
|
+
commit(['-c', 'user.name=Emberflow', '-c', 'user.email=emberflow@localhost']);
|
|
208
|
+
}
|
|
209
|
+
return 'initialized a git repository and committed the scaffold (chore: emberflow init)';
|
|
210
|
+
}
|
|
136
211
|
export async function runInit(cwd, opts) {
|
|
137
212
|
const written = [];
|
|
138
213
|
const language = opts?.language ?? 'javascript';
|
|
@@ -216,6 +291,10 @@ export async function runInit(cwd, opts) {
|
|
|
216
291
|
for (const path of written) {
|
|
217
292
|
console.log(`[emberflow] created ${path}`);
|
|
218
293
|
}
|
|
294
|
+
// Skills are copied BEFORE the git init/commit below so repo-scope skill
|
|
295
|
+
// files (e.g. .claude/skills/emberflow-basics/SKILL.md) land in the same
|
|
296
|
+
// scaffold commit instead of being left untracked.
|
|
297
|
+
let skillsWritten = [];
|
|
219
298
|
const skillsOpt = opts?.skills === undefined ? { scope: 'repo', home: homedir() } : opts.skills;
|
|
220
299
|
if (skillsOpt !== false) {
|
|
221
300
|
const pkgRoot = opts?.packageRoot ?? join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -223,7 +302,6 @@ export async function runInit(cwd, opts) {
|
|
|
223
302
|
if (existsSync(skillsSrc)) {
|
|
224
303
|
const presence = detectHarnesses(cwd, skillsOpt.home);
|
|
225
304
|
const skillDirs = resolveSkillDirs(presence, skillsOpt.scope, cwd, skillsOpt.home);
|
|
226
|
-
const skillsWritten = [];
|
|
227
305
|
for (const dir of skillDirs) {
|
|
228
306
|
copySkillsRecursive(skillsSrc, dir, skillsWritten);
|
|
229
307
|
}
|
|
@@ -237,5 +315,21 @@ export async function runInit(cwd, opts) {
|
|
|
237
315
|
console.log(" - Other agents: add it to your agent's MCP server config (name/location varies by tool).");
|
|
238
316
|
console.log(JSON.stringify(MCP_SNIPPET, null, 2));
|
|
239
317
|
}
|
|
318
|
+
// Agent features (snapshot/diff/revert) require git — create the repo + an
|
|
319
|
+
// initial commit of just the scaffold unless the user opted out or is already
|
|
320
|
+
// inside a repo. Best-effort: a git failure must never fail `init` itself.
|
|
321
|
+
if (opts?.git !== false) {
|
|
322
|
+
try {
|
|
323
|
+
// A `--global` skills install writes into the user's homedir, not the
|
|
324
|
+
// project — only fold in skill paths that actually live under cwd.
|
|
325
|
+
const repoScopeSkills = skillsWritten.filter((p) => !relative(cwd, p).startsWith('..'));
|
|
326
|
+
const summary = initGitRepo(cwd, [...written, ...repoScopeSkills]);
|
|
327
|
+
if (summary)
|
|
328
|
+
console.log(`[emberflow] ${summary}`);
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
console.warn(`[emberflow] could not initialize git (agent features need a repo — run \`git init && git add -A && git commit -m "initial"\`): ${err instanceof Error ? err.message : String(err)}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
240
334
|
return 0;
|
|
241
335
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { modelRejectionHint } from './modelRejectionHint.js';
|
|
1
2
|
/** Map a codex mcp_tool_call item → an 'mcp' AgentEvent, tolerating shape drift. */
|
|
2
3
|
function mcpEventFrom(item) {
|
|
3
4
|
const server = item.server ?? item.invocation?.server ?? 'mcp';
|
|
@@ -6,6 +7,23 @@ function mcpEventFrom(item) {
|
|
|
6
7
|
const output = raw === undefined ? undefined : typeof raw === 'string' ? raw : JSON.stringify(raw, null, 2);
|
|
7
8
|
return { type: 'mcp', mcpServer: server, mcpTool: tool, mcpStatus: item.status, ...(output !== undefined ? { output } : {}) };
|
|
8
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Codex wraps API failures as JSON-in-a-string: `turn.failed`'s error.message
|
|
12
|
+
* (and top-level `error`'s message) is often itself a serialized
|
|
13
|
+
* `{"type":"error","status":400,"error":{"message":"…"}}` payload. Dig out the
|
|
14
|
+
* innermost human-readable message, falling back to the raw string.
|
|
15
|
+
*/
|
|
16
|
+
function extractCodexErrorMessage(raw) {
|
|
17
|
+
if (!raw)
|
|
18
|
+
return 'codex turn failed';
|
|
19
|
+
try {
|
|
20
|
+
const inner = JSON.parse(raw);
|
|
21
|
+
return inner.error?.message ?? inner.message ?? raw;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return raw;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
9
27
|
export function parseCodexLine(line) {
|
|
10
28
|
const trimmed = line.trim();
|
|
11
29
|
if (!trimmed)
|
|
@@ -68,6 +86,19 @@ export function parseCodexLine(line) {
|
|
|
68
86
|
}
|
|
69
87
|
case 'turn.completed':
|
|
70
88
|
return { type: 'done', usage: parsed.usage };
|
|
89
|
+
// The turn itself failed (e.g. the API rejected the request) — this IS the
|
|
90
|
+
// run failing, unlike an `error` ITEM above. Codex exits 1 after emitting
|
|
91
|
+
// it; without handling it here the adapter can only synthesize a generic
|
|
92
|
+
// "codex exited with code 1" and the real cause stays buried.
|
|
93
|
+
case 'turn.failed': {
|
|
94
|
+
const text = extractCodexErrorMessage(parsed.error?.message);
|
|
95
|
+
const hint = modelRejectionHint('codex', text);
|
|
96
|
+
return { type: 'error', text: hint ? `${text} (${hint})` : text };
|
|
97
|
+
}
|
|
98
|
+
// Top-level `error` lines precede turn.failed with the same payload —
|
|
99
|
+
// surface as a visible diagnostic, let turn.failed be the terminal event.
|
|
100
|
+
case 'error':
|
|
101
|
+
return { type: 'message', text: `⚠ ${extractCodexErrorMessage(parsed.message)}` };
|
|
71
102
|
default:
|
|
72
103
|
return null;
|
|
73
104
|
}
|
|
@@ -2,22 +2,32 @@ import type { AgentKind } from './types.js';
|
|
|
2
2
|
export interface DetectedAgent {
|
|
3
3
|
kind: AgentKind;
|
|
4
4
|
version: string | null;
|
|
5
|
+
/** The concrete binary the newest version was found at — what spawns should use. */
|
|
6
|
+
bin: string;
|
|
5
7
|
}
|
|
6
8
|
/**
|
|
7
|
-
* Real
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Real probe: runs `<bin> --version` and parses the first semver-ish token out
|
|
10
|
+
* of stdout. Returns `undefined` when the binary isn't present/runnable (spawn
|
|
11
|
+
* error, timeout, or nonzero exit); `null` version when it ran but produced no
|
|
12
|
+
* parseable version string.
|
|
11
13
|
*/
|
|
12
14
|
export declare function probe(bin: string): {
|
|
13
15
|
version: string | null;
|
|
14
16
|
} | undefined;
|
|
17
|
+
/** Numeric segment compare; a parseable version always beats null.
|
|
18
|
+
* Shared: agent-CLI detection here, and the package update check
|
|
19
|
+
* (server/updateCheck.ts) both rank semver-ish strings with it. */
|
|
20
|
+
export declare function newer(a: string | null, b: string | null): boolean;
|
|
15
21
|
/**
|
|
16
|
-
* Detects which coding-agent CLIs are available
|
|
17
|
-
* each
|
|
18
|
-
*
|
|
19
|
-
*
|
|
22
|
+
* Detects which coding-agent CLIs are available — probing PATH plus the known
|
|
23
|
+
* install locations for each kind — and keeps the NEWEST version found per
|
|
24
|
+
* kind, with the binary path it came from. `probeBin` is injectable so tests
|
|
25
|
+
* can stub presence/version per location without shelling out.
|
|
20
26
|
*/
|
|
21
27
|
export declare function detectAgents(probeBin?: (bin: string) => {
|
|
22
28
|
version: string | null;
|
|
23
29
|
} | undefined): DetectedAgent[];
|
|
30
|
+
/** Cached detection — the newest binary per kind, refreshed every 30s. */
|
|
31
|
+
export declare function detectAgentsCached(): DetectedAgent[];
|
|
32
|
+
/** The concrete binary path spawns should use for `kind` — the newest found. */
|
|
33
|
+
export declare function resolveAgentBin(kind: AgentKind): string | undefined;
|