@unoverse-platform/studio 0.1.54 → 0.1.55

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
@@ -162,6 +162,13 @@ await scaffoldIfEmpty();
162
162
  */
163
163
  const projectRoot = findProjectRootFromCwd();
164
164
  process.env.UNOVERSE_HOME = projectRoot;
165
+ // Samples this workspace has never seen arrive on launch (new ones ship with Studio
166
+ // releases); anything it ever had — edited or deleted — is never touched. scaffold.mjs
167
+ // topUpSamples explains the record that makes that distinction safe.
168
+ {
169
+ const { topUpSamples } = await import("../local/scaffold.mjs");
170
+ try { topUpSamples(projectRoot); } catch { /* a read-only folder must not block launch */ }
171
+ }
165
172
  console.log(`\n Studio reading: ${designHome(projectRoot)}\n`);
166
173
 
167
174
  /**
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * See docs/architecture/LOCAL_STUDIO.md.
15
15
  */
16
- import { existsSync, mkdirSync, writeFileSync, cpSync, readdirSync } from "node:fs";
16
+ import { existsSync, mkdirSync, writeFileSync, cpSync, readdirSync, readFileSync } from "node:fs";
17
17
  import { designHome } from "./design-home.mjs";
18
18
  import { join, dirname, resolve } from "node:path";
19
19
  import { fileURLToPath } from "node:url";
@@ -84,6 +84,60 @@ export function createProject(home, name) {
84
84
  return root;
85
85
  }
86
86
 
