@workflow-manager/runner 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -0
- package/dist/engine.d.ts +2 -0
- package/dist/engine.js +217 -0
- package/dist/events.d.ts +7 -0
- package/dist/events.js +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +413 -0
- package/dist/mockExecutor.d.ts +2 -0
- package/dist/mockExecutor.js +110 -0
- package/dist/opencodeExecutor.d.ts +3 -0
- package/dist/opencodeExecutor.js +174 -0
- package/dist/parser.d.ts +5 -0
- package/dist/parser.js +101 -0
- package/dist/remote/api.d.ts +99 -0
- package/dist/remote/api.js +81 -0
- package/dist/remote/commands.d.ts +8 -0
- package/dist/remote/commands.js +183 -0
- package/dist/remote/config.d.ts +8 -0
- package/dist/remote/config.js +39 -0
- package/dist/remote/telemetry.d.ts +12 -0
- package/dist/remote/telemetry.js +69 -0
- package/dist/remote/types.d.ts +7 -0
- package/dist/remote/types.js +1 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.js +1 -0
- package/man/wfm.1 +96 -0
- package/package.json +65 -0
- package/skills/README.md +63 -0
- package/skills/workflow-manager-cli/README.md +11 -0
- package/skills/workflow-manager-cli/SKILL.md +111 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
function asRecord(value) {
|
|
3
|
+
return value && typeof value === "object" ? value : {};
|
|
4
|
+
}
|
|
5
|
+
function normalizeTimeout(value, fallbackMs = 15000) {
|
|
6
|
+
const timeout = Number(value ?? fallbackMs);
|
|
7
|
+
if (!Number.isFinite(timeout) || timeout < 0) {
|
|
8
|
+
return fallbackMs;
|
|
9
|
+
}
|
|
10
|
+
return Math.floor(timeout);
|
|
11
|
+
}
|
|
12
|
+
export function shouldUseRealOpencode(step) {
|
|
13
|
+
const payload = asRecord(step.taskSpec?.payload);
|
|
14
|
+
return payload.useRealAdapter === true && payload.opencodeSmokeTest === true;
|
|
15
|
+
}
|
|
16
|
+
export function executeOpencodeStep(step, input, attempt) {
|
|
17
|
+
const startedAt = Date.now();
|
|
18
|
+
const payload = asRecord(step.taskSpec?.payload);
|
|
19
|
+
const opencodeArgs = Array.isArray(payload.opencodeArgs)
|
|
20
|
+
? payload.opencodeArgs.map((arg) => String(arg))
|
|
21
|
+
: ["--version"];
|
|
22
|
+
const timeoutMs = normalizeTimeout(payload.timeoutMs, 15000);
|
|
23
|
+
let child;
|
|
24
|
+
try {
|
|
25
|
+
child = spawnSync("opencode", opencodeArgs, {
|
|
26
|
+
encoding: "utf-8",
|
|
27
|
+
timeout: timeoutMs,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
return {
|
|
32
|
+
step_id: step.key,
|
|
33
|
+
execution_status: "FAILED",
|
|
34
|
+
qa_routing: {
|
|
35
|
+
action: "PROCEED",
|
|
36
|
+
feedback_reason: err.message,
|
|
37
|
+
},
|
|
38
|
+
mutated_payload: {
|
|
39
|
+
stepKey: step.key,
|
|
40
|
+
attempt,
|
|
41
|
+
adapter: input.priming_configuration.adapter ?? "opencode",
|
|
42
|
+
realOpencode: true,
|
|
43
|
+
command: "opencode",
|
|
44
|
+
args: opencodeArgs,
|
|
45
|
+
timeoutMs,
|
|
46
|
+
},
|
|
47
|
+
metadata: {
|
|
48
|
+
execution_time_ms: Date.now() - startedAt,
|
|
49
|
+
external_intervention_required: false,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const stdout = child.stdout ?? "";
|
|
54
|
+
const stderr = child.stderr ?? "";
|
|
55
|
+
const status = typeof child.status === "number" ? child.status : 1;
|
|
56
|
+
const output = `${stdout}\n${stderr}`;
|
|
57
|
+
const expectContains = payload.expectContains ? String(payload.expectContains) : undefined;
|
|
58
|
+
const expectPattern = payload.expectPattern ? String(payload.expectPattern) : undefined;
|
|
59
|
+
const containsExpected = expectContains
|
|
60
|
+
? output.toLowerCase().includes(expectContains.toLowerCase())
|
|
61
|
+
: true;
|
|
62
|
+
let matchesPattern = true;
|
|
63
|
+
if (expectPattern) {
|
|
64
|
+
try {
|
|
65
|
+
matchesPattern = new RegExp(expectPattern).test(output);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return {
|
|
69
|
+
step_id: step.key,
|
|
70
|
+
execution_status: "FAILED",
|
|
71
|
+
qa_routing: {
|
|
72
|
+
action: "PROCEED",
|
|
73
|
+
feedback_reason: `Invalid expectPattern regex: ${err.message}`,
|
|
74
|
+
},
|
|
75
|
+
mutated_payload: {
|
|
76
|
+
stepKey: step.key,
|
|
77
|
+
attempt,
|
|
78
|
+
adapter: input.priming_configuration.adapter ?? "opencode",
|
|
79
|
+
realOpencode: true,
|
|
80
|
+
command: "opencode",
|
|
81
|
+
args: opencodeArgs,
|
|
82
|
+
exitStatus: status,
|
|
83
|
+
stdout,
|
|
84
|
+
stderr,
|
|
85
|
+
expectPattern,
|
|
86
|
+
},
|
|
87
|
+
metadata: {
|
|
88
|
+
execution_time_ms: Date.now() - startedAt,
|
|
89
|
+
external_intervention_required: false,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (child.error || status !== 0) {
|
|
95
|
+
return {
|
|
96
|
+
step_id: step.key,
|
|
97
|
+
execution_status: "FAILED",
|
|
98
|
+
qa_routing: {
|
|
99
|
+
action: "PROCEED",
|
|
100
|
+
feedback_reason: child.error?.message ?? `opencode exited with status ${status}`,
|
|
101
|
+
},
|
|
102
|
+
mutated_payload: {
|
|
103
|
+
stepKey: step.key,
|
|
104
|
+
attempt,
|
|
105
|
+
adapter: input.priming_configuration.adapter ?? "opencode",
|
|
106
|
+
realOpencode: true,
|
|
107
|
+
command: "opencode",
|
|
108
|
+
args: opencodeArgs,
|
|
109
|
+
exitStatus: status,
|
|
110
|
+
stdout,
|
|
111
|
+
stderr,
|
|
112
|
+
},
|
|
113
|
+
metadata: {
|
|
114
|
+
execution_time_ms: Date.now() - startedAt,
|
|
115
|
+
external_intervention_required: false,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (!containsExpected || !matchesPattern) {
|
|
120
|
+
const feedback = !containsExpected
|
|
121
|
+
? `Output did not contain expected token: ${expectContains}`
|
|
122
|
+
: `Output did not match expected pattern: ${expectPattern}`;
|
|
123
|
+
return {
|
|
124
|
+
step_id: step.key,
|
|
125
|
+
execution_status: "QA_REJECTED",
|
|
126
|
+
qa_routing: {
|
|
127
|
+
action: "RETRY_CURRENT",
|
|
128
|
+
feedback_reason: feedback,
|
|
129
|
+
},
|
|
130
|
+
mutated_payload: {
|
|
131
|
+
stepKey: step.key,
|
|
132
|
+
attempt,
|
|
133
|
+
adapter: input.priming_configuration.adapter ?? "opencode",
|
|
134
|
+
realOpencode: true,
|
|
135
|
+
command: "opencode",
|
|
136
|
+
args: opencodeArgs,
|
|
137
|
+
exitStatus: status,
|
|
138
|
+
stdout,
|
|
139
|
+
stderr,
|
|
140
|
+
containsExpected,
|
|
141
|
+
matchesPattern,
|
|
142
|
+
},
|
|
143
|
+
metadata: {
|
|
144
|
+
execution_time_ms: Date.now() - startedAt,
|
|
145
|
+
external_intervention_required: false,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
step_id: step.key,
|
|
151
|
+
execution_status: "SUCCESS",
|
|
152
|
+
qa_routing: {
|
|
153
|
+
action: "PROCEED",
|
|
154
|
+
feedback_reason: "",
|
|
155
|
+
},
|
|
156
|
+
mutated_payload: {
|
|
157
|
+
stepKey: step.key,
|
|
158
|
+
attempt,
|
|
159
|
+
adapter: input.priming_configuration.adapter ?? "opencode",
|
|
160
|
+
realOpencode: true,
|
|
161
|
+
command: "opencode",
|
|
162
|
+
args: opencodeArgs,
|
|
163
|
+
exitStatus: status,
|
|
164
|
+
stdout,
|
|
165
|
+
stderr,
|
|
166
|
+
containsExpected,
|
|
167
|
+
matchesPattern,
|
|
168
|
+
},
|
|
169
|
+
metadata: {
|
|
170
|
+
execution_time_ms: Date.now() - startedAt,
|
|
171
|
+
external_intervention_required: false,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { WorkflowDefinition } from "./types.js";
|
|
2
|
+
export declare function parseWorkflowMarkdown(filePath: string): WorkflowDefinition;
|
|
3
|
+
export declare function parseWorkflowJson(filePath: string): WorkflowDefinition;
|
|
4
|
+
export declare function parseWorkflowFile(filePath: string): WorkflowDefinition;
|
|
5
|
+
export declare function validateWorkflow(def: WorkflowDefinition): string[];
|
package/dist/parser.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
const SUPPORTED_ADAPTERS = ["mock", "opencode", "codex", "claude-code"];
|
|
5
|
+
function normalizeWorkflow(data, source) {
|
|
6
|
+
if (!data.key || !data.title || !Array.isArray(data.steps)) {
|
|
7
|
+
throw new Error(`Invalid workflow ${source}: key, title, and steps are required`);
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
key: data.key,
|
|
11
|
+
title: data.title,
|
|
12
|
+
description: data.description,
|
|
13
|
+
objectives: data.objectives ?? [],
|
|
14
|
+
inputSchema: data.inputSchema ?? {},
|
|
15
|
+
outputSchema: data.outputSchema ?? {},
|
|
16
|
+
defaultRetryPolicy: data.defaultRetryPolicy ?? { maxAttempts: 1 },
|
|
17
|
+
steps: data.steps.map((s) => ({
|
|
18
|
+
...s,
|
|
19
|
+
dependsOn: s.dependsOn ?? [],
|
|
20
|
+
retryPolicy: s.retryPolicy ?? data.defaultRetryPolicy ?? { maxAttempts: 1 },
|
|
21
|
+
validation: s.validation ?? { mode: "none", required: false, autoConfirm: true },
|
|
22
|
+
taskSpec: s.taskSpec
|
|
23
|
+
? {
|
|
24
|
+
...s.taskSpec,
|
|
25
|
+
adapterKey: (s.taskSpec.adapterKey ?? "mock"),
|
|
26
|
+
init: {
|
|
27
|
+
context: s.taskSpec.init?.context ?? {},
|
|
28
|
+
skills: s.taskSpec.init?.skills ?? [],
|
|
29
|
+
mcps: s.taskSpec.init?.mcps ?? [],
|
|
30
|
+
systemPrompts: s.taskSpec.init?.systemPrompts ?? [],
|
|
31
|
+
model: s.taskSpec.init?.model,
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
: undefined,
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function parseWorkflowMarkdown(filePath) {
|
|
39
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
40
|
+
const parsed = matter(raw);
|
|
41
|
+
const data = parsed.data;
|
|
42
|
+
return normalizeWorkflow(data, "markdown");
|
|
43
|
+
}
|
|
44
|
+
export function parseWorkflowJson(filePath) {
|
|
45
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
46
|
+
const data = JSON.parse(raw);
|
|
47
|
+
return normalizeWorkflow(data, "json");
|
|
48
|
+
}
|
|
49
|
+
export function parseWorkflowFile(filePath) {
|
|
50
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
51
|
+
if (ext === ".json") {
|
|
52
|
+
return parseWorkflowJson(filePath);
|
|
53
|
+
}
|
|
54
|
+
return parseWorkflowMarkdown(filePath);
|
|
55
|
+
}
|
|
56
|
+
export function validateWorkflow(def) {
|
|
57
|
+
const errors = [];
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
if (!def.key.trim()) {
|
|
60
|
+
errors.push("Workflow key is required");
|
|
61
|
+
}
|
|
62
|
+
if (!def.title.trim()) {
|
|
63
|
+
errors.push("Workflow title is required");
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(def.steps) || def.steps.length === 0) {
|
|
66
|
+
errors.push("Workflow must define at least one step");
|
|
67
|
+
return errors;
|
|
68
|
+
}
|
|
69
|
+
for (const step of def.steps) {
|
|
70
|
+
if (!step.key || !step.key.trim()) {
|
|
71
|
+
errors.push("Each step must define a non-empty key");
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (seen.has(step.key))
|
|
75
|
+
errors.push(`Duplicate step key: ${step.key}`);
|
|
76
|
+
seen.add(step.key);
|
|
77
|
+
if (!["task", "approval", "system"].includes(step.kind)) {
|
|
78
|
+
errors.push(`Invalid step kind for ${step.key}: ${step.kind}`);
|
|
79
|
+
}
|
|
80
|
+
if (step.kind === "task" && !step.taskSpec) {
|
|
81
|
+
errors.push(`Task step ${step.key} is missing taskSpec`);
|
|
82
|
+
}
|
|
83
|
+
if (step.kind === "approval" && !step.approvalSpec) {
|
|
84
|
+
errors.push(`Approval step ${step.key} is missing approvalSpec`);
|
|
85
|
+
}
|
|
86
|
+
for (const dep of step.dependsOn ?? []) {
|
|
87
|
+
if (!def.steps.some((s) => s.key === dep)) {
|
|
88
|
+
errors.push(`Step ${step.key} depends on unknown step ${dep}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const adapter = step.taskSpec?.adapterKey;
|
|
92
|
+
if (adapter && !SUPPORTED_ADAPTERS.includes(adapter)) {
|
|
93
|
+
errors.push(`Unsupported adapter for ${step.key}: ${adapter}`);
|
|
94
|
+
}
|
|
95
|
+
const mode = step.validation?.mode ?? step.approvalSpec?.validation?.mode;
|
|
96
|
+
if (mode && !["none", "human", "external"].includes(mode)) {
|
|
97
|
+
errors.push(`Invalid validation mode for ${step.key}: ${mode}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return errors;
|
|
101
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { WorkflowDefinition } from "../types.js";
|
|
2
|
+
export interface RemoteSearchResultItem {
|
|
3
|
+
owner: string;
|
|
4
|
+
ownerDisplayName: string | null;
|
|
5
|
+
slug: string;
|
|
6
|
+
title: string;
|
|
7
|
+
description: string | null;
|
|
8
|
+
visibility: string;
|
|
9
|
+
latestVersion: string | null;
|
|
10
|
+
sourceFormat: string | null;
|
|
11
|
+
publishedState: string | null;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
}
|
|
15
|
+
export interface WhoAmIResponse {
|
|
16
|
+
userId: string;
|
|
17
|
+
username: string | null;
|
|
18
|
+
displayName: string | null;
|
|
19
|
+
authMethod: string;
|
|
20
|
+
scopes: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface PublishRequest {
|
|
23
|
+
slug: string;
|
|
24
|
+
title: string;
|
|
25
|
+
description?: string | null;
|
|
26
|
+
visibility: "public" | "private";
|
|
27
|
+
versionLabel: string;
|
|
28
|
+
sourceFormat: "markdown" | "json";
|
|
29
|
+
rawSource: string;
|
|
30
|
+
definition: WorkflowDefinition;
|
|
31
|
+
tags?: string[];
|
|
32
|
+
changelog?: string | null;
|
|
33
|
+
publishedState: "draft" | "published";
|
|
34
|
+
}
|
|
35
|
+
export interface PullResponse {
|
|
36
|
+
owner: string;
|
|
37
|
+
slug: string;
|
|
38
|
+
title: string;
|
|
39
|
+
description: string | null;
|
|
40
|
+
visibility: string;
|
|
41
|
+
version: string;
|
|
42
|
+
sourceFormat: "markdown" | "json";
|
|
43
|
+
rawSource: string;
|
|
44
|
+
definition: WorkflowDefinition;
|
|
45
|
+
changelog: string | null;
|
|
46
|
+
publishedState: string;
|
|
47
|
+
createdAt: string;
|
|
48
|
+
}
|
|
49
|
+
export interface RunTelemetryPayload {
|
|
50
|
+
workflowKey: string;
|
|
51
|
+
workflowTitle?: string | null;
|
|
52
|
+
runId: string;
|
|
53
|
+
terminalState: "succeeded" | "failed" | "waiting_for_approval" | "cancelled";
|
|
54
|
+
totalSteps: number;
|
|
55
|
+
succeededSteps: number;
|
|
56
|
+
failedSteps: number;
|
|
57
|
+
waitingSteps: number;
|
|
58
|
+
cancelledSteps: number;
|
|
59
|
+
retriedSteps: number;
|
|
60
|
+
eventCount: number;
|
|
61
|
+
durationMs: number;
|
|
62
|
+
effectivenessScore: number;
|
|
63
|
+
outputKeys: string[];
|
|
64
|
+
sourceName?: string | null;
|
|
65
|
+
sourceFormat?: string | null;
|
|
66
|
+
cliVersion?: string | null;
|
|
67
|
+
failureReason?: string | null;
|
|
68
|
+
metadata?: Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
export interface WorkflowRunInsightsResponse {
|
|
71
|
+
items: Array<{
|
|
72
|
+
workflowKey: string;
|
|
73
|
+
workflowTitle: string | null;
|
|
74
|
+
totalRuns: number;
|
|
75
|
+
successfulRuns: number;
|
|
76
|
+
failedRuns: number;
|
|
77
|
+
approvalRuns: number;
|
|
78
|
+
successRate: number;
|
|
79
|
+
averageEffectiveness: number;
|
|
80
|
+
averageDurationMs: number;
|
|
81
|
+
lastRunAt: string | null;
|
|
82
|
+
latestRun: Record<string, unknown> | null;
|
|
83
|
+
recentRuns: Array<Record<string, unknown>>;
|
|
84
|
+
}>;
|
|
85
|
+
}
|
|
86
|
+
export declare function fetchWhoAmI(): Promise<WhoAmIResponse>;
|
|
87
|
+
export declare function searchRemoteWorkflows(query: string): Promise<{
|
|
88
|
+
items: RemoteSearchResultItem[];
|
|
89
|
+
count: number;
|
|
90
|
+
query: string;
|
|
91
|
+
}>;
|
|
92
|
+
export declare function publishRemoteWorkflow(request: PublishRequest): Promise<Record<string, unknown>>;
|
|
93
|
+
export declare function pullRemoteWorkflow(owner: string, slug: string, version?: string): Promise<PullResponse>;
|
|
94
|
+
export declare function trackRunTelemetry(payload: RunTelemetryPayload): Promise<{
|
|
95
|
+
id: string;
|
|
96
|
+
workflowKey: string;
|
|
97
|
+
terminalState: string;
|
|
98
|
+
}>;
|
|
99
|
+
export declare function fetchWorkflowRunInsights(workflowKey?: string): Promise<WorkflowRunInsightsResponse>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { resolveAuthToken } from "./config.js";
|
|
2
|
+
const DEFAULT_REMOTE_URL = "https://whairnylpdvxxgbygbzu.supabase.co";
|
|
3
|
+
const DEFAULT_PUBLISHABLE_KEY = "sb_publishable_t5VATQUjIOtHrtK3wFi5Cw_Q088yz0Z";
|
|
4
|
+
function remoteBaseUrl() {
|
|
5
|
+
return process.env.WORKFLOW_MANAGER_REMOTE_URL ?? DEFAULT_REMOTE_URL;
|
|
6
|
+
}
|
|
7
|
+
function publishableKey() {
|
|
8
|
+
return process.env.WORKFLOW_MANAGER_REMOTE_PUBLISHABLE_KEY ?? DEFAULT_PUBLISHABLE_KEY;
|
|
9
|
+
}
|
|
10
|
+
function buildHeaders(body, token) {
|
|
11
|
+
const headers = {
|
|
12
|
+
apikey: publishableKey(),
|
|
13
|
+
};
|
|
14
|
+
if (body !== undefined) {
|
|
15
|
+
headers["Content-Type"] = "application/json";
|
|
16
|
+
}
|
|
17
|
+
if (token) {
|
|
18
|
+
headers.Authorization = `Bearer ${token}`;
|
|
19
|
+
}
|
|
20
|
+
return headers;
|
|
21
|
+
}
|
|
22
|
+
async function remoteFetch(path, init = {}, requireAuth = false) {
|
|
23
|
+
const token = resolveAuthToken();
|
|
24
|
+
if (requireAuth && !token) {
|
|
25
|
+
throw new Error("Not authenticated. Run `wfm auth login --token <token>` first.");
|
|
26
|
+
}
|
|
27
|
+
const res = await fetch(`${remoteBaseUrl()}/functions/v1/${path}`, {
|
|
28
|
+
...init,
|
|
29
|
+
headers: {
|
|
30
|
+
...buildHeaders(init.body, token),
|
|
31
|
+
...(init.headers ?? {}),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const text = await res.text();
|
|
35
|
+
const payload = text ? JSON.parse(text) : {};
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const message = typeof payload.error === "string"
|
|
38
|
+
? payload.error
|
|
39
|
+
: typeof payload.message === "string"
|
|
40
|
+
? payload.message
|
|
41
|
+
: `Remote request failed with status ${res.status}`;
|
|
42
|
+
throw new Error(message);
|
|
43
|
+
}
|
|
44
|
+
return payload;
|
|
45
|
+
}
|
|
46
|
+
export async function fetchWhoAmI() {
|
|
47
|
+
return remoteFetch("auth-whoami", { method: "GET" }, true);
|
|
48
|
+
}
|
|
49
|
+
export async function searchRemoteWorkflows(query) {
|
|
50
|
+
const params = new URLSearchParams();
|
|
51
|
+
if (query.trim()) {
|
|
52
|
+
params.set("q", query.trim());
|
|
53
|
+
}
|
|
54
|
+
return remoteFetch(`search-workflows?${params.toString()}`, { method: "GET" });
|
|
55
|
+
}
|
|
56
|
+
export async function publishRemoteWorkflow(request) {
|
|
57
|
+
return remoteFetch("publish-workflow", {
|
|
58
|
+
method: "POST",
|
|
59
|
+
body: JSON.stringify(request),
|
|
60
|
+
}, true);
|
|
61
|
+
}
|
|
62
|
+
export async function pullRemoteWorkflow(owner, slug, version) {
|
|
63
|
+
const params = new URLSearchParams({ owner, slug });
|
|
64
|
+
if (version) {
|
|
65
|
+
params.set("version", version);
|
|
66
|
+
}
|
|
67
|
+
return remoteFetch(`pull-workflow?${params.toString()}`, { method: "GET" });
|
|
68
|
+
}
|
|
69
|
+
export async function trackRunTelemetry(payload) {
|
|
70
|
+
return remoteFetch("track-run-telemetry", {
|
|
71
|
+
method: "POST",
|
|
72
|
+
body: JSON.stringify(payload),
|
|
73
|
+
}, true);
|
|
74
|
+
}
|
|
75
|
+
export async function fetchWorkflowRunInsights(workflowKey) {
|
|
76
|
+
const params = new URLSearchParams();
|
|
77
|
+
if (workflowKey) {
|
|
78
|
+
params.set("workflowKey", workflowKey);
|
|
79
|
+
}
|
|
80
|
+
return remoteFetch(`workflow-run-insights${params.toString() ? `?${params.toString()}` : ""}`, { method: "GET" }, true);
|
|
81
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function cmdAuth(args: string[]): Promise<number>;
|
|
2
|
+
export declare function cmdSearch(args: string[]): Promise<number>;
|
|
3
|
+
export declare function cmdPublish(filePath: string, args: string[]): Promise<number>;
|
|
4
|
+
export declare function cmdPull(reference: string, args: string[]): Promise<number>;
|
|
5
|
+
export declare function cmdRemoteInfo(reference: string): Promise<number>;
|
|
6
|
+
export declare function pullOutputPathForTest(reference: string, output?: string, sourceFormat?: "markdown" | "json"): string;
|
|
7
|
+
export declare function slugifyForTest(value: string): string;
|
|
8
|
+
export declare function sourceFormatFromPathForTest(filePath: string): "markdown" | "json";
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseWorkflowFile, validateWorkflow } from "../parser.js";
|
|
4
|
+
import { publishRemoteWorkflow, pullRemoteWorkflow, searchRemoteWorkflows, fetchWhoAmI } from "./api.js";
|
|
5
|
+
import { clearRemoteConfig, saveRemoteConfig } from "./config.js";
|
|
6
|
+
function getFlag(args, name) {
|
|
7
|
+
const idx = args.indexOf(name);
|
|
8
|
+
if (idx >= 0 && idx + 1 < args.length) {
|
|
9
|
+
return args[idx + 1];
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
function hasFlag(args, name) {
|
|
14
|
+
return args.includes(name);
|
|
15
|
+
}
|
|
16
|
+
function slugify(value) {
|
|
17
|
+
return value
|
|
18
|
+
.trim()
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
21
|
+
.replace(/^-+|-+$/g, "")
|
|
22
|
+
.slice(0, 63);
|
|
23
|
+
}
|
|
24
|
+
function splitOwnerSlug(value) {
|
|
25
|
+
const [owner, slug, extra] = value.split("/");
|
|
26
|
+
if (!owner || !slug || extra) {
|
|
27
|
+
throw new Error("Expected workflow reference in the form <owner>/<slug>");
|
|
28
|
+
}
|
|
29
|
+
return { owner, slug };
|
|
30
|
+
}
|
|
31
|
+
function sourceFormatFromPath(filePath) {
|
|
32
|
+
return path.extname(filePath).toLowerCase() === ".json" ? "json" : "markdown";
|
|
33
|
+
}
|
|
34
|
+
function normalizeTags(raw) {
|
|
35
|
+
if (!raw) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return [...new Set(raw.split(",").map((tag) => tag.trim().toLowerCase()).filter(Boolean))];
|
|
39
|
+
}
|
|
40
|
+
export async function cmdAuth(args) {
|
|
41
|
+
const subcommand = args[0];
|
|
42
|
+
if (subcommand === "login") {
|
|
43
|
+
const token = getFlag(args, "--token");
|
|
44
|
+
if (!token) {
|
|
45
|
+
console.error("Missing required flag: --token");
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
saveRemoteConfig({ token });
|
|
49
|
+
try {
|
|
50
|
+
const profile = await fetchWhoAmI();
|
|
51
|
+
console.log(`Authenticated as ${profile.username ?? profile.userId}`);
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
clearRemoteConfig();
|
|
56
|
+
console.error(`Auth error: ${error.message}`);
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (subcommand === "whoami") {
|
|
61
|
+
try {
|
|
62
|
+
const profile = await fetchWhoAmI();
|
|
63
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error(`Auth error: ${error.message}`);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (subcommand === "logout") {
|
|
72
|
+
clearRemoteConfig();
|
|
73
|
+
console.log("Removed local remote authentication token");
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
console.error("Usage: wfm auth <login|whoami|logout>");
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
export async function cmdSearch(args) {
|
|
80
|
+
try {
|
|
81
|
+
const query = args.join(" ").trim();
|
|
82
|
+
const result = await searchRemoteWorkflows(query);
|
|
83
|
+
if (result.items.length === 0) {
|
|
84
|
+
console.log("No workflows found");
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
for (const item of result.items) {
|
|
88
|
+
console.log(`${item.owner}/${item.slug} - ${item.title}`);
|
|
89
|
+
if (item.description) {
|
|
90
|
+
console.log(` ${item.description}`);
|
|
91
|
+
}
|
|
92
|
+
console.log(` version=${item.latestVersion ?? "n/a"} visibility=${item.visibility} format=${item.sourceFormat ?? "n/a"}`);
|
|
93
|
+
}
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
console.error(`Search error: ${error.message}`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export async function cmdPublish(filePath, args) {
|
|
102
|
+
try {
|
|
103
|
+
const resolvedPath = path.resolve(filePath);
|
|
104
|
+
const rawSource = fs.readFileSync(resolvedPath, "utf-8");
|
|
105
|
+
const workflow = parseWorkflowFile(resolvedPath);
|
|
106
|
+
const errors = validateWorkflow(workflow);
|
|
107
|
+
if (errors.length > 0) {
|
|
108
|
+
console.error(`Invalid workflow: ${errors.join("; ")}`);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
const slug = slugify(getFlag(args, "--slug") ?? workflow.key);
|
|
112
|
+
const title = getFlag(args, "--title")?.trim() || workflow.title;
|
|
113
|
+
const description = getFlag(args, "--description")?.trim() || workflow.description || null;
|
|
114
|
+
const versionLabel = getFlag(args, "--version")?.trim() || `v${Date.now()}`;
|
|
115
|
+
const visibility = (getFlag(args, "--visibility")?.trim().toLowerCase() ?? "private");
|
|
116
|
+
const publishedState = hasFlag(args, "--draft") ? "draft" : "published";
|
|
117
|
+
const tags = normalizeTags(getFlag(args, "--tag"));
|
|
118
|
+
const changelog = getFlag(args, "--changelog")?.trim() || null;
|
|
119
|
+
const result = await publishRemoteWorkflow({
|
|
120
|
+
slug,
|
|
121
|
+
title,
|
|
122
|
+
description,
|
|
123
|
+
visibility,
|
|
124
|
+
versionLabel,
|
|
125
|
+
sourceFormat: sourceFormatFromPath(resolvedPath),
|
|
126
|
+
rawSource,
|
|
127
|
+
definition: workflow,
|
|
128
|
+
tags,
|
|
129
|
+
changelog,
|
|
130
|
+
publishedState,
|
|
131
|
+
});
|
|
132
|
+
console.log(JSON.stringify(result, null, 2));
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error(`Publish error: ${error.message}`);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export async function cmdPull(reference, args) {
|
|
141
|
+
try {
|
|
142
|
+
const { owner, slug } = splitOwnerSlug(reference);
|
|
143
|
+
const version = getFlag(args, "--version");
|
|
144
|
+
const pulled = await pullRemoteWorkflow(owner, slug, version);
|
|
145
|
+
const outputPath = getFlag(args, "--output") ??
|
|
146
|
+
path.resolve(`${slug}.${pulled.sourceFormat === "json" ? "json" : "md"}`);
|
|
147
|
+
fs.writeFileSync(outputPath, pulled.rawSource, "utf-8");
|
|
148
|
+
const validationErrors = validateWorkflow(parseWorkflowFile(outputPath));
|
|
149
|
+
if (validationErrors.length > 0) {
|
|
150
|
+
fs.rmSync(outputPath);
|
|
151
|
+
console.error(`Pulled workflow failed local validation: ${validationErrors.join("; ")}`);
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
console.log(`Pulled ${owner}/${slug}@${pulled.version} -> ${path.resolve(outputPath)}`);
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error(`Pull error: ${error.message}`);
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export async function cmdRemoteInfo(reference) {
|
|
163
|
+
try {
|
|
164
|
+
const { owner, slug } = splitOwnerSlug(reference);
|
|
165
|
+
const pulled = await pullRemoteWorkflow(owner, slug);
|
|
166
|
+
console.log(JSON.stringify(pulled, null, 2));
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
console.error(`Remote info error: ${error.message}`);
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export function pullOutputPathForTest(reference, output, sourceFormat = "markdown") {
|
|
175
|
+
const { slug } = splitOwnerSlug(reference);
|
|
176
|
+
return output ?? path.resolve(`${slug}.${sourceFormat === "json" ? "json" : "md"}`);
|
|
177
|
+
}
|
|
178
|
+
export function slugifyForTest(value) {
|
|
179
|
+
return slugify(value);
|
|
180
|
+
}
|
|
181
|
+
export function sourceFormatFromPathForTest(filePath) {
|
|
182
|
+
return sourceFormatFromPath(filePath);
|
|
183
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface RemoteConfig {
|
|
2
|
+
token?: string;
|
|
3
|
+
}
|
|
4
|
+
export declare function configFilePath(): string;
|
|
5
|
+
export declare function loadRemoteConfig(): RemoteConfig;
|
|
6
|
+
export declare function saveRemoteConfig(config: RemoteConfig): void;
|
|
7
|
+
export declare function clearRemoteConfig(): void;
|
|
8
|
+
export declare function resolveAuthToken(): string | undefined;
|