editor-sdk 0.1.5 → 0.1.8

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
@@ -20,14 +20,39 @@ Authenticate with your registry API key.
20
20
  npx editor-sdk login
21
21
  ```
22
22
 
23
- ### 2. Add Component
23
+ You can specify a custom registry URL:
24
+ ```bash
25
+ npx editor-sdk login --host https://your-registry.com
26
+ ```
27
+
28
+ ### 2. Configure Registry URL (Optional)
29
+ Set a default registry URL to avoid specifying `--host` every time.
30
+ ```bash
31
+ # View current configuration
32
+ npx editor-sdk config
33
+
34
+ # Set default registry URL
35
+ npx editor-sdk config --set-registry https://your-registry.com
36
+
37
+ # For production
38
+ npx editor-sdk config --set-registry https://api.yourdomain.com
39
+ ```
40
+
41
+ Configuration is saved to `~/.3d-editor/config.json`.
42
+
43
+ ### 3. Add Component
24
44
  Fetch a component by its ID.
25
45
  ```bash
26
- npx editor-sdk add <component-id> --host <registry-url>
46
+ npx editor-sdk add <component-id>
27
47
  ```
28
- *Example:*
48
+
49
+ *Examples:*
29
50
  ```bash
30
- npx editor-sdk add my-scene-123 --host http://localhost:3000
51
+ # Uses configured registry URL
52
+ npx editor-sdk add my-scene-123
53
+
54
+ # Override with --host flag
55
+ npx editor-sdk add my-scene-123 --host http://localhost:8080
31
56
  ```
32
57
 
33
58
  ---
@@ -53,10 +78,52 @@ export default function My3DComponent() {
53
78
  | Prop | Type | Description |
54
79
  |------|------|-------------|
55
80
  | `id` | `string` | The unique ID of the component in the registry. |
56
- | `registryUrl` | `string` | Base URL of the registry (e.g. `https://api.myregistry.com`). |
81
+ | `registryUrl` | `string` | (Optional) Base URL of the registry. Auto-detected if not provided. |
57
82
  | `token` | `string` | (Optional) Auth token for private components. |
58
83
  | `onLoad` | `() => void` | Callback when the model finishes loading. |
59
84
  | `onError` | `(err) => void` | Callback if loading fails. |
85
+ | `animation` | `object` | (Optional) Override animation settings. |
86
+ | `position` | `[x, y, z]` | (Optional) Override position. |
87
+ | `scale` | `[x, y, z]` | (Optional) Override scale. |
88
+
89
+ ---
90
+
91
+ ## 🌐 Registry URL Configuration
92
+
93
+ The SDK automatically detects the registry URL using the following priority:
94
+
95
+ 1. **Explicit prop**: `<SecureScene registryUrl="..." />`
96
+ 2. **Environment variable**: `NEXT_PUBLIC_REGISTRY_URL`
97
+ 3. **Auto-detect localhost**: `http://localhost:{current-port}` (works on any port!)
98
+ 4. **Auto-detect domain**: Uses `window.location.origin` for deployed apps
99
+ 5. **Fallback**: `http://localhost:3000`
100
+
101
+ ### Development (Any Port)
102
+ The SDK automatically works on **any localhost port**:
103
+ ```tsx
104
+ // Works on localhost:3000, localhost:8080, localhost:3001, etc.
105
+ <SecureScene id="my-component-123" />
106
+ ```
107
+
108
+ ### Production (Environment Variable)
109
+ Set the registry URL in your environment:
110
+ ```bash
111
+ # .env.production
112
+ NEXT_PUBLIC_REGISTRY_URL=https://api.yourdomain.com
113
+ ```
114
+
115
+ ```tsx
116
+ // Component automatically uses NEXT_PUBLIC_REGISTRY_URL
117
+ <SecureScene id="my-component-123" />
118
+ ```
119
+
120
+ ### Override for Specific Component
121
+ ```tsx
122
+ <SecureScene
123
+ id="my-component-123"
124
+ registryUrl="https://custom-registry.com"
125
+ />
126
+ ```
60
127
 
61
128
  ---
62
129
 
