@relayflows/sdk 2.0.24 → 2.0.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/dist/authored-flow-executor.d.ts +3 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +29 -1
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-loader.d.ts +15 -2
- package/dist/authored-flow-loader.d.ts.map +1 -1
- package/dist/authored-flow-loader.js +33 -4
- package/dist/authored-flow-loader.js.map +1 -1
- package/dist/authored-hooks.d.ts +29 -0
- package/dist/authored-hooks.d.ts.map +1 -0
- package/dist/authored-hooks.js +117 -0
- package/dist/authored-hooks.js.map +1 -0
- package/dist/authored-node-entry.js +1 -0
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-root.d.ts +7 -0
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +14 -1
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-source-authority.d.ts.map +1 -1
- package/dist/authored-source-authority.js +6 -0
- package/dist/authored-source-authority.js.map +1 -1
- package/dist/bundle-extensions.d.ts +19 -0
- package/dist/bundle-extensions.d.ts.map +1 -0
- package/dist/bundle-extensions.js +95 -0
- package/dist/bundle-extensions.js.map +1 -0
- package/dist/bundle-typescript.js +1 -1
- package/dist/bundle-typescript.js.map +1 -1
- package/dist/bundle.d.ts +11 -0
- package/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js +13 -4
- package/dist/bundle.js.map +1 -1
- package/dist/cli/add-extension.d.ts +28 -0
- package/dist/cli/add-extension.d.ts.map +1 -0
- package/dist/cli/add-extension.js +135 -0
- package/dist/cli/add-extension.js.map +1 -0
- package/dist/cli/add.d.ts +3 -0
- package/dist/cli/add.d.ts.map +1 -1
- package/dist/cli/add.js +8 -0
- package/dist/cli/add.js.map +1 -1
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +7 -2
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/check-helper-body.d.ts.map +1 -1
- package/dist/cli/check-helper-body.js +4 -0
- package/dist/cli/check-helper-body.js.map +1 -1
- package/dist/cli/check-triggers.d.ts.map +1 -1
- package/dist/cli/check-triggers.js +25 -2
- package/dist/cli/check-triggers.js.map +1 -1
- package/dist/cli/check-typescript.d.ts.map +1 -1
- package/dist/cli/check-typescript.js +5 -1
- package/dist/cli/check-typescript.js.map +1 -1
- package/dist/cli/check.d.ts +24 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-deploy.d.ts +1 -0
- package/dist/cli/cloud-deploy.d.ts.map +1 -1
- package/dist/cli/cloud-deploy.js +11 -1
- package/dist/cli/cloud-deploy.js.map +1 -1
- package/dist/cli/plugin.d.ts +50 -0
- package/dist/cli/plugin.d.ts.map +1 -0
- package/dist/cli/plugin.js +295 -0
- package/dist/cli/plugin.js.map +1 -0
- package/dist/cli-commands.d.ts +46 -3
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +32 -2
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -1
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +5 -0
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +7 -6
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-run.d.ts +2 -0
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +14 -4
- package/dist/cloud-run.js.map +1 -1
- package/dist/failure-kinds.d.ts +2 -2
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/flow-extension-compat.d.ts +18 -0
- package/dist/flow-extension-compat.d.ts.map +1 -0
- package/dist/flow-extension-compat.js +33 -0
- package/dist/flow-extension-compat.js.map +1 -0
- package/dist/flow-extension-loader.d.ts +61 -0
- package/dist/flow-extension-loader.d.ts.map +1 -0
- package/dist/flow-extension-loader.js +173 -0
- package/dist/flow-extension-loader.js.map +1 -0
- package/dist/flow-extension-manifest.d.ts +61 -0
- package/dist/flow-extension-manifest.d.ts.map +1 -0
- package/dist/flow-extension-manifest.js +188 -0
- package/dist/flow-extension-manifest.js.map +1 -0
- package/dist/flow-extension-submit.d.ts +38 -0
- package/dist/flow-extension-submit.d.ts.map +1 -0
- package/dist/flow-extension-submit.js +94 -0
- package/dist/flow-extension-submit.js.map +1 -0
- package/dist/flow-requirements.d.ts +13 -3
- package/dist/flow-requirements.d.ts.map +1 -1
- package/dist/flow-requirements.js +30 -0
- package/dist/flow-requirements.js.map +1 -1
- package/dist/plugin-github.d.ts +35 -0
- package/dist/plugin-github.d.ts.map +1 -0
- package/dist/plugin-github.js +92 -0
- package/dist/plugin-github.js.map +1 -0
- package/dist/plugin-loader.d.ts.map +1 -1
- package/dist/plugin-loader.js +5 -1
- package/dist/plugin-loader.js.map +1 -1
- package/dist/plugin-lock.d.ts +79 -0
- package/dist/plugin-lock.d.ts.map +1 -0
- package/dist/plugin-lock.js +203 -0
- package/dist/plugin-lock.js.map +1 -0
- package/dist/plugin-manifest.d.ts +5 -1
- package/dist/plugin-manifest.d.ts.map +1 -1
- package/dist/plugin-manifest.js +17 -0
- package/dist/plugin-manifest.js.map +1 -1
- package/dist/plugin-source.d.ts +38 -0
- package/dist/plugin-source.d.ts.map +1 -0
- package/dist/plugin-source.js +84 -0
- package/dist/plugin-source.js.map +1 -0
- package/dist/plugin-store.d.ts +30 -0
- package/dist/plugin-store.d.ts.map +1 -0
- package/dist/plugin-store.js +133 -0
- package/dist/plugin-store.js.map +1 -0
- package/dist/semver-range.d.ts +10 -0
- package/dist/semver-range.d.ts.map +1 -0
- package/dist/semver-range.js +88 -0
- package/dist/semver-range.js.map +1 -0
- package/package.json +2 -2
- package/src/authored-flow-executor.ts +36 -1
- package/src/authored-flow-loader.ts +49 -6
- package/src/authored-hooks.ts +136 -0
- package/src/authored-node-entry.ts +1 -0
- package/src/authored-root.ts +22 -1
- package/src/authored-source-authority.ts +6 -0
- package/src/bundle-extensions.ts +85 -0
- package/src/bundle-typescript.ts +1 -1
- package/src/bundle.ts +14 -4
- package/src/cli/add-extension.ts +134 -0
- package/src/cli/add.ts +9 -0
- package/src/cli/build.ts +7 -2
- package/src/cli/check-helper-body.ts +4 -0
- package/src/cli/check-triggers.ts +27 -2
- package/src/cli/check-typescript.ts +5 -1
- package/src/cli/check.ts +23 -0
- package/src/cli/cloud-deploy.ts +11 -1
- package/src/cli/plugin.ts +286 -0
- package/src/cli-commands.ts +32 -2
- package/src/cli.ts +23 -1
- package/src/cloud-deploy.ts +18 -8
- package/src/cloud-run.ts +16 -6
- package/src/flow-extension-compat.ts +35 -0
- package/src/flow-extension-loader.ts +217 -0
- package/src/flow-extension-manifest.ts +190 -0
- package/src/flow-extension-submit.ts +120 -0
- package/src/flow-requirements.ts +44 -3
- package/src/plugin-github.ts +98 -0
- package/src/plugin-loader.ts +5 -1
- package/src/plugin-lock.ts +194 -0
- package/src/plugin-manifest.ts +17 -0
- package/src/plugin-source.ts +91 -0
- package/src/plugin-store.ts +105 -0
- package/src/semver-range.ts +74 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { CliIo } from '../cli.js';
|
|
4
|
+
import { sha256 } from '../bundle.js';
|
|
5
|
+
import { assertCompatible, runtimeVersions, type RuntimeVersions } from '../flow-extension-compat.js';
|
|
6
|
+
import { validateFlowExtensionManifest, type FlowExtensionManifest } from '../flow-extension-manifest.js';
|
|
7
|
+
import { fetchGithubPlugin, resolveGithubSha, type FetchLike, type FetchedPlugin } from '../plugin-github.js';
|
|
8
|
+
import { PLUGIN_LOCK_FILE, lockWithPlugin, readPluginLock, recoverFlowsAndLock, writeFlowsAndLock } from '../plugin-lock.js';
|
|
9
|
+
import { findPluginProject } from '../plugin-loader.js';
|
|
10
|
+
import { PluginError } from '../plugin-manifest.js';
|
|
11
|
+
import { canonicalPluginRef, parsePluginSource } from '../plugin-source.js';
|
|
12
|
+
import { materializePlugin } from '../plugin-store.js';
|
|
13
|
+
|
|
14
|
+
export { assertCompatible, runtimeVersions };
|
|
15
|
+
|
|
16
|
+
/** Parse and validate the manifest inside a fetched plugin, checking that any self-declared source is the one it came from. */
|
|
17
|
+
export function extensionManifestOf(plugin: FetchedPlugin): { manifest: FlowExtensionManifest; manifestSha256: string } {
|
|
18
|
+
const file = plugin.files.find(f => f.path === 'flows-plugin.json');
|
|
19
|
+
if (file === undefined) throw new PluginError('plugin_manifest_missing', 'Plugin has no flows-plugin.json.');
|
|
20
|
+
let input: unknown;
|
|
21
|
+
try { input = JSON.parse(file.data.toString('utf8')); }
|
|
22
|
+
catch { throw new PluginError('plugin_manifest_invalid', 'flows-plugin.json is unreadable or invalid JSON.'); }
|
|
23
|
+
const manifest = validateFlowExtensionManifest(input);
|
|
24
|
+
const { source } = plugin;
|
|
25
|
+
if (manifest.source !== undefined && (manifest.source.owner !== source.owner || manifest.source.repo !== source.repo || manifest.source.path !== source.path
|
|
26
|
+
|| (manifest.source.sha !== undefined && manifest.source.sha !== source.sha))) {
|
|
27
|
+
throw new PluginError('plugin_source_drift', `flows-plugin.json declares source ${manifest.source.owner}/${manifest.source.repo}#${manifest.source.path}, but it was fetched from ${source.owner}/${source.repo}#${source.path}.`);
|
|
28
|
+
}
|
|
29
|
+
if (!plugin.files.some(f => f.path === manifest.entry)) throw new PluginError('plugin_manifest_invalid', `entry ${manifest.entry} is not in the plugin.`);
|
|
30
|
+
return { manifest, manifestSha256: sha256(file.data) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function eventKeys(manifest: FlowExtensionManifest): readonly string[] {
|
|
34
|
+
return manifest.triggers.map(t => `${t.provider} ${t.event}[${t.actions.join(',')}]`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatBudget(budget: FlowExtensionManifest['permissions']['budget']): string {
|
|
38
|
+
if (budget === undefined) return 'inherits base';
|
|
39
|
+
return [budget.dollars === undefined ? '' : `$${budget.dollars}`, budget.wallclock ?? ''].filter(Boolean).join(' / ') || 'inherits base';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function describeExtension(manifest: FlowExtensionManifest): string[] {
|
|
43
|
+
const p = manifest.permissions;
|
|
44
|
+
return [
|
|
45
|
+
` integrations: ${p.integrations.join(', ') || 'none'}; harnesses: ${p.harnesses.join(', ') || 'none'}; mcp: ${p.mcp.join(', ') || 'none'}`,
|
|
46
|
+
` events: ${eventKeys(manifest).join('; ') || 'none'}`,
|
|
47
|
+
` hooks: ${manifest.extends.hooks.join(', ') || 'none'}; handlers: ${manifest.extends.handlers ? 'yes' : 'no'}`,
|
|
48
|
+
` writes (declared, unenforced): ${p.writes.join(', ') || 'none'}`,
|
|
49
|
+
` budget: ${formatBudget(p.budget)}`,
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Permissions / events / budget (and version/hooks) changes between two manifests. */
|
|
54
|
+
export function diffExtension(before: FlowExtensionManifest, after: FlowExtensionManifest): string[] {
|
|
55
|
+
const lines: string[] = [];
|
|
56
|
+
const change = (label: string, oldValue: string, newValue: string): void => {
|
|
57
|
+
if (oldValue !== newValue) lines.push(` ${label}: ${oldValue} → ${newValue}`);
|
|
58
|
+
};
|
|
59
|
+
const setDiff = (label: string, oldList: readonly string[], newList: readonly string[]): void => {
|
|
60
|
+
const removed = oldList.filter(x => !newList.includes(x));
|
|
61
|
+
const added = newList.filter(x => !oldList.includes(x));
|
|
62
|
+
if (removed.length === 0 && added.length === 0) return;
|
|
63
|
+
lines.push(` ${label}: ${[...removed.map(x => `-${x}`), ...added.map(x => `+${x}`)].join(', ')}`);
|
|
64
|
+
};
|
|
65
|
+
change('version', before.version, after.version);
|
|
66
|
+
setDiff('integrations', before.permissions.integrations, after.permissions.integrations);
|
|
67
|
+
setDiff('harnesses', before.permissions.harnesses, after.permissions.harnesses);
|
|
68
|
+
setDiff('mcp', before.permissions.mcp, after.permissions.mcp);
|
|
69
|
+
setDiff('writes', before.permissions.writes, after.permissions.writes);
|
|
70
|
+
setDiff('events', eventKeys(before), eventKeys(after));
|
|
71
|
+
setDiff('hooks', before.extends.hooks, after.extends.hooks);
|
|
72
|
+
change('budget', formatBudget(before.permissions.budget), formatBudget(after.permissions.budget));
|
|
73
|
+
if (lines.length === 0) lines.push(' (no permissions/events/budget changes)');
|
|
74
|
+
return lines;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface AddExtensionOptions {
|
|
78
|
+
cwd?: string;
|
|
79
|
+
fetch?: FetchLike;
|
|
80
|
+
now?: () => Date;
|
|
81
|
+
versions?: RuntimeVersions;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `flows add github:<owner>/<repo>@<ref>#<path>` — resolve the ref to a commit,
|
|
86
|
+
* fetch the plugin directory, validate its schema-2 manifest, materialize it
|
|
87
|
+
* under `.flows/plugins`, and record the canonical reference in `flows.json`
|
|
88
|
+
* plus the provenance in `flows.lock.json`. Runtime composition is a later
|
|
89
|
+
* slice: installing records a declaration, it does not enable execution.
|
|
90
|
+
*/
|
|
91
|
+
export async function addExtensionPlugin(input: string, io: CliIo, options: AddExtensionOptions = {}): Promise<0 | 2> {
|
|
92
|
+
try {
|
|
93
|
+
// Parse before touching the filesystem or the network: a malformed
|
|
94
|
+
// reference is refused offline, with the same code from any directory.
|
|
95
|
+
const requested = parsePluginSource(input);
|
|
96
|
+
const root = findPluginProject(options.cwd ?? process.cwd());
|
|
97
|
+
if (!root) throw new PluginError('plugin_manifest_invalid', 'flows add requires a project with flows.json.');
|
|
98
|
+
recoverFlowsAndLock(root);
|
|
99
|
+
const configPath = join(root, 'flows.json');
|
|
100
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
101
|
+
if (!config || Array.isArray(config) || typeof config !== 'object' || (config.plugins !== undefined && (!Array.isArray(config.plugins) || !config.plugins.every((p: unknown) => typeof p === 'string')))) throw new PluginError('plugin_manifest_invalid', 'Invalid flows.json plugins list.');
|
|
102
|
+
const lock = readPluginLock(root);
|
|
103
|
+
const source = await resolveGithubSha(requested, options.fetch);
|
|
104
|
+
const fetched = await fetchGithubPlugin(source, options.fetch);
|
|
105
|
+
const { manifest, manifestSha256 } = extensionManifestOf(fetched);
|
|
106
|
+
assertCompatible(manifest, options.versions ?? runtimeVersions());
|
|
107
|
+
const ref = canonicalPluginRef(source);
|
|
108
|
+
const declared: string[] = config.plugins ?? [];
|
|
109
|
+
for (const other of lock.plugins) {
|
|
110
|
+
if (other.name === manifest.name && canonicalPluginRef({ ...other.source, ref: other.source.sha }) !== ref) {
|
|
111
|
+
throw new PluginError('plugin_manifest_invalid', `Plugin ${manifest.name} is already installed from ${other.source.owner}/${other.source.repo}@${other.source.sha}; remove it before installing another source under the same name.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const { directory, digest } = await materializePlugin(root, manifest.name, fetched.files);
|
|
115
|
+
if (digest !== fetched.digest) throw new PluginError('plugin_source_drift', 'Materialized digest differs from the fetched digest.');
|
|
116
|
+
const plugins = declared.includes(ref) ? declared : [...declared, ref];
|
|
117
|
+
const next = lockWithPlugin(lock, plugins, {
|
|
118
|
+
name: manifest.name, version: manifest.version,
|
|
119
|
+
source: { host: 'github', owner: source.owner, repo: source.repo, sha: source.sha, path: source.path },
|
|
120
|
+
digest, manifestSha256, resolvedAt: (options.now ?? (() => new Date()))().toISOString(),
|
|
121
|
+
});
|
|
122
|
+
writeFlowsAndLock(root, configPath, config, plugins, next);
|
|
123
|
+
io.stdout(`Added ${manifest.name}@${manifest.version} (flow-extension) from ${ref}`);
|
|
124
|
+
io.stdout(` digest sha256:${digest}`);
|
|
125
|
+
io.stdout(` materialized at ${directory}`);
|
|
126
|
+
for (const line of describeExtension(manifest)) io.stdout(line);
|
|
127
|
+
io.stdout(` recorded in flows.json and ${PLUGIN_LOCK_FILE}`);
|
|
128
|
+
return 0;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const refusal = error instanceof PluginError ? error : new PluginError('plugin_manifest_invalid', (error as Error).message);
|
|
131
|
+
io.stderr(`REFUSED [${refusal.code}] ${refusal.message}`);
|
|
132
|
+
return 2;
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/cli/add.ts
CHANGED
|
@@ -3,16 +3,25 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import type { CliIo } from '../cli.js';
|
|
5
5
|
import { findPluginProject, probePlugin, readPlugin } from '../plugin-loader.js';
|
|
6
|
+
import { recoverFlowsAndLock } from '../plugin-lock.js';
|
|
6
7
|
import { PluginError, pluginPackageName } from '../plugin-manifest.js';
|
|
8
|
+
import { isGithubPluginRef } from '../plugin-source.js';
|
|
9
|
+
import { addExtensionPlugin, type AddExtensionOptions } from './add-extension.js';
|
|
7
10
|
|
|
8
11
|
export async function addPlugin(name: string, io: CliIo, options: {
|
|
9
12
|
cwd?: string;
|
|
10
13
|
install?: (packageName: string, root: string) => void;
|
|
14
|
+
/** GitHub-sourced flow extensions only; ignored for helper packages. */
|
|
15
|
+
extension?: Omit<AddExtensionOptions, 'cwd'>;
|
|
11
16
|
} = {}): Promise<0 | 2> {
|
|
17
|
+
// A GitHub reference is a schema-2 flow extension; everything else is the
|
|
18
|
+
// helper-package path below, which this branch leaves exactly as it was.
|
|
19
|
+
if (isGithubPluginRef(name)) return addExtensionPlugin(name, io, { ...options.extension, ...(options.cwd === undefined ? {} : { cwd: options.cwd }) });
|
|
12
20
|
try {
|
|
13
21
|
const packageName = pluginPackageName(name);
|
|
14
22
|
const root = findPluginProject(options.cwd ?? process.cwd());
|
|
15
23
|
if (!root) throw new PluginError('plugin_manifest_invalid', 'flows add requires a project with flows.json.');
|
|
24
|
+
recoverFlowsAndLock(root);
|
|
16
25
|
const configPath = join(root, 'flows.json');
|
|
17
26
|
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
18
27
|
if (!config || Array.isArray(config) || typeof config !== 'object' || (config.plugins !== undefined && (!Array.isArray(config.plugins) || !config.plugins.every((p: unknown) => typeof p === 'string')))) throw new PluginError('plugin_manifest_invalid', 'Invalid flows.json plugins list.');
|
package/src/cli/build.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile, lstat } from 'node:fs/promises';
|
|
|
2
2
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
3
3
|
import { parse } from 'yaml';
|
|
4
4
|
import { sealBundle, verifyBundle, type BundleFile } from '../bundle.js';
|
|
5
|
+
import { collectBundleExtensions, verifyBundlePluginLock } from '../bundle-extensions.js';
|
|
5
6
|
import { canonicalize } from '../canonical.js';
|
|
6
7
|
import { compileSpec, toKernelSpec } from '../compile.js';
|
|
7
8
|
import { preflight } from '../preflight.js';
|
|
@@ -60,6 +61,7 @@ export async function runBuild(args: BuildArgs, io: CliIo): Promise<0 | 2> {
|
|
|
60
61
|
try {
|
|
61
62
|
if (args.verify) {
|
|
62
63
|
const digest = `sha256:${await verifyBundle(args.value)}`;
|
|
64
|
+
await verifyBundlePluginLock(args.value);
|
|
63
65
|
// `--json` is declared on the verb, not on one of its forms: a verify
|
|
64
66
|
// under it emits the same single object a `--json` consumer parses.
|
|
65
67
|
if (args.json) io.stdout(JSON.stringify({ ok: true, verified: true, bundle: args.value, digest }));
|
|
@@ -107,7 +109,7 @@ export async function buildFlow(path: string, out: string, warn: (line: string)
|
|
|
107
109
|
files.push(...result.files);
|
|
108
110
|
} else if (['.yaml', '.yml'].includes(extname(input))) {
|
|
109
111
|
authoring = compileSpec(parse(await readFile(input, 'utf8')));
|
|
110
|
-
files.push({ path: 'lockfile.json', data: canonicalize({ version:
|
|
112
|
+
files.push({ path: 'lockfile.json', data: canonicalize({ version: 2, plugins: [] }) });
|
|
111
113
|
} else throw new Error('build expects a .yaml, .yml, or .ts flow');
|
|
112
114
|
|
|
113
115
|
// The current preflight API reports uncollected environment facts as
|
|
@@ -148,7 +150,10 @@ export async function buildFlow(path: string, out: string, warn: (line: string)
|
|
|
148
150
|
...(authored ? { dynamicSteps: 'exported spec declaration checked; body was not executed during build' } : {}) },
|
|
149
151
|
}) },
|
|
150
152
|
);
|
|
151
|
-
|
|
153
|
+
const extensions = await collectBundleExtensions(input);
|
|
154
|
+
const bundled = files.filter(file => file.path !== 'lockfile.json');
|
|
155
|
+
bundled.push({ path: 'lockfile.json', data: canonicalize(extensions.lock) }, ...extensions.files);
|
|
156
|
+
return sealBundle({ name: authoring.name ?? 'flow', out, files: bundled,
|
|
152
157
|
repo: await repositoryRoot(directory), warn });
|
|
153
158
|
}
|
|
154
159
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadAuthoredFlow } from '../authored-flow-loader.js';
|
|
2
|
+
import { PluginError } from '../plugin-manifest.js';
|
|
2
3
|
import { checkSlackHelpers } from '../slack-preflight.js';
|
|
3
4
|
import { inputFailureReport, type CheckExecution } from './check.js';
|
|
4
5
|
|
|
@@ -8,6 +9,9 @@ export async function checkHelperBody(path: string): Promise<CheckExecution> {
|
|
|
8
9
|
const { handle, getDefinition } = await loadAuthoredFlow(path);
|
|
9
10
|
return { report: { ...checkSlackHelpers(getDefinition(handle)), path } };
|
|
10
11
|
} catch (error) {
|
|
12
|
+
if (error instanceof PluginError) {
|
|
13
|
+
return { report: inputFailureReport({ kind: error.code, message: error.message }, path) };
|
|
14
|
+
}
|
|
11
15
|
return { report: inputFailureReport({ kind: 'invalid_spec',
|
|
12
16
|
message: error instanceof Error ? error.message : 'Could not import authored flow.' }, path) };
|
|
13
17
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { dirname, resolve } from 'node:path';
|
|
2
2
|
import { loadAuthoredFlow, type LoadedAuthoredFlow } from '../authored-flow-loader.js';
|
|
3
|
+
import { probeFlowExtension } from '../flow-extension-loader.js';
|
|
3
4
|
import { preflightWebhookTriggers } from '../preflight.js';
|
|
4
5
|
import { preflightProviderTriggers } from '../provider-trigger-contract.js';
|
|
5
6
|
import { scheduleLowering } from '../schedule-trigger.js';
|
|
6
7
|
import { checkSlackHelpers } from '../slack-preflight.js';
|
|
7
|
-
import { flowRequirements } from '../flow-requirements.js';
|
|
8
|
+
import { flowRequirements, mergeFlowExtensionRequirements } from '../flow-requirements.js';
|
|
9
|
+
import { PluginError } from '../plugin-manifest.js';
|
|
8
10
|
import { inputFailureReport, readProjectConfig, type CheckReport } from './check.js';
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -22,6 +24,7 @@ export async function checkAuthoredTriggers(path: string): Promise<{
|
|
|
22
24
|
const loaded = await loadAuthoredFlow(path);
|
|
23
25
|
const definition = loaded.getDefinition(loaded.handle);
|
|
24
26
|
const config = readProjectConfig(dirname(resolve(path)));
|
|
27
|
+
for (const extension of loaded.extensions ?? []) await probeFlowExtension(extension.manifest);
|
|
25
28
|
const triggers = (definition.handlers ?? []).map(handler => handler.trigger);
|
|
26
29
|
const triggerDiagnostics = preflightWebhookTriggers(triggers, config.executors);
|
|
27
30
|
// Registration answers "may this inbox run here"; the provider contract
|
|
@@ -40,6 +43,17 @@ export async function checkAuthoredTriggers(path: string): Promise<{
|
|
|
40
43
|
const lowering = scheduleLowering(definition.name, trigger);
|
|
41
44
|
return [{ handler, ...lowering }];
|
|
42
45
|
});
|
|
46
|
+
// `?? []` tolerates the partial loader doubles the direct-run tests install.
|
|
47
|
+
const extensions = (loaded.extensions ?? []).map(extension => ({
|
|
48
|
+
name: extension.name, version: extension.version, ref: extension.ref, digest: extension.digest,
|
|
49
|
+
handlers: extension.handlers.length,
|
|
50
|
+
hooks: extension.manifest?.extends.hooks ?? [],
|
|
51
|
+
}));
|
|
52
|
+
const declaredHooks = definition.header?.hooks ?? [];
|
|
53
|
+
const implementations = (loaded.extensions ?? []).flatMap(extension =>
|
|
54
|
+
(extension.manifest?.extends.hooks ?? []).map(hook => ({ hook, plugin: extension.name })));
|
|
55
|
+
const hooks = declaredHooks.length > 0 || implementations.length > 0
|
|
56
|
+
? { declared: declaredHooks, implementations } : undefined;
|
|
43
57
|
return {
|
|
44
58
|
loaded,
|
|
45
59
|
report: {
|
|
@@ -47,11 +61,22 @@ export async function checkAuthoredTriggers(path: string): Promise<{
|
|
|
47
61
|
ok: !diagnostics.some(diagnostic => diagnostic.severity === 'refusal'),
|
|
48
62
|
path, gates: [], resolutions: [], diagnostics,
|
|
49
63
|
...(schedules.length === 0 ? {} : { schedules }),
|
|
50
|
-
|
|
64
|
+
...(extensions.length === 0 ? {} : { extensions }),
|
|
65
|
+
...(hooks === undefined ? {} : { hooks }),
|
|
66
|
+
requirements: mergeFlowExtensionRequirements(
|
|
67
|
+
flowRequirements(definition, { projectCli: config.cli }),
|
|
68
|
+
(loaded.extensions ?? []).map(extension => ({
|
|
69
|
+
name: extension.name, permissions: extension.manifest.permissions,
|
|
70
|
+
})),
|
|
71
|
+
),
|
|
51
72
|
...(config.path === undefined ? {} : { projectConfigPath: config.path }),
|
|
52
73
|
},
|
|
53
74
|
};
|
|
54
75
|
} catch (error) {
|
|
76
|
+
// A flow-extension refusal keeps its own code (plugin_source_drift,
|
|
77
|
+
// plugin_incompatible, …): the operator needs to know which record
|
|
78
|
+
// disagreed, not that "the spec is invalid".
|
|
79
|
+
if (error instanceof PluginError) return { report: inputFailureReport({ kind: error.code, message: error.message }, path) };
|
|
55
80
|
return { report: inputFailureReport({
|
|
56
81
|
kind: typeof error === 'object' && error !== null && 'kind' in error && error.kind === 'config_invalid'
|
|
57
82
|
? 'config_invalid' : 'invalid_spec',
|
|
@@ -2,6 +2,7 @@ import type { LoadedPlugin } from '../plugin-loader.js';
|
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import type { AuthoredFlowDefinition } from '../authored-flow.js';
|
|
4
4
|
import { loadAuthoredFlow } from '../authored-flow-loader.js';
|
|
5
|
+
import { PluginError } from '../plugin-manifest.js';
|
|
5
6
|
import { preflight } from '../preflight.js';
|
|
6
7
|
import { SPEC_SCHEMA_VERSION, type McpServerConfig } from '../spec.js';
|
|
7
8
|
import { inputFailureReport, readProjectConfig, type CheckReport } from './check.js';
|
|
@@ -18,6 +19,9 @@ export async function checkTypeScriptFlow(path: string): Promise<{ report: Check
|
|
|
18
19
|
const { handle, getDefinition } = await loadAuthoredFlow(path);
|
|
19
20
|
return await checkMcpHeader(getDefinition(handle), path);
|
|
20
21
|
} catch (error) {
|
|
22
|
+
if (error instanceof PluginError) {
|
|
23
|
+
return { report: inputFailureReport({ kind: error.code, message: error.message }, path) };
|
|
24
|
+
}
|
|
21
25
|
return { report: inputFailureReport({ kind: 'invalid_spec', message: (error as Error).message }, path) };
|
|
22
26
|
}
|
|
23
27
|
}
|
|
@@ -28,7 +32,7 @@ export async function checkMcpHeader(
|
|
|
28
32
|
path: string,
|
|
29
33
|
): Promise<CheckedMcp> {
|
|
30
34
|
const empty = { servers: Object.freeze({}), inventory: Object.freeze({}) };
|
|
31
|
-
const KNOWN_HEADER_FIELDS = new Set(['tools', 'budget', 'identity', 'memory', 'workspace', 'use']);
|
|
35
|
+
const KNOWN_HEADER_FIELDS = new Set(['tools', 'budget', 'identity', 'memory', 'workspace', 'use', 'version', 'hooks']);
|
|
32
36
|
const header = definition.header ?? {};
|
|
33
37
|
const unsupported = Object.keys(header).filter(key => !KNOWN_HEADER_FIELDS.has(key));
|
|
34
38
|
if (header.tools?.relayfile !== undefined) unsupported.push('tools.relayfile');
|
package/src/cli/check.ts
CHANGED
|
@@ -61,9 +61,32 @@ export interface CheckReport {
|
|
|
61
61
|
schedules?: ScheduleInspection[];
|
|
62
62
|
/** Integrations, harnesses and MCP servers the flow declares it needs (`flow-requirements.ts`). */
|
|
63
63
|
requirements?: FlowRequirements;
|
|
64
|
+
/** Schema-2 flow extensions composed onto the authored flow, in lock order (`flow-extension-loader.ts`). */
|
|
65
|
+
extensions?: ExtensionInspection[];
|
|
66
|
+
/** Base hook points and which plugins implement them. */
|
|
67
|
+
hooks?: HookInspection;
|
|
64
68
|
diagnostics: Array<PreflightDiagnostic | CheckInputDiagnostic | CheckWarningDiagnostic>;
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
export interface ExtensionInspection {
|
|
72
|
+
name: string;
|
|
73
|
+
version: string;
|
|
74
|
+
/** Canonical `github:<owner>/<repo>@<sha>#<path>`. */
|
|
75
|
+
ref: string;
|
|
76
|
+
digest: string;
|
|
77
|
+
/** How many `.on()` handlers it appends after the base flow's own. */
|
|
78
|
+
handlers: number;
|
|
79
|
+
/** Hook names this extension implements, in manifest order. */
|
|
80
|
+
hooks?: readonly string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface HookInspection {
|
|
84
|
+
/** Names the base flow header declares. */
|
|
85
|
+
declared: readonly string[];
|
|
86
|
+
/** Plugin implementations in lock order. */
|
|
87
|
+
implementations: readonly { hook: string; plugin: string }[];
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
export interface ScheduleInspection {
|
|
68
91
|
/** Position among the flow's handlers, so two identical declarations stay distinct. */
|
|
69
92
|
handler: number;
|
package/src/cli/cloud-deploy.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface CloudDeployArgs {
|
|
|
19
19
|
/** Refuse a missing integration instead of offering to connect it. */
|
|
20
20
|
noConnect: boolean;
|
|
21
21
|
json: boolean;
|
|
22
|
+
plugins: string[];
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -39,6 +40,7 @@ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs |
|
|
|
39
40
|
let noConnect = false;
|
|
40
41
|
let json = false;
|
|
41
42
|
const on: string[] = [];
|
|
43
|
+
const plugins: string[] = [];
|
|
42
44
|
for (let i = 0; i < args.length; i++) {
|
|
43
45
|
const arg = args[i]!;
|
|
44
46
|
if (arg === '--json') {
|
|
@@ -63,6 +65,13 @@ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs |
|
|
|
63
65
|
i += 1;
|
|
64
66
|
continue;
|
|
65
67
|
}
|
|
68
|
+
if (arg === '--plugin') {
|
|
69
|
+
const next = args[i + 1];
|
|
70
|
+
if (next === undefined || next.startsWith('-')) return undefined;
|
|
71
|
+
plugins.push(next);
|
|
72
|
+
i += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
66
75
|
if (arg === '--repo' || arg === '--approver' || arg === '--name' || arg === '--on') {
|
|
67
76
|
const next = args[i + 1];
|
|
68
77
|
if (next === undefined || next.startsWith('-')) return undefined;
|
|
@@ -78,7 +87,7 @@ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs |
|
|
|
78
87
|
value = arg;
|
|
79
88
|
}
|
|
80
89
|
if (value === undefined || repo === undefined || on.length === 0) return undefined;
|
|
81
|
-
return { command: 'cloud-deploy', value, repo, on, approver, name, agents, draft, noConnect, json };
|
|
90
|
+
return { command: 'cloud-deploy', value, repo, on, approver, name, agents, draft, noConnect, json, plugins };
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
function describeSource(source: FlowTriggerSource): string {
|
|
@@ -109,6 +118,7 @@ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promi
|
|
|
109
118
|
...(args.name === undefined ? {} : { name: args.name }),
|
|
110
119
|
...(agents === undefined ? {} : { agents }),
|
|
111
120
|
...(connect === undefined ? {} : { connect }),
|
|
121
|
+
...(args.plugins.length === 0 ? {} : { plugins: args.plugins }),
|
|
112
122
|
});
|
|
113
123
|
if (args.json) {
|
|
114
124
|
io.stdout(JSON.stringify({ ok: true, ...deployment }));
|