@unoverse-platform/studio 0.1.8 → 0.1.10

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
@@ -19,7 +19,7 @@
19
19
  import { createServer } from "vite";
20
20
  import { dirname, resolve, join } from "path";
21
21
  import { fileURLToPath } from "url";
22
- import { existsSync } from "fs";
22
+ import { existsSync, readdirSync } from "fs";
23
23
  import { createInterface } from "readline/promises";
24
24
  import { createFirstProject, checkProjectName } from "../local/scaffold.mjs";
25
25
 
@@ -47,6 +47,33 @@ function findProjectRootFromCwd() {
47
47
  }
48
48
  }
49
49
 
50
+ /**
51
+ * A project ONE LEVEL DOWN.
52
+ *
53
+ * The walk above only goes UP, which was right while a project WAS the folder you stood
54
+ * in. Now that the first run creates ./<name>/, a second run from the parent would find no
55
+ * rx/ above it and offer to scaffold a SECOND project beside the first one. So look down
56
+ * as well.
57
+ *
58
+ * ONE level, and only when there is exactly one candidate. A deeper scan would adopt some
59
+ * unrelated project three folders away, and picking between several would be a coin toss;
60
+ * reading the wrong project's disk is silent, because both folders are perfectly valid.
61
+ */
62
+ function findProjectBelowCwd() {
63
+ const cwd = process.cwd();
64
+ const found = readdirSync(cwd, { withFileTypes: true })
65
+ .filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules")
66
+ .filter((e) => existsSync(join(cwd, e.name, "rx")))
67
+ .map((e) => e.name);
68
+
69
+ if (found.length === 1) return join(cwd, found[0]);
70
+ if (found.length > 1) {
71
+ console.log(`\n Several projects here: ${found.join(", ")}\n\n cd into the one you want.\n`);
72
+ process.exit(1);
73
+ }
74
+ return null;
75
+ }
76
+
50
77
  async function scaffoldIfEmpty() {
51
78
  const cwd = process.cwd();
52
79
  // The same walk findProjectRoot does. A project ABOVE us is a project; do not offer to
@@ -58,24 +85,52 @@ async function scaffoldIfEmpty() {
58
85
  dir = parent;
59
86
  }
60
87
 
88
+ // A project made by an earlier run, in the folder that run created.
89
+ const below = findProjectBelowCwd();
90
+ if (below) {
91
+ process.chdir(below);
92
+ return;
93
+ }
94
+
61
95
  console.log(`\n No project here yet.\n\n ${cwd}\n`);
62
96
  const rl = createInterface({ input: process.stdin, output: process.stdout });
63
97
 
64
98
  // Ask until the name is usable. It becomes a folder AND an item's `org` column, so a
65
99
  // rejection here is cheaper than a rename after something has been published.
66
- let name;
100
+ let name, home;
67
101
  for (;;) {
68
102
  name = (await rl.question(" Name your project [my-project]: ")).trim() || "my-project";
69
103
  const bad = checkProjectName(name);
70
- if (!bad) break;
71
- console.log(` ${bad}\n`);
104
+ if (bad) {
105
+ console.log(` ${bad}\n`);
106
+ continue;
107
+ }
108
+
109
+ /**
110
+ * THE NAME OWNS A FOLDER. Studio used to scaffold into the cwd, which made whatever
111
+ * directory you happened to be standing in your repository: run it once in a scratch
112
+ * folder and that scratch folder now holds a real project, with the name you typed
113
+ * used only for rx/<name>/. Asking for a name and then not putting it anywhere you
114
+ * can see reads as the tool ignoring you.
115
+ */
116
+ home = join(cwd, name);
117
+ if (existsSync(home) && readdirSync(home).some((f) => !f.startsWith("."))) {
118
+ console.log(` ./${name} already has something in it\n`);
119
+ continue;
120
+ }
121
+ break;
72
122
  }
73
123
  rl.close();
74
124
 
75
125
  // The same call the UI's New project makes, so the first project and the fifth are
76
- // identical (local/scaffold.mjs).
77
- createFirstProject(cwd, name);
78
- console.log(`\n Created rx/${name}/, prompts/ and nodes/\n`);
126
+ // identical (local/scaffold.mjs). rx/<name>/ stays nested: every subfolder of rx/ IS a
127
+ // project (base collect.ts listProjects) and its folder name is the `org` that
128
+ // publishes, so a flat rx/ could hold exactly one project and would read `components`
129
+ // and `templates` as orgs.
130
+ createFirstProject(home, name);
131
+ // Studio reads from the cwd, and the project is no longer the folder we launched in.
132
+ process.chdir(home);
133
+ console.log(`\n Created ${name}/ with rx/${name}/, prompts/ and nodes/\n`);
79
134
  }
