@sensolus/create-snt-agent-app 0.1.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/index.js ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * npm create @sensolus/snt-agent-app my-app
4
+ *
5
+ * Copies the template into ./my-app, substituting {{APP_NAME}}.
6
+ * Files prefixed with _ are renamed to dotfiles (npm publish strips real dotfiles).
7
+ */
8
+ import { cp, readdir, readFile, writeFile, rename, stat } from 'node:fs/promises'
9
+ import { existsSync } from 'node:fs'
10
+ import path from 'node:path'
11
+ import { fileURLToPath } from 'node:url'
12
+
13
+ const templateDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'template')
14
+
15
+ const rawName = process.argv[2]
16
+ if (!rawName) {
17
+ console.error('Usage: npm create @sensolus/snt-agent-app <app-name>')
18
+ process.exit(1)
19
+ }
20
+ const appName = rawName.toLowerCase().replace(/[^a-z0-9-_]/g, '-')
21
+ const targetDir = path.resolve(process.cwd(), appName)
22
+
23
+ if (existsSync(targetDir)) {
24
+ console.error(`Error: directory ${appName} already exists.`)
25
+ process.exit(1)
26
+ }
27
+
28
+ await cp(templateDir, targetDir, { recursive: true })
29
+
30
+ // Rename _gitignore -> .gitignore etc.
31
+ for (const entry of await readdir(targetDir)) {
32
+ if (entry.startsWith('_')) {
33
+ await rename(path.join(targetDir, entry), path.join(targetDir, '.' + entry.slice(1)))
34
+ }
35
+ }
36
+
37
+ // Substitute {{APP_NAME}} in text files
38
+ async function substitute(dir) {
39
+ for (const entry of await readdir(dir)) {
40
+ const p = path.join(dir, entry)
41
+ if ((await stat(p)).isDirectory()) { await substitute(p); continue }
42
+ if (!/\.(json|js|jsx|html|md|py|txt|sh|css)$|^\.[a-z]+$/i.test(entry)) continue
43
+ const content = await readFile(p, 'utf8')
44
+ if (content.includes('{{APP_NAME}}')) {
45
+ await writeFile(p, content.replaceAll('{{APP_NAME}}', appName))
46
+ }
47
+ }
48
+ }
49
+ await substitute(targetDir)
50
+
51
+ console.log(`
52
+ Created ${appName}/
53
+
54
+ Next steps:
55
+ cd ${appName}
56
+ npm install
57
+ pip install -r backend/requirements.txt
58
+ npm run dev # frontend on :3000
59
+ python backend/app.py # backend on :5000 (separate terminal)
60
+
61
+ Rules: widgets/theme/i18n come from @sensolus/snt-agent-kit — import, don't copy.
62
+ ESLint enforces no deep imports and no Snt* re-declarations.
63
+ `)
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@sensolus/create-snt-agent-app",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a new Sensolus agent app: React frontend wired to @sensolus/snt-agent-kit + Flask API proxy backend.",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-snt-agent-app": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "template"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "license": "UNLICENSED"
17
+ }
@@ -0,0 +1,26 @@
1
+ # {{APP_NAME}}
2
+
3
+ Sensolus agent app, generated by `@sensolus/create-snt-agent-app`.
4
+
5
+ ## Run
6
+
7
+ ```bash
8
+ npm install
9
+ pip install -r backend/requirements.txt
10
+ npm run dev # frontend :3000 (proxies /api -> :5000)
11
+ python backend/app.py # backend :5000, separate terminal
12
+ ```
13
+
14
+ ## Rules
15
+
16
+ - UI widgets, theme, colors and i18n framework come from **`@sensolus/snt-agent-kit`** —
17
+ import them, never copy their source into this repo.
18
+ - `npm run lint` enforces: no deep imports into the kit, no re-declaring `Snt*` components.
19
+ - Need widget customization? Use slots/render props; if none fits, PR the
20
+ `snt-agent-kit` repo. There is no eject path.
21
+ - Kit updates: `npm update @sensolus/snt-agent-kit`.
22
+
23
+ ## i18n
24
+
25
+ App keys live in `src/i18n/messages.js`, merged via `<LocaleProvider messages>`.
26
+ Common strings (`common.*`, `table.*`) ship with the kit.
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ dist/
3
+ *.log
4
+ .env
5
+ __pycache__/
6
+ .venv/
7
+ .DS_Store
@@ -0,0 +1,70 @@
1
+ """
2
+ {{APP_NAME}} backend — Flask proxy to the Sensolus public API.
3
+
4
+ Avoids CORS by forwarding /api/* to https://<SENSOLUS_DOMAIN>/rest/api/v2/*.
5
+
6
+ Auth (same contract as sample_micro_app):
7
+ 1. Session cookie `prod-sensolus-token` -> Authorization: Bearer <token>
8
+ 2. SENSOLUS_API_KEY env var -> ?apiKey=<key> query parameter
9
+ Cookie wins if both are present.
10
+ """
11
+ import os
12
+
13
+ import requests
14
+ from flask import Flask, jsonify, request, send_from_directory
15
+
16
+ PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17
+
18
+ SENSOLUS_DOMAIN = os.environ.get("SENSOLUS_DOMAIN", "cloud.sensolus.com")
19
+ SENSOLUS_BASE_URL = f"https://{SENSOLUS_DOMAIN}/rest/api/v2"
20
+ SENSOLUS_COOKIE_NAME = os.environ.get("SENSOLUS_COOKIE_NAME", "prod-sensolus-token")
21
+ SENSOLUS_API_KEY = os.environ.get("SENSOLUS_API_KEY")
22
+
23
+ app = Flask(__name__, static_folder=os.path.join(PROJECT_ROOT, "dist"))
24
+
25
+
26
+ def _auth(params, headers):
27
+ """Apply Sensolus auth to outgoing request parts. Cookie beats API key."""
28
+ token = request.cookies.get(SENSOLUS_COOKIE_NAME)
29
+ if token:
30
+ headers["Authorization"] = f"Bearer {token}"
31
+ elif SENSOLUS_API_KEY:
32
+ params["apiKey"] = SENSOLUS_API_KEY
33
+ return params, headers
34
+
35
+
36
+ @app.route("/api/loginInfo")
37
+ def login_info():
38
+ """User preferences for the kit's LocaleProvider (language, timezone, units)."""
39
+ params, headers = _auth({}, {})
40
+ try:
41
+ resp = requests.get(f"{SENSOLUS_BASE_URL}/loginInfo", params=params, headers=headers, timeout=10)
42
+ if resp.ok:
43
+ return jsonify(resp.json())
44
+ except requests.RequestException:
45
+ pass
46
+ return jsonify({}) # kit falls back to defaults
47
+
48
+
49
+ @app.route("/api/<path:subpath>")
50
+ def api_proxy(subpath):
51
+ """Generic GET proxy to the Sensolus API."""
52
+ params, headers = _auth(dict(request.args), {})
53
+ try:
54
+ resp = requests.get(f"{SENSOLUS_BASE_URL}/{subpath}", params=params, headers=headers, timeout=30)
55
+ return (resp.content, resp.status_code, {"Content-Type": resp.headers.get("Content-Type", "application/json")})
56
+ except requests.RequestException as exc:
57
+ return jsonify({"error": str(exc)}), 502
58
+
59
+
60
+ # --- Serve built frontend (production) ---------------------------------------
61
+ @app.route("/", defaults={"path": ""})
62
+ @app.route("/<path:path>")
63
+ def serve_frontend(path):
64
+ if path and os.path.exists(os.path.join(app.static_folder, path)):
65
+ return send_from_directory(app.static_folder, path)
66
+ return send_from_directory(app.static_folder, "index.html")
67
+
68
+
69
+ if __name__ == "__main__":
70
+ app.run(host="0.0.0.0", port=5000, debug=True)
@@ -0,0 +1,2 @@
1
+ flask>=3.0
2
+ requests>=2.31
@@ -0,0 +1,45 @@
1
+ import js from '@eslint/js'
2
+ import react from 'eslint-plugin-react'
3
+
4
+ /**
5
+ * Guardrails for the locked @sensolus/snt-agent-kit (do not remove):
6
+ * 1. no deep imports into the kit's dist/
7
+ * 2. no re-declaring Snt* components in app code (use kit slots/props, or PR the kit)
8
+ */
9
+ export default [
10
+ js.configs.recommended,
11
+ {
12
+ files: ['src/**/*.{js,jsx}'],
13
+ languageOptions: {
14
+ ecmaVersion: 'latest',
15
+ sourceType: 'module',
16
+ parserOptions: { ecmaFeatures: { jsx: true } },
17
+ globals: { fetch: 'readonly', console: 'readonly', window: 'readonly', document: 'readonly' },
18
+ },
19
+ plugins: { react },
20
+ rules: {
21
+ 'react/jsx-uses-react': 'error',
22
+ 'react/jsx-uses-vars': 'error',
23
+ 'no-restricted-imports': ['error', {
24
+ patterns: [{
25
+ group: ['@sensolus/snt-agent-kit/*', '!@sensolus/snt-agent-kit/theme.css'],
26
+ message: 'Deep imports into @sensolus/snt-agent-kit are forbidden. Import from the package root (theme.css is the only allowed subpath).',
27
+ }],
28
+ }],
29
+ 'no-restricted-syntax': ['error',
30
+ {
31
+ selector: 'FunctionDeclaration[id.name=/^Snt[A-Z]/]',
32
+ message: 'Do not re-declare Snt* components in app code. Use kit slots/render props, or open a PR on the agent-kit.',
33
+ },
34
+ {
35
+ selector: 'VariableDeclarator[id.name=/^Snt[A-Z]/]',
36
+ message: 'Do not re-declare Snt* components in app code. Use kit slots/render props, or open a PR on the agent-kit.',
37
+ },
38
+ {
39
+ selector: 'ClassDeclaration[id.name=/^Snt[A-Z]/]',
40
+ message: 'Do not re-declare Snt* components in app code. Use kit slots/render props, or open a PR on the agent-kit.',
41
+ },
42
+ ],
43
+ },
44
+ },
45
+ ]
@@ -0,0 +1,12 @@
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>{{APP_NAME}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.jsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "{{APP_NAME}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "lint": "eslint src"
11
+ },
12
+ "dependencies": {
13
+ "@sensolus/snt-agent-kit": "^0.1.0",
14
+ "react": "^19.2.3",
15
+ "react-dom": "^19.2.3",
16
+ "react-router-dom": "^7.1.1"
17
+ },
18
+ "devDependencies": {
19
+ "@eslint/js": "^9.18.0",
20
+ "@vitejs/plugin-react": "^5.1.2",
21
+ "eslint": "^9.18.0",
22
+ "eslint-plugin-react": "^7.37.0",
23
+ "vite": "^7.3.1"
24
+ }
25
+ }
@@ -0,0 +1,38 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { SntPageHeader, SntTable, SntBadge, SntButton, SntToolbar, SntLoadingOverlay, useLocale } from '@sensolus/snt-agent-kit'
3
+
4
+ export default function App() {
5
+ const { t } = useLocale()
6
+ const [orgs, setOrgs] = useState(null)
7
+ const [loading, setLoading] = useState(false)
8
+
9
+ async function load() {
10
+ setLoading(true)
11
+ try {
12
+ const res = await fetch('/api/organisations')
13
+ setOrgs(res.ok ? await res.json() : [])
14
+ } finally {
15
+ setLoading(false)
16
+ }
17
+ }
18
+
19
+ useEffect(() => { load() }, [])
20
+
21
+ if (loading || orgs === null) return <SntLoadingOverlay message={t('common.loading')} />
22
+
23
+ return (
24
+ <div className="app-container">
25
+ <SntPageHeader title={t('app.title')} />
26
+ <SntToolbar>
27
+ <SntButton variant="secondary" onClick={load}>{t('common.reload')}</SntButton>
28
+ </SntToolbar>
29
+ <SntTable
30
+ data={orgs}
31
+ columns={[
32
+ { key: 'name', header: t('app.org.name') },
33
+ { key: 'status', header: t('app.org.status'), render: (row, val) => <SntBadge variant={val === 'ACTIVE' ? 'success' : 'secondary'} text={String(val ?? '')} /> },
34
+ ]}
35
+ />
36
+ </div>
37
+ )
38
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * App-owned translation keys, merged into the kit's common strings via
3
+ * <LocaleProvider messages={messages}>. Add languages/keys as needed;
4
+ * English is the fallback for missing keys.
5
+ */
6
+ export const messages = {
7
+ en: {
8
+ 'app.title': '{{APP_NAME}}',
9
+ 'app.org.name': 'Name',
10
+ 'app.org.status': 'Status',
11
+ },
12
+ nl: {
13
+ 'app.title': '{{APP_NAME}}',
14
+ 'app.org.name': 'Naam',
15
+ 'app.org.status': 'Status',
16
+ },
17
+ }
@@ -0,0 +1,15 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import { LocaleProvider } from '@sensolus/snt-agent-kit'
4
+ import '@sensolus/snt-agent-kit/theme.css'
5
+ import App from './App'
6
+ import { messages } from './i18n/messages'
7
+ import './styles/app.css'
8
+
9
+ ReactDOM.createRoot(document.getElementById('root')).render(
10
+ <React.StrictMode>
11
+ <LocaleProvider messages={messages}>
12
+ <App />
13
+ </LocaleProvider>
14
+ </React.StrictMode>
15
+ )
@@ -0,0 +1,6 @@
1
+ /* App-specific styles. Theme variables (--snt-*) come from @sensolus/snt-agent-kit/theme.css */
2
+ .app-container {
3
+ max-width: 1200px;
4
+ margin: 0 auto;
5
+ padding: 16px;
6
+ }
@@ -0,0 +1,19 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 3000,
8
+ host: true,
9
+ proxy: {
10
+ '/api': {
11
+ target: 'http://localhost:5000',
12
+ changeOrigin: true,
13
+ },
14
+ },
15
+ },
16
+ build: {
17
+ outDir: 'dist',
18
+ },
19
+ })