@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 +7 -0
- package/local/scaffold.mjs +68 -41
- package/package.json +1 -1
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
|
/**
|
package/local/scaffold.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
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
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
}
|