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 CHANGED
@@ -2,12 +2,15 @@
2
2
 
3
3
  Give your AI agent a workflow it can't break.
4
4
 
5
- This is a convenience wrapper for [`@lumenflow/cli`](https://www.npmjs.com/package/@lumenflow/cli). It exists so `npx lumenflow init` works.
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's init command.
4
- * This package exists so `npx lumenflow init` resolves correctly
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-1690
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
- // Walk up from this file to find @lumenflow/cli in node_modules.
15
- // This avoids Node's exports resolution which blocks subpath access.
16
- const __dirname = dirname(fileURLToPath(import.meta.url));
17
- let dir = __dirname;
18
- let initPath;
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
- while (dir !== dirname(dir)) {
21
- const candidate = join(dir, 'node_modules', '@lumenflow', 'cli', 'dist', 'init.js');
22
- if (existsSync(candidate)) {
23
- initPath = candidate;
24
- break;
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
- dir = dirname(dir);
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
- if (!initPath) {
30
- console.error('[lumenflow] Could not find @lumenflow/cli. Run: npm install @lumenflow/cli');
31
- process.exit(1);
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
- try {
35
- execFileSync(process.execPath, [initPath, ...process.argv.slice(2)], {
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.1.2",
4
- "description": "Give your AI agent a workflow it can't break. CLI wrapper for @lumenflow/cli.",
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.1.2"
28
+ "@lumenflow/cli": "^3.2.0"
29
29
  },
30
30
  "publishConfig": {
31
31
  "access": "public"