80
135
 
81
136
  await scaffoldIfEmpty();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -28,6 +28,7 @@
28
28
  "@unoverse-platform/base": "^0.3.0",
29
29
  "@unoverse-platform/marketplace": "*",
30
30
  "@vitejs/plugin-react": "^4.3.4",
31
+ "markdown-to-jsx": "^7.7.17",
31
32
  "oidc-client-ts": "^3.4.1",
32
33
  "react": "^18.2.0",
33
34
  "react-dom": "^18.2.0",
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { useEffect, useState } from "react";
13
13
  import { fetchUnoverse } from "studio-host";
14
- import { usePersistentState } from "studio-host";
14
+ import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
15
15
  import { IdentityHeader } from "./IdentityHeader";
16
16
 
17
17
  interface SkillSummary {
@@ -33,6 +33,7 @@ interface SkillDetail extends SkillSummary {
33
33
 
34
34
  export function SkillsView() {
35
35
  const [skills, setSkills] = useState<SkillSummary[]>([]);
36
+ const available = useRegistryToggles("skill");
36
37
  const [selected, setSelected] = usePersistentState<string | null>("wb:skills:selected", null);
37
38
  const [detail, setDetail] = useState<SkillDetail | null>(null);
38
39
  const [loadingDetail, setLoadingDetail] = useState(false);
@@ -122,14 +123,17 @@ export function SkillsView() {
122
123
  <div className="flex h-full items-center justify-center text-sm text-gray-500">Select a skill</div>
123
124
  ) : (
124
125
  <div className="mx-auto max-w-3xl p-8">
126
+ {/* AVAILABLE — the master switch. Which workflows actually carry this skill is
127
+ installed per workflow, in Canvas's Content Library. */}
125
128
  <IdentityHeader
129
+ right={<ToggleSwitch on={available.isEnabled(selected)} onClick={() => available.toggle(selected)} label="Available" />}
126
130
  name={detail?.name ?? selected}
127
131
  category={detail?.category}
128
132
  version={detail?.version}
129
133
  description={detail?.description}
130
134
  whenToUse={detail?.whenToUse}
131
135
  />
132
- <p className="mb-3 text-[11px] text-gray-500">Whether this skill is on a map is switched per workflow, in Canvas’s Content Library: spatial is unique to each workflow.</p>
136
+ <p className="mb-3 text-[11px] text-gray-500">Available means workflows may install this skill. Which ones actually have it is chosen per workflow, in Canvas’s Content Library.</p>
133
137
 
134
138
  {detail?.triggers && detail.triggers.length > 0 && (
135
139
  <div className="mb-5 flex flex-wrap gap-1.5">
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from "react";
11
11
  import { useAuth } from "react-oidc-context";
12
- import { usePersistentState } from "studio-host";
12
+ import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
13
13
  import { ComponentStore } from "@unoverse/sdk";
14
14
  import { StreamedUnoverseTemplate, useUnoverseConnection, computeAppWidth, resolveActiveLayout, type ResolvedTheme, type UseVoiceServiceConfig } from "@unoverse/sdk";
15
15
  import { NativeAppHost } from "./NativeAppHost";
@@ -131,6 +131,7 @@ export function TemplatesView({
131
131
  // MOCK (offline) vs LIVE (real data plane). A fresh store per mode keeps the two paths
132
132
  // from bleeding into each other.
133
133
  const [live, setLive] = useState(defaultLive);
134
+ const available = useRegistryToggles("template");
134
135
  const [templates, setTemplates] = useState<Def[]>([]);
135
136
  const [components, setComponents] = useState<Def[]>([]);
136
137
  const [selected, setSelected] = usePersistentState<string | null>("wb:templates:selected", null);
@@ -635,11 +636,11 @@ export function TemplatesView({
635
636
  })}
636
637
  </div>
637
638
  )}
638
- {/* NO SPATIAL TOGGLE HERE. It wrote `plugin_state.enabled`, which the spatial catalogue
639
- deliberately IGNORES (`routeRegistrySpatial`): published IS available. What is actually
640
- on a map is the PER-WORKFLOW row, switched in Canvas's Content Library — spatial is
641
- unique to each workflow, and this screen has no workflow. A control that reads as the
642
- answer to "is this in spatial?" while writing where nothing reads is worse than none. */}
639
+ {/* AVAILABLE — the master switch (see ComponentsView for the two-level model).
640
+ Studio offers; each canvas installs from what is offered. */}
641
+ {selected && (
642
+ <ToggleSwitch on={available.isEnabled(selected)} onClick={() => available.toggle(selected)} label="Available" />
643
+ )}
643
644
  {selected && (
644
645
  <button
645
646
  onClick={() => setMetaOpen((o) => !o)}