@unoverse-platform/studio 0.1.52 → 0.1.54

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/bin/studio.mjs CHANGED
@@ -21,6 +21,7 @@ import { dirname, resolve, join, basename } from "path";
21
21
  import { fileURLToPath } from "url";
22
22
  import { existsSync, readdirSync } from "fs";
23
23
  import { createInterface } from "readline/promises";
24
+ import { execSync } from "child_process";
24
25
  import { designHome, hasDesignHome } from "../local/design-home.mjs";
25
26
  import { createFirstProject, checkProjectName } from "../local/scaffold.mjs";
26
27
 
@@ -163,6 +164,54 @@ const projectRoot = findProjectRootFromCwd();
163
164
  process.env.UNOVERSE_HOME = projectRoot;
164
165
  console.log(`\n Studio reading: ${designHome(projectRoot)}\n`);
165
166
 
167
+ /**
168
+ * LAUNCHING STUDIO IS THE DECISION; A STALE STUDIO YIELDS THE PORT.
169
+ *
170
+ * Studio is always 4108 with strictPort, so the address in your browser's history is
171
+ * always the right one. The price of one fixed port used to be a raw vite "Port 4108
172
+ * is already in use" whenever a previous Studio was still running — the tab from the
173
+ * last session, forgotten, holding the port for a project you have moved on from.
174
+ * Typing `unoverse studio` again IS the answer to "which one do you want": the old
175
+ * instance is stopped and this one takes the port.
176
+ *
177
+ * ONLY a recognisable Studio is stopped. Anything else on 4108 is somebody's own
178
+ * process, and killing it on sight would be worse than the error, so that case names
179
+ * the process and stops.
180
+ */
181
+ function freePortOfStaleStudio(port) {
182
+ let pids = [];
183
+ try {
184
+ pids = execSync(`lsof -nP -ti tcp:${port} -s tcp:LISTEN`, { stdio: ["ignore", "pipe", "ignore"] })
185
+ .toString().trim().split("\n").filter(Boolean);
186
+ } catch {
187
+ return; // nothing listening, or no lsof: let vite find out
188
+ }
189
+ for (const pid of pids) {
190
+ if (pid === String(process.pid)) continue;
191
+ let command = "";
192
+ try {
193
+ command = execSync(`ps -p ${pid} -o command=`, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
194
+ } catch { continue; } // already gone
195
+ if (/unoverse-studio|@unoverse-platform\/studio|packages\/studio/.test(command)) {
196
+ console.log(` A previous Studio was still holding :${port}. Stopping it and taking over.`);
197
+ try { process.kill(Number(pid), "SIGTERM"); } catch { /* already gone */ }
198
+ // SIGTERM first so it closes cleanly; escalate only if it lingers.
199
+ const deadline = Date.now() + 3000;
200
+ for (;;) {
201
+ try { process.kill(Number(pid), 0); } catch { break; } // no longer exists
202
+ if (Date.now() > deadline) { try { process.kill(Number(pid), "SIGKILL"); } catch { /* gone */ } break; }
203
+ execSync("sleep 0.2");
204
+ }
205
+ } else {
206
+ console.error(`\n Port ${port} is held by something that is not Studio:\n ${command}\n`);
207
+ console.error(` Stop it (kill ${pid}) or find out what it is (lsof -nP -i :${port}), then run unoverse studio again.\n`);
208
+ process.exit(1);
209
+ }
210
+ }
211
+ }
212
+
213
+ freePortOfStaleStudio(4108);
214
+
166
215
  const server = await createServer({
167
216
  // The APP is the vite root; the developer's project is found by walking up from the
168
217
  // working directory (findProjectRoot), so Studio works from anywhere inside it.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: Concise Answers
3
+ description: Keeps replies short, direct, and free of filler — the answer first, detail only on request
4
+ tags: [formatting, tone]
5
+ ---
6
+
7
+ <!-- A WORKED EXAMPLE prompt block, shipped with Studio so prompts/blocks/ is never an
8
+ empty folder. Edit it, copy it, or delete it — it is yours. A block is a reusable
9
+ prompt fragment workflows compose into an agent's system prompt: smaller than a
10
+ skill, no routing of its own, and it must stand alone (no references to other
11
+ blocks). The folder above it is its category. -->
12
+
13
+ Answer the question first, in one or two sentences, before any background.
14
+
15
+ Keep every reply as short as it can be while still being complete. Cut greetings,
16
+ restatements of the question, and closing filler ("I hope this helps").
17
+
18
+ Use plain words over formal ones. Prefer an example over an explanation when one
19
+ example makes the point.
20
+
21
+ If the full answer would be long, give the short version and offer the detail:
22
+ "Want the full breakdown?"
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: sample-complaint-handling
3
+ title: Complaint Handling
4
+ description: How the assistant responds when a customer is unhappy, from first reply to escalation
5
+ whenToUse: Handle an unhappy customer — a complaint, frustration with the product or service, a request for a refund, or a demand to speak to a person. Not for neutral questions or how-to requests.
6
+ version: 1.0.0
7
+ category: ai
8
+ triggers:
9
+ - complaint
10
+ - refund
11
+ - unhappy
12
+ - speak to a human
13
+ - escalate
14
+ ---
15
+
16
+ # Complaint Handling
17
+
18
+ <!-- A WORKED EXAMPLE, shipped with Studio so prompts/skills/ is never an empty folder.
19
+ Edit it, copy it, or delete it — it is yours. An agent skill is prose the
20
+ platform's AI agents discover at runtime and follow: the frontmatter above is the
21
+ routing (whenToUse is ranked against what the user actually says), the body below
22
+ is the behavior. Format + rules: https://docs.unoverse.ai/nodes/node-discoverability.md -->
23
+
24
+ ## When to Use This Skill
25
+
26
+ The customer is expressing dissatisfaction: something broke, something was late, they
27
+ were charged wrongly, or they are simply frustrated. The goal of every reply is that
28
+ the customer feels heard first and helped second.
29
+
30
+ ## Approach
31
+
32
+ 1. **Acknowledge before anything else.** One sentence that names their problem in
33
+ their words. Never open with policy.
34
+ 2. **Ask for the one detail you need**, if any, and only one at a time.
35
+ 3. **Offer the concrete next step you can actually do.** Do not promise outcomes you
36
+ cannot see through.
37
+ 4. **Escalate when asked, and say so plainly.** If the customer asks for a person
38
+ twice, or mentions legal action, stop resolving and hand off: "I'm passing this to
39
+ our team; you'll hear from a person."
40
+
41
+ ## Do / Don't
42
+
43
+ - Do keep replies to three sentences or fewer while the customer is upset.
44
+ - Do use their name if you have it.
45
+ - Don't defend the company or explain why the problem happened, unless they ask.
46
+ - Don't offer discounts or refunds beyond what the workflow's tools allow.
47
+
48
+ ## Examples
49
+
50
+ Customer: "This is the second time my order arrived broken. Ridiculous."
51
+ Reply: "Twice in a row is genuinely not okay — I'm sorry. I can send a replacement
52
+ today or refund the order in full: which would you prefer?"
@@ -0,0 +1,23 @@
1
+ # Your project's styles
2
+
3
+ This folder restyles the whole design system for your brand — without editing a
4
+ single component. Three rules:
5
+
6
+ 1. **Token NAMES are the contract, VALUES are yours.** Components reference names
7
+ like `text.primary` and `action.primary`; you change what those names mean.
8
+ 2. **Name a token to override it; omit it to inherit.** The cascade is per token,
9
+ never per file. Never copy a whole file to change one line — the untouched lines
10
+ would stop tracking platform improvements.
11
+ 3. **Ingredients in `base/`, meaning in `themes/`.** `base/color.yaml` names your
12
+ brand's raw colors (`color.brand.ink`); a theme file assigns them to roles
13
+ (`text.primary: {color.brand.ink}`).
14
+
15
+ Start here:
16
+
17
+ - `base/color.yaml` — name your brand ingredients (a worked example is inside).
18
+ - Want your own theme? Copy the foundation's theme file whole — the ONE place
19
+ copying is correct, because a theme must assign every role:
20
+ https://github.com/unoverse-platform/marketplace/tree/main/definitions/styles/themes
21
+ → save as `themes/light.yaml` here, then point roles at your `color.brand.*`.
22
+
23
+ Full guide: https://docs.unoverse.ai/design/styles-and-tokens.md
@@ -0,0 +1,18 @@
1
+ # BRAND INGREDIENTS — raw colors named for what they ARE (ink, paper, a signature
2
+ # hue), never for where they are used ("button-blue" is a decision, not an ingredient;
3
+ # a theme file makes decisions). These are inert until a theme assigns them to roles,
4
+ # so this file is safe to grow before you have a theme at all.
5
+ #
6
+ # Delete the examples and name your own. Guide: see README.md in this folder.
7
+ color:
8
+ $type: color
9
+ brand:
10
+ ink:
11
+ $value: "#1a1a2e"
12
+ $description: "Near-black with a hint of the brand hue — body text and headings"
13
+ paper:
14
+ $value: "#fbfaf8"
15
+ $description: "Warm off-white — page and card backgrounds"
16
+ signature:
17
+ $value: "#4f46e5"
18
+ $description: "The brand's signature hue — actions and accents"
@@ -71,6 +71,16 @@ export function createProject(home, name) {
71
71
 
72
72
  for (const d of ["components/welcome", "templates", "styles"]) mkdirSync(join(root, d), { recursive: true });
73
73
  writeFileSync(join(root, "components/welcome/welcome.yaml"), WELCOME);
74
+
75
+ // Worked example styles (sample-styles: brand ingredients + the override rules), so
76
+ // styles/ is never an empty folder with no hint of the token cascade. Per PROJECT,
77
+ // because every project is a publishable org with its own pack. Copied only when the
78
+ // folder is empty: these are the developer's files, and a later run must never put
79
+ // back what they threw away.
80
+ const sampleStyles = join(dirname(fileURLToPath(import.meta.url)), "sample-styles");
81
+ if (existsSync(sampleStyles) && readdirSync(join(root, "styles")).length === 0) {
82
+ cpSync(sampleStyles, join(root, "styles"), { recursive: true });
83
+ }
74
84
  return root;
75
85
  }
76
86
 
@@ -94,6 +104,17 @@ export function createFirstProject(home, name) {
94
104
  cpSync(samples, join(home, "nodes/samples"), { recursive: true });
95
105
  }
96
106
 
107
+ // A worked example agent skill and prompt block, same reason and same rule as the
108
+ // sample nodes: prompts/ must never be two empty folders, and the samples are the
109
+ // developer's to edit or delete — copied only when absent, never restored.
110
+ const samplePrompts = join(dirname(fileURLToPath(import.meta.url)), "sample-prompts");
111
+ if (existsSync(join(samplePrompts, "skills")) && !existsSync(join(home, "prompts/skills/sample-complaint-handling"))) {
112
+ cpSync(join(samplePrompts, "skills"), join(home, "prompts/skills"), { recursive: true });
113
+ }
114
+ if (existsSync(join(samplePrompts, "blocks")) && !existsSync(join(home, "prompts/blocks/formatting/sample-concise-answers.md"))) {
115
+ cpSync(join(samplePrompts, "blocks"), join(home, "prompts/blocks"), { recursive: true });
116
+ }
117
+
97
118
  // The Claude Code authoring skills, so "open the folder and describe what you want"
98
119
  // works in every Studio project from its first minute. Vendored into the published
99
120
  // app (vendor/skills, written by prepack from the monorepo's .claude/skills, the one
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
package/src/NodesView.tsx CHANGED
@@ -383,7 +383,12 @@ export function NodesView() {
383
383
  </div>
384
384
  <div className="ml-auto flex shrink-0 items-center gap-2">
385
385
  {current.testData && (
386
- <button onClick={loadSample} className="rounded border border-gray-700 px-3 py-1.5 text-sm font-medium text-gray-300 hover:bg-white/5">
386
+ <button
387
+ onClick={loadSample}
388
+ disabled={!current?.testData}
389
+ title={current?.testData ? "Fill the form from the node's test fixture (test.yaml)" : "This node declares no test fixture"}
390
+ className="rounded border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40"
391
+ >
387
392
  Load sample
388
393
  </button>
389
394
  )}