@portel/photon-core 2.3.0 → 2.4.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/dist/asset-discovery.d.ts +25 -0
- package/dist/asset-discovery.d.ts.map +1 -0
- package/dist/asset-discovery.js +144 -0
- package/dist/asset-discovery.js.map +1 -0
- package/dist/class-detection.d.ts +32 -0
- package/dist/class-detection.d.ts.map +1 -0
- package/dist/class-detection.js +86 -0
- package/dist/class-detection.js.map +1 -0
- package/dist/compiler.d.ts +22 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +48 -0
- package/dist/compiler.js.map +1 -0
- package/dist/env-utils.d.ts +61 -0
- package/dist/env-utils.d.ts.map +1 -0
- package/dist/env-utils.js +171 -0
- package/dist/env-utils.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -1
- package/dist/mime-types.d.ts +13 -0
- package/dist/mime-types.d.ts.map +1 -0
- package/dist/mime-types.js +47 -0
- package/dist/mime-types.js.map +1 -0
- package/dist/rendering/index.d.ts +49 -0
- package/dist/rendering/index.d.ts.map +1 -1
- package/dist/rendering/index.js +153 -0
- package/dist/rendering/index.js.map +1 -1
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +3 -0
- package/dist/schema-extractor.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/validation.d.ts +51 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +249 -0
- package/dist/validation.js.map +1 -0
- package/dist/version-check.d.ts +22 -0
- package/dist/version-check.d.ts.map +1 -0
- package/dist/version-check.js +91 -0
- package/dist/version-check.js.map +1 -0
- package/package.json +2 -2
- package/src/asset-discovery.ts +160 -0
- package/src/class-detection.ts +94 -0
- package/src/compiler.ts +57 -0
- package/src/env-utils.ts +216 -0
- package/src/index.ts +80 -0
- package/src/mime-types.ts +49 -0
- package/src/rendering/index.ts +197 -0
- package/src/schema-extractor.ts +4 -0
- package/src/types.ts +2 -0
- package/src/validation.ts +363 -0
- package/src/version-check.ts +92 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset Discovery Utilities
|
|
3
|
+
*
|
|
4
|
+
* Discover and extract UI, prompt, and resource assets from Photon files.
|
|
5
|
+
* Extracted from photon's loader.ts.
|
|
6
|
+
*
|
|
7
|
+
* Depends on: getMimeType (from ./mime-types), SchemaExtractor (from ./schema-extractor)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from 'fs/promises';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { getMimeType } from './mime-types.js';
|
|
13
|
+
import { SchemaExtractor } from './schema-extractor.js';
|
|
14
|
+
import type { PhotonAssets } from './types.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if a file or directory exists
|
|
18
|
+
*/
|
|
19
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(filePath);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Discover and extract assets from a Photon file
|
|
30
|
+
*
|
|
31
|
+
* Convention:
|
|
32
|
+
* - Asset folder: {photon-name}/ next to {photon-name}.photon.ts
|
|
33
|
+
* - Subfolder: ui/, prompts/, resources/
|
|
34
|
+
*
|
|
35
|
+
* @param photonPath - Absolute path to the .photon.ts file
|
|
36
|
+
* @param source - Source code content of the Photon file
|
|
37
|
+
*/
|
|
38
|
+
export async function discoverAssets(
|
|
39
|
+
photonPath: string,
|
|
40
|
+
source: string,
|
|
41
|
+
): Promise<PhotonAssets | undefined> {
|
|
42
|
+
const extractor = new SchemaExtractor();
|
|
43
|
+
const dir = path.dirname(photonPath);
|
|
44
|
+
const basename = path.basename(photonPath, '.photon.ts');
|
|
45
|
+
|
|
46
|
+
// Convention: asset folder has same name as photon (without .photon.ts)
|
|
47
|
+
const assetFolder = path.join(dir, basename);
|
|
48
|
+
|
|
49
|
+
// Check if asset folder exists
|
|
50
|
+
let folderExists = false;
|
|
51
|
+
try {
|
|
52
|
+
const stat = await fs.stat(assetFolder);
|
|
53
|
+
folderExists = stat.isDirectory();
|
|
54
|
+
} catch {
|
|
55
|
+
// Folder doesn't exist
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Extract explicit asset declarations from source annotations
|
|
59
|
+
const assets = extractor.extractAssets(source, folderExists ? assetFolder : undefined);
|
|
60
|
+
|
|
61
|
+
// If no folder exists and no explicit declarations, skip
|
|
62
|
+
if (
|
|
63
|
+
!folderExists &&
|
|
64
|
+
assets.ui.length === 0 &&
|
|
65
|
+
assets.prompts.length === 0 &&
|
|
66
|
+
assets.resources.length === 0
|
|
67
|
+
) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (folderExists) {
|
|
72
|
+
// Resolve paths for explicitly declared assets
|
|
73
|
+
for (const ui of assets.ui) {
|
|
74
|
+
ui.resolvedPath = path.resolve(assetFolder, ui.path.replace(/^\.\//, ''));
|
|
75
|
+
}
|
|
76
|
+
for (const prompt of assets.prompts) {
|
|
77
|
+
prompt.resolvedPath = path.resolve(assetFolder, prompt.path.replace(/^\.\//, ''));
|
|
78
|
+
}
|
|
79
|
+
for (const resource of assets.resources) {
|
|
80
|
+
resource.resolvedPath = path.resolve(assetFolder, resource.path.replace(/^\.\//, ''));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Auto-discover assets from folder structure
|
|
84
|
+
await autoDiscoverAssets(assetFolder, assets);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return assets;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Auto-discover assets from the ui/, prompts/, resources/ subdirectories
|
|
92
|
+
*/
|
|
93
|
+
export async function autoDiscoverAssets(
|
|
94
|
+
assetFolder: string,
|
|
95
|
+
assets: PhotonAssets,
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
// Auto-discover UI files
|
|
98
|
+
const uiDir = path.join(assetFolder, 'ui');
|
|
99
|
+
if (await fileExists(uiDir)) {
|
|
100
|
+
try {
|
|
101
|
+
const files = await fs.readdir(uiDir);
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
const id = path.basename(file, path.extname(file));
|
|
104
|
+
if (!assets.ui.find((u) => u.id === id)) {
|
|
105
|
+
assets.ui.push({
|
|
106
|
+
id,
|
|
107
|
+
path: `./ui/${file}`,
|
|
108
|
+
resolvedPath: path.join(uiDir, file),
|
|
109
|
+
mimeType: getMimeType(file),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// Ignore errors
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Auto-discover prompt files
|
|
119
|
+
const promptsDir = path.join(assetFolder, 'prompts');
|
|
120
|
+
if (await fileExists(promptsDir)) {
|
|
121
|
+
try {
|
|
122
|
+
const files = await fs.readdir(promptsDir);
|
|
123
|
+
for (const file of files) {
|
|
124
|
+
if (file.endsWith('.md') || file.endsWith('.txt')) {
|
|
125
|
+
const id = path.basename(file, path.extname(file));
|
|
126
|
+
if (!assets.prompts.find((p) => p.id === id)) {
|
|
127
|
+
assets.prompts.push({
|
|
128
|
+
id,
|
|
129
|
+
path: `./prompts/${file}`,
|
|
130
|
+
resolvedPath: path.join(promptsDir, file),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// Ignore errors
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Auto-discover resource files
|
|
141
|
+
const resourcesDir = path.join(assetFolder, 'resources');
|
|
142
|
+
if (await fileExists(resourcesDir)) {
|
|
143
|
+
try {
|
|
144
|
+
const files = await fs.readdir(resourcesDir);
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
const id = path.basename(file, path.extname(file));
|
|
147
|
+
if (!assets.resources.find((r) => r.id === id)) {
|
|
148
|
+
assets.resources.push({
|
|
149
|
+
id,
|
|
150
|
+
path: `./resources/${file}`,
|
|
151
|
+
resolvedPath: path.join(resourcesDir, file),
|
|
152
|
+
mimeType: getMimeType(file),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// Ignore errors
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Class Detection Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared logic for detecting Photon classes in ES modules.
|
|
5
|
+
* Extracted from photon, ncp, and lumina loaders.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a value is a class constructor
|
|
10
|
+
*/
|
|
11
|
+
export function isClass(fn: unknown): fn is new (...args: unknown[]) => unknown {
|
|
12
|
+
return typeof fn === 'function' && /^\s*class\s+/.test(fn.toString());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if a class has async methods (instance or static)
|
|
17
|
+
*
|
|
18
|
+
* Checks for AsyncFunction, AsyncGeneratorFunction, and GeneratorFunction
|
|
19
|
+
* on both prototype (instance methods) and the class itself (static methods).
|
|
20
|
+
*/
|
|
21
|
+
export function hasAsyncMethods(ClassConstructor: new (...args: unknown[]) => unknown): boolean {
|
|
22
|
+
const asyncCtorNames = new Set([
|
|
23
|
+
'AsyncFunction',
|
|
24
|
+
'AsyncGeneratorFunction',
|
|
25
|
+
'GeneratorFunction',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
// Check instance methods on prototype
|
|
29
|
+
const prototype = ClassConstructor.prototype;
|
|
30
|
+
for (const key of Object.getOwnPropertyNames(prototype)) {
|
|
31
|
+
if (key === 'constructor') continue;
|
|
32
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, key);
|
|
33
|
+
if (descriptor && typeof descriptor.value === 'function') {
|
|
34
|
+
if (asyncCtorNames.has(descriptor.value.constructor.name)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check static methods on the class itself
|
|
41
|
+
for (const key of Object.getOwnPropertyNames(ClassConstructor)) {
|
|
42
|
+
if (['length', 'name', 'prototype'].includes(key)) continue;
|
|
43
|
+
const descriptor = Object.getOwnPropertyDescriptor(ClassConstructor, key);
|
|
44
|
+
if (descriptor && typeof descriptor.value === 'function') {
|
|
45
|
+
if (asyncCtorNames.has(descriptor.value.constructor.name)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find a single Photon class in a module
|
|
56
|
+
*
|
|
57
|
+
* Priority: default export first, then named exports.
|
|
58
|
+
* Returns the first class with async methods, or null.
|
|
59
|
+
*/
|
|
60
|
+
export function findPhotonClass(module: Record<string, unknown>): (new (...args: unknown[]) => unknown) | null {
|
|
61
|
+
// Try default export first
|
|
62
|
+
if (module.default && isClass(module.default)) {
|
|
63
|
+
if (hasAsyncMethods(module.default)) {
|
|
64
|
+
return module.default;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Try named exports
|
|
69
|
+
for (const exportedItem of Object.values(module)) {
|
|
70
|
+
if (isClass(exportedItem) && hasAsyncMethods(exportedItem)) {
|
|
71
|
+
return exportedItem;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Find all Photon classes in a module
|
|
80
|
+
*
|
|
81
|
+
* Returns every exported class that has async methods.
|
|
82
|
+
* Used by NCP which may load multiple classes from one file.
|
|
83
|
+
*/
|
|
84
|
+
export function findPhotonClasses(module: Record<string, unknown>): Array<new (...args: unknown[]) => unknown> {
|
|
85
|
+
const classes: Array<new (...args: unknown[]) => unknown> = [];
|
|
86
|
+
|
|
87
|
+
for (const exportedItem of Object.values(module)) {
|
|
88
|
+
if (isClass(exportedItem) && hasAsyncMethods(exportedItem)) {
|
|
89
|
+
classes.push(exportedItem);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return classes;
|
|
94
|
+
}
|
package/src/compiler.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript Compiler Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared esbuild-based TypeScript compilation with caching.
|
|
5
|
+
* Extracted from photon and ncp loaders.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: No esbuild dependency in package.json — the consumer must provide it.
|
|
8
|
+
* Uses `await import('esbuild')` for dynamic resolution.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs/promises';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as crypto from 'crypto';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compile a .photon.ts file to JavaScript and cache the result
|
|
17
|
+
*
|
|
18
|
+
* @param tsFilePath - Absolute path to the TypeScript source file
|
|
19
|
+
* @param options.cacheDir - Directory to store compiled output
|
|
20
|
+
* @param options.content - Optional pre-read file content (avoids extra read)
|
|
21
|
+
* @returns Absolute path to the compiled .mjs file
|
|
22
|
+
*/
|
|
23
|
+
export async function compilePhotonTS(
|
|
24
|
+
tsFilePath: string,
|
|
25
|
+
options: { cacheDir: string; content?: string },
|
|
26
|
+
): Promise<string> {
|
|
27
|
+
const source = options.content ?? (await fs.readFile(tsFilePath, 'utf-8'));
|
|
28
|
+
const hash = crypto.createHash('sha256').update(source).digest('hex').slice(0, 16);
|
|
29
|
+
|
|
30
|
+
const fileName = path.basename(tsFilePath, '.ts');
|
|
31
|
+
const cachedJsPath = path.join(options.cacheDir, `${fileName}.${hash}.mjs`);
|
|
32
|
+
|
|
33
|
+
// Check if cached version exists
|
|
34
|
+
try {
|
|
35
|
+
await fs.access(cachedJsPath);
|
|
36
|
+
return cachedJsPath;
|
|
37
|
+
} catch {
|
|
38
|
+
// Cache miss — compile
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Dynamic import — consumer must have esbuild installed
|
|
42
|
+
const esbuild = await import('esbuild');
|
|
43
|
+
const result = await esbuild.transform(source, {
|
|
44
|
+
loader: 'ts',
|
|
45
|
+
format: 'esm',
|
|
46
|
+
target: 'es2022',
|
|
47
|
+
sourcemap: 'inline',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Ensure cache directory exists
|
|
51
|
+
await fs.mkdir(options.cacheDir, { recursive: true });
|
|
52
|
+
|
|
53
|
+
// Write compiled JavaScript
|
|
54
|
+
await fs.writeFile(cachedJsPath, result.code, 'utf-8');
|
|
55
|
+
|
|
56
|
+
return cachedJsPath;
|
|
57
|
+
}
|
package/src/env-utils.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Variable Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared logic for resolving constructor parameters from environment variables.
|
|
5
|
+
* Extracted from photon's config-docs.ts and lumina's photon-loader.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Minimal constructor parameter info needed for env resolution.
|
|
10
|
+
* Compatible with both photon-core's full ConstructorParam and
|
|
11
|
+
* lumina's simplified version (which omits isPrimitive).
|
|
12
|
+
*/
|
|
13
|
+
export interface EnvConstructorParam {
|
|
14
|
+
name: string;
|
|
15
|
+
type: string;
|
|
16
|
+
isOptional: boolean;
|
|
17
|
+
hasDefault: boolean;
|
|
18
|
+
defaultValue?: any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Info about a missing required parameter
|
|
23
|
+
*/
|
|
24
|
+
export interface MissingParamInfo {
|
|
25
|
+
paramName: string;
|
|
26
|
+
envVarName: string;
|
|
27
|
+
type: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convert a photon name and parameter name to an environment variable name
|
|
32
|
+
*
|
|
33
|
+
* @example toEnvVarName('my-mcp', 'apiKey') → 'MY_MCP_API_KEY'
|
|
34
|
+
*/
|
|
35
|
+
export function toEnvVarName(photonName: string, paramName: string): string {
|
|
36
|
+
const prefix = photonName.toUpperCase().replace(/-/g, '_');
|
|
37
|
+
const suffix = paramName
|
|
38
|
+
.replace(/([A-Z])/g, '_$1')
|
|
39
|
+
.toUpperCase()
|
|
40
|
+
.replace(/^_/, '');
|
|
41
|
+
return `${prefix}_${suffix}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse an environment variable string value to the appropriate type
|
|
46
|
+
*/
|
|
47
|
+
export function parseEnvValue(value: string, type: string): any {
|
|
48
|
+
switch (type) {
|
|
49
|
+
case 'number':
|
|
50
|
+
return parseFloat(value);
|
|
51
|
+
case 'boolean':
|
|
52
|
+
return value.toLowerCase() === 'true';
|
|
53
|
+
case 'string':
|
|
54
|
+
default:
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate a sensible example value for a parameter based on its name and type
|
|
61
|
+
*/
|
|
62
|
+
export function generateExampleValue(paramName: string, paramType: string): string | null {
|
|
63
|
+
const lowerName = paramName.toLowerCase();
|
|
64
|
+
|
|
65
|
+
if (lowerName.includes('apikey') || lowerName.includes('api_key')) {
|
|
66
|
+
return 'sk_your_api_key_here';
|
|
67
|
+
}
|
|
68
|
+
if (lowerName.includes('token') || lowerName.includes('secret')) {
|
|
69
|
+
return 'your_secret_token';
|
|
70
|
+
}
|
|
71
|
+
if (lowerName.includes('url') || lowerName.includes('endpoint')) {
|
|
72
|
+
return 'https://api.example.com';
|
|
73
|
+
}
|
|
74
|
+
if (lowerName.includes('host') || lowerName.includes('server')) {
|
|
75
|
+
return 'localhost';
|
|
76
|
+
}
|
|
77
|
+
if (lowerName.includes('port')) {
|
|
78
|
+
return '5432';
|
|
79
|
+
}
|
|
80
|
+
if (lowerName.includes('database') || lowerName.includes('db')) {
|
|
81
|
+
return 'my_database';
|
|
82
|
+
}
|
|
83
|
+
if (lowerName.includes('user') || lowerName.includes('username')) {
|
|
84
|
+
return 'admin';
|
|
85
|
+
}
|
|
86
|
+
if (lowerName.includes('password')) {
|
|
87
|
+
return 'your_secure_password';
|
|
88
|
+
}
|
|
89
|
+
if (lowerName.includes('path') || lowerName.includes('dir')) {
|
|
90
|
+
return '/path/to/directory';
|
|
91
|
+
}
|
|
92
|
+
if (lowerName.includes('name')) {
|
|
93
|
+
return 'my-service';
|
|
94
|
+
}
|
|
95
|
+
if (lowerName.includes('region')) {
|
|
96
|
+
return 'us-east-1';
|
|
97
|
+
}
|
|
98
|
+
if (paramType === 'boolean') {
|
|
99
|
+
return 'true';
|
|
100
|
+
}
|
|
101
|
+
if (paramType === 'number') {
|
|
102
|
+
return '3000';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Generate documentation and example env vars for constructor parameters
|
|
110
|
+
*/
|
|
111
|
+
export function summarizeConstructorParams(
|
|
112
|
+
params: EnvConstructorParam[],
|
|
113
|
+
photonName: string,
|
|
114
|
+
): { docs: string; exampleEnv: Record<string, string> } {
|
|
115
|
+
const docs = params
|
|
116
|
+
.map((param) => {
|
|
117
|
+
const envVarName = toEnvVarName(photonName, param.name);
|
|
118
|
+
const required = !param.isOptional && !param.hasDefault;
|
|
119
|
+
const status = required ? '[REQUIRED]' : '[OPTIONAL]';
|
|
120
|
+
const defaultInfo = param.hasDefault
|
|
121
|
+
? ` (default: ${JSON.stringify(param.defaultValue)})`
|
|
122
|
+
: '';
|
|
123
|
+
const exampleValue = generateExampleValue(param.name, param.type);
|
|
124
|
+
|
|
125
|
+
let line = ` • ${envVarName} ${status}`;
|
|
126
|
+
line += `\n Type: ${param.type}${defaultInfo}`;
|
|
127
|
+
if (exampleValue) {
|
|
128
|
+
line += `\n Example: ${envVarName}="${exampleValue}"`;
|
|
129
|
+
}
|
|
130
|
+
return line;
|
|
131
|
+
})
|
|
132
|
+
.join('\n\n');
|
|
133
|
+
|
|
134
|
+
const exampleEnv: Record<string, string> = {};
|
|
135
|
+
params.forEach((param) => {
|
|
136
|
+
const envVarName = toEnvVarName(photonName, param.name);
|
|
137
|
+
if (!param.isOptional && !param.hasDefault) {
|
|
138
|
+
exampleEnv[envVarName] =
|
|
139
|
+
generateExampleValue(param.name, param.type) || `your-${param.name}`;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return { docs, exampleEnv };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Generate a user-friendly error message for missing configuration
|
|
148
|
+
*/
|
|
149
|
+
export function generateConfigErrorMessage(
|
|
150
|
+
photonName: string,
|
|
151
|
+
missing: MissingParamInfo[],
|
|
152
|
+
): string {
|
|
153
|
+
const envVarList = missing
|
|
154
|
+
.map((m) => ` • ${m.envVarName} (${m.paramName}: ${m.type})`)
|
|
155
|
+
.join('\n');
|
|
156
|
+
const exampleEnv = Object.fromEntries(
|
|
157
|
+
missing.map((m) => [m.envVarName, `<your-${m.paramName}>`]),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return `
|
|
161
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
162
|
+
⚠️ Configuration Warning: ${photonName} MCP
|
|
163
|
+
|
|
164
|
+
Missing required environment variables:
|
|
165
|
+
${envVarList}
|
|
166
|
+
|
|
167
|
+
Tools will fail until configuration is fixed.
|
|
168
|
+
|
|
169
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
170
|
+
|
|
171
|
+
To fix, add environment variables to your MCP client config:
|
|
172
|
+
|
|
173
|
+
{
|
|
174
|
+
"mcpServers": {
|
|
175
|
+
"${photonName}": {
|
|
176
|
+
"command": "npx",
|
|
177
|
+
"args": ["@portel/photon", "${photonName}"],
|
|
178
|
+
"env": ${JSON.stringify(exampleEnv, null, 8).replace(/\n/g, '\n ')}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
Or run: photon ${photonName} --config
|
|
184
|
+
|
|
185
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
186
|
+
`.trim();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Resolve constructor arguments from environment variables
|
|
191
|
+
*
|
|
192
|
+
* @returns values array (aligned with params) and configError string if any required params are missing
|
|
193
|
+
*/
|
|
194
|
+
export function resolveEnvArgs(
|
|
195
|
+
params: EnvConstructorParam[],
|
|
196
|
+
photonName: string,
|
|
197
|
+
): { values: any[]; missing: MissingParamInfo[] } {
|
|
198
|
+
const values: any[] = [];
|
|
199
|
+
const missing: MissingParamInfo[] = [];
|
|
200
|
+
|
|
201
|
+
for (const param of params) {
|
|
202
|
+
const envVarName = toEnvVarName(photonName, param.name);
|
|
203
|
+
const envValue = process.env[envVarName];
|
|
204
|
+
|
|
205
|
+
if (envValue !== undefined) {
|
|
206
|
+
values.push(parseEnvValue(envValue, param.type));
|
|
207
|
+
} else if (param.hasDefault || param.isOptional) {
|
|
208
|
+
values.push(undefined);
|
|
209
|
+
} else {
|
|
210
|
+
missing.push({ paramName: param.name, envVarName, type: param.type });
|
|
211
|
+
values.push(undefined);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { values, missing };
|
|
216
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -360,3 +360,83 @@ export {
|
|
|
360
360
|
deletePhotonConfig,
|
|
361
361
|
listConfiguredPhotons,
|
|
362
362
|
} from './config.js';
|
|
363
|
+
|
|
364
|
+
// ===== CLASS DETECTION =====
|
|
365
|
+
// Shared Photon class detection for loaders
|
|
366
|
+
export {
|
|
367
|
+
isClass,
|
|
368
|
+
hasAsyncMethods,
|
|
369
|
+
findPhotonClass,
|
|
370
|
+
findPhotonClasses,
|
|
371
|
+
} from './class-detection.js';
|
|
372
|
+
|
|
373
|
+
// ===== ENVIRONMENT UTILITIES =====
|
|
374
|
+
// Env var resolution for constructor injection
|
|
375
|
+
export {
|
|
376
|
+
toEnvVarName,
|
|
377
|
+
parseEnvValue,
|
|
378
|
+
generateExampleValue,
|
|
379
|
+
summarizeConstructorParams,
|
|
380
|
+
generateConfigErrorMessage,
|
|
381
|
+
resolveEnvArgs,
|
|
382
|
+
type MissingParamInfo,
|
|
383
|
+
type EnvConstructorParam,
|
|
384
|
+
} from './env-utils.js';
|
|
385
|
+
|
|
386
|
+
// ===== TYPESCRIPT COMPILER =====
|
|
387
|
+
// esbuild-based .photon.ts compilation with caching
|
|
388
|
+
export { compilePhotonTS } from './compiler.js';
|
|
389
|
+
|
|
390
|
+
// ===== MIME TYPES =====
|
|
391
|
+
// Extension-to-MIME mapping for assets
|
|
392
|
+
export { getMimeType } from './mime-types.js';
|
|
393
|
+
|
|
394
|
+
// ===== VERSION CHECK =====
|
|
395
|
+
// @runtime tag parsing and semver-lite compatibility check
|
|
396
|
+
export {
|
|
397
|
+
parseRuntimeRequirement,
|
|
398
|
+
checkRuntimeCompatibility,
|
|
399
|
+
} from './version-check.js';
|
|
400
|
+
|
|
401
|
+
// ===== VALIDATION =====
|
|
402
|
+
// Input validation utilities and error base classes
|
|
403
|
+
export {
|
|
404
|
+
PhotonError,
|
|
405
|
+
ValidationError,
|
|
406
|
+
type ValidationResult,
|
|
407
|
+
type Validator,
|
|
408
|
+
combineResults,
|
|
409
|
+
isString,
|
|
410
|
+
isNumber,
|
|
411
|
+
isBoolean,
|
|
412
|
+
isObject,
|
|
413
|
+
isArray,
|
|
414
|
+
notEmpty,
|
|
415
|
+
hasLength,
|
|
416
|
+
matchesPattern,
|
|
417
|
+
isEmail,
|
|
418
|
+
isUrl,
|
|
419
|
+
inRange,
|
|
420
|
+
isPositive,
|
|
421
|
+
isInteger,
|
|
422
|
+
hasArrayLength,
|
|
423
|
+
arrayOf,
|
|
424
|
+
hasFields,
|
|
425
|
+
oneOf,
|
|
426
|
+
validate,
|
|
427
|
+
validateOrThrow,
|
|
428
|
+
pathExists,
|
|
429
|
+
hasExtension,
|
|
430
|
+
assertDefined,
|
|
431
|
+
assertString,
|
|
432
|
+
assertNumber,
|
|
433
|
+
assertObject,
|
|
434
|
+
assertArray,
|
|
435
|
+
} from './validation.js';
|
|
436
|
+
|
|
437
|
+
// ===== ASSET DISCOVERY =====
|
|
438
|
+
// Discover UI, prompt, and resource assets from Photon files
|
|
439
|
+
export {
|
|
440
|
+
discoverAssets,
|
|
441
|
+
autoDiscoverAssets,
|
|
442
|
+
} from './asset-discovery.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MIME Type Utilities
|
|
3
|
+
*
|
|
4
|
+
* Extension-to-MIME mapping for Photon assets.
|
|
5
|
+
* Extracted from photon's loader.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
|
|
10
|
+
const MIME_TYPES: Record<string, string> = {
|
|
11
|
+
html: 'text/html',
|
|
12
|
+
htm: 'text/html',
|
|
13
|
+
css: 'text/css',
|
|
14
|
+
js: 'application/javascript',
|
|
15
|
+
mjs: 'application/javascript',
|
|
16
|
+
jsx: 'text/jsx',
|
|
17
|
+
ts: 'text/typescript',
|
|
18
|
+
tsx: 'text/tsx',
|
|
19
|
+
json: 'application/json',
|
|
20
|
+
yaml: 'application/yaml',
|
|
21
|
+
yml: 'application/yaml',
|
|
22
|
+
xml: 'application/xml',
|
|
23
|
+
md: 'text/markdown',
|
|
24
|
+
txt: 'text/plain',
|
|
25
|
+
png: 'image/png',
|
|
26
|
+
jpg: 'image/jpeg',
|
|
27
|
+
jpeg: 'image/jpeg',
|
|
28
|
+
gif: 'image/gif',
|
|
29
|
+
svg: 'image/svg+xml',
|
|
30
|
+
webp: 'image/webp',
|
|
31
|
+
ico: 'image/x-icon',
|
|
32
|
+
woff: 'font/woff',
|
|
33
|
+
woff2: 'font/woff2',
|
|
34
|
+
ttf: 'font/ttf',
|
|
35
|
+
otf: 'font/otf',
|
|
36
|
+
pdf: 'application/pdf',
|
|
37
|
+
zip: 'application/zip',
|
|
38
|
+
csv: 'text/csv',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get MIME type from a filename based on its extension
|
|
43
|
+
*
|
|
44
|
+
* @returns The MIME type string, or 'application/octet-stream' for unknown extensions
|
|
45
|
+
*/
|
|
46
|
+
export function getMimeType(filename: string): string {
|
|
47
|
+
const ext = path.extname(filename).toLowerCase().slice(1);
|
|
48
|
+
return MIME_TYPES[ext] || 'application/octet-stream';
|
|
49
|
+
}
|