@@ -1,102 +1,151 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState, Suspense } from 'react';
2
+ import { useEffect, useState, Suspense, useRef } from 'react';
3
3
  import { useGLTF, Html } from '@react-three/drei';
4
- // import { PrimitiveProps } from '@react-three/fiber';
4
+ import { useFrame } from '@react-three/fiber';
5
5
  // Default Registry URL (can be overridden)
6
- const DEFAULT_REGISTRY_URL = 'http://localhost:3000';
7
- const CACHE_duration_MS = 50 * 60 * 1000; // 50 minutes (S3 URLs last 60m)
6
+ // Supports environment variable or falls back to localhost detection
7
+ const getDefaultRegistryUrl = () => {
8
+ // 1. Check for environment variable (for production)
9
+ if (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_REGISTRY_URL) {
10
+ return process.env.NEXT_PUBLIC_REGISTRY_URL;
11
+ }
12
+ // 2. Auto-detect localhost with current port (for development)
13
+ if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
14
+ const port = window.location.port || '3000';
15
+ return `http://localhost:${port}`;
16
+ }
17
+ // 3. Use current origin if on a whitelisted domain
18
+ if (typeof window !== 'undefined') {
19
+ return window.location.origin;
20
+ }
21
+ // 4. Fallback
22
+ return 'http://localhost:3000';
23
+ };
24
+ const DEFAULT_REGISTRY_URL = getDefaultRegistryUrl();
25
+ const CACHE_DURATION_MS = 50 * 60 * 1000; // 50 minutes
26
+ // --- Internal Animator (Copied from Editor) ---
27
+ function SecureAnimator({ children, animation, ...props }) {
28
+ const ref = useRef(null);
29
+ useFrame((state, delta) => {
30
+ if (!ref.current || !animation || animation.type === 'none')
31
+ return;
32
+ const speed = animation.speed || 1;
33
+ const intensity = animation.intensity || 1;
34
+ const axis = animation.axis || 'y';
35
+ // Time based animations
36
+ const t = state.clock.getElapsedTime();
37
+ switch (animation.type) {
38
+ case 'rotate':
39
+ ref.current.rotation[axis] += delta * speed;
40
+ break;
41
+ case 'float':
42
+ const startY = 0; // Relative to parent
43
+ ref.current.position.y = startY + Math.sin(t * speed) * (0.5 * intensity);
44
+ break;
45
+ case 'pulse':
46
+ const scaleBase = 1;
47
+ const scale = scaleBase + Math.sin(t * speed * 2) * (0.1 * intensity);
48
+ ref.current.scale.set(scale, scale, scale);
49
+ break;
50
+ case 'bounce':
51
+ ref.current.position.y = Math.abs(Math.sin(t * speed * 2)) * intensity;
52
+ break;
53
+ case 'shake':
54
+ ref.current.position.x = Math.sin(t * speed * 10) * (0.05 * intensity);
55
+ break;
56
+ }
57
+ });
58
+ return _jsx("group", { ref: ref, ...props, children: children });
59
+ }
60
+ // Loader wrapper
8
61
  function SceneLoader({ url, ...props }) {
9
- // useGLTF will suspend automatically
10
62
  const { scene } = useGLTF(url);
11
- return _jsx("primitive", { object: scene, ...props });
63
+ // Clone scene to avoid shared state issues if reused
64
+ const cloned = scene.clone();
65
+ return _jsx("primitive", { object: cloned, ...props });
12
66
  }
