lumenflow 3.1.2 → 3.2.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 +4 -1
- package/__tests__/lumenflow-router.test.ts +95 -0
- package/bin/lumenflow.mjs +209 -24
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Give your AI agent a workflow it can't break.
|
|
4
4
|
|
|
5
|
-
This
|
|
5
|
+
This package routes `npx lumenflow` to the full
|
|
6
|
+
[`@lumenflow/cli`](https://www.npmjs.com/package/@lumenflow/cli) command surface.
|
|
6
7
|
|
|
7
8
|
## Quick Start
|
|
8
9
|
|
|
9
10
|
```bash
|
|
11
|
+
npx lumenflow --help
|
|
10
12
|
npx lumenflow init
|
|
13
|
+
npx lumenflow wu:status --id WU-1234
|
|
11
14
|
```
|
|
12
15
|
|
|
13
16
|
Or install as a dev dependency:
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DEFAULT_DISPATCH, resolveDispatchTarget } from '../bin/lumenflow.mjs';
|
|
3
|
+
|
|
4
|
+
const SAMPLE_MANIFEST = Object.freeze([
|
|
5
|
+
{
|
|
6
|
+
name: 'wu:claim',
|
|
7
|
+
binName: 'wu-claim',
|
|
8
|
+
binPath: './dist/wu-claim.js',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: 'lane:status',
|
|
12
|
+
binName: 'lane-status',
|
|
13
|
+
binPath: './dist/lane-status.js',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'lumenflow-integrate',
|
|
17
|
+
binName: 'lumenflow-integrate',
|
|
18
|
+
binPath: './dist/commands/integrate.js',
|
|
19
|
+
},
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const COMMAND_FLAG_HELP = '--help';
|
|
23
|
+
const COMMAND_TOKEN_INIT = 'init';
|
|
24
|
+
const COMMAND_TOKEN_WU_CLAIM = 'wu:claim';
|
|
25
|
+
const COMMAND_TOKEN_WU_CLAIM_BIN = 'wu-claim';
|
|
26
|
+
const COMMAND_TOKEN_LANE_STATUS = 'lane:status';
|
|
27
|
+
const COMMAND_TOKEN_UNKNOWN = 'totallyunknowncommand';
|
|
28
|
+
const FLAG_FORCE = '--force';
|
|
29
|
+
const EMPTY_MANIFEST: readonly Record<string, string>[] = Object.freeze([]);
|
|
30
|
+
|
|
31
|
+
describe('resolveDispatchTarget', () => {
|
|
32
|
+
it('routes no-argument invocation to commands entry', () => {
|
|
33
|
+
const target = resolveDispatchTarget([], SAMPLE_MANIFEST);
|
|
34
|
+
expect(target).toStrictEqual({
|
|
35
|
+
entryRelativePath: DEFAULT_DISPATCH.commandsEntry,
|
|
36
|
+
forwardedArgs: [],
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('routes help flag to commands entry', () => {
|
|
41
|
+
const target = resolveDispatchTarget([COMMAND_FLAG_HELP], SAMPLE_MANIFEST);
|
|
42
|
+
expect(target).toStrictEqual({
|
|
43
|
+
entryRelativePath: DEFAULT_DISPATCH.commandsEntry,
|
|
44
|
+
forwardedArgs: [],
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('routes explicit command names through manifest mapping', () => {
|
|
49
|
+
const target = resolveDispatchTarget([COMMAND_TOKEN_WU_CLAIM, FLAG_FORCE], SAMPLE_MANIFEST);
|
|
50
|
+
expect(target).toStrictEqual({
|
|
51
|
+
entryRelativePath: 'wu-claim.js',
|
|
52
|
+
forwardedArgs: [FLAG_FORCE],
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('routes explicit bin names through manifest mapping', () => {
|
|
57
|
+
const target = resolveDispatchTarget([COMMAND_TOKEN_WU_CLAIM_BIN, FLAG_FORCE], SAMPLE_MANIFEST);
|
|
58
|
+
expect(target).toStrictEqual({
|
|
59
|
+
entryRelativePath: 'wu-claim.js',
|
|
60
|
+
forwardedArgs: [FLAG_FORCE],
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('routes init token to init entry and strips command token', () => {
|
|
65
|
+
const target = resolveDispatchTarget([COMMAND_TOKEN_INIT, FLAG_FORCE], SAMPLE_MANIFEST);
|
|
66
|
+
expect(target).toStrictEqual({
|
|
67
|
+
entryRelativePath: DEFAULT_DISPATCH.initEntry,
|
|
68
|
+
forwardedArgs: [FLAG_FORCE],
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('falls back to init entry for unknown command tokens', () => {
|
|
73
|
+
const target = resolveDispatchTarget([COMMAND_TOKEN_UNKNOWN, FLAG_FORCE], SAMPLE_MANIFEST);
|
|
74
|
+
expect(target).toStrictEqual({
|
|
75
|
+
entryRelativePath: DEFAULT_DISPATCH.initEntry,
|
|
76
|
+
forwardedArgs: [COMMAND_TOKEN_UNKNOWN, FLAG_FORCE],
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('derives colon-dispatched entrypoints when manifest data is unavailable', () => {
|
|
81
|
+
const target = resolveDispatchTarget([COMMAND_TOKEN_LANE_STATUS, FLAG_FORCE], EMPTY_MANIFEST);
|
|
82
|
+
expect(target).toStrictEqual({
|
|
83
|
+
entryRelativePath: 'lane-status.js',
|
|
84
|
+
forwardedArgs: [FLAG_FORCE],
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('derives hyphenated entrypoints when manifest data is unavailable', () => {
|
|
89
|
+
const target = resolveDispatchTarget([COMMAND_TOKEN_WU_CLAIM_BIN, FLAG_FORCE], EMPTY_MANIFEST);
|
|
90
|
+
expect(target).toStrictEqual({
|
|
91
|
+
entryRelativePath: 'wu-claim.js',
|
|
92
|
+
forwardedArgs: [FLAG_FORCE],
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
package/bin/lumenflow.mjs
CHANGED
|
@@ -1,40 +1,225 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Thin wrapper that delegates to @lumenflow/cli
|
|
4
|
-
* This package exists so `npx lumenflow
|
|
3
|
+
* Thin wrapper that delegates to @lumenflow/cli command entrypoints.
|
|
4
|
+
* This package exists so `npx lumenflow` resolves to the full CLI surface
|
|
5
5
|
* (npm requires the bare package name to match for npx resolution).
|
|
6
6
|
*
|
|
7
|
-
* WU-
|
|
7
|
+
* WU-1977
|
|
8
8
|
*/
|
|
9
9
|
import { execFileSync } from 'node:child_process';
|
|
10
10
|
import { existsSync } from 'node:fs';
|
|
11
11
|
import { dirname, join } from 'node:path';
|
|
12
|
-
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const NODE_MODULES_DIR = 'node_modules';
|
|
15
|
+
const CLI_SCOPE_DIR = '@lumenflow';
|
|
16
|
+
const CLI_PACKAGE_DIR = 'cli';
|
|
17
|
+
const CLI_DIST_DIR = 'dist';
|
|
18
|
+
const CLI_PACKAGE_MANIFEST_FILE = 'package.json';
|
|
19
|
+
const CLI_PUBLIC_MANIFEST_FILE = 'public-manifest.js';
|
|
20
|
+
const CLI_ENTRY_INIT = 'init.js';
|
|
21
|
+
const CLI_ENTRY_COMMANDS = 'commands.js';
|
|
22
|
+
const CLI_ENTRY_DIST_PREFIX = `./${CLI_DIST_DIR}/`;
|
|
23
|
+
const COMMAND_HELP = 'help';
|
|
24
|
+
const COMMAND_INIT = 'init';
|
|
25
|
+
const COMMANDS_BIN_NAME = 'lumenflow-commands';
|
|
26
|
+
const HELP_FLAG_SHORT = '-h';
|
|
27
|
+
const HELP_FLAG_LONG = '--help';
|
|
28
|
+
const COMMAND_DELIMITER = ':';
|
|
29
|
+
const ENTRY_FILE_SUFFIX = '.js';
|
|
30
|
+
const EXIT_CODE_ERROR = 1;
|
|
31
|
+
const ERROR_PREFIX = '[lumenflow]';
|
|
32
|
+
const CLI_INSTALL_HINT = 'Run: npm install @lumenflow/cli';
|
|
19
33
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
export const DEFAULT_DISPATCH = Object.freeze({
|
|
35
|
+
initEntry: CLI_ENTRY_INIT,
|
|
36
|
+
commandsEntry: CLI_ENTRY_COMMANDS,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {{name?: string, binName?: string, binPath?: string}} ManifestCommandLike
|
|
41
|
+
* @typedef {{entryRelativePath: string, forwardedArgs: string[]}} DispatchTarget
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Normalize manifest binPath values to dist-relative entry paths.
|
|
46
|
+
* Examples:
|
|
47
|
+
* ./dist/wu-claim.js -> wu-claim.js
|
|
48
|
+
* ./dist/commands/integrate.js -> commands/integrate.js
|
|
49
|
+
*/
|
|
50
|
+
function normalizeManifestBinPath(binPath) {
|
|
51
|
+
if (typeof binPath !== 'string' || binPath.length === 0) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (binPath.startsWith(CLI_ENTRY_DIST_PREFIX)) {
|
|
55
|
+
return binPath.slice(CLI_ENTRY_DIST_PREFIX.length);
|
|
25
56
|
}
|
|
26
|
-
|
|
57
|
+
if (binPath.startsWith('./')) {
|
|
58
|
+
return binPath.slice(2);
|
|
59
|
+
}
|
|
60
|
+
return binPath;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isHelpToken(token) {
|
|
64
|
+
return token === COMMAND_HELP || token === HELP_FLAG_SHORT || token === HELP_FLAG_LONG;
|
|
27
65
|
}
|
|
28
66
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
67
|
+
function commandTokenToEntryRelativePath(token) {
|
|
68
|
+
if (typeof token !== 'string' || token.length === 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (token.includes(COMMAND_DELIMITER)) {
|
|
72
|
+
return `${token.replaceAll(COMMAND_DELIMITER, '-')}${ENTRY_FILE_SUFFIX}`;
|
|
73
|
+
}
|
|
74
|
+
if (token.includes('-')) {
|
|
75
|
+
return `${token}${ENTRY_FILE_SUFFIX}`;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build lookup tables from the public manifest.
|
|
82
|
+
*
|
|
83
|
+
* @param {ManifestCommandLike[]} manifest
|
|
84
|
+
*/
|
|
85
|
+
function buildManifestDispatchIndex(manifest) {
|
|
86
|
+
/** @type {Map<string, string>} */
|
|
87
|
+
const byName = new Map();
|
|
88
|
+
/** @type {Map<string, string>} */
|
|
89
|
+
const byBinName = new Map();
|
|
90
|
+
|
|
91
|
+
for (const command of manifest) {
|
|
92
|
+
const entryRelativePath = normalizeManifestBinPath(command?.binPath);
|
|
93
|
+
if (!entryRelativePath) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (typeof command?.name === 'string' && command.name.length > 0) {
|
|
97
|
+
byName.set(command.name, entryRelativePath);
|
|
98
|
+
}
|
|
99
|
+
if (typeof command?.binName === 'string' && command.binName.length > 0) {
|
|
100
|
+
byBinName.set(command.binName, entryRelativePath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { byName, byBinName };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolve wrapper argv into a specific @lumenflow/cli dist entrypoint.
|
|
109
|
+
*
|
|
110
|
+
* @param {string[]} argv
|
|
111
|
+
* @param {ManifestCommandLike[]} manifest
|
|
112
|
+
* @returns {DispatchTarget}
|
|
113
|
+
*/
|
|
114
|
+
export function resolveDispatchTarget(argv, manifest) {
|
|
115
|
+
const [commandToken, ...remainingArgs] = argv;
|
|
116
|
+
|
|
117
|
+
if (commandToken === undefined || isHelpToken(commandToken)) {
|
|
118
|
+
return {
|
|
119
|
+
entryRelativePath: DEFAULT_DISPATCH.commandsEntry,
|
|
120
|
+
forwardedArgs: [],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (commandToken === COMMAND_INIT) {
|
|
125
|
+
return {
|
|
126
|
+
entryRelativePath: DEFAULT_DISPATCH.initEntry,
|
|
127
|
+
forwardedArgs: remainingArgs,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const dispatchIndex = buildManifestDispatchIndex(manifest);
|
|
132
|
+
const manifestEntry =
|
|
133
|
+
dispatchIndex.byName.get(commandToken) ?? dispatchIndex.byBinName.get(commandToken);
|
|
134
|
+
if (manifestEntry) {
|
|
135
|
+
return {
|
|
136
|
+
entryRelativePath: manifestEntry,
|
|
137
|
+
forwardedArgs: remainingArgs,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const fallbackEntry = commandTokenToEntryRelativePath(commandToken);
|
|
142
|
+
if (fallbackEntry) {
|
|
143
|
+
return {
|
|
144
|
+
entryRelativePath: fallbackEntry,
|
|
145
|
+
forwardedArgs: remainingArgs,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Backward-compatible fallback: unknown tokens continue through init parser.
|
|
150
|
+
return {
|
|
151
|
+
entryRelativePath: DEFAULT_DISPATCH.initEntry,
|
|
152
|
+
forwardedArgs: argv,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function loadPublicManifest(manifestPath) {
|
|
157
|
+
if (!existsSync(manifestPath)) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
return import(pathToFileURL(manifestPath).href)
|
|
161
|
+
.then((module) =>
|
|
162
|
+
typeof module.getPublicManifest === 'function' ? module.getPublicManifest() : [],
|
|
163
|
+
)
|
|
164
|
+
.then((manifest) => (Array.isArray(manifest) ? manifest : []))
|
|
165
|
+
.catch(() => []);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function findCliPackageRoot(startDir) {
|
|
169
|
+
let dir = startDir;
|
|
170
|
+
while (dir !== dirname(dir)) {
|
|
171
|
+
const candidateRoot = join(dir, NODE_MODULES_DIR, CLI_SCOPE_DIR, CLI_PACKAGE_DIR);
|
|
172
|
+
const candidateManifest = join(candidateRoot, CLI_PACKAGE_MANIFEST_FILE);
|
|
173
|
+
const candidateInitEntry = join(candidateRoot, CLI_DIST_DIR, CLI_ENTRY_INIT);
|
|
174
|
+
if (existsSync(candidateManifest) && existsSync(candidateInitEntry)) {
|
|
175
|
+
return candidateRoot;
|
|
176
|
+
}
|
|
177
|
+
dir = dirname(dir);
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function printResolutionError() {
|
|
183
|
+
console.error(`${ERROR_PREFIX} Could not find @lumenflow/cli. ${CLI_INSTALL_HINT}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function main() {
|
|
187
|
+
const wrapperDir = dirname(fileURLToPath(import.meta.url));
|
|
188
|
+
const cliPackageRoot = findCliPackageRoot(wrapperDir);
|
|
189
|
+
|
|
190
|
+
if (!cliPackageRoot) {
|
|
191
|
+
printResolutionError();
|
|
192
|
+
process.exit(EXIT_CODE_ERROR);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const manifestPath = join(cliPackageRoot, CLI_DIST_DIR, CLI_PUBLIC_MANIFEST_FILE);
|
|
196
|
+
const manifest = await loadPublicManifest(manifestPath);
|
|
197
|
+
|
|
198
|
+
const dispatchTarget = resolveDispatchTarget(process.argv.slice(2), manifest);
|
|
199
|
+
const entryPath = join(cliPackageRoot, CLI_DIST_DIR, dispatchTarget.entryRelativePath);
|
|
200
|
+
const initialArgs = process.argv.slice(2);
|
|
201
|
+
|
|
202
|
+
if (!existsSync(entryPath)) {
|
|
203
|
+
const initFallbackPath = join(cliPackageRoot, CLI_DIST_DIR, DEFAULT_DISPATCH.initEntry);
|
|
204
|
+
const fallbackArgs =
|
|
205
|
+
dispatchTarget.entryRelativePath === DEFAULT_DISPATCH.commandsEntry
|
|
206
|
+
? [HELP_FLAG_LONG]
|
|
207
|
+
: initialArgs;
|
|
208
|
+
execFileSync(process.execPath, [initFallbackPath, ...fallbackArgs], {
|
|
209
|
+
stdio: 'inherit',
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
execFileSync(process.execPath, [entryPath, ...dispatchTarget.forwardedArgs], {
|
|
216
|
+
stdio: 'inherit',
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
process.exit(error?.status ?? EXIT_CODE_ERROR);
|
|
220
|
+
}
|
|
32
221
|
}
|
|
33
222
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
stdio: 'inherit',
|
|
37
|
-
});
|
|
38
|
-
} catch (err) {
|
|
39
|
-
process.exit(err.status ?? 1);
|
|
223
|
+
if (import.meta.main) {
|
|
224
|
+
void main();
|
|
40
225
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lumenflow",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Give your AI agent a workflow it can't break.
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "Give your AI agent a workflow it can't break. Full command router wrapper for @lumenflow/cli.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lumenflow",
|
|
7
7
|
"workflow",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"lumenflow": "./bin/lumenflow.mjs"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@lumenflow/cli": "^3.
|
|
28
|
+
"@lumenflow/cli": "^3.2.0"
|
|
29
29
|
},
|
|
30
30
|
"publishConfig": {
|
|
31
31
|
"access": "public"
|