@shellui/cli 0.0.1 → 0.0.5

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.
@@ -0,0 +1,69 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import pc from 'picocolors';
4
+ import { loadTypeScriptConfig, loadJsonConfig } from './config-loaders.js';
5
+
6
+ /**
7
+ * Merge Sentry config from env into the loaded config. Only adds sentry when
8
+ * SENTRY_DSN is set and Sentry is not disabled via SENTRY_ENABLED=false|0.
9
+ * @param {Object} config - Loaded config object
10
+ * @returns {Object} Config with sentry merged from env when enabled
11
+ */
12
+ function mergeSentryFromEnv(config) {
13
+ const dsn = process.env.SENTRY_DSN;
14
+ const enabled = process.env.SENTRY_ENABLED;
15
+ if (!dsn || enabled === 'false' || enabled === '0') {
16
+ return config;
17
+ }
18
+ return {
19
+ ...config,
20
+ sentry: {
21
+ dsn,
22
+ ...(process.env.SENTRY_ENVIRONMENT && { environment: process.env.SENTRY_ENVIRONMENT }),
23
+ ...(process.env.SENTRY_RELEASE && { release: process.env.SENTRY_RELEASE }),
24
+ },
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Load configuration from shellui.config.ts or shellui.config.json file
30
+ * Prefers TypeScript config over JSON config. Sentry is merged from env on load
31
+ * (SENTRY_DSN, SENTRY_ENABLED, SENTRY_ENVIRONMENT, SENTRY_RELEASE).
32
+ * @param {string} root - Root directory to search for config (default: current working directory)
33
+ * @returns {Promise<Object>} Configuration object
34
+ */
35
+ export async function loadConfig(root = '.') {
36
+ const cwd = process.cwd();
37
+ const configDir = path.resolve(cwd, root);
38
+ const tsConfigPath = path.join(configDir, 'shellui.config.ts');
39
+ const jsonConfigPath = path.join(configDir, 'shellui.config.json');
40
+
41
+ let config = {};
42
+ let activeConfigPath = null;
43
+
44
+ // Prefer TypeScript config over JSON
45
+ if (fs.existsSync(tsConfigPath)) {
46
+ activeConfigPath = tsConfigPath;
47
+ } else if (fs.existsSync(jsonConfigPath)) {
48
+ activeConfigPath = jsonConfigPath;
49
+ }
50
+
51
+ if (activeConfigPath) {
52
+ try {
53
+ if (activeConfigPath === tsConfigPath) {
54
+ config = await loadTypeScriptConfig(activeConfigPath, configDir);
55
+ } else {
56
+ config = loadJsonConfig(activeConfigPath);
57
+ }
58
+ } catch (e) {
59
+ console.error(pc.red(`Failed to load config from ${activeConfigPath}: ${e.message}`));
60
+ if (e.stack) {
61
+ console.error(pc.red(e.stack));
62
+ }
63
+ }
64
+ } else {
65
+ console.log(pc.yellow(`No shellui.config.ts or shellui.config.json found, using defaults.`));
66
+ }
67
+
68
+ return mergeSentryFromEnv(config);
69
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Utilities index - Export all utility functions
3
+ *
4
+ * This is the main entry point for all utility functions.
5
+ * Import from './utils/index.js' to get all utilities.
6
+ */
7
+
8
+ export { resolvePackagePath, resolveSdkEntry } from './package-path.js';
9
+ export { loadConfig } from './config.js';
10
+ export {
11
+ getCoreSrcPath,
12
+ createResolveAlias,
13
+ createPostCSSConfig,
14
+ createViteDefine,
15
+ } from './vite.js';
@@ -0,0 +1,54 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ import { createRequire } from 'module';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const require = createRequire(import.meta.url);
8
+
9
+ /**
10
+ * Resolve the path to a package in the monorepo or node_modules
11
+ * @param {string} packageName - The name of the package
12
+ * @returns {string} The absolute path to the package
13
+ */
14
+ export function resolvePackagePath(packageName) {
15
+ try {
16
+ // Try to resolve the package using require.resolve
17
+ const packageJsonPath = require.resolve(`${packageName}/package.json`);
18
+ return path.dirname(packageJsonPath);
19
+ } catch (e) {
20
+ // Fallback: assume workspace structure or pnpm symlinked node_modules
21
+ // Go up from cli/src/utils/package-path.js -> cli/src/utils -> cli/src -> cli -> packages -> packageName
22
+ const packagesDir = path.resolve(__dirname, '../../../');
23
+ const resolved = path.join(packagesDir, packageName.replace('@shellui/', ''));
24
+ // Resolve symlinks to get the canonical path — pnpm uses symlinks that
25
+ // point to different .pnpm/ directories; Vite resolves real paths so we
26
+ // need to be consistent to avoid mismatched root vs input paths.
27
+ try {
28
+ return fs.realpathSync(resolved);
29
+ } catch {
30
+ return resolved;
31
+ }
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Resolve the @shellui/sdk entry point for Vite alias.
37
+ * In workspace/monorepo mode, returns the source path (src/index.ts) so Vite
38
+ * can process TypeScript directly without a pre-build step.
39
+ * When installed from npm (no source), returns null so Vite uses normal
40
+ * node_modules resolution (dist/index.js via package exports).
41
+ * @returns {string|null} Absolute path to SDK source entry, or null to use normal resolution
42
+ */
43
+ export function resolveSdkEntry() {
44
+ const sdkPackagePath = resolvePackagePath('@shellui/sdk');
45
+ const srcEntry = path.join(sdkPackagePath, 'src', 'index.ts');
46
+
47
+ // In workspace mode, source is available — alias to it for a no-build dev flow
48
+ if (fs.existsSync(srcEntry)) {
49
+ return srcEntry;
50
+ }
51
+
52
+ // When installed from npm only dist/ exists — let Vite resolve through package exports
53
+ return null;
54
+ }
@@ -0,0 +1,151 @@
1
+ import { build } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { createResolveAlias } from './vite.js';
6
+
7
+ /**
8
+ * Vite plugin to build and serve service worker in dev mode
9
+ */
10
+ export function serviceWorkerDevPlugin(corePackagePath, coreSrcPath) {
11
+ let swCode = null;
12
+ let isBuilding = false;
13
+ let buildError = null;
14
+
15
+ const swPath = path.join(corePackagePath, 'src', 'service-worker', 'sw-dev.ts');
16
+
17
+ async function buildServiceWorker() {
18
+ if (isBuilding) {
19
+ return;
20
+ }
21
+
22
+ if (!fs.existsSync(swPath)) {
23
+ buildError = `Service worker source not found at: ${swPath}`;
24
+ console.warn(buildError);
25
+ return;
26
+ }
27
+
28
+ isBuilding = true;
29
+ buildError = null;
30
+ try {
31
+ // Use Vite's build API to properly resolve imports and bundle dependencies
32
+ // Use same root and resolve config as the main Vite server for consistent resolution
33
+ const result = await build({
34
+ root: coreSrcPath,
35
+ plugins: [react()],
36
+ resolve: {
37
+ alias: createResolveAlias(),
38
+ },
39
+ build: {
40
+ write: false,
41
+ sourcemap: false, // Disable source maps for service worker in dev mode
42
+ rollupOptions: {
43
+ input: swPath,
44
+ output: {
45
+ format: 'es',
46
+ entryFileNames: 'sw.js',
47
+ },
48
+ },
49
+ },
50
+ });
51
+
52
+ // Extract the built code from the output
53
+ // Vite build with write: false returns a RollupOutput object with output array
54
+ let outputChunk = null;
55
+
56
+ if (result && Array.isArray(result) && result.length > 0) {
57
+ // Handle array format (multiple outputs)
58
+ const buildOutput = result[0];
59
+ if (buildOutput && buildOutput.output && Array.isArray(buildOutput.output)) {
60
+ outputChunk = buildOutput.output.find((o) => o.type === 'chunk');
61
+ }
62
+ } else if (result && result.output && Array.isArray(result.output)) {
63
+ // Handle single output format
64
+ outputChunk = result.output.find((o) => o.type === 'chunk');
65
+ } else if (result && result.code) {
66
+ // Handle direct code format (unlikely but possible)
67
+ outputChunk = { code: result.code };
68
+ }
69
+
70
+ if (outputChunk && outputChunk.code) {
71
+ // Strip any source map references from the code
72
+ // This prevents browser devtools from trying to load non-existent source maps
73
+ swCode = outputChunk.code.replace(/\/\/# sourceMappingURL=.*$/gm, '');
74
+ console.log('✓ Dev service worker built successfully');
75
+ } else {
76
+ buildError = `Service worker build completed but no output chunk found. Result type: ${typeof result}, isArray: ${Array.isArray(result)}`;
77
+ console.warn(buildError);
78
+ console.warn('Build result structure:', JSON.stringify(Object.keys(result || {}), null, 2));
79
+ }
80
+ } catch (error) {
81
+ buildError = error.message;
82
+ console.error('Failed to build dev service worker:', error.message);
83
+ if (error.stack) {
84
+ console.error(error.stack);
85
+ }
86
+ } finally {
87
+ isBuilding = false;
88
+ }
89
+ }
90
+
91
+ return {
92
+ name: 'shellui-service-worker-dev',
93
+ async buildStart() {
94
+ // Try to build immediately when plugin loads
95
+ await buildServiceWorker();
96
+ },
97
+ configureServer(server) {
98
+ // Also build when server is ready (in case buildStart was too early)
99
+ server.httpServer?.once('listening', async () => {
100
+ if (!swCode && !buildError) {
101
+ await buildServiceWorker();
102
+ }
103
+ });
104
+
105
+ // Handle source map requests first - return empty source map instead of 404
106
+ // This prevents browser devtools from showing errors when source maps don't exist
107
+ // (e.g., from React DevTools or other browser extensions)
108
+ server.middlewares.use((req, res, next) => {
109
+ if (req.url && req.url.endsWith('.map')) {
110
+ // Return a valid but empty source map to satisfy devtools
111
+ // This prevents errors while still disabling source maps in dev mode
112
+ res.statusCode = 200;
113
+ res.setHeader('Content-Type', 'application/json');
114
+ res.end(
115
+ JSON.stringify({
116
+ version: 3,
117
+ sources: [],
118
+ names: [],
119
+ mappings: '',
120
+ file: req.url.replace('.map', ''),
121
+ }),
122
+ );
123
+ return;
124
+ }
125
+ next();
126
+ });
127
+
128
+ // Serve the service worker at /sw.js
129
+ server.middlewares.use(async (req, res, next) => {
130
+ if (req.url === '/sw.js' || req.url.startsWith('/sw.js?')) {
131
+ // Ensure it's built (try one more time if not ready)
132
+ if (!swCode && !buildError) {
133
+ await buildServiceWorker();
134
+ }
135
+
136
+ if (swCode) {
137
+ res.setHeader('Content-Type', 'application/javascript');
138
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
139
+ res.end(swCode);
140
+ } else {
141
+ res.statusCode = 404;
142
+ const errorMsg = buildError || 'Service worker not available';
143
+ res.end(`// ${errorMsg}\n// Service worker build failed or not ready`);
144
+ }
145
+ } else {
146
+ next();
147
+ }
148
+ });
149
+ },
150
+ };
151
+ }
@@ -0,0 +1,82 @@
1
+ import path from 'path';
2
+ import tailwindcssPlugin from '@tailwindcss/postcss';
3
+ import autoprefixerPlugin from 'autoprefixer';
4
+ import { resolvePackagePath, resolveSdkEntry } from './index.js';
5
+
6
+ /**
7
+ * Get the path to the core package source directory
8
+ * @returns {string} Absolute path to core package src directory
9
+ */
10
+ export function getCoreSrcPath() {
11
+ const corePackagePath = resolvePackagePath('@shellui/core');
12
+ return path.join(corePackagePath, 'src');
13
+ }
14
+
15
+ /**
16
+ * Create Vite resolve.alias configuration.
17
+ * Always sets '@' to core/src. Sets '@shellui/sdk' to source entry when in
18
+ * workspace mode; omits the alias when installed from npm so Vite resolves
19
+ * through the package's exports field (dist/index.js).
20
+ * @returns {Object} Vite resolve.alias object
21
+ */
22
+ export function createResolveAlias() {
23
+ const corePackagePath = resolvePackagePath('@shellui/core');
24
+ const sdkEntry = resolveSdkEntry();
25
+
26
+ const alias = {
27
+ '@': path.join(corePackagePath, 'src'),
28
+ };
29
+
30
+ if (sdkEntry) {
31
+ alias['@shellui/sdk'] = sdkEntry;
32
+ }
33
+
34
+ return alias;
35
+ }
36
+
37
+ /**
38
+ * Create PostCSS configuration for Vite.
39
+ * Provides Tailwind CSS v4 and autoprefixer plugins programmatically so the
40
+ * CLI owns all CSS build dependencies — core doesn't need to ship postcss.config
41
+ * or have CSS tooling in its own dependencies.
42
+ * @returns {Object} PostCSS configuration for Vite's css.postcss option
43
+ */
44
+ export function createPostCSSConfig() {
45
+ return {
46
+ plugins: [tailwindcssPlugin(), autoprefixerPlugin()],
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Create Vite define configuration for ShellUI config injection
52
+ * App config is in __SHELLUI_CONFIG__; Sentry is in three separate globals
53
+ * (__SHELLUI_SENTRY_DSN__, __SHELLUI_SENTRY_ENVIRONMENT__, __SHELLUI_SENTRY_RELEASE__).
54
+ * @param {Object} config - Configuration object
55
+ * @returns {Object} Vite define configuration
56
+ */
57
+ export function createViteDefine(config) {
58
+ // Ensure config is serializable; omit sentry so it is only in the three Sentry globals
59
+ const serializableConfig = JSON.parse(JSON.stringify(config));
60
+ delete serializableConfig.sentry;
61
+
62
+ // Verify navigation is preserved after serialization
63
+ if (config.navigation && !serializableConfig.navigation) {
64
+ console.warn('Warning: Navigation was lost during serialization. This should not happen.');
65
+ }
66
+
67
+ const sentry = config?.sentry;
68
+ // Double-stringify: Vite's define inserts the value as-is into the code
69
+ // If we pass '{"title":"shellui"}', Vite inserts it as: const x = {"title":"shellui"}; (invalid - object literal)
70
+ // If we pass '"{\"title\":\"shellui\"}"', Vite inserts it as: const x = "{\"title\":\"shellui\"}"; (valid - string literal)
71
+ // So we need to double-stringify to ensure it's inserted as a string
72
+ const configString = JSON.stringify(JSON.stringify(serializableConfig));
73
+
74
+ return {
75
+ __SHELLUI_CONFIG__: configString,
76
+ __SHELLUI_SENTRY_DSN__: sentry?.dsn ? JSON.stringify(sentry.dsn) : 'undefined',
77
+ __SHELLUI_SENTRY_ENVIRONMENT__: sentry?.environment
78
+ ? JSON.stringify(sentry.environment)
79
+ : 'undefined',
80
+ __SHELLUI_SENTRY_RELEASE__: sentry?.release ? JSON.stringify(sentry.release) : 'undefined',
81
+ };
82
+ }
package/src/app.jsx DELETED
@@ -1,31 +0,0 @@
1
- import React from 'react';
2
- import ReactDOM from 'react-dom/client';
3
-
4
- const App = () => {
5
- // __SHELLUI_CONFIG__ is replaced by Vite at build time
6
- const config = typeof __SHELLUI_CONFIG__ !== 'undefined' ? __SHELLUI_CONFIG__ : {};
7
-
8
- return (
9
- <div style={{ fontFamily: 'system-ui, sans-serif', padding: '2rem' }}>
10
- <h1>ShellUI</h1>
11
- <p>Welcome to ShellUI</p>
12
-
13
- <div style={{
14
- marginTop: '2rem',
15
- padding: '1rem',
16
- background: '#f5f5f5',
17
- borderRadius: '8px'
18
- }}>
19
- <h2>Configuration</h2>
20
- <pre>{JSON.stringify(config, null, 2)}</pre>
21
- </div>
22
- </div>
23
- );
24
- };
25
-
26
- ReactDOM.createRoot(document.getElementById('root')).render(
27
- <React.StrictMode>
28
- <App />
29
- </React.StrictMode>
30
- );
31
-
package/src/index.html DELETED
@@ -1,12 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>ShellUI</title>
7
- </head>
8
- <body>
9
- <div id="root"></div>
10
- <script type="module" src="/app.jsx"></script>
11
- </body>
12
- </html>