13
67
  export function SecureScene({ id, registryUrl = DEFAULT_REGISTRY_URL, token, onLoad, onError, ...props }) {
14
- const [modelUrl, setModelUrl] = useState(null);
68
+ const [metadata, setMetadata] = useState(null);
15
69
  const [error, setError] = useState(null);
16
70
  const [isDev, setIsDev] = useState(false);
17
71
  useEffect(() => {
18
72
  let mounted = true;
19
- // Check Dev Environment
20
73
  if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
21
74
  setIsDev(true);
22
75
  }
23
- async function fetchMetadata() {
76
+ async function fetchMetadata(force = false) {
24
77
  try {
25
- // 0. Check Cache (LocalStorage)
26
78
  const cacheKey = `3d-editor:cache:${id}`;
27
- const cached = localStorage.getItem(cacheKey);
28
- if (cached) {
29
- const data = JSON.parse(cached);
30
- const age = Date.now() - (data.timestamp || 0);
31
- if (age < CACHE_duration_MS) {
32
- if (mounted) {
33
- console.log(`[SecureScene] Using cached URL for ${id}`);
34
- setModelUrl(data.modelUrl);
35
- onLoad?.();
79
+ // 0. Check Cache (if not forced)
80
+ if (!force) {
81
+ const cached = localStorage.getItem(cacheKey);
82
+ if (cached) {
83
+ const data = JSON.parse(cached);
84
+ const age = Date.now() - (data.timestamp || 0);
85
+ if (age < CACHE_DURATION_MS) {
86
+ if (mounted) {
87
+ setMetadata(data);
88
+ onLoad?.();
89
+ }
90
+ return;
91
+ }
92
+ else {
93
+ localStorage.removeItem(cacheKey);
36
94
  }
37
- return; // Exit early
38
- }
39
- else {
40
- console.log(`[SecureScene] Cache expired for ${id}`);
41
- localStorage.removeItem(cacheKey);
42
95
  }
43
96
  }
44
- // 1. Call the Registry API
45
- const res = await fetch(`${registryUrl}/api/registry/${id}`, {
46
- headers: {
47
- // Future: 'Authorization': `Bearer ${token}`
48
- 'Content-Type': 'application/json',
49
- // Referer is sent automatically by browser
50
- }
51
- });
52
- if (!res.ok) {
97
+ // 1. Call Registry
98
+ const res = await fetch(`${registryUrl}/api/registry/${id}`);
99
+ if (!res.ok)
53
100
  throw new Error(`Registry Error: ${res.statusText}`);
54
- }
55
101
  const data = await res.json();
56
- if (!data.modelUrl || data.modelUrl === 'none') {
57
- throw new Error("No model URL found for this component");
58
- }
59
- // 2. Cache it
102
+ // Cache it
60
103
  try {
61
- const cachePayload = { ...data, timestamp: Date.now() };
62
- localStorage.setItem(cacheKey, JSON.stringify(cachePayload));
63
- }
64
- catch (e) {
65
- // Ignore quota errors
104
+ localStorage.setItem(cacheKey, JSON.stringify({ ...data, timestamp: Date.now() }));
66
105
  }
106
+ catch (e) { }
67
107
  if (mounted) {
68
- setModelUrl(data.modelUrl);
108
+ // Only update if changed? React handles basic diff, but for animations it's fine.
109
+ setMetadata(data);
69
110
  onLoad?.();
70
111
  }
71
112
  }
72
113
  catch (err) {
73
114
  if (mounted) {
115
+ // Only log error if not polling (to avoid spam) or if critical?
116
+ // For now, log.
74
117
  console.error("SecureScene Error:", err);
75
- setError(err.message);
76
- onError?.(err);
118
+ if (!force) {
119
+ setError(err.message);
120
+ onError?.(err);
121
+ }
77
122
  }
78
123
  }
79
124
  }
80
125
  fetchMetadata();
81
- return () => { mounted = false; };
82
- }, [id, registryUrl, token, onLoad, onError]);
126
+ // Polling disabled to prevent infinite fetch loops
127
+ // Only fetch once on mount or when id/registryUrl changes
128
+ return () => {
129
+ mounted = false;
130
+ };
131
+ }, [id, registryUrl]); // Removed onLoad and onError from dependencies
83
132
  if (error) {
84
- return (_jsxs("group", { children: [_jsxs("mesh", { children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { color: "red", wireframe: true })] }), _jsx(Html, { position: [0, 1, 0], center: true, children: _jsxs("div", { style: { color: 'red', background: 'white', padding: 4 }, children: ["Error: ", error] }) })] }));
133
+ return (_jsx("group", { children: _jsx(Html, { position: [0, 1, 0], center: true, children: _jsxs("div", { style: { color: 'red', background: 'white', padding: 4, border: '1px solid red' }, children: ["Error: ", error] }) }) }));
85
134
  }
