nodepyx 1.0.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.
Files changed (184) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +399 -0
  3. package/binding.gyp +73 -0
  4. package/dist/core/PyCallable.d.ts +65 -0
  5. package/dist/core/PyCallable.d.ts.map +1 -0
  6. package/dist/core/PyCallable.js +109 -0
  7. package/dist/core/PyCallable.js.map +1 -0
  8. package/dist/core/PyContext.d.ts +76 -0
  9. package/dist/core/PyContext.d.ts.map +1 -0
  10. package/dist/core/PyContext.js +228 -0
  11. package/dist/core/PyContext.js.map +1 -0
  12. package/dist/core/PyIterator.d.ts +84 -0
  13. package/dist/core/PyIterator.d.ts.map +1 -0
  14. package/dist/core/PyIterator.js +243 -0
  15. package/dist/core/PyIterator.js.map +1 -0
  16. package/dist/core/PyModule.d.ts +55 -0
  17. package/dist/core/PyModule.d.ts.map +1 -0
  18. package/dist/core/PyModule.js +172 -0
  19. package/dist/core/PyModule.js.map +1 -0
  20. package/dist/core/PyProxy.d.ts +65 -0
  21. package/dist/core/PyProxy.d.ts.map +1 -0
  22. package/dist/core/PyProxy.js +483 -0
  23. package/dist/core/PyProxy.js.map +1 -0
  24. package/dist/core/PyRuntime.d.ts +105 -0
  25. package/dist/core/PyRuntime.d.ts.map +1 -0
  26. package/dist/core/PyRuntime.js +438 -0
  27. package/dist/core/PyRuntime.js.map +1 -0
  28. package/dist/env/CondaManager.d.ts +118 -0
  29. package/dist/env/CondaManager.d.ts.map +1 -0
  30. package/dist/env/CondaManager.js +401 -0
  31. package/dist/env/CondaManager.js.map +1 -0
  32. package/dist/env/PackageInstaller.d.ts +233 -0
  33. package/dist/env/PackageInstaller.d.ts.map +1 -0
  34. package/dist/env/PackageInstaller.js +609 -0
  35. package/dist/env/PackageInstaller.js.map +1 -0
  36. package/dist/env/PythonDetector.d.ts +103 -0
  37. package/dist/env/PythonDetector.d.ts.map +1 -0
  38. package/dist/env/PythonDetector.js +381 -0
  39. package/dist/env/PythonDetector.js.map +1 -0
  40. package/dist/env/VenvManager.d.ts +117 -0
  41. package/dist/env/VenvManager.d.ts.map +1 -0
  42. package/dist/env/VenvManager.js +331 -0
  43. package/dist/env/VenvManager.js.map +1 -0
  44. package/dist/index.d.ts +169 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +393 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/plugins/Plugin.interface.d.ts +41 -0
  49. package/dist/plugins/Plugin.interface.d.ts.map +1 -0
  50. package/dist/plugins/Plugin.interface.js +12 -0
  51. package/dist/plugins/Plugin.interface.js.map +1 -0
  52. package/dist/plugins/PluginManager.d.ts +26 -0
  53. package/dist/plugins/PluginManager.d.ts.map +1 -0
  54. package/dist/plugins/PluginManager.js +174 -0
  55. package/dist/plugins/PluginManager.js.map +1 -0
  56. package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
  57. package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
  58. package/dist/plugins/builtin/NumpyPlugin.js +41 -0
  59. package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
  60. package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
  61. package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
  62. package/dist/plugins/builtin/PandasPlugin.js +57 -0
  63. package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
  64. package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
  65. package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
  66. package/dist/plugins/builtin/TorchPlugin.js +50 -0
  67. package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
  68. package/dist/plugins/index.d.ts +7 -0
  69. package/dist/plugins/index.d.ts.map +1 -0
  70. package/dist/plugins/index.js +12 -0
  71. package/dist/plugins/index.js.map +1 -0
  72. package/dist/serialization/DataFrameBridge.d.ts +141 -0
  73. package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
  74. package/dist/serialization/DataFrameBridge.js +355 -0
  75. package/dist/serialization/DataFrameBridge.js.map +1 -0
  76. package/dist/serialization/MsgPackSerializer.d.ts +45 -0
  77. package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
  78. package/dist/serialization/MsgPackSerializer.js +242 -0
  79. package/dist/serialization/MsgPackSerializer.js.map +1 -0
  80. package/dist/serialization/NumpyBridge.d.ts +96 -0
  81. package/dist/serialization/NumpyBridge.d.ts.map +1 -0
  82. package/dist/serialization/NumpyBridge.js +323 -0
  83. package/dist/serialization/NumpyBridge.js.map +1 -0
  84. package/dist/serialization/Serializer.d.ts +78 -0
  85. package/dist/serialization/Serializer.d.ts.map +1 -0
  86. package/dist/serialization/Serializer.js +281 -0
  87. package/dist/serialization/Serializer.js.map +1 -0
  88. package/dist/types/PythonTypeMapper.d.ts +87 -0
  89. package/dist/types/PythonTypeMapper.d.ts.map +1 -0
  90. package/dist/types/PythonTypeMapper.js +449 -0
  91. package/dist/types/PythonTypeMapper.js.map +1 -0
  92. package/dist/types/StubCache.d.ts +109 -0
  93. package/dist/types/StubCache.d.ts.map +1 -0
  94. package/dist/types/StubCache.js +333 -0
  95. package/dist/types/StubCache.js.map +1 -0
  96. package/dist/types/TypeGenerator.d.ts +139 -0
  97. package/dist/types/TypeGenerator.d.ts.map +1 -0
  98. package/dist/types/TypeGenerator.js +372 -0
  99. package/dist/types/TypeGenerator.js.map +1 -0
  100. package/dist/types/addon.d.ts +114 -0
  101. package/dist/types/addon.d.ts.map +1 -0
  102. package/dist/types/addon.js +32 -0
  103. package/dist/types/addon.js.map +1 -0
  104. package/dist/types/config.d.ts +175 -0
  105. package/dist/types/config.d.ts.map +1 -0
  106. package/dist/types/config.js +35 -0
  107. package/dist/types/config.js.map +1 -0
  108. package/dist/types/index.d.ts +10 -0
  109. package/dist/types/index.d.ts.map +1 -0
  110. package/dist/types/index.js +12 -0
  111. package/dist/types/index.js.map +1 -0
  112. package/dist/types/python.d.ts +235 -0
  113. package/dist/types/python.d.ts.map +1 -0
  114. package/dist/types/python.js +7 -0
  115. package/dist/types/python.js.map +1 -0
  116. package/dist/utils/ErrorTranslator.d.ts +83 -0
  117. package/dist/utils/ErrorTranslator.d.ts.map +1 -0
  118. package/dist/utils/ErrorTranslator.js +210 -0
  119. package/dist/utils/ErrorTranslator.js.map +1 -0
  120. package/dist/utils/Logger.d.ts +27 -0
  121. package/dist/utils/Logger.d.ts.map +1 -0
  122. package/dist/utils/Logger.js +115 -0
  123. package/dist/utils/Logger.js.map +1 -0
  124. package/dist/utils/MemoryMonitor.d.ts +44 -0
  125. package/dist/utils/MemoryMonitor.d.ts.map +1 -0
  126. package/dist/utils/MemoryMonitor.js +143 -0
  127. package/dist/utils/MemoryMonitor.js.map +1 -0
  128. package/package.json +177 -0
  129. package/python/error_handler.py +433 -0
  130. package/python/nodepyx_runtime.py +575 -0
  131. package/python/serializer.py +379 -0
  132. package/python/type_inspector.py +288 -0
  133. package/scripts/build-native.js +68 -0
  134. package/scripts/download-prebuilds.js +99 -0
  135. package/scripts/generate-stubs.js +405 -0
  136. package/scripts/install.js +260 -0
  137. package/src/core/PyCallable.ts +137 -0
  138. package/src/core/PyContext.ts +296 -0
  139. package/src/core/PyIterator.ts +294 -0
  140. package/src/core/PyModule.ts +194 -0
  141. package/src/core/PyProxy.ts +605 -0
  142. package/src/core/PyRuntime.ts +504 -0
  143. package/src/env/CondaManager.ts +451 -0
  144. package/src/env/PackageInstaller.ts +738 -0
  145. package/src/env/PythonDetector.ts +414 -0
  146. package/src/env/VenvManager.ts +396 -0
  147. package/src/index.ts +425 -0
  148. package/src/native/gil_guard.cpp +26 -0
  149. package/src/native/gil_guard.h +175 -0
  150. package/src/native/nodepyx_addon.cpp +886 -0
  151. package/src/native/python_bridge.cpp +790 -0
  152. package/src/native/python_bridge.h +257 -0
  153. package/src/native/thread_pool.cpp +336 -0
  154. package/src/native/thread_pool.h +175 -0
  155. package/src/native/type_converter.cpp +901 -0
  156. package/src/native/type_converter.h +272 -0
  157. package/src/nextjs/PyProvider.tsx +123 -0
  158. package/src/nextjs/index.ts +21 -0
  159. package/src/nextjs/usePython.ts +106 -0
  160. package/src/nextjs/withnodepyx.ts +88 -0
  161. package/src/plugins/Plugin.interface.ts +51 -0
  162. package/src/plugins/PluginManager.ts +155 -0
  163. package/src/plugins/builtin/NumpyPlugin.ts +36 -0
  164. package/src/plugins/builtin/PandasPlugin.ts +49 -0
  165. package/src/plugins/builtin/TorchPlugin.ts +56 -0
  166. package/src/plugins/index.ts +7 -0
  167. package/src/serialization/DataFrameBridge.ts +398 -0
  168. package/src/serialization/MsgPackSerializer.ts +220 -0
  169. package/src/serialization/NumpyBridge.ts +332 -0
  170. package/src/serialization/Serializer.ts +320 -0
  171. package/src/types/PythonTypeMapper.ts +495 -0
  172. package/src/types/StubCache.ts +340 -0
  173. package/src/types/TypeGenerator.ts +491 -0
  174. package/src/types/addon.ts +170 -0
  175. package/src/types/config.ts +226 -0
  176. package/src/types/index.ts +55 -0
  177. package/src/types/python.ts +309 -0
  178. package/src/types/stubs/numpy.d.ts +441 -0
  179. package/src/types/stubs/pandas.d.ts +575 -0
  180. package/src/types/stubs/sklearn.d.ts +728 -0
  181. package/src/types/stubs/torch.d.ts +694 -0
  182. package/src/utils/ErrorTranslator.ts +220 -0
  183. package/src/utils/Logger.ts +119 -0
  184. package/src/utils/MemoryMonitor.ts +175 -0
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * nodepyx — Native Build Script
4
+ * Orchestrates the node-gyp build process with proper error reporting.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { spawnSync } = require('child_process');
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+
13
+ const ROOT = path.resolve(__dirname, '..');
14
+
15
+ function run(cmd, args, options = {}) {
16
+ const result = spawnSync(cmd, args, {
17
+ cwd: ROOT,
18
+ stdio: 'inherit',
19
+ ...options,
20
+ });
21
+
22
+ if (result.status !== 0) {
23
+ throw new Error(`Command failed: ${cmd} ${args.join(' ')} (exit code ${result.status})`);
24
+ }
25
+
26
+ return result;
27
+ }
28
+
29
+ async function main() {
30
+ const release = !process.argv.includes('--debug');
31
+ const args = ['rebuild'];
32
+
33
+ if (release) {
34
+ args.push('--release');
35
+ } else {
36
+ args.push('--debug');
37
+ }
38
+
39
+ if (process.argv.includes('--verbose')) {
40
+ args.push('--verbose');
41
+ }
42
+
43
+ console.log(`[build-native] Building ${release ? 'release' : 'debug'} native addon...`);
44
+
45
+ try {
46
+ run('node-gyp', args);
47
+ } catch {
48
+ console.log('[build-native] Trying via npx...');
49
+ run('npx', ['node-gyp', ...args]);
50
+ }
51
+
52
+ // Verify the build output
53
+ const buildDir = release ? 'Release' : 'Debug';
54
+ const addonPath = path.join(ROOT, 'build', buildDir, 'nodepyx_addon.node');
55
+
56
+ if (fs.existsSync(addonPath)) {
57
+ const stat = fs.statSync(addonPath);
58
+ console.log(`[build-native] ✓ Built: ${path.relative(ROOT, addonPath)} (${(stat.size / 1024).toFixed(1)}KB)`);
59
+ } else {
60
+ throw new Error(`Build succeeded but addon not found at: ${addonPath}`);
61
+ }
62
+ }
63
+
64
+ main().catch(err => {
65
+ console.error(`[build-native] ERROR: ${err.message}`);
66
+ process.exit(1);
67
+ });
68
+
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * nodepyx — Pre-built Binary Downloader
4
+ * Downloads pre-compiled native addon from GitHub releases.
5
+ * Falls back to source build if download fails.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const https = require('https');
13
+ const { execSync } = require('child_process');
14
+ const os = require('os');
15
+
16
+ const ROOT = path.resolve(__dirname, '..');
17
+ const pkg = require(path.join(ROOT, 'package.json'));
18
+
19
+ const GITHUB_ORG = 'nodepyx';
20
+ const GITHUB_REPO = 'nodepyx';
21
+ const VERSION = pkg.version;
22
+
23
+ function getPlatformDir() {
24
+ return `${process.platform}-${process.arch}`;
25
+ }
26
+
27
+ function getNodeABI() {
28
+ return `node-v${process.versions.modules}`;
29
+ }
30
+
31
+ function getDownloadUrl() {
32
+ const platformDir = getPlatformDir();
33
+ const nodeABI = getNodeABI();
34
+ const filename = `nodepyx_addon-${nodeABI}.node`;
35
+
36
+ return `https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/releases/download/v${VERSION}/${platformDir}/${filename}`;
37
+ }
38
+
39
+ function downloadFile(url, dest) {
40
+ return new Promise((resolve, reject) => {
41
+ const file = fs.createWriteStream(dest);
42
+
43
+ https.get(url, { headers: { 'User-Agent': `nodepyx/${VERSION}` } }, (response) => {
44
+ if (response.statusCode === 302 || response.statusCode === 301) {
45
+ // Follow redirect
46
+ file.close();
47
+ fs.unlinkSync(dest);
48
+ downloadFile(response.headers.location, dest).then(resolve).catch(reject);
49
+ return;
50
+ }
51
+
52
+ if (response.statusCode !== 200) {
53
+ file.close();
54
+ fs.unlinkSync(dest);
55
+ reject(new Error(`HTTP ${response.statusCode} for ${url}`));
56
+ return;
57
+ }
58
+
59
+ response.pipe(file);
60
+ file.on('finish', () => {
61
+ file.close();
62
+ resolve();
63
+ });
64
+ }).on('error', (err) => {
65
+ file.close();
66
+ try { fs.unlinkSync(dest); } catch {}
67
+ reject(err);
68
+ });
69
+ });
70
+ }
71
+
72
+ async function main() {
73
+ const platformDir = getPlatformDir();
74
+ const nodeABI = getNodeABI();
75
+ const prebuildsDir = path.join(ROOT, 'prebuilds', platformDir);
76
+ const addonFile = `nodepyx_addon-${nodeABI}.node`;
77
+ const outputPath = path.join(prebuildsDir, addonFile);
78
+ const downloadUrl = getDownloadUrl();
79
+
80
+ console.log(`[nodepyx] Downloading pre-built binary for ${platformDir} / ${nodeABI}`);
81
+ console.log(`[nodepyx] URL: ${downloadUrl}`);
82
+
83
+ fs.mkdirSync(prebuildsDir, { recursive: true });
84
+
85
+ try {
86
+ await downloadFile(downloadUrl, outputPath);
87
+ console.log(`[nodepyx] ✓ Downloaded to ${path.relative(ROOT, outputPath)}`);
88
+ } catch (err) {
89
+ console.warn(`[nodepyx] Download failed: ${err.message}`);
90
+ console.warn('[nodepyx] Will build from source instead.');
91
+ process.exit(1);
92
+ }
93
+ }
94
+
95
+ main().catch(err => {
96
+ console.error(`[nodepyx] Error: ${err.message}`);
97
+ process.exit(1);
98
+ });
99
+
@@ -0,0 +1,405 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * nodepyx — generate-stubs.js
4
+ * CLI script to pre-generate TypeScript stubs for Python modules.
5
+ *
6
+ * Usage:
7
+ * node scripts/generate-stubs.js [modules...]
8
+ * node scripts/generate-stubs.js pandas numpy sklearn torch
9
+ * node scripts/generate-stubs.js --all # regenerate all cached
10
+ * node scripts/generate-stubs.js --clear # clear stub cache
11
+ * node scripts/generate-stubs.js --list # list cached stubs
12
+ * node scripts/generate-stubs.js --stubs-dir=./custom/path pandas
13
+ *
14
+ * When called with no module arguments, generates stubs for the
15
+ * default set: pandas, numpy, sklearn, torch, matplotlib, scipy.
16
+ *
17
+ * This script can run standalone (without nodepyx being fully initialized)
18
+ * by spawning a Python subprocess to run type_inspector.py.
19
+ */
20
+
21
+ 'use strict';
22
+
23
+ const path = require('path');
24
+ const fs = require('fs');
25
+ const { execFileSync, spawnSync } = require('child_process');
26
+
27
+ // ── Parse CLI args ────────────────────────────────────────────────────────
28
+
29
+ const DEFAULT_MODULES = ['pandas', 'numpy', 'sklearn', 'torch', 'matplotlib', 'scipy'];
30
+
31
+ const args = process.argv.slice(2);
32
+ const flags = new Set(args.filter(a => a.startsWith('--')));
33
+ const modules = args.filter(a => !a.startsWith('--'));
34
+
35
+ // Parse --stubs-dir=<path>
36
+ let stubsDir = path.join(process.cwd(), '.nodepyx', 'stubs');
37
+ for (const flag of flags) {
38
+ const match = flag.match(/^--stubs-dir=(.+)$/);
39
+ if (match) {
40
+ stubsDir = path.resolve(match[1]);
41
+ flags.delete(flag);
42
+ }
43
+ }
44
+
45
+ const flagClear = flags.has('--clear');
46
+ const flagList = flags.has('--list');
47
+ const flagAll = flags.has('--all');
48
+ const flagHelp = flags.has('--help') || flags.has('-h');
49
+
50
+ if (flagHelp) {
51
+ console.log(`
52
+ nodepyx generate-stubs — Generate TypeScript stubs from Python modules
53
+
54
+ Usage:
55
+ node scripts/generate-stubs.js [options] [modules...]
56
+
57
+ Options:
58
+ --all Regenerate all currently cached stubs
59
+ --clear Clear the stub cache
60
+ --list List all cached stubs
61
+ --stubs-dir=<dir> Override stub cache directory (default: ./.nodepyx/stubs)
62
+ --help, -h Show this help
63
+
64
+ Examples:
65
+ node scripts/generate-stubs.js pandas numpy
66
+ node scripts/generate-stubs.js --all
67
+ node scripts/generate-stubs.js --clear
68
+ node scripts/generate-stubs.js --stubs-dir=./types pandas
69
+ `);
70
+ process.exit(0);
71
+ }
72
+
73
+ // ── Locate Python ──────────────────────────────────────────────────────────
74
+
75
+ function findPython() {
76
+ const candidates = [
77
+ process.env.nodepyx_PYTHON,
78
+ process.env.PYTHON,
79
+ 'python3',
80
+ 'python',
81
+ ].filter(Boolean);
82
+
83
+ for (const cmd of candidates) {
84
+ try {
85
+ const result = spawnSync(cmd, ['--version'], { encoding: 'utf8', timeout: 5000 });
86
+ if (result.status === 0) return cmd;
87
+ } catch {
88
+ // ignore
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+
94
+ // ── Run Python introspection ────────────────────────────────────────────────
95
+
96
+ const INSPECTOR_PY = path.join(__dirname, '..', 'python', 'type_inspector.py');
97
+
98
+ function inspectModule(pythonCmd, moduleName) {
99
+ const result = spawnSync(
100
+ pythonCmd,
101
+ [INSPECTOR_PY, moduleName],
102
+ { encoding: 'utf8', timeout: 30_000 },
103
+ );
104
+
105
+ if (result.status !== 0 || result.stderr) {
106
+ const err = result.stderr?.trim() || 'Unknown error';
107
+ return { name: moduleName, error: err };
108
+ }
109
+
110
+ try {
111
+ return JSON.parse(result.stdout);
112
+ } catch {
113
+ return { name: moduleName, error: 'Failed to parse inspector output' };
114
+ }
115
+ }
116
+
117
+ // ── TypeScript stub generation (pure JS, no TS compilation needed) ──────────
118
+
119
+ const BUILTIN_TYPE_MAP = {
120
+ str: 'string', int: 'number', float: 'number', bool: 'boolean',
121
+ None: 'null', NoneType: 'null', bytes: 'Uint8Array', bytearray: 'Uint8Array',
122
+ list: 'unknown[]', tuple: 'readonly unknown[]', dict: 'Record<string, unknown>',
123
+ set: 'Set<unknown>', frozenset: 'ReadonlySet<unknown>', Any: 'unknown',
124
+ object: 'PyProxy', type: 'PyProxy', module: 'PyProxy',
125
+ DataFrame: 'DataFrameResult', Series: 'SeriesResult',
126
+ ndarray: 'Float64Array | Float32Array | Int32Array | Uint8Array',
127
+ Tensor: 'Float32Array | Float64Array',
128
+ Callable: '(...args: unknown[]) => unknown',
129
+ datetime: 'Date', date: 'Date', Path: 'string',
130
+ NoReturn: 'never', Never: 'never',
131
+ };
132
+
133
+ function mapPyType(pyType) {
134
+ if (!pyType) return 'unknown';
135
+ const t = pyType.trim();
136
+ if (BUILTIN_TYPE_MAP[t]) return BUILTIN_TYPE_MAP[t];
137
+
138
+ // Optional[X]
139
+ const optMatch = t.match(/^Optional\[(.+)\]$/i);
140
+ if (optMatch) return `${mapPyType(optMatch[1])} | null`;
141
+
142
+ // List[X] / Sequence[X]
143
+ const listMatch = t.match(/^(?:List|Sequence|Iterable|Set|FrozenSet)\[(.+)\]$/i);
144
+ if (listMatch) return `Array<${mapPyType(listMatch[1])}>`;
145
+
146
+ // Dict[K, V]
147
+ const dictMatch = t.match(/^(?:Dict|Mapping)\[(.+),\s*(.+)\]$/i);
148
+ if (dictMatch) return `Record<${mapPyType(dictMatch[1])}, ${mapPyType(dictMatch[2])}>`;
149
+
150
+ // Union[A, B]
151
+ const unionMatch = t.match(/^Union\[(.+)\]$/i);
152
+ if (unionMatch) {
153
+ const parts = splitParams(unionMatch[1]);
154
+ return parts.map(mapPyType).join(' | ');
155
+ }
156
+
157
+ return 'PyProxy';
158
+ }
159
+
160
+ function splitParams(str) {
161
+ const parts = [];
162
+ let depth = 0, curr = '';
163
+ for (const ch of str) {
164
+ if (ch === '[') { depth++; curr += ch; }
165
+ else if (ch === ']') { depth--; curr += ch; }
166
+ else if (ch === ',' && depth === 0) { parts.push(curr.trim()); curr = ''; }
167
+ else { curr += ch; }
168
+ }
169
+ if (curr.trim()) parts.push(curr.trim());
170
+ return parts;
171
+ }
172
+
173
+ function mapReturnType(pyType) {
174
+ if (!pyType || pyType === 'None' || pyType === 'NoneType') return 'Promise<void>';
175
+ return `Promise<${mapPyType(pyType)}>`;
176
+ }
177
+
178
+ function escape(str) {
179
+ return (str || '').replace(/\*\//g, '* /').slice(0, 200);
180
+ }
181
+
182
+ function generateDTS(moduleName, inspection) {
183
+ if (!inspection || inspection.error) {
184
+ return [
185
+ `// nodepyx — Fallback stub for '${moduleName}'`,
186
+ `// ${inspection?.error || 'Module not available'}`,
187
+ ``,
188
+ `declare module 'nodepyx/${moduleName}' {`,
189
+ ` const [key: string]: unknown;`,
190
+ `}`,
191
+ ``,
192
+ ].join('\n');
193
+ }
194
+
195
+ const lines = [
196
+ `// =============================================================`,
197
+ `// nodepyx — Auto-generated TypeScript stub`,
198
+ `// Module : ${moduleName}`,
199
+ inspection.version ? `// Version: ${inspection.version}` : null,
200
+ `// Generated: ${new Date().toISOString()}`,
201
+ `// DO NOT EDIT — regenerate with: npx nodepyx generate-stubs ${moduleName}`,
202
+ `// =============================================================`,
203
+ ``,
204
+ `import type { PyProxy, DataFrameResult, SeriesResult } from 'nodepyx';`,
205
+ ``,
206
+ `declare module 'nodepyx/${moduleName}' {`,
207
+ ``,
208
+ ].filter(l => l !== null);
209
+
210
+ // Constants
211
+ for (const c of inspection.constants || []) {
212
+ if (c.name.startsWith('_')) continue;
213
+ if (c.docstring) lines.push(` /** ${escape(c.docstring)} */`);
214
+ lines.push(` export const ${c.name}: ${mapPyType(c.type || 'Any')};`);
215
+ }
216
+
217
+ // Functions
218
+ for (const fn of inspection.functions || []) {
219
+ if (fn.name.startsWith('_')) continue;
220
+ if (fn.docstring) lines.push(` /** ${escape(fn.docstring)} */`);
221
+ const params = (fn.parameters || [])
222
+ .filter(p => p.name !== 'self' && p.name !== 'cls')
223
+ .slice(0, 20)
224
+ .map(p => {
225
+ const tsType = mapPyType(p.type || 'Any');
226
+ if (p.kind === 'var_positional') return `...${p.name}: Array<${tsType}>`;
227
+ if (p.kind === 'var_keyword') return `${p.name}?: Record<string, ${tsType}>`;
228
+ if (p.optional || p.has_default) return `${p.name}?: ${tsType} | null`;
229
+ return `${p.name}: ${tsType}`;
230
+ });
231
+ lines.push(` export function ${fn.name}(${params.join(', ')}): ${mapReturnType(fn.return_type)};`);
232
+ lines.push(``);
233
+ }
234
+
235
+ // Classes
236
+ for (const cls of inspection.classes || []) {
237
+ if (cls.name.startsWith('_')) continue;
238
+ if (cls.docstring) lines.push(` /** ${escape(cls.docstring)} */`);
239
+ lines.push(` export class ${cls.name} {`);
240
+ lines.push(` constructor(...args: unknown[]);`);
241
+
242
+ const props = [...(cls.properties || []), ...(cls.members || [])].slice(0, 100);
243
+ for (const prop of props) {
244
+ if (prop.name.startsWith('_')) continue;
245
+ const ro = prop.read_only ? 'readonly ' : '';
246
+ lines.push(` ${ro}${prop.name}: Promise<${mapPyType(prop.type || 'Any')}>;`);
247
+ }
248
+
249
+ const methods = [
250
+ ...(cls.methods || []).filter(m => m.name !== '__init__'),
251
+ ...(cls.static_methods || []),
252
+ ].slice(0, 100);
253
+
254
+ for (const m of methods) {
255
+ if (m.name.startsWith('__') && !['__len__', '__str__', '__iter__'].includes(m.name)) continue;
256
+ if (m.docstring) lines.push(` /** ${escape(m.docstring)} */`);
257
+ const isStatic = (cls.static_methods || []).some(s => s.name === m.name);
258
+ const params = (m.parameters || [])
259
+ .filter(p => p.name !== 'self' && p.name !== 'cls')
260
+ .slice(0, 15)
261
+ .map(p => {
262
+ const tsType = mapPyType(p.type || 'Any');
263
+ if (p.optional || p.has_default) return `${p.name}?: ${tsType} | null`;
264
+ return `${p.name}: ${tsType}`;
265
+ });
266
+ const prefix = isStatic ? 'static ' : '';
267
+ lines.push(` ${prefix}${m.name}(${params.join(', ')}): ${mapReturnType(m.return_type)};`);
268
+ }
269
+
270
+ lines.push(` [key: string]: unknown;`);
271
+ lines.push(` }`);
272
+ lines.push(``);
273
+ }
274
+
275
+ lines.push(`}`);
276
+ lines.push(``);
277
+
278
+ return lines.join('\n');
279
+ }
280
+
281
+ // ── Stub cache I/O ────────────────────────────────────────────────────────
282
+
283
+ function ensureDir(dir) {
284
+ fs.mkdirSync(dir, { recursive: true });
285
+ }
286
+
287
+ function metaPath() {
288
+ return path.join(stubsDir, '_meta.json');
289
+ }
290
+
291
+ function loadMeta() {
292
+ try {
293
+ if (fs.existsSync(metaPath())) {
294
+ return JSON.parse(fs.readFileSync(metaPath(), 'utf8'));
295
+ }
296
+ } catch { /* ignore */ }
297
+ return { stubs: {} };
298
+ }
299
+
300
+ function saveMeta(meta) {
301
+ fs.writeFileSync(metaPath(), JSON.stringify(meta, null, 2), 'utf8');
302
+ }
303
+
304
+ function stubFilePath(moduleName) {
305
+ return path.join(stubsDir, `${moduleName.replace(/\./g, '-')}.d.ts`);
306
+ }
307
+
308
+ function writeStub(moduleName, content, libVersion) {
309
+ ensureDir(stubsDir);
310
+ const fp = stubFilePath(moduleName);
311
+ const meta = loadMeta();
312
+ fs.writeFileSync(fp, content, 'utf8');
313
+ meta.stubs[moduleName] = {
314
+ nodepyxVersion: '1.0.0',
315
+ libVersion: libVersion || 'unknown',
316
+ generatedAt: Date.now(),
317
+ contentHash: content.length.toString(16),
318
+ };
319
+ saveMeta(meta);
320
+ return fp;
321
+ }
322
+
323
+ // ── Main ──────────────────────────────────────────────────────────────────
324
+
325
+ async function main() {
326
+ const meta = loadMeta();
327
+
328
+ if (flagClear) {
329
+ console.log(`🗑 Clearing stub cache at ${stubsDir}…`);
330
+ for (const name of Object.keys(meta.stubs)) {
331
+ const fp = stubFilePath(name);
332
+ if (fs.existsSync(fp)) fs.unlinkSync(fp);
333
+ }
334
+ saveMeta({ stubs: {} });
335
+ console.log('✅ Cache cleared.');
336
+ return;
337
+ }
338
+
339
+ if (flagList) {
340
+ const names = Object.keys(meta.stubs);
341
+ if (names.length === 0) {
342
+ console.log('No cached stubs found.');
343
+ } else {
344
+ console.log(`Cached stubs in ${stubsDir}:`);
345
+ for (const [name, info] of Object.entries(meta.stubs)) {
346
+ const age = Math.round((Date.now() - (info.generatedAt || 0)) / 1000 / 60);
347
+ console.log(` • ${name.padEnd(30)} v${info.libVersion} (${age} min ago)`);
348
+ }
349
+ }
350
+ return;
351
+ }
352
+
353
+ // Determine which modules to process
354
+ let targetModules;
355
+ if (flagAll) {
356
+ targetModules = Object.keys(meta.stubs).length > 0
357
+ ? Object.keys(meta.stubs)
358
+ : DEFAULT_MODULES;
359
+ } else if (modules.length > 0) {
360
+ targetModules = modules;
361
+ } else {
362
+ targetModules = DEFAULT_MODULES;
363
+ }
364
+
365
+ const pythonCmd = findPython();
366
+ if (!pythonCmd) {
367
+ console.error('❌ Could not find Python executable. Set nodepyx_PYTHON env var.');
368
+ process.exit(1);
369
+ }
370
+
371
+ console.log(`🐍 Using Python: ${pythonCmd}`);
372
+ console.log(`📁 Stubs directory: ${stubsDir}`);
373
+ console.log(`📦 Modules: ${targetModules.join(', ')}`);
374
+ console.log('');
375
+
376
+ let generated = 0;
377
+ let errors = 0;
378
+
379
+ for (const moduleName of targetModules) {
380
+ process.stdout.write(` Generating ${moduleName}… `);
381
+
382
+ const inspection = inspectModule(pythonCmd, moduleName);
383
+ const libVersion = inspection.version || 'unknown';
384
+ const dts = generateDTS(moduleName, inspection);
385
+ const fp = writeStub(moduleName, dts, libVersion);
386
+
387
+ if (inspection.error) {
388
+ console.log(`⚠ (fallback stub — ${inspection.error.slice(0, 60)})`);
389
+ errors++;
390
+ } else {
391
+ const lineCount = dts.split('\n').length;
392
+ console.log(`✅ ${lineCount} lines → ${path.relative(process.cwd(), fp)}`);
393
+ generated++;
394
+ }
395
+ }
396
+
397
+ console.log('');
398
+ console.log(`Done: ${generated} generated, ${errors} errors.`);
399
+ }
400
+
401
+ main().catch(err => {
402
+ console.error('Fatal error:', err.message);
403
+ process.exit(1);
404
+ });
405
+