@fieldwangai/agentflow 0.1.25
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/LICENSE +21 -0
- package/README.md +201 -0
- package/README.zh-CN.md +201 -0
- package/agents/agentflow-node-executor-code.md +32 -0
- package/agents/agentflow-node-executor-planning.md +32 -0
- package/agents/agentflow-node-executor-requirement.md +32 -0
- package/agents/agentflow-node-executor-test.md +32 -0
- package/agents/agentflow-node-executor-ui.md +32 -0
- package/agents/agentflow-node-executor.md +32 -0
- package/agents/agents.json +8 -0
- package/agents/en/agentflow-node-executor.md +32 -0
- package/agents/zh/agentflow-node-executor.md +32 -0
- package/bin/agentflow.mjs +52 -0
- package/bin/ensure-workspace-reference.mjs +35 -0
- package/bin/lib/agent-runners.mjs +1199 -0
- package/bin/lib/agents-path.mjs +61 -0
- package/bin/lib/api-runner.mjs +361 -0
- package/bin/lib/apply.mjs +852 -0
- package/bin/lib/catalog-agents.mjs +300 -0
- package/bin/lib/catalog-flows.mjs +532 -0
- package/bin/lib/composer-agent.mjs +884 -0
- package/bin/lib/composer-flow-instances.mjs +68 -0
- package/bin/lib/composer-flow-skeleton.mjs +334 -0
- package/bin/lib/composer-flow-validate.mjs +47 -0
- package/bin/lib/composer-log.mjs +197 -0
- package/bin/lib/composer-model-router.mjs +160 -0
- package/bin/lib/composer-node-schema.mjs +299 -0
- package/bin/lib/composer-planner.mjs +749 -0
- package/bin/lib/composer-script-ops.mjs +233 -0
- package/bin/lib/composer-skill-router.mjs +384 -0
- package/bin/lib/flow-import.mjs +305 -0
- package/bin/lib/flow-normalize.mjs +71 -0
- package/bin/lib/flow-write.mjs +395 -0
- package/bin/lib/help.mjs +139 -0
- package/bin/lib/hub-login.mjs +54 -0
- package/bin/lib/hub-publish.mjs +159 -0
- package/bin/lib/hub-remote.mjs +189 -0
- package/bin/lib/hub.mjs +299 -0
- package/bin/lib/i18n.mjs +233 -0
- package/bin/lib/locales/en.json +344 -0
- package/bin/lib/locales/zh.json +344 -0
- package/bin/lib/log.mjs +37 -0
- package/bin/lib/main.mjs +611 -0
- package/bin/lib/model-config.mjs +118 -0
- package/bin/lib/model-lists.mjs +188 -0
- package/bin/lib/node-exec-context.mjs +336 -0
- package/bin/lib/node-execute.mjs +513 -0
- package/bin/lib/normalize-node-tool-command.mjs +97 -0
- package/bin/lib/paths.mjs +216 -0
- package/bin/lib/pipeline-scripts.mjs +41 -0
- package/bin/lib/recent-runs.mjs +173 -0
- package/bin/lib/run-apply-active-lock.mjs +82 -0
- package/bin/lib/run-events.mjs +85 -0
- package/bin/lib/run-node-statuses-from-disk.mjs +85 -0
- package/bin/lib/schedule-config.mjs +227 -0
- package/bin/lib/scheduler.mjs +312 -0
- package/bin/lib/table.mjs +4 -0
- package/bin/lib/terminal.mjs +42 -0
- package/bin/lib/ui-print.mjs +94 -0
- package/bin/lib/ui-server.mjs +2113 -0
- package/bin/lib/workspace-tree.mjs +266 -0
- package/bin/lib/workspace.mjs +180 -0
- package/bin/pipeline/build-node-prompt.mjs +179 -0
- package/bin/pipeline/check-cache.mjs +191 -0
- package/bin/pipeline/check-flow.mjs +543 -0
- package/bin/pipeline/collect-nodes.mjs +212 -0
- package/bin/pipeline/compute-cache-md5.mjs +177 -0
- package/bin/pipeline/ensure-run-dir.mjs +71 -0
- package/bin/pipeline/extract-thinking.mjs +308 -0
- package/bin/pipeline/gc.mjs +129 -0
- package/bin/pipeline/get-env.mjs +83 -0
- package/bin/pipeline/get-exec-id.mjs +145 -0
- package/bin/pipeline/get-ready-nodes.mjs +435 -0
- package/bin/pipeline/get-resolved-values.mjs +337 -0
- package/bin/pipeline/load-key.mjs +62 -0
- package/bin/pipeline/parse-bool.mjs +33 -0
- package/bin/pipeline/parse-flow.mjs +698 -0
- package/bin/pipeline/post-process-control-if.mjs +23 -0
- package/bin/pipeline/post-process-node.mjs +490 -0
- package/bin/pipeline/pre-process-node.mjs +449 -0
- package/bin/pipeline/resolve-inputs.mjs +201 -0
- package/bin/pipeline/run-log.mjs +34 -0
- package/bin/pipeline/run-tool-nodejs.mjs +160 -0
- package/bin/pipeline/save-key.mjs +93 -0
- package/bin/pipeline/snapshot-prior-round.mjs +70 -0
- package/bin/pipeline/validate-flow.mjs +825 -0
- package/bin/pipeline/validate-for-ui.mjs +226 -0
- package/bin/pipeline/validate-script-output.mjs +130 -0
- package/bin/pipeline/write-result.mjs +182 -0
- package/builtin/nodes/agent_subAgent.md +14 -0
- package/builtin/nodes/control_agent_toBool.md +20 -0
- package/builtin/nodes/control_anyOne.md +17 -0
- package/builtin/nodes/control_end.md +11 -0
- package/builtin/nodes/control_if.md +20 -0
- package/builtin/nodes/control_start.md +11 -0
- package/builtin/nodes/control_toBool.md +21 -0
- package/builtin/nodes/provide_file.md +11 -0
- package/builtin/nodes/provide_str.md +11 -0
- package/builtin/nodes/tool_get_env.md +14 -0
- package/builtin/nodes/tool_load_key.md +20 -0
- package/builtin/nodes/tool_nodejs.md +40 -0
- package/builtin/nodes/tool_print.md +14 -0
- package/builtin/nodes/tool_save_key.md +20 -0
- package/builtin/nodes/tool_user_ask.md +23 -0
- package/builtin/nodes/tool_user_check.md +22 -0
- package/builtin/pipelines/module-migrate/flow.yaml +819 -0
- package/builtin/pipelines/module-migrate/scripts/check_imports.mjs +700 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Makefile +362 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp.d +1 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o.d +17 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o.d +5 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o.d +8 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/tree_sitter_kotlin_binding.node.d +1 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/tree_sitter_kotlin_binding.node +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/binding.Makefile +6 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/gyp-mac-tool +768 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.Makefile +6 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.target.mk +122 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_except.target.mk +126 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_maybe.target.mk +122 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/tree_sitter_kotlin_binding.target.mk +203 -0
- package/builtin/pipelines/new/flow.yaml +545 -0
- package/builtin/pipelines/new/scripts/check-flow.mjs +9 -0
- package/builtin/pipelines/new/scripts/collect-nodes.mjs +211 -0
- package/builtin/pipelines/scripts/adjust-node-positions.mjs +113 -0
- package/builtin/web-ui/dist/agentflow-icon.svg +23 -0
- package/builtin/web-ui/dist/assets/index-CZkUPcXE.css +1 -0
- package/builtin/web-ui/dist/assets/index-DkkhNESc.js +190 -0
- package/builtin/web-ui/dist/index.html +24 -0
- package/package.json +67 -0
- package/reference/flow-control-capabilities.md +274 -0
- package/reference/flow-layout.md +84 -0
- package/reference/flow-prompt-handler-check.md +12 -0
- package/reference/flow-result-semantics.md +14 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 写入 flow.yaml(仅 user / workspace)。
|
|
3
|
+
* - user → ~/agentflow/pipelines/<flowId>/flow.yaml
|
|
4
|
+
* - workspace → <workspaceRoot>/.workspace/agentflow/pipelines/<flowId>/flow.yaml
|
|
5
|
+
* 包内 builtin/pipelines 不可直接覆盖写入。
|
|
6
|
+
*/
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import yaml from "js-yaml";
|
|
10
|
+
import {
|
|
11
|
+
ARCHIVED_PIPELINES_DIR_NAME,
|
|
12
|
+
getAgentflowDataRoot,
|
|
13
|
+
getUserPipelinesRoot,
|
|
14
|
+
LEGACY_PIPELINES_DIR,
|
|
15
|
+
PIPELINES_DIR,
|
|
16
|
+
} from "./paths.mjs";
|
|
17
|
+
import { getFlowYamlAbs } from "./catalog-flows.mjs";
|
|
18
|
+
import { normalizeFlowYamlText } from "./flow-normalize.mjs";
|
|
19
|
+
|
|
20
|
+
export const FLOW_YAML_FILENAME = "flow.yaml";
|
|
21
|
+
|
|
22
|
+
/** @typedef {"user" | "workspace"} FlowWriteSource */
|
|
23
|
+
|
|
24
|
+
/** 用户新建流水线 ID:英文字母开头,仅字母、数字、下划线、连字符 */
|
|
25
|
+
export const USER_PIPELINE_ID_RE = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|
26
|
+
|
|
27
|
+
const USER_PIPELINE_ID_MAX = 128;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} flowId
|
|
31
|
+
* @returns {{ ok: true, flowId: string } | { ok: false, error: string }}
|
|
32
|
+
*/
|
|
33
|
+
export function validateUserPipelineId(flowId) {
|
|
34
|
+
if (flowId == null || typeof flowId !== "string") {
|
|
35
|
+
return { ok: false, error: "缺少流水线名称" };
|
|
36
|
+
}
|
|
37
|
+
const t = flowId.trim();
|
|
38
|
+
if (!t) {
|
|
39
|
+
return { ok: false, error: "流水线名称不能为空" };
|
|
40
|
+
}
|
|
41
|
+
if (t.length > USER_PIPELINE_ID_MAX) {
|
|
42
|
+
return { ok: false, error: `流水线名称过长(最多 ${USER_PIPELINE_ID_MAX} 字符)` };
|
|
43
|
+
}
|
|
44
|
+
if (!USER_PIPELINE_ID_RE.test(t)) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: "名称须以英文字母开头,仅可使用字母、数字、下划线 _ 与连字符 -",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return { ok: true, flowId: t };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 空流水线 flow.yaml(instances / edges 为空;介绍写入 ui.description)
|
|
55
|
+
* @param {{ description?: string }} [options]
|
|
56
|
+
*/
|
|
57
|
+
export function buildEmptyUserFlowYaml(options = {}) {
|
|
58
|
+
/** @type {Record<string, unknown>} */
|
|
59
|
+
const ui = { nodePositions: {} };
|
|
60
|
+
const d = options.description;
|
|
61
|
+
if (d != null && String(d).trim() !== "") {
|
|
62
|
+
ui.description = String(d).trim();
|
|
63
|
+
}
|
|
64
|
+
return yaml.dump({ instances: {}, edges: [], ui }, { lineWidth: -1 });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} workspaceRoot
|
|
69
|
+
* @param {FlowWriteSource} source
|
|
70
|
+
*/
|
|
71
|
+
function getPipelinesRootByWriteSource(workspaceRoot, source) {
|
|
72
|
+
if (source === "workspace") {
|
|
73
|
+
return path.join(path.resolve(workspaceRoot), PIPELINES_DIR);
|
|
74
|
+
}
|
|
75
|
+
return getUserPipelinesRoot();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 工作区内已存在的流水线目录(新路径优先,其次旧版 .cursor 路径)。
|
|
80
|
+
* @returns {string | null}
|
|
81
|
+
*/
|
|
82
|
+
function resolveExistingWorkspaceFlowDir(workspaceRoot, flowId) {
|
|
83
|
+
const root = path.resolve(workspaceRoot);
|
|
84
|
+
for (const rel of [PIPELINES_DIR, LEGACY_PIPELINES_DIR]) {
|
|
85
|
+
const d = path.join(root, rel, flowId);
|
|
86
|
+
if (fs.existsSync(path.join(d, FLOW_YAML_FILENAME))) return d;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {string} workspaceRoot
|
|
93
|
+
* @param {string} flowId
|
|
94
|
+
* @param {FlowWriteSource} flowSource
|
|
95
|
+
* @returns {{ flowDir: string, error?: string }}
|
|
96
|
+
*/
|
|
97
|
+
export function resolveFlowDirForWrite(workspaceRoot, flowId, flowSource) {
|
|
98
|
+
if (!workspaceRoot || !flowId) {
|
|
99
|
+
return { flowDir: "", error: "workspaceRoot and flowId are required" };
|
|
100
|
+
}
|
|
101
|
+
if (/[/\\.]/.test(flowId) || flowId === "..") {
|
|
102
|
+
return { flowDir: "", error: "invalid flowId" };
|
|
103
|
+
}
|
|
104
|
+
if (flowSource === "builtin") {
|
|
105
|
+
return { flowDir: "", error: "cannot write package builtin pipelines; use workspace or user" };
|
|
106
|
+
}
|
|
107
|
+
if (flowSource !== "user" && flowSource !== "workspace") {
|
|
108
|
+
return { flowDir: "", error: "flowSource must be user or workspace" };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let boundariesBase;
|
|
112
|
+
if (flowSource === "user") {
|
|
113
|
+
boundariesBase = path.resolve(getAgentflowDataRoot());
|
|
114
|
+
try {
|
|
115
|
+
if (fs.existsSync(boundariesBase)) boundariesBase = fs.realpathSync(boundariesBase);
|
|
116
|
+
} catch (_) {
|
|
117
|
+
/* keep boundariesBase */
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
const normalized = path.normalize(workspaceRoot);
|
|
121
|
+
try {
|
|
122
|
+
boundariesBase = fs.realpathSync(normalized);
|
|
123
|
+
} catch {
|
|
124
|
+
return { flowDir: "", error: "base path does not exist or is inaccessible" };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const pipelinesRoot = getPipelinesRootByWriteSource(workspaceRoot, flowSource);
|
|
129
|
+
const flowDir = path.join(pipelinesRoot, flowId);
|
|
130
|
+
const resolvedFlowDir = path.resolve(flowDir);
|
|
131
|
+
const baseWithSep = boundariesBase.endsWith(path.sep) ? boundariesBase : boundariesBase + path.sep;
|
|
132
|
+
if (resolvedFlowDir !== boundariesBase && !resolvedFlowDir.startsWith(baseWithSep)) {
|
|
133
|
+
return { flowDir: "", error: "flow path is outside base path" };
|
|
134
|
+
}
|
|
135
|
+
return { flowDir };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {string} workspaceRoot
|
|
140
|
+
* @param {string} flowId
|
|
141
|
+
* @param {FlowWriteSource} flowSource
|
|
142
|
+
* @param {string} flowYaml
|
|
143
|
+
* @returns {{ success: true } | { success: false, error: string }}
|
|
144
|
+
*/
|
|
145
|
+
/**
|
|
146
|
+
* @param {string} workspaceRoot
|
|
147
|
+
* @param {string} flowId
|
|
148
|
+
* @param {FlowWriteSource} flowSource
|
|
149
|
+
* @returns {{ flowDir: string, error?: string }}
|
|
150
|
+
*/
|
|
151
|
+
export function resolveArchivedFlowDirForWrite(workspaceRoot, flowId, flowSource) {
|
|
152
|
+
if (!workspaceRoot || !flowId) {
|
|
153
|
+
return { flowDir: "", error: "workspaceRoot and flowId are required" };
|
|
154
|
+
}
|
|
155
|
+
if (/[/\\.]/.test(flowId) || flowId === "..") {
|
|
156
|
+
return { flowDir: "", error: "invalid flowId" };
|
|
157
|
+
}
|
|
158
|
+
if (flowSource !== "user" && flowSource !== "workspace") {
|
|
159
|
+
return { flowDir: "", error: "flowSource must be user or workspace" };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let boundariesBase;
|
|
163
|
+
if (flowSource === "user") {
|
|
164
|
+
boundariesBase = path.resolve(getAgentflowDataRoot());
|
|
165
|
+
try {
|
|
166
|
+
if (fs.existsSync(boundariesBase)) boundariesBase = fs.realpathSync(boundariesBase);
|
|
167
|
+
} catch (_) {
|
|
168
|
+
/* keep */
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
const normalized = path.normalize(workspaceRoot);
|
|
172
|
+
try {
|
|
173
|
+
boundariesBase = fs.realpathSync(normalized);
|
|
174
|
+
} catch {
|
|
175
|
+
return { flowDir: "", error: "base path does not exist or is inaccessible" };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const pipelinesRoot = getPipelinesRootByWriteSource(workspaceRoot, flowSource);
|
|
180
|
+
const flowDir = path.join(pipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId);
|
|
181
|
+
const resolvedFlowDir = path.resolve(flowDir);
|
|
182
|
+
const baseWithSep = boundariesBase.endsWith(path.sep) ? boundariesBase : boundariesBase + path.sep;
|
|
183
|
+
if (resolvedFlowDir !== boundariesBase && !resolvedFlowDir.startsWith(baseWithSep)) {
|
|
184
|
+
return { flowDir: "", error: "flow path is outside base path" };
|
|
185
|
+
}
|
|
186
|
+
return { flowDir };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {string} workspaceRoot
|
|
191
|
+
* @param {string} flowId
|
|
192
|
+
* @param {FlowWriteSource} flowSource
|
|
193
|
+
* @param {string} flowYaml
|
|
194
|
+
* @param {{ archived?: boolean }} [opts]
|
|
195
|
+
* @returns {{ success: true } | { success: false, error: string }}
|
|
196
|
+
*/
|
|
197
|
+
export function writeFlowYaml(workspaceRoot, flowId, flowSource, flowYaml, opts = {}) {
|
|
198
|
+
const archived = Boolean(opts.archived);
|
|
199
|
+
const { flowDir, error } = archived
|
|
200
|
+
? resolveArchivedFlowDirForWrite(workspaceRoot, flowId, flowSource)
|
|
201
|
+
: resolveFlowDirForWrite(workspaceRoot, flowId, flowSource);
|
|
202
|
+
if (error) return { success: false, error };
|
|
203
|
+
try {
|
|
204
|
+
fs.mkdirSync(flowDir, { recursive: true });
|
|
205
|
+
const normalized = normalizeFlowYamlText(flowYaml ?? "").text;
|
|
206
|
+
fs.writeFileSync(path.join(flowDir, FLOW_YAML_FILENAME), normalized, "utf-8");
|
|
207
|
+
return { success: true };
|
|
208
|
+
} catch (e) {
|
|
209
|
+
return { success: false, error: (e && e.message) || String(e) };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 将活跃流水线目录移入 `pipelines/_archived/<flowId>/`(仅 user / workspace)。
|
|
215
|
+
* @param {string} workspaceRoot
|
|
216
|
+
* @param {string} flowId
|
|
217
|
+
* @param {FlowWriteSource} flowSource
|
|
218
|
+
* @returns {{ success: true } | { success: false, error: string }}
|
|
219
|
+
*/
|
|
220
|
+
export function archiveFlowPipeline(workspaceRoot, flowId, flowSource) {
|
|
221
|
+
if (flowSource !== "user" && flowSource !== "workspace") {
|
|
222
|
+
return { success: false, error: "仅支持用户目录或工作区流水线归档" };
|
|
223
|
+
}
|
|
224
|
+
const yamlRes = getFlowYamlAbs(workspaceRoot, flowId, flowSource, { archived: false });
|
|
225
|
+
if (yamlRes.error || !yamlRes.path) {
|
|
226
|
+
return { success: false, error: yamlRes.error || "找不到流水线" };
|
|
227
|
+
}
|
|
228
|
+
const fromDir = path.dirname(yamlRes.path);
|
|
229
|
+
const sep = path.sep;
|
|
230
|
+
if (fromDir.split(sep).includes(ARCHIVED_PIPELINES_DIR_NAME)) {
|
|
231
|
+
return { success: false, error: "该流水线已在归档目录中" };
|
|
232
|
+
}
|
|
233
|
+
const toRes = resolveArchivedFlowDirForWrite(workspaceRoot, flowId, flowSource);
|
|
234
|
+
if (toRes.error || !toRes.flowDir) {
|
|
235
|
+
return { success: false, error: toRes.error || "无法解析归档路径" };
|
|
236
|
+
}
|
|
237
|
+
const toDir = toRes.flowDir;
|
|
238
|
+
if (fs.existsSync(toDir)) {
|
|
239
|
+
return { success: false, error: "归档位置已存在同名目录" };
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
fs.mkdirSync(path.dirname(toDir), { recursive: true });
|
|
243
|
+
fs.renameSync(fromDir, toDir);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
return { success: false, error: (e && e.message) || String(e) };
|
|
246
|
+
}
|
|
247
|
+
return { success: true };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 在用户目录与工作区之间移动整个流水线目录(含 nodes 等)。
|
|
252
|
+
* @param {string} workspaceRoot
|
|
253
|
+
* @param {string} flowId
|
|
254
|
+
* @param {"user" | "workspace"} fromSource
|
|
255
|
+
* @param {"user" | "workspace"} toSource
|
|
256
|
+
* @returns {{ success: true, flowSource: "user" | "workspace" } | { success: false, error: string }}
|
|
257
|
+
*/
|
|
258
|
+
export function moveFlowDirectory(workspaceRoot, flowId, fromSource, toSource) {
|
|
259
|
+
if (fromSource === toSource) {
|
|
260
|
+
return { success: false, error: "fromSource and toSource must differ" };
|
|
261
|
+
}
|
|
262
|
+
if (
|
|
263
|
+
(fromSource !== "user" && fromSource !== "workspace") ||
|
|
264
|
+
(toSource !== "user" && toSource !== "workspace")
|
|
265
|
+
) {
|
|
266
|
+
return { success: false, error: "only user and workspace are allowed for move" };
|
|
267
|
+
}
|
|
268
|
+
let fromDir;
|
|
269
|
+
if (fromSource === "workspace") {
|
|
270
|
+
const w = resolveFlowDirForWrite(workspaceRoot, flowId, "workspace");
|
|
271
|
+
if (w.error || !w.flowDir) return { success: false, error: w.error || "invalid source path" };
|
|
272
|
+
fromDir = resolveExistingWorkspaceFlowDir(workspaceRoot, flowId);
|
|
273
|
+
if (!fromDir) return { success: false, error: "source flow not found" };
|
|
274
|
+
} else {
|
|
275
|
+
const fromRes = resolveFlowDirForWrite(workspaceRoot, flowId, fromSource);
|
|
276
|
+
if (fromRes.error || !fromRes.flowDir) return { success: false, error: fromRes.error || "invalid source path" };
|
|
277
|
+
fromDir = fromRes.flowDir;
|
|
278
|
+
}
|
|
279
|
+
const toRes = resolveFlowDirForWrite(workspaceRoot, flowId, toSource);
|
|
280
|
+
if (toRes.error || !toRes.flowDir) return { success: false, error: toRes.error || "invalid target path" };
|
|
281
|
+
const toDir = toRes.flowDir;
|
|
282
|
+
if (!fs.existsSync(path.join(fromDir, FLOW_YAML_FILENAME))) {
|
|
283
|
+
return { success: false, error: "source flow not found" };
|
|
284
|
+
}
|
|
285
|
+
if (fs.existsSync(toDir)) {
|
|
286
|
+
return { success: false, error: "target location already exists" };
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
fs.mkdirSync(path.dirname(toDir), { recursive: true });
|
|
290
|
+
fs.renameSync(fromDir, toDir);
|
|
291
|
+
} catch (e) {
|
|
292
|
+
return { success: false, error: (e && e.message) || String(e) };
|
|
293
|
+
}
|
|
294
|
+
return { success: true, flowSource: toSource };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* @param {string} flowDir
|
|
299
|
+
* @param {string} workspaceRoot
|
|
300
|
+
* @param {FlowWriteSource} flowSource
|
|
301
|
+
* @param {string} flowId
|
|
302
|
+
* @returns {{ ok: true } | { ok: false, error: string }}
|
|
303
|
+
*/
|
|
304
|
+
function assertFlowDirIsSafeToDelete(flowDir, workspaceRoot, flowSource, flowId) {
|
|
305
|
+
let realDir;
|
|
306
|
+
try {
|
|
307
|
+
realDir = fs.realpathSync(flowDir);
|
|
308
|
+
} catch {
|
|
309
|
+
return { ok: false, error: "流水线目录不可访问" };
|
|
310
|
+
}
|
|
311
|
+
if (path.basename(realDir) !== flowId) {
|
|
312
|
+
return { ok: false, error: "目录与流水线 ID 不匹配" };
|
|
313
|
+
}
|
|
314
|
+
const root = path.resolve(workspaceRoot);
|
|
315
|
+
/** @type {string[]} */
|
|
316
|
+
const allowedRoots = [];
|
|
317
|
+
if (flowSource === "user") {
|
|
318
|
+
try {
|
|
319
|
+
allowedRoots.push(fs.realpathSync(getUserPipelinesRoot()));
|
|
320
|
+
} catch {
|
|
321
|
+
allowedRoots.push(path.resolve(getUserPipelinesRoot()));
|
|
322
|
+
}
|
|
323
|
+
for (const rel of [PIPELINES_DIR, LEGACY_PIPELINES_DIR]) {
|
|
324
|
+
const base = path.join(root, rel);
|
|
325
|
+
if (fs.existsSync(base)) {
|
|
326
|
+
try {
|
|
327
|
+
allowedRoots.push(fs.realpathSync(base));
|
|
328
|
+
} catch {
|
|
329
|
+
allowedRoots.push(path.resolve(base));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
for (const rel of [PIPELINES_DIR, LEGACY_PIPELINES_DIR]) {
|
|
335
|
+
const base = path.join(root, rel);
|
|
336
|
+
if (fs.existsSync(base)) {
|
|
337
|
+
try {
|
|
338
|
+
allowedRoots.push(fs.realpathSync(base));
|
|
339
|
+
} catch {
|
|
340
|
+
allowedRoots.push(path.resolve(base));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for (const allowed of allowedRoots) {
|
|
347
|
+
const sep = allowed.endsWith(path.sep) ? allowed : allowed + path.sep;
|
|
348
|
+
if (realDir !== allowed && !realDir.startsWith(sep)) continue;
|
|
349
|
+
const rel = path.relative(allowed, realDir);
|
|
350
|
+
const parts = rel.split(path.sep).filter(Boolean);
|
|
351
|
+
if (parts.length === 1 && parts[0] === flowId) return { ok: true };
|
|
352
|
+
if (
|
|
353
|
+
parts.length === 2 &&
|
|
354
|
+
parts[0] === ARCHIVED_PIPELINES_DIR_NAME &&
|
|
355
|
+
parts[1] === flowId
|
|
356
|
+
) {
|
|
357
|
+
return { ok: true };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return { ok: false, error: "拒绝删除:路径不在允许的 pipelines 目录内" };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* 永久删除流水线目录(含 flow.yaml、scripts 等)。仅 user / workspace;内置只读不可删。
|
|
365
|
+
* @param {string} workspaceRoot
|
|
366
|
+
* @param {string} flowId
|
|
367
|
+
* @param {FlowWriteSource} flowSource
|
|
368
|
+
* @param {{ archived?: boolean }} [opts]
|
|
369
|
+
* @returns {{ success: true } | { success: false, error: string }}
|
|
370
|
+
*/
|
|
371
|
+
export function deleteFlowPipeline(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
372
|
+
if (flowSource === "builtin") {
|
|
373
|
+
return { success: false, error: "内置流水线不可删除" };
|
|
374
|
+
}
|
|
375
|
+
if (flowSource !== "user" && flowSource !== "workspace") {
|
|
376
|
+
return { success: false, error: "仅支持删除用户目录或工作区流水线" };
|
|
377
|
+
}
|
|
378
|
+
if (!flowId || typeof flowId !== "string" || /[/\\.]/.test(flowId) || flowId === "..") {
|
|
379
|
+
return { success: false, error: "invalid flowId" };
|
|
380
|
+
}
|
|
381
|
+
const archived = Boolean(opts.archived);
|
|
382
|
+
const yamlRes = getFlowYamlAbs(workspaceRoot, flowId, flowSource, { archived });
|
|
383
|
+
if (yamlRes.error || !yamlRes.path) {
|
|
384
|
+
return { success: false, error: yamlRes.error || "找不到流水线" };
|
|
385
|
+
}
|
|
386
|
+
const flowDir = path.dirname(yamlRes.path);
|
|
387
|
+
const guard = assertFlowDirIsSafeToDelete(flowDir, workspaceRoot, flowSource, flowId);
|
|
388
|
+
if (!guard.ok) return { success: false, error: guard.error };
|
|
389
|
+
try {
|
|
390
|
+
fs.rmSync(flowDir, { recursive: true, force: true });
|
|
391
|
+
return { success: true };
|
|
392
|
+
} catch (e) {
|
|
393
|
+
return { success: false, error: (e && e.message) || String(e) };
|
|
394
|
+
}
|
|
395
|
+
}
|
package/bin/lib/help.mjs
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { log } from "./log.mjs";
|
|
2
|
+
import { t } from "./i18n.mjs";
|
|
3
|
+
|
|
4
|
+
export function printHelp() {
|
|
5
|
+
const lang = process.env.AGENTFLOW_LANG || "zh";
|
|
6
|
+
const isZh = lang === "zh";
|
|
7
|
+
|
|
8
|
+
// 根据语言输出不同的帮助文本
|
|
9
|
+
if (isZh) {
|
|
10
|
+
log.info(`
|
|
11
|
+
AgentFlow CLI — 使用 Cursor / OpenCode / Claude Code CLI 流式输出驱动 apply/replay。
|
|
12
|
+
|
|
13
|
+
用法:
|
|
14
|
+
agentflow login [--provider github|google] 登录 AgentFlow Hub(默认 GitHub)
|
|
15
|
+
agentflow logout 退出 Hub 登录
|
|
16
|
+
agentflow publish <FlowName> [--title <t>] [--description <d>] [--tags <t1,t2>] 发布流程到 Hub
|
|
17
|
+
agentflow list-remote [--search <q>] [--sort popular|trending] [--json] 浏览 Hub 上的流程
|
|
18
|
+
agentflow download <slug|title> [--user|--workspace] [--as <id>] [--raw [--output <dir>]] 从 Hub 下载流程(默认 --user 安装到 ~/agentflow/pipelines/<id>;--workspace 安装到当前工程 .workspace/agentflow/pipelines/<id>;--raw 仅保留压缩包)
|
|
19
|
+
agentflow list 列出所有流水线
|
|
20
|
+
agentflow ui [--host <addr>] [--port <n>] [--scheduler] [--no-open] 本地 HTTP:流水线列表 + React Flow 节点流程图编辑保存(默认 127.0.0.1:8765;可用 AGENTFLOW_UI_HOST)
|
|
21
|
+
agentflow scheduler start [--poll-ms <ms>] 启动定时执行调度器(读取各流水线 schedule.json)
|
|
22
|
+
agentflow scheduler status [--json] 查看定时执行配置与状态
|
|
23
|
+
agentflow apply <FlowName> [uuid] 或 agentflow apply <uuid>(由 uuid 反查 pipeline)
|
|
24
|
+
agentflow validate <FlowName> [uuid] 校验流程;终端下输出易读结果,--json 或管道时输出 JSON;传 uuid 时写入 runDir/intermediate/validation.json
|
|
25
|
+
agentflow resume <FlowName> <uuid> [instanceId] 将 pending 与 failed 节点标为已确认并继续 apply
|
|
26
|
+
agentflow replay [flowName] <uuid> <instanceId>
|
|
27
|
+
agentflow run-status <flowName> <uuid> 输出该次运行的节点状态 JSON(供 UI 展示 success/pending 等角标)
|
|
28
|
+
agentflow extract-thinking <flowName> <uuid> 从该次 run 的 logs/log.txt 提取 thinking,写入 logs/thinking_by_session_and_nodes.md
|
|
29
|
+
agentflow extract-thinking -list 列出所有存在 logs/log.txt 的 run(可接 --json)
|
|
30
|
+
agentflow update-model-lists 拉取 Cursor / OpenCode 模型列表并写入 ~/agentflow/model-lists.json;--json 时输出 { cursor, opencode }
|
|
31
|
+
agentflow write-flow <flowId> --json --flow-source <user|workspace> 从 stdin 读入 YAML 写入用户目录或工作区(builtin 已弃用,将视为 workspace)
|
|
32
|
+
agentflow --help
|
|
33
|
+
|
|
34
|
+
选项:
|
|
35
|
+
--workspace-root <path> 工作区根目录(默认:当前目录)
|
|
36
|
+
--dry-run (仅 apply)打印就绪节点后退出,不执行 Cursor agent
|
|
37
|
+
--model <name> 后端模型。默认走 Cursor;前缀 opencode:<model>、claude-code:<model>、api:<provider>/<model> 可切换后端。覆盖 CURSOR_AGENT_MODEL。
|
|
38
|
+
--input <name>=<value> (仅 apply)覆盖 flow 中 provide 节点的值。value 前缀 file: 表示文件路径。可多次使用。
|
|
39
|
+
--debug 显示调试日志(灰色,低优先级)
|
|
40
|
+
--force 传递 --force/--trust 给 Cursor;设置 OPENCODE_PERMISSION 允许 OpenCode 的 external_directory(默认开启)。使用 --no-force 禁用。
|
|
41
|
+
--parallel 并行运行同轮就绪节点(默认关闭)。多个 Cursor CLI 进程可能竞争 ~/.cursor/cli-config.json。
|
|
42
|
+
--machine-readable 向 stdout 每行输出一个 JSON 事件(apply-start/node-start/node-done/node-failed/apply-done/apply-paused)。供 UI 运行按钮使用:解析 stdout 显示当前节点;Cursor agent 输出转到 stderr。
|
|
43
|
+
--lang <code> 设置语言:en、zh(默认:zh,或从 LANG 环境变量检测)
|
|
44
|
+
|
|
45
|
+
路径说明:
|
|
46
|
+
runBuild 目录:<flowRuntimeRoot>/runBuild(每个 flow 与其 pipeline 源同 root)
|
|
47
|
+
user-scope:~/agentflow/pipelines/<flow>/runBuild
|
|
48
|
+
workspace-scope:<workspaceRoot>/.workspace/agentflow/pipelines/<flow>/runBuild
|
|
49
|
+
旧位置(仅历史兼容读取):
|
|
50
|
+
<workspaceRoot>/.workspace/agentflow/runBuild
|
|
51
|
+
~/agentflow/runBuild
|
|
52
|
+
|
|
53
|
+
Apply:构建运行目录,解析流程,循环运行就绪节点。
|
|
54
|
+
使用 -ai / --ai:运行单步供外部(AI)多轮控制:
|
|
55
|
+
agentflow apply -ai ensure-run-dir <workspaceRoot> [uuid] <flowName>
|
|
56
|
+
agentflow apply -ai parse-flow <workspaceRoot> <flowName> <uuid> [flowDir]
|
|
57
|
+
agentflow apply -ai get-ready-nodes <workspaceRoot> <flowName> <uuid>
|
|
58
|
+
agentflow apply -ai pre-process-node <workspaceRoot> <flowName> <uuid> <instanceId>
|
|
59
|
+
agentflow apply -ai post-process-node <workspaceRoot> <flowName> <uuid> <instanceId> [execId]
|
|
60
|
+
agentflow apply -ai write-result <workspaceRoot> <flowName> <uuid> <instanceId> --json '<JSON>'
|
|
61
|
+
agentflow apply -ai run-tool-nodejs <workspaceRoot> <flowName> <uuid> <instanceId> [execId] -- <scriptCmd> [args...]
|
|
62
|
+
agentflow apply -ai get-env <workspaceRoot> <flowName> <uuid> <instanceId> <execId> <key>
|
|
63
|
+
agentflow apply -ai validate-flow <workspaceRoot> <flowName> <flowDir> [uuid]
|
|
64
|
+
agentflow apply -ai collect-nodes <workspaceRoot> <flowName> [runDir]
|
|
65
|
+
agentflow apply -ai gc <workspaceRoot> [--list] [--dry-run] [--delete] [--keep N] [--older-than N]
|
|
66
|
+
agentflow apply -ai extract-thinking <workspaceRoot> <flowName> <uuid>
|
|
67
|
+
Resume:将 pending 和 failed 节点标记为成功(例如 UserCheck 确认后或重试失败后),然后继续 apply。
|
|
68
|
+
Replay:运行单个节点(pre-process → execute → post-process)。
|
|
69
|
+
|
|
70
|
+
需要:Node >=18,以下任一 CLI 在 PATH 中用于节点执行:Cursor CLI('agent',默认)、OpenCode CLI('opencode',env 覆盖 OPENCODE_CMD)、Claude Code CLI('claude',env 覆盖 CLAUDE_CODE_CMD,需先 'claude /login')。
|
|
71
|
+
Apply/replay 脚本已打包在 agentflow 包中(bin/pipeline/)。
|
|
72
|
+
`);
|
|
73
|
+
} else {
|
|
74
|
+
// 英文版本
|
|
75
|
+
log.info(`
|
|
76
|
+
AgentFlow CLI — drive apply/replay with Cursor / OpenCode / Claude Code CLI streaming.
|
|
77
|
+
|
|
78
|
+
Usage:
|
|
79
|
+
agentflow login [--provider github|google] Login to AgentFlow Hub (default: GitHub)
|
|
80
|
+
agentflow logout Sign out of Hub
|
|
81
|
+
agentflow publish <FlowName> [--title <t>] [--description <d>] [--tags <t1,t2>] Publish flow to Hub
|
|
82
|
+
agentflow list-remote [--search <q>] [--sort popular|trending] [--json] Browse flows on Hub
|
|
83
|
+
agentflow download <slug|title> [--user|--workspace] [--as <id>] [--raw [--output <dir>]] Download flow (default --user → ~/agentflow/pipelines/<id>; --workspace → current project's .workspace/agentflow/pipelines/<id>; --raw keeps the archive)
|
|
84
|
+
agentflow list List all pipelines
|
|
85
|
+
agentflow ui [--host <addr>] [--port <n>] [--scheduler] [--no-open] Local HTTP: pipeline list + React Flow node diagram editor (default 127.0.0.1:8765; AGENTFLOW_UI_HOST supported)
|
|
86
|
+
agentflow scheduler start [--poll-ms <ms>] Start the scheduled-run scheduler (reads each pipeline schedule.json)
|
|
87
|
+
agentflow scheduler status [--json] Show scheduled-run configuration and state
|
|
88
|
+
agentflow apply <FlowName> [uuid] Or agentflow apply <uuid> (resolve pipeline from uuid)
|
|
89
|
+
agentflow validate <FlowName> [uuid] Validate flow; readable output in terminal, JSON with --json or pipe; writes to runDir/intermediate/validation.json when uuid provided
|
|
90
|
+
agentflow resume <FlowName> <uuid> [instanceId] Mark pending and failed nodes as acknowledged and continue apply
|
|
91
|
+
agentflow replay [flowName] <uuid> <instanceId>
|
|
92
|
+
agentflow run-status <flowName> <uuid> Output node status JSON for this run (for UI success/pending badges)
|
|
93
|
+
agentflow extract-thinking <flowName> <uuid> Extract thinking from run logs/log.txt, write to logs/thinking_by_session_and_nodes.md
|
|
94
|
+
agentflow extract-thinking -list List all runs with logs/log.txt (use --json)
|
|
95
|
+
agentflow update-model-lists Fetch Cursor / OpenCode model lists to ~/agentflow/model-lists.json; --json outputs { cursor, opencode }
|
|
96
|
+
agentflow write-flow <flowId> --json --flow-source <user|workspace> Read YAML from stdin and write to user dir or workspace (builtin deprecated, treated as workspace)
|
|
97
|
+
agentflow --help
|
|
98
|
+
|
|
99
|
+
Options:
|
|
100
|
+
--workspace-root <path> Workspace root (default: cwd)
|
|
101
|
+
--dry-run (apply only) Print ready nodes and exit without running Cursor agent
|
|
102
|
+
--model <name> Backend model. Default routes to Cursor; prefixes opencode:<model>, claude-code:<model>, api:<provider>/<model> switch backend. Overrides CURSOR_AGENT_MODEL.
|
|
103
|
+
--input <name>=<value> (apply only) Override provide node values in flow. Prefix value with file: for file paths. Can be used multiple times.
|
|
104
|
+
--debug Show debug logs (gray, low priority)
|
|
105
|
+
--force Pass --force/--trust to Cursor; set OPENCODE_PERMISSION to allow external_directory for OpenCode (default: on). Use --no-force to disable.
|
|
106
|
+
--parallel Run same-round ready nodes in parallel (default: off). Multiple Cursor CLI processes may race on ~/.cursor/cli-config.json.
|
|
107
|
+
--machine-readable Emit one JSON event per line to stdout (apply-start/node-start/node-done/node-failed/apply-done/apply-paused). For UI run button: parse stdout to show current node; Cursor agent output goes to stderr.
|
|
108
|
+
--lang <code> Set language: en, zh (default: en, or auto-detect from LANG env)
|
|
109
|
+
|
|
110
|
+
Path notes:
|
|
111
|
+
runBuild dir: <flowRuntimeRoot>/runBuild (per-flow, co-located with pipeline source)
|
|
112
|
+
user-scope: ~/agentflow/pipelines/<flow>/runBuild
|
|
113
|
+
workspace-scope: <workspaceRoot>/.workspace/agentflow/pipelines/<flow>/runBuild
|
|
114
|
+
Legacy dirs (read-only compatibility):
|
|
115
|
+
<workspaceRoot>/.workspace/agentflow/runBuild
|
|
116
|
+
~/agentflow/runBuild
|
|
117
|
+
|
|
118
|
+
Apply: builds run dir, parses flow, runs ready nodes in a loop.
|
|
119
|
+
With -ai / --ai: run a single step for external (AI) multi-round control:
|
|
120
|
+
agentflow apply -ai ensure-run-dir <workspaceRoot> [uuid] <flowName>
|
|
121
|
+
agentflow apply -ai parse-flow <workspaceRoot> <flowName> <uuid> [flowDir]
|
|
122
|
+
agentflow apply -ai get-ready-nodes <workspaceRoot> <flowName> <uuid>
|
|
123
|
+
agentflow apply -ai pre-process-node <workspaceRoot> <flowName> <uuid> <instanceId>
|
|
124
|
+
agentflow apply -ai post-process-node <workspaceRoot> <flowName> <uuid> <instanceId> [execId]
|
|
125
|
+
agentflow apply -ai write-result <workspaceRoot> <flowName> <uuid> <instanceId> --json '<JSON>'
|
|
126
|
+
agentflow apply -ai run-tool-nodejs <workspaceRoot> <flowName> <uuid> <instanceId> [execId] -- <scriptCmd> [args...]
|
|
127
|
+
agentflow apply -ai get-env <workspaceRoot> <flowName> <uuid> <instanceId> <execId> <key>
|
|
128
|
+
agentflow apply -ai validate-flow <workspaceRoot> <flowName> <flowDir> [uuid]
|
|
129
|
+
agentflow apply -ai collect-nodes <workspaceRoot> <flowName> [runDir]
|
|
130
|
+
agentflow apply -ai gc <workspaceRoot> [--list] [--dry-run] [--delete] [--keep N] [--older-than N]
|
|
131
|
+
agentflow apply -ai extract-thinking <workspaceRoot> <flowName> <uuid>
|
|
132
|
+
Resume: marks pending and failed node(s) as success (e.g. after UserCheck confirm or retry failed), then continues apply.
|
|
133
|
+
Replay: runs a single node (pre-process → execute → post-process).
|
|
134
|
+
|
|
135
|
+
Requires: Node >=18, any of Cursor CLI ('agent', default), OpenCode CLI ('opencode', env OPENCODE_CMD), or Claude Code CLI ('claude', env CLAUDE_CODE_CMD, run 'claude /login' first) in PATH for node execution.
|
|
136
|
+
Apply/replay scripts are bundled in the agentflow package (bin/pipeline/).
|
|
137
|
+
`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentflow login [--provider github|google]
|
|
3
|
+
* agentflow logout
|
|
4
|
+
*/
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { log } from "./log.mjs";
|
|
10
|
+
import { getStoredSession, getUserProfile, loginWithBrowser } from "./hub.mjs";
|
|
11
|
+
|
|
12
|
+
export async function hubLogin(argv) {
|
|
13
|
+
// Check if already logged in
|
|
14
|
+
const existing = await getStoredSession();
|
|
15
|
+
if (existing?.access_token) {
|
|
16
|
+
const user = await getUserProfile(existing.access_token);
|
|
17
|
+
if (user?.id) {
|
|
18
|
+
log.info(chalk.green("✓") + " Already logged in as " + chalk.bold(user.email || user.id));
|
|
19
|
+
log.info(" Use " + chalk.dim("agentflow logout") + " to sign out.");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Parse --provider flag
|
|
25
|
+
let provider = "github";
|
|
26
|
+
const providerIdx = argv.indexOf("--provider");
|
|
27
|
+
if (providerIdx >= 0 && argv[providerIdx + 1]) {
|
|
28
|
+
provider = argv[providerIdx + 1];
|
|
29
|
+
}
|
|
30
|
+
if (!["github", "google"].includes(provider)) {
|
|
31
|
+
throw new Error("Invalid provider: " + provider + ". Use github or google.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
log.info("Logging in to AgentFlow Hub via " + chalk.bold(provider) + "...");
|
|
35
|
+
const session = await loginWithBrowser(provider);
|
|
36
|
+
|
|
37
|
+
// Fetch user info
|
|
38
|
+
const user = await getUserProfile(session.access_token);
|
|
39
|
+
if (user?.id) {
|
|
40
|
+
log.info(chalk.green("✓") + " Logged in as " + chalk.bold(user.email || user.id));
|
|
41
|
+
} else {
|
|
42
|
+
log.info(chalk.green("✓") + " Login successful. Token stored.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function hubLogout() {
|
|
47
|
+
const configPath = path.join(os.homedir(), ".agentflow", "hub.json");
|
|
48
|
+
try {
|
|
49
|
+
fs.unlinkSync(configPath);
|
|
50
|
+
log.info(chalk.green("✓") + " Logged out. Token removed.");
|
|
51
|
+
} catch {
|
|
52
|
+
log.info("Not logged in.");
|
|
53
|
+
}
|
|
54
|
+
}
|