86
- if (!modelUrl) {
87
- // Loading State
135
+ if (!metadata)
88
136
  return null;
137
+ // Render Logic
138
+ // Extract animation from props if it exists, to apply to the top-level container
139
+ const { animation, ...restProps } = props;
140
+ // 1. Multi-Model Support (sceneData)
141
+ if (metadata.sceneData && metadata.sceneData.objects) {
142
+ return (_jsx(SecureAnimator, { animation: animation, ...restProps, children: metadata.sceneData.objects.map((obj, index) => (_jsx("group", { position: obj.position || [0, 0, 0], rotation: obj.rotation || [0, 0, 0], scale: obj.scale || [1, 1, 1], children: _jsx(SecureAnimator, { animation: obj.animation, children: obj.type === 'imported' ? (_jsx(Suspense, { fallback: null, children: _jsx(SceneLoader, { url: obj.modelUrl || obj.modelPath }) })) : (
143
+ // Basic Primitive Support
144
+ _jsxs("mesh", { children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { color: obj.material?.color || "gray" })] })) }) }, `${obj.id}-${index}`))) }));
145
+ }
146
+ // 2. Legacy / Single Model Fallback
147
+ if (metadata.modelUrl && metadata.modelUrl !== 'none') {
148
+ return (_jsx(SecureAnimator, { animation: animation, ...restProps, children: _jsx(Suspense, { fallback: null, children: _jsx(SceneLoader, { url: metadata.modelUrl }) }) }));
89
149
  }
90
- return (_jsxs("group", { children: [_jsx(Suspense, { fallback: null, children: _jsx(SceneLoader, { url: modelUrl, ...props }) }), isDev && (_jsx(Html, { position: [0, 0, 0], fullscreen: true, style: { pointerEvents: 'none' }, children: _jsx("div", { style: {
91
- position: 'absolute',
92
- bottom: 10,
93
- right: 10,
94
- background: 'rgba(0,0,0,0.7)',
95
- color: '#0f0',
96
- padding: '4px 8px',
97
- borderRadius: 4,
98
- fontSize: '10px',
99
- fontFamily: 'monospace',
100
- border: '1px solid #0f0'
101
- }, children: "DEV MODE: SECURE" }) }))] }));
150
+ return null;
102
151
  }
package/dist/cli.js CHANGED
@@ -36,32 +36,81 @@ program
36
36
  program
37
37
  .command('login')
38
38
  .description('Authenticate with the 3D Editor Registry')