87
+ /**
88
+ * The worked example content (sample nodes, a sample agent skill, a sample prompt
89
+ * block), keyed by the workspace path each lands at. `dir` is relative to this file.
90
+ *
91
+ * A NEW ENTRY HERE REACHES EXISTING WORKSPACES on their next Studio launch, because
92
+ * topUpSamples runs at every launch, not only at scaffold. The record below is what
93
+ * makes that safe.
94
+ */
95
+ const SAMPLES = [
96
+ { id: "nodes/samples", dir: "sample-nodes/samples", to: "nodes/samples" },
97
+ { id: "prompts/skills/sample-complaint-handling", dir: "sample-prompts/skills/sample-complaint-handling", to: "prompts/skills/sample-complaint-handling" },
98
+ { id: "prompts/blocks/sample-concise-answers", dir: "sample-prompts/blocks/formatting/sample-concise-answers.md", to: "prompts/blocks/formatting/sample-concise-answers.md" },
99
+ ];
100
+
101
+ /**
102
+ * Install every sample this workspace has NEVER SEEN, and only those.
103
+ *
104
+ * "Absent from disk" is ambiguous — it means either "deleted on purpose" or "shipped
105
+ * after this workspace was made" — so presence is not the test. The test is the
106
+ * RECORD (.unoverse/samples.json): a sample id listed there was installed here once,
107
+ * and whatever the developer did to it since (edit, rename, delete) is theirs and
108
+ * permanent. A sample id NOT listed is genuinely new to this workspace and arrives.
109
+ * That is what lets `unoverse studio` top up new samples on every launch without ever
110
+ * resurrecting deleted work or overwriting changed files.
111
+ */
112
+ export function topUpSamples(home) {
113
+ const record = join(home, ".unoverse", "samples.json");
114
+ let seen = [];
115
+ try {
116
+ seen = JSON.parse(readFileSync(record, "utf8")).installed ?? [];
117
+ } catch {
118
+ // No record: a workspace from before records existed. Treat every sample already
119
+ // on disk as seen (it was installed by an old scaffold), so nothing is re-copied.
120
+ seen = SAMPLES.filter((s) => existsSync(join(home, s.to))).map((s) => s.id);
121
+ }
122
+ const here = dirname(fileURLToPath(import.meta.url));
123
+ let changed = false;
124
+ for (const s of SAMPLES) {
125
+ if (seen.includes(s.id)) continue;
126
+ const src = join(here, s.dir);
127
+ if (!existsSync(src)) continue;
128
+ if (!existsSync(join(home, s.to))) {
129
+ mkdirSync(dirname(join(home, s.to)), { recursive: true });
130
+ cpSync(src, join(home, s.to), { recursive: true });
131
+ }
132
+ seen.push(s.id);
133
+ changed = true;
134
+ }
135
+ if (changed || !existsSync(record)) {
136
+ mkdirSync(dirname(record), { recursive: true });
137
+ writeFileSync(record, JSON.stringify({ installed: seen }, null, 2) + "\n");
138
+ }
139
+ }
140
+
87
141
  /**
88
142
  * The FIRST project, plus the folders that live beside the design tree rather than inside it.
89
143
  *
@@ -94,47 +148,20 @@ export function createFirstProject(home, name) {
94
148
  const root = createProject(home, name);
95
149
  for (const d of ["prompts/skills", "prompts/blocks", "nodes"]) mkdirSync(join(home, d), { recursive: true });
96
150
 
97
- // Two worked example nodes (sample-nodes/samples: a PromiseNode and a CallbackNode,
98
- // heavily commented, linking docs.unoverse.ai/nodes), so `nodes/` is never an empty
99
- // folder with no hint of what goes in it. Copied only when absent: these are the
100
- // developer's files to edit or delete, and a later scaffold run must never put back
101
- // what they threw away or overwrite what they changed.
102
- const samples = join(dirname(fileURLToPath(import.meta.url)), "sample-nodes/samples");
103
- if (existsSync(samples) && !existsSync(join(home, "nodes/samples"))) {
104
- cpSync(samples, join(home, "nodes/samples"), { recursive: true });
105
- }
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
- }
151
+ topUpSamples(home);
117
152
 
118
- // The Claude Code authoring skills, so "open the folder and describe what you want"
119
- // works in every Studio project from its first minute. Vendored into the published
120
- // app (vendor/skills, written by prepack from the monorepo's .claude/skills, the one
121
- // authoring home, mirrored publicly to github.com/unoverse-platform/skills); read
122
- // from that home when running as source. `unoverse update` refreshes installed
123
- // copies from the public mirror.
153
+ // SCAFFOLD WRITES NO `.claude/skills` (2026-08-21). It used to copy the Claude Code
154
+ // authoring skills out of `vendor/skills` and into `<home>/.claude/skills`, which meant
155
+ // every remote developer who launched Studio got the platform repo's own `.claude`
156
+ // folder reproduced on their disk, pinned to whatever Studio version they happened to
157
+ // install and sitting in a workspace they might commit.
124
158
  //
125
- // ALL of them. One Studio project is where a person designs a brand AND writes the node
126
- // behind it, so they need the whole set: naming one skill here shipped the authoring
127
- // playbooks and silently withheld the rest.
128
- const app = resolve(dirname(fileURLToPath(import.meta.url)), "..");
129
- const skillsDir = [join(app, "vendor/skills"), resolve(app, "../../.claude/skills")].find((d) => existsSync(d));
130
- if (skillsDir) {
131
- const names = readdirSync(skillsDir, { withFileTypes: true })
132
- .filter((e) => e.isDirectory() && existsSync(join(skillsDir, e.name, "SKILL.md")))
133
- .map((e) => e.name);
134
- if (names.length) mkdirSync(join(home, ".claude/skills"), { recursive: true });
135
- for (const name of names) {
136
- cpSync(join(skillsDir, name), join(home, ".claude/skills", name), { recursive: true });
137
- }
138
- }
159
+ // The skills are PER DEVELOPER, not per project (the same reason prompts/ and nodes/ are
160
+ // made once here and never by `createProject`). They now install to `~/.claude/skills`
161
+ // from the public mirror, on `unoverse update` — see packages/cli/lib/skills.mjs.
162
+ //
163
+ // `vendor/skills` STAYS, and is not a distribution channel: it is what Studio's builder
164
+ // MCP reads to teach a connected agent the authoring rules (local/mcp.ts SKILL_DIRS).
165
+ // Nothing about that copy reaches the developer's filesystem.
139
166
  return root;
140
167
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.54",
3
+ "version": "0.1.55",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {