@sentientui/cli 0.2.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/index.cjs +294 -0
- package/dist/index.js +273 -0
- package/package.json +30 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
11
|
+
var __defNormalProp = (obj, key2, value) => key2 in obj ? __defProp(obj, key2, { enumerable: true, configurable: true, writable: true, value }) : obj[key2] = value;
|
|
12
|
+
var __spreadValues = (a, b) => {
|
|
13
|
+
for (var prop in b || (b = {}))
|
|
14
|
+
if (__hasOwnProp.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
if (__getOwnPropSymbols)
|
|
17
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
18
|
+
if (__propIsEnum.call(b, prop))
|
|
19
|
+
__defNormalProp(a, prop, b[prop]);
|
|
20
|
+
}
|
|
21
|
+
return a;
|
|
22
|
+
};
|
|
23
|
+
var __copyProps = (to, from, except, desc) => {
|
|
24
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
25
|
+
for (let key2 of __getOwnPropNames(from))
|
|
26
|
+
if (!__hasOwnProp.call(to, key2) && key2 !== except)
|
|
27
|
+
__defProp(to, key2, { get: () => from[key2], enumerable: !(desc = __getOwnPropDesc(from, key2)) || desc.enumerable });
|
|
28
|
+
}
|
|
29
|
+
return to;
|
|
30
|
+
};
|
|
31
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
32
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
33
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
34
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
35
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
36
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
37
|
+
mod
|
|
38
|
+
));
|
|
39
|
+
|
|
40
|
+
// src/init.ts
|
|
41
|
+
var import_node_child_process = require("child_process");
|
|
42
|
+
|
|
43
|
+
// src/detect.ts
|
|
44
|
+
var import_node_fs = require("fs");
|
|
45
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
46
|
+
function readPackageJson(cwd) {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse((0, import_node_fs.readFileSync)(import_node_path.default.join(cwd, "package.json"), "utf-8"));
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function detectFramework(cwd) {
|
|
54
|
+
const pkg = readPackageJson(cwd);
|
|
55
|
+
if (!pkg) return "unknown";
|
|
56
|
+
const deps = __spreadValues(__spreadValues({}, pkg.dependencies), pkg.devDependencies);
|
|
57
|
+
const has = (dir) => (0, import_node_fs.existsSync)(import_node_path.default.join(cwd, dir));
|
|
58
|
+
if (deps["next"]) {
|
|
59
|
+
if (has("app") || has(import_node_path.default.join("src", "app"))) return "next-app";
|
|
60
|
+
if (has("pages") || has(import_node_path.default.join("src", "pages"))) return "next-pages";
|
|
61
|
+
return "next-app";
|
|
62
|
+
}
|
|
63
|
+
if (deps["@remix-run/react"] || deps["@remix-run/node"]) return "remix";
|
|
64
|
+
if (deps["react-scripts"]) return "cra";
|
|
65
|
+
if (deps["vite"]) return "vite";
|
|
66
|
+
return "unknown";
|
|
67
|
+
}
|
|
68
|
+
function detectPackageManager(cwd) {
|
|
69
|
+
if ((0, import_node_fs.existsSync)(import_node_path.default.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
70
|
+
if ((0, import_node_fs.existsSync)(import_node_path.default.join(cwd, "yarn.lock"))) return "yarn";
|
|
71
|
+
if ((0, import_node_fs.existsSync)(import_node_path.default.join(cwd, "bun.lockb")) || (0, import_node_fs.existsSync)(import_node_path.default.join(cwd, "bun.lock"))) return "bun";
|
|
72
|
+
return "npm";
|
|
73
|
+
}
|
|
74
|
+
function installCommand(pm, pkgName) {
|
|
75
|
+
switch (pm) {
|
|
76
|
+
case "pnpm":
|
|
77
|
+
return `pnpm add ${pkgName}`;
|
|
78
|
+
case "yarn":
|
|
79
|
+
return `yarn add ${pkgName}`;
|
|
80
|
+
case "bun":
|
|
81
|
+
return `bun add ${pkgName}`;
|
|
82
|
+
case "npm":
|
|
83
|
+
return `npm install ${pkgName}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/env-file.ts
|
|
88
|
+
var import_node_fs2 = require("fs");
|
|
89
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
90
|
+
function envVarName(framework) {
|
|
91
|
+
return framework === "vite" ? "VITE_SENTIENT_API_KEY" : "NEXT_PUBLIC_SENTIENT_API_KEY";
|
|
92
|
+
}
|
|
93
|
+
function writeEnvFile(cwd, framework, key2) {
|
|
94
|
+
const varName = envVarName(framework);
|
|
95
|
+
const file = import_node_path2.default.join(cwd, ".env.local");
|
|
96
|
+
const block = [
|
|
97
|
+
"",
|
|
98
|
+
"# SentientUI \u2014 leave empty for local mode (decisions are simulated on-device; nothing is sent)",
|
|
99
|
+
`${varName}=${key2 != null ? key2 : ""}`,
|
|
100
|
+
""
|
|
101
|
+
].join("\n");
|
|
102
|
+
if (!(0, import_node_fs2.existsSync)(file)) {
|
|
103
|
+
(0, import_node_fs2.writeFileSync)(file, block.trimStart(), "utf-8");
|
|
104
|
+
return "created";
|
|
105
|
+
}
|
|
106
|
+
const existing = (0, import_node_fs2.readFileSync)(file, "utf-8");
|
|
107
|
+
const assigned = existing.split(/\r?\n/).some((line) => line.trim().startsWith(`${varName}=`));
|
|
108
|
+
if (assigned) return "kept";
|
|
109
|
+
(0, import_node_fs2.appendFileSync)(file, block, "utf-8");
|
|
110
|
+
return "appended";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/scaffold.ts
|
|
114
|
+
var import_node_fs3 = require("fs");
|
|
115
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
116
|
+
var EXAMPLE = `'use client';
|
|
117
|
+
import { useAdaptiveTokens } from '@sentientui/react';
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* SentientUI example \u2014 Rung 1 (Style): a hero that adapts per visitor persona.
|
|
121
|
+
*
|
|
122
|
+
* The optimizer picks one value per dim ('tone' here) for each visitor and
|
|
123
|
+
* serializes it as data attributes; your CSS does the rest. With no API key
|
|
124
|
+
* the SDK runs in local mode: decisions are simulated deterministically so
|
|
125
|
+
* you can build and style every state before creating an account.
|
|
126
|
+
*
|
|
127
|
+
* Try it: append ?sentient_persona=buyer (or researcher / deal_seeker /
|
|
128
|
+
* browser) to any URL to preview that persona.
|
|
129
|
+
*
|
|
130
|
+
* Canonical persona CSS hooks \u2014 copy into your global stylesheet:
|
|
131
|
+
*
|
|
132
|
+
* .adaptive-hero[data-tone='urgent'] .adaptive-hero-cta { background: #dc2626; }
|
|
133
|
+
* .adaptive-hero[data-tone='calm'] .adaptive-hero-cta { background: #2563eb; }
|
|
134
|
+
*
|
|
135
|
+
* html[data-sentient-persona='buyer'] .adaptive-hero-sub::after { content: ' Start in minutes.'; }
|
|
136
|
+
* html[data-sentient-persona='researcher'] .adaptive-hero-sub::after { content: ' Compare every feature first.'; }
|
|
137
|
+
* html[data-sentient-persona='deal_seeker'] .adaptive-hero-sub::after { content: ' See what it costs.'; }
|
|
138
|
+
* html[data-sentient-persona='browser'] .adaptive-hero-sub::after { content: ' Take a look around.'; }
|
|
139
|
+
*/
|
|
140
|
+
export function AdaptiveExampleHero() {
|
|
141
|
+
const t = useAdaptiveTokens('example-hero', {
|
|
142
|
+
tone: ['calm', 'urgent'], // first value = baseline
|
|
143
|
+
});
|
|
144
|
+
return (
|
|
145
|
+
<section {...t.props} className="adaptive-hero">
|
|
146
|
+
<h1>This hero adapts to every visitor</h1>
|
|
147
|
+
<p className="adaptive-hero-sub">Current tone: {t.tokens.tone}.</p>
|
|
148
|
+
<button className="adaptive-hero-cta">Get started</button>
|
|
149
|
+
</section>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
function scaffoldExample(cwd) {
|
|
154
|
+
const base = (0, import_node_fs3.existsSync)(import_node_path3.default.join(cwd, "src")) && !(0, import_node_fs3.existsSync)(import_node_path3.default.join(cwd, "components")) ? import_node_path3.default.join(cwd, "src", "components") : import_node_path3.default.join(cwd, "components");
|
|
155
|
+
const file = import_node_path3.default.join(base, "adaptive-example.tsx");
|
|
156
|
+
if ((0, import_node_fs3.existsSync)(file)) return null;
|
|
157
|
+
(0, import_node_fs3.mkdirSync)(base, { recursive: true });
|
|
158
|
+
(0, import_node_fs3.writeFileSync)(file, EXAMPLE, "utf-8");
|
|
159
|
+
return file;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/snippets.ts
|
|
163
|
+
function wrapSnippet(framework, envVar) {
|
|
164
|
+
switch (framework) {
|
|
165
|
+
case "next-app":
|
|
166
|
+
return `// app/layout.tsx
|
|
167
|
+
import { AdaptiveProvider } from '@sentientui/react';
|
|
168
|
+
|
|
169
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
170
|
+
return (
|
|
171
|
+
<html lang="en" suppressHydrationWarning>
|
|
172
|
+
<body>
|
|
173
|
+
<AdaptiveProvider apiKey={process.env.${envVar} ?? ''} context="landing">
|
|
174
|
+
{children}
|
|
175
|
+
</AdaptiveProvider>
|
|
176
|
+
</body>
|
|
177
|
+
</html>
|
|
178
|
+
);
|
|
179
|
+
}`;
|
|
180
|
+
case "next-pages":
|
|
181
|
+
return `// pages/_app.tsx
|
|
182
|
+
import type { AppProps } from 'next/app';
|
|
183
|
+
import { AdaptiveProvider } from '@sentientui/react';
|
|
184
|
+
|
|
185
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
186
|
+
return (
|
|
187
|
+
<AdaptiveProvider apiKey={process.env.${envVar} ?? ''} context="landing">
|
|
188
|
+
<Component {...pageProps} />
|
|
189
|
+
</AdaptiveProvider>
|
|
190
|
+
);
|
|
191
|
+
}`;
|
|
192
|
+
case "vite":
|
|
193
|
+
return `// src/main.tsx
|
|
194
|
+
import { createRoot } from 'react-dom/client';
|
|
195
|
+
import { AdaptiveProvider } from '@sentientui/react';
|
|
196
|
+
import App from './App';
|
|
197
|
+
|
|
198
|
+
createRoot(document.getElementById('root')!).render(
|
|
199
|
+
<AdaptiveProvider apiKey={import.meta.env.${envVar} ?? ''} context="landing">
|
|
200
|
+
<App />
|
|
201
|
+
</AdaptiveProvider>,
|
|
202
|
+
);`;
|
|
203
|
+
case "remix":
|
|
204
|
+
return `// app/root.tsx \u2014 wrap your <Outlet /> inside <body>
|
|
205
|
+
import { AdaptiveProvider } from '@sentientui/react';
|
|
206
|
+
|
|
207
|
+
// inside the Layout/App component body:
|
|
208
|
+
<AdaptiveProvider apiKey="" context="landing">
|
|
209
|
+
<Outlet />
|
|
210
|
+
</AdaptiveProvider>`;
|
|
211
|
+
case "cra":
|
|
212
|
+
return `// src/index.tsx
|
|
213
|
+
import { createRoot } from 'react-dom/client';
|
|
214
|
+
import { AdaptiveProvider } from '@sentientui/react';
|
|
215
|
+
import App from './App';
|
|
216
|
+
|
|
217
|
+
createRoot(document.getElementById('root')!).render(
|
|
218
|
+
<AdaptiveProvider apiKey={process.env.${envVar} ?? ''} context="landing">
|
|
219
|
+
<App />
|
|
220
|
+
</AdaptiveProvider>,
|
|
221
|
+
);`;
|
|
222
|
+
case "unknown":
|
|
223
|
+
return unknownInstructions();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function unknownInstructions() {
|
|
227
|
+
return `[sentientui] could not detect a supported framework (Next.js, Vite, Remix, CRA).
|
|
228
|
+
Manual setup:
|
|
229
|
+
1. Install the SDK: npm install @sentientui/react
|
|
230
|
+
2. Wrap your app root in: <AdaptiveProvider apiKey="" context="landing">\u2026</AdaptiveProvider>
|
|
231
|
+
3. Leave the key empty for local mode; decisions are simulated on-device.
|
|
232
|
+
4. Open your app with ?sentient_persona=buyer to preview a persona.`;
|
|
233
|
+
}
|
|
234
|
+
function finale(framework) {
|
|
235
|
+
const port = framework === "vite" ? 5173 : 3e3;
|
|
236
|
+
return `open http://localhost:${port}?sentient_persona=buyer`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/init.ts
|
|
240
|
+
function runInit(opts) {
|
|
241
|
+
var _a, _b, _c;
|
|
242
|
+
const log = (_a = opts.log) != null ? _a : ((line) => console.log(line));
|
|
243
|
+
const exec = (_b = opts.exec) != null ? _b : ((command2, cwd) => (0, import_node_child_process.execSync)(command2, { cwd, stdio: "inherit" }));
|
|
244
|
+
const framework = detectFramework(opts.cwd);
|
|
245
|
+
log(`[sentientui] detected framework: ${framework}`);
|
|
246
|
+
if (framework === "unknown") {
|
|
247
|
+
log(unknownInstructions());
|
|
248
|
+
return { framework };
|
|
249
|
+
}
|
|
250
|
+
const pm = detectPackageManager(opts.cwd);
|
|
251
|
+
const install = installCommand(pm, "@sentientui/react");
|
|
252
|
+
log(`[sentientui] installing @sentientui/react (${install})`);
|
|
253
|
+
exec(install, opts.cwd);
|
|
254
|
+
const envResult = writeEnvFile(opts.cwd, framework, opts.key);
|
|
255
|
+
log(
|
|
256
|
+
`[sentientui] .env.local ${envResult}: ${envVarName(framework)}=${(_c = opts.key) != null ? _c : "(empty \u2014 local mode)"}`
|
|
257
|
+
);
|
|
258
|
+
const scaffolded = scaffoldExample(opts.cwd);
|
|
259
|
+
log(
|
|
260
|
+
scaffolded ? `[sentientui] scaffolded ${scaffolded}` : "[sentientui] adaptive-example.tsx already exists \u2014 left untouched"
|
|
261
|
+
);
|
|
262
|
+
log("");
|
|
263
|
+
log("Wrap your app:");
|
|
264
|
+
log(wrapSnippet(framework, envVarName(framework)));
|
|
265
|
+
log("");
|
|
266
|
+
log(`Then run your dev server and: ${finale(framework)}`);
|
|
267
|
+
return { framework };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/index.ts
|
|
271
|
+
function parseArgs(argv) {
|
|
272
|
+
const args = [...argv];
|
|
273
|
+
const command2 = args.shift();
|
|
274
|
+
let key2;
|
|
275
|
+
for (let i = 0; i < args.length; i++) {
|
|
276
|
+
if (args[i] === "--key") {
|
|
277
|
+
key2 = args[i + 1];
|
|
278
|
+
i++;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return { command: command2, key: key2 };
|
|
282
|
+
}
|
|
283
|
+
var { command, key } = parseArgs(process.argv.slice(2));
|
|
284
|
+
if (command === "init") {
|
|
285
|
+
try {
|
|
286
|
+
runInit({ cwd: process.cwd(), key });
|
|
287
|
+
} catch (err) {
|
|
288
|
+
console.error(`[sentientui] init failed: ${String(err)}`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
console.log("Usage: npx sentientui init [--key pk_...] [--yes]");
|
|
293
|
+
process.exit(command ? 1 : 0);
|
|
294
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
6
|
+
var __defNormalProp = (obj, key2, value) => key2 in obj ? __defProp(obj, key2, { enumerable: true, configurable: true, writable: true, value }) : obj[key2] = value;
|
|
7
|
+
var __spreadValues = (a, b) => {
|
|
8
|
+
for (var prop in b || (b = {}))
|
|
9
|
+
if (__hasOwnProp.call(b, prop))
|
|
10
|
+
__defNormalProp(a, prop, b[prop]);
|
|
11
|
+
if (__getOwnPropSymbols)
|
|
12
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
13
|
+
if (__propIsEnum.call(b, prop))
|
|
14
|
+
__defNormalProp(a, prop, b[prop]);
|
|
15
|
+
}
|
|
16
|
+
return a;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/init.ts
|
|
20
|
+
import { execSync } from "child_process";
|
|
21
|
+
|
|
22
|
+
// src/detect.ts
|
|
23
|
+
import { existsSync, readFileSync } from "fs";
|
|
24
|
+
import path from "path";
|
|
25
|
+
function readPackageJson(cwd) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(path.join(cwd, "package.json"), "utf-8"));
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function detectFramework(cwd) {
|
|
33
|
+
const pkg = readPackageJson(cwd);
|
|
34
|
+
if (!pkg) return "unknown";
|
|
35
|
+
const deps = __spreadValues(__spreadValues({}, pkg.dependencies), pkg.devDependencies);
|
|
36
|
+
const has = (dir) => existsSync(path.join(cwd, dir));
|
|
37
|
+
if (deps["next"]) {
|
|
38
|
+
if (has("app") || has(path.join("src", "app"))) return "next-app";
|
|
39
|
+
if (has("pages") || has(path.join("src", "pages"))) return "next-pages";
|
|
40
|
+
return "next-app";
|
|
41
|
+
}
|
|
42
|
+
if (deps["@remix-run/react"] || deps["@remix-run/node"]) return "remix";
|
|
43
|
+
if (deps["react-scripts"]) return "cra";
|
|
44
|
+
if (deps["vite"]) return "vite";
|
|
45
|
+
return "unknown";
|
|
46
|
+
}
|
|
47
|
+
function detectPackageManager(cwd) {
|
|
48
|
+
if (existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
49
|
+
if (existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
|
|
50
|
+
if (existsSync(path.join(cwd, "bun.lockb")) || existsSync(path.join(cwd, "bun.lock"))) return "bun";
|
|
51
|
+
return "npm";
|
|
52
|
+
}
|
|
53
|
+
function installCommand(pm, pkgName) {
|
|
54
|
+
switch (pm) {
|
|
55
|
+
case "pnpm":
|
|
56
|
+
return `pnpm add ${pkgName}`;
|
|
57
|
+
case "yarn":
|
|
58
|
+
return `yarn add ${pkgName}`;
|
|
59
|
+
case "bun":
|
|
60
|
+
return `bun add ${pkgName}`;
|
|
61
|
+
case "npm":
|
|
62
|
+
return `npm install ${pkgName}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/env-file.ts
|
|
67
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, appendFileSync, writeFileSync } from "fs";
|
|
68
|
+
import path2 from "path";
|
|
69
|
+
function envVarName(framework) {
|
|
70
|
+
return framework === "vite" ? "VITE_SENTIENT_API_KEY" : "NEXT_PUBLIC_SENTIENT_API_KEY";
|
|
71
|
+
}
|
|
72
|
+
function writeEnvFile(cwd, framework, key2) {
|
|
73
|
+
const varName = envVarName(framework);
|
|
74
|
+
const file = path2.join(cwd, ".env.local");
|
|
75
|
+
const block = [
|
|
76
|
+
"",
|
|
77
|
+
"# SentientUI \u2014 leave empty for local mode (decisions are simulated on-device; nothing is sent)",
|
|
78
|
+
`${varName}=${key2 != null ? key2 : ""}`,
|
|
79
|
+
""
|
|
80
|
+
].join("\n");
|
|
81
|
+
if (!existsSync2(file)) {
|
|
82
|
+
writeFileSync(file, block.trimStart(), "utf-8");
|
|
83
|
+
return "created";
|
|
84
|
+
}
|
|
85
|
+
const existing = readFileSync2(file, "utf-8");
|
|
86
|
+
const assigned = existing.split(/\r?\n/).some((line) => line.trim().startsWith(`${varName}=`));
|
|
87
|
+
if (assigned) return "kept";
|
|
88
|
+
appendFileSync(file, block, "utf-8");
|
|
89
|
+
return "appended";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/scaffold.ts
|
|
93
|
+
import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
94
|
+
import path3 from "path";
|
|
95
|
+
var EXAMPLE = `'use client';
|
|
96
|
+
import { useAdaptiveTokens } from '@sentientui/react';
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* SentientUI example \u2014 Rung 1 (Style): a hero that adapts per visitor persona.
|
|
100
|
+
*
|
|
101
|
+
* The optimizer picks one value per dim ('tone' here) for each visitor and
|
|
102
|
+
* serializes it as data attributes; your CSS does the rest. With no API key
|
|
103
|
+
* the SDK runs in local mode: decisions are simulated deterministically so
|
|
104
|
+
* you can build and style every state before creating an account.
|
|
105
|
+
*
|
|
106
|
+
* Try it: append ?sentient_persona=buyer (or researcher / deal_seeker /
|
|
107
|
+
* browser) to any URL to preview that persona.
|
|
108
|
+
*
|
|
109
|
+
* Canonical persona CSS hooks \u2014 copy into your global stylesheet:
|
|
110
|
+
*
|
|
111
|
+
* .adaptive-hero[data-tone='urgent'] .adaptive-hero-cta { background: #dc2626; }
|
|
112
|
+
* .adaptive-hero[data-tone='calm'] .adaptive-hero-cta { background: #2563eb; }
|
|
113
|
+
*
|
|
114
|
+
* html[data-sentient-persona='buyer'] .adaptive-hero-sub::after { content: ' Start in minutes.'; }
|
|
115
|
+
* html[data-sentient-persona='researcher'] .adaptive-hero-sub::after { content: ' Compare every feature first.'; }
|
|
116
|
+
* html[data-sentient-persona='deal_seeker'] .adaptive-hero-sub::after { content: ' See what it costs.'; }
|
|
117
|
+
* html[data-sentient-persona='browser'] .adaptive-hero-sub::after { content: ' Take a look around.'; }
|
|
118
|
+
*/
|
|
119
|
+
export function AdaptiveExampleHero() {
|
|
120
|
+
const t = useAdaptiveTokens('example-hero', {
|
|
121
|
+
tone: ['calm', 'urgent'], // first value = baseline
|
|
122
|
+
});
|
|
123
|
+
return (
|
|
124
|
+
<section {...t.props} className="adaptive-hero">
|
|
125
|
+
<h1>This hero adapts to every visitor</h1>
|
|
126
|
+
<p className="adaptive-hero-sub">Current tone: {t.tokens.tone}.</p>
|
|
127
|
+
<button className="adaptive-hero-cta">Get started</button>
|
|
128
|
+
</section>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
`;
|
|
132
|
+
function scaffoldExample(cwd) {
|
|
133
|
+
const base = existsSync3(path3.join(cwd, "src")) && !existsSync3(path3.join(cwd, "components")) ? path3.join(cwd, "src", "components") : path3.join(cwd, "components");
|
|
134
|
+
const file = path3.join(base, "adaptive-example.tsx");
|
|
135
|
+
if (existsSync3(file)) return null;
|
|
136
|
+
mkdirSync(base, { recursive: true });
|
|
137
|
+
writeFileSync2(file, EXAMPLE, "utf-8");
|
|
138
|
+
return file;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/snippets.ts
|
|
142
|
+
function wrapSnippet(framework, envVar) {
|
|
143
|
+
switch (framework) {
|
|
144
|
+
case "next-app":
|
|
145
|
+
return `// app/layout.tsx
|
|
146
|
+
import { AdaptiveProvider } from '@sentientui/react';
|
|
147
|
+
|
|
148
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
149
|
+
return (
|
|
150
|
+
<html lang="en" suppressHydrationWarning>
|
|
151
|
+
<body>
|
|
152
|
+
<AdaptiveProvider apiKey={process.env.${envVar} ?? ''} context="landing">
|
|
153
|
+
{children}
|
|
154
|
+
</AdaptiveProvider>
|
|
155
|
+
</body>
|
|
156
|
+
</html>
|
|
157
|
+
);
|
|
158
|
+
}`;
|
|
159
|
+
case "next-pages":
|
|
160
|
+
return `// pages/_app.tsx
|
|
161
|
+
import type { AppProps } from 'next/app';
|
|
162
|
+
import { AdaptiveProvider } from '@sentientui/react';
|
|
163
|
+
|
|
164
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
165
|
+
return (
|
|
166
|
+
<AdaptiveProvider apiKey={process.env.${envVar} ?? ''} context="landing">
|
|
167
|
+
<Component {...pageProps} />
|
|
168
|
+
</AdaptiveProvider>
|
|
169
|
+
);
|
|
170
|
+
}`;
|
|
171
|
+
case "vite":
|
|
172
|
+
return `// src/main.tsx
|
|
173
|
+
import { createRoot } from 'react-dom/client';
|
|
174
|
+
import { AdaptiveProvider } from '@sentientui/react';
|
|
175
|
+
import App from './App';
|
|
176
|
+
|
|
177
|
+
createRoot(document.getElementById('root')!).render(
|
|
178
|
+
<AdaptiveProvider apiKey={import.meta.env.${envVar} ?? ''} context="landing">
|
|
179
|
+
<App />
|
|
180
|
+
</AdaptiveProvider>,
|
|
181
|
+
);`;
|
|
182
|
+
case "remix":
|
|
183
|
+
return `// app/root.tsx \u2014 wrap your <Outlet /> inside <body>
|
|
184
|
+
import { AdaptiveProvider } from '@sentientui/react';
|
|
185
|
+
|
|
186
|
+
// inside the Layout/App component body:
|
|
187
|
+
<AdaptiveProvider apiKey="" context="landing">
|
|
188
|
+
<Outlet />
|
|
189
|
+
</AdaptiveProvider>`;
|
|
190
|
+
case "cra":
|
|
191
|
+
return `// src/index.tsx
|
|
192
|
+
import { createRoot } from 'react-dom/client';
|
|
193
|
+
import { AdaptiveProvider } from '@sentientui/react';
|
|
194
|
+
import App from './App';
|
|
195
|
+
|
|
196
|
+
createRoot(document.getElementById('root')!).render(
|
|
197
|
+
<AdaptiveProvider apiKey={process.env.${envVar} ?? ''} context="landing">
|
|
198
|
+
<App />
|
|
199
|
+
</AdaptiveProvider>,
|
|
200
|
+
);`;
|
|
201
|
+
case "unknown":
|
|
202
|
+
return unknownInstructions();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function unknownInstructions() {
|
|
206
|
+
return `[sentientui] could not detect a supported framework (Next.js, Vite, Remix, CRA).
|
|
207
|
+
Manual setup:
|
|
208
|
+
1. Install the SDK: npm install @sentientui/react
|
|
209
|
+
2. Wrap your app root in: <AdaptiveProvider apiKey="" context="landing">\u2026</AdaptiveProvider>
|
|
210
|
+
3. Leave the key empty for local mode; decisions are simulated on-device.
|
|
211
|
+
4. Open your app with ?sentient_persona=buyer to preview a persona.`;
|
|
212
|
+
}
|
|
213
|
+
function finale(framework) {
|
|
214
|
+
const port = framework === "vite" ? 5173 : 3e3;
|
|
215
|
+
return `open http://localhost:${port}?sentient_persona=buyer`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/init.ts
|
|
219
|
+
function runInit(opts) {
|
|
220
|
+
var _a, _b, _c;
|
|
221
|
+
const log = (_a = opts.log) != null ? _a : ((line) => console.log(line));
|
|
222
|
+
const exec = (_b = opts.exec) != null ? _b : ((command2, cwd) => execSync(command2, { cwd, stdio: "inherit" }));
|
|
223
|
+
const framework = detectFramework(opts.cwd);
|
|
224
|
+
log(`[sentientui] detected framework: ${framework}`);
|
|
225
|
+
if (framework === "unknown") {
|
|
226
|
+
log(unknownInstructions());
|
|
227
|
+
return { framework };
|
|
228
|
+
}
|
|
229
|
+
const pm = detectPackageManager(opts.cwd);
|
|
230
|
+
const install = installCommand(pm, "@sentientui/react");
|
|
231
|
+
log(`[sentientui] installing @sentientui/react (${install})`);
|
|
232
|
+
exec(install, opts.cwd);
|
|
233
|
+
const envResult = writeEnvFile(opts.cwd, framework, opts.key);
|
|
234
|
+
log(
|
|
235
|
+
`[sentientui] .env.local ${envResult}: ${envVarName(framework)}=${(_c = opts.key) != null ? _c : "(empty \u2014 local mode)"}`
|
|
236
|
+
);
|
|
237
|
+
const scaffolded = scaffoldExample(opts.cwd);
|
|
238
|
+
log(
|
|
239
|
+
scaffolded ? `[sentientui] scaffolded ${scaffolded}` : "[sentientui] adaptive-example.tsx already exists \u2014 left untouched"
|
|
240
|
+
);
|
|
241
|
+
log("");
|
|
242
|
+
log("Wrap your app:");
|
|
243
|
+
log(wrapSnippet(framework, envVarName(framework)));
|
|
244
|
+
log("");
|
|
245
|
+
log(`Then run your dev server and: ${finale(framework)}`);
|
|
246
|
+
return { framework };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/index.ts
|
|
250
|
+
function parseArgs(argv) {
|
|
251
|
+
const args = [...argv];
|
|
252
|
+
const command2 = args.shift();
|
|
253
|
+
let key2;
|
|
254
|
+
for (let i = 0; i < args.length; i++) {
|
|
255
|
+
if (args[i] === "--key") {
|
|
256
|
+
key2 = args[i + 1];
|
|
257
|
+
i++;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return { command: command2, key: key2 };
|
|
261
|
+
}
|
|
262
|
+
var { command, key } = parseArgs(process.argv.slice(2));
|
|
263
|
+
if (command === "init") {
|
|
264
|
+
try {
|
|
265
|
+
runInit({ cwd: process.cwd(), key });
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.error(`[sentientui] init failed: ${String(err)}`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
console.log("Usage: npx sentientui init [--key pk_...] [--yes]");
|
|
272
|
+
process.exit(command ? 1 : 0);
|
|
273
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sentientui/cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "SentientUI CLI — `npx sentientui init` adds adaptive UI to an existing app in one command",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sentientui": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.10.2",
|
|
19
|
+
"tsup": "^8.3.5",
|
|
20
|
+
"tsx": "^4.19.2",
|
|
21
|
+
"typescript": "^5.7.2",
|
|
22
|
+
"vitest": "^2.1.8"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"dev": "tsx src/index.ts"
|
|
29
|
+
}
|
|
30
|
+
}
|