39
- .action(async () => {
40
- console.log(chalk.blue('🔐 Authenticating with 3D Editor...'));
39
+ .option('--host <url>', 'Registry Host URL', 'http://localhost:3000')
40
+ .action(async (options) => {
41
+ console.log(chalk.blue('🔐 Authenticating with 3D Editor... (v0.1.6)'));
41
42
  const response = await prompts({
42
43
  type: 'password',
43
44
  name: 'apiKey',
44
45
  message: 'Enter your API Key:',
45
- validate: value => value.length < 5 ? 'API Key seems too short' : true
46
+ validate: value => value.length < 1 ? 'API Key cannot be empty' : true
46
47
  });
47
48
  if (response.apiKey) {
48
- saveConfig({ apiKey: response.apiKey });
49
- console.log(chalk.green('✓ Logged in successfully!'));
49
+ const host = options.host;
50
+ console.log(chalk.gray(`Connecting to ${host}...`));
51
+ try {
52
+ const res = await fetch(`${host}/api/registry/verify`, {
53
+ headers: { 'Authorization': `Bearer ${response.apiKey}` }
54
+ });
55
+ if (!res.ok) {
56
+ console.error(chalk.red('\n❌ Login Failed: Invalid API Key.'));
57
+ console.error(chalk.yellow('Please check your key and try again.'));
58
+ return;
59
+ }
60
+ const data = await res.json();
61
+ console.log(chalk.green(`\n✓ Verified as ${data.user.email}`));
62
+ saveConfig({ apiKey: response.apiKey });
63
+ console.log(chalk.green('✓ Logged in successfully!'));
64
+ console.log(chalk.gray(`Credentials saved to ${CONFIG_FILE}`));
65
+ }
66
+ catch (err) {
67
+ console.error(chalk.red('\n❌ Connection Failed:'), err.message);
68
+ console.error(chalk.yellow(`Could not reach ${host}`));
69
+ }
50
70
  }
51
71
  else {
52
72
  console.log(chalk.yellow('Login cancelled.'));
53
73
  }
54
74
  });
75
+ // Config Command
76
+ program
77
+ .command('config')
78
+ .description('Configure registry settings')
79
+ .option('--set-registry <url>', 'Set the default registry URL')
80
+ .option('--show', 'Show current configuration')
81
+ .action(async (options) => {
82
+ const config = loadConfig();
83
+ if (options.setRegistry) {
84
+ config.registryUrl = options.setRegistry;
85
+ saveConfig(config);
86
+ console.log(chalk.green(`✓ Registry URL set to: ${options.setRegistry}`));
87
+ console.log(chalk.gray(`Saved to ${CONFIG_FILE}`));
88
+ return;
89
+ }
90
+ if (options.show || (!options.setRegistry)) {
91
+ console.log(chalk.blue('📋 Current Configuration:'));
92
+ console.log(chalk.gray(`Config file: ${CONFIG_FILE}`));
93
+ console.log('');
94
+ console.log(`Registry URL: ${config.registryUrl || chalk.gray('(auto-detect)')}`);
95
+ console.log(`API Key: ${config.apiKey ? chalk.green('✓ Set') : chalk.yellow('✗ Not set')}`);
96
+ console.log('');
97
+ console.log(chalk.gray('Use --set-registry <url> to change the registry URL'));
98
+ console.log(chalk.gray('Example: npx editor-sdk config --set-registry https://your-domain.com'));
99
+ }
100
+ });
55
101
  // Add Command
56
102
  program
57
103
  .command('add <componentId>')
58
104
  .description('Add a component by ID')
59
105
  .option('-p, --package', 'Install as a node_module package', false)
60
- .option('--host <url>', 'Registry Host URL', 'http://localhost:3000') // Default to local for dev
106
+ .option('-f, --force', 'Force overwrite of existing files', false)
107
+ .option('--host <url>', 'Registry Host URL (overrides config)')
61
108
  .action(async (componentId, options) => {
62
109
  try {
63
110
  const config = loadConfig();
64
111
  const apiKey = config.apiKey;
112
+ // Use --host flag, then config, then default
113
+ const defaultHost = options.host || config.registryUrl || 'http://localhost:3000';
65
114
  if (!apiKey) {
66
115
  console.warn(chalk.yellow('⚠ You are not logged in. Public components only.'));
67
116
  console.warn(chalk.yellow('Run `npx 3d-editor login` to access private components.'));
@@ -75,18 +124,32 @@ program
75
124
  idFromArg = parts[parts.length - 1] || 'component';
76
125
  }
77
126
  else {
78
- registryUrl = `${options.host}/api/registry/${componentId}`;
127
+ registryUrl = `${defaultHost}/api/registry/${componentId}`;
79
128
  }
80
129
  console.log(chalk.blue(`📦 Fetching component from ${registryUrl}...`));
81
130
  const res = await fetch(registryUrl, {
82
131
  headers: apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {}
83
132
  });
84
133
  if (!res.ok) {
134
+ const errorBody = await res.text();
135
+ try {
136
+ const json = JSON.parse(errorBody);
137
+ if (json.details) {
138
+ console.error(chalk.red(`Registry Error: ${json.error}`));
139
+ console.error(chalk.red(`Details: ${json.details}`));
140
+ if (json.stack)
141
+ console.error(chalk.gray(json.stack));
142
+ throw new Error("Remote operation failed");
143
+ }
144
+ }
145
+ catch (e) {
146
+ // Not JSON or parse error, ignore
147
+ }
85
148
  if (res.status === 404)
86
149
  throw new Error(`Component '${componentId}' not found.`);
87
150
  if (res.status === 401)
88
151
  throw new Error(`Unauthorized. Please login.`);
89
- throw new Error(`Registry Error: ${res.statusText}`);
152
+ throw new Error(`Registry Error: ${res.statusText} - ${errorBody.slice(0, 100)}`);
90
153
  }
91
154
  const data = await res.json();
92
155
  const { name, id } = data; // We don't need assets/code/etc anymore for the shell
@@ -146,7 +209,21 @@ program
146
209
  else if (!fs.existsSync(path.dirname(targetFile))) {
147
210
  fs.mkdirSync(path.dirname(targetFile), { recursive: true });
148
211
  }
212
+ // Check if file exists and ask for permission if not forced
213
+ if (fs.existsSync(targetFile) && !options.force) {
214
+ const { overwrite } = await prompts({
215
+ type: 'confirm',
216
+ name: 'overwrite',
217
+ message: `File ${componentName}.tsx already exists. Overwrite?`,
218
+ initial: false
219
+ });
220
+ if (!overwrite) {
221
+ console.log(chalk.yellow('ℹ Action cancelled. File was not modified.'));
222
+ process.exit(0);
223
+ }
224
+ }
149
225
  // The Shell Code
226
+ // REMOVED registryUrl explicit prop to allow default or parent override
150
227
  const shellCode = `import React from 'react';
151
228
  import { SecureScene } from 'editor-sdk';
152
229
 
@@ -154,7 +231,6 @@ export default function ${componentName}(props: any) {
154
231
  return (
155
232
  <SecureScene
156
233
  id="${id}"
157
- registryUrl="${options.host}"
158
234
  {...props}
159
235
  />
160
236
  );
package/package.json CHANGED
@@ -1,37 +1,39 @@
1
- {
2
- "name": "editor-sdk",
3
- "version": "0.1.5",
4
- "main": "dist/index.js",
5
- "module": "dist/index.mjs",
6
- "types": "dist/index.d.ts",
7
- "bin": {
8
- "editor-sdk": "./dist/cli.js"
9
- },
10
- "scripts": {
11
- "build": "tsc",
12
- "dev": "tsc --watch"
13
- },
14
- "peerDependencies": {
15
- "@react-three/drei": ">=9.0.0",
16
- "@react-three/fiber": ">=8.0.0",
17
- "react": ">=18.0.0",
18
- "react-dom": ">=18.0.0",
19
- "three": ">=0.160.0"
20
- },
21
- "dependencies": {
22
- "commander": "^11.1.0",
23
- "node-fetch": "^3.3.2",
24
- "chalk": "^4.1.2",
25
- "prompts": "^2.4.2"
26
- },
27
- "files": [
28
- "dist"
29
- ],
30
- "devDependencies": {
31
- "@types/react": "^18.3.27",
32
- "@types/three": "^0.160.0",
33
- "@types/node": "^20.0.0",
34
- "@types/prompts": "^2.4.9",
35
- "typescript": "^5.9.3"
36
- }
1
+ {
2
+ "name": "editor-sdk",
3
+ "version": "0.1.8",
4
+ "main": "dist/index.js",
5
+ "module": "dist/index.mjs",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "editor-sdk": "./dist/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch"
13
+ },
14
+ "peerDependencies": {
15
+ "@react-three/drei": ">=9.0.0",
16
+ "@react-three/fiber": ">=8.0.0",
17
+ "react": ">=18.0.0",
18
+ "react-dom": ">=18.0.0",
19
+ "three": ">=0.160.0"
20
+ },
21
+ "dependencies": {
22
+ "chalk": "^4.1.2",
23
+ "commander": "^11.1.0",
24
+ "node-fetch": "^3.3.2",
25
+ "prompts": "^2.4.2"
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "devDependencies": {
31
+ "@react-three/drei": ">=9.0.0",
32
+ "@react-three/fiber": ">=8.0.0",
33
+ "@types/node": "^20.0.0",
34
+ "@types/prompts": "^2.4.9",
35
+ "@types/react": "^18.3.27",
36
+ "@types/three": "^0.160.0",
37
+ "typescript": "^5.9.3"
38
+ }
37
39
  }