dot-studio 0.0.1

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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/client/assets/index-C2eIILoa.css +41 -0
  4. package/client/assets/index-DUPZ_Lw5.js +616 -0
  5. package/client/assets/index.es-Btlrnc3g.js +1 -0
  6. package/client/index.html +14 -0
  7. package/dist/cli.js +196 -0
  8. package/dist/server/index.js +79 -0
  9. package/dist/server/lib/act-runtime.js +1282 -0
  10. package/dist/server/lib/cache.js +31 -0
  11. package/dist/server/lib/config.js +53 -0
  12. package/dist/server/lib/dot-authoring.js +245 -0
  13. package/dist/server/lib/dot-loader.js +61 -0
  14. package/dist/server/lib/dot-login.js +190 -0
  15. package/dist/server/lib/model-catalog.js +111 -0
  16. package/dist/server/lib/opencode-auth.js +69 -0
  17. package/dist/server/lib/opencode-errors.js +220 -0
  18. package/dist/server/lib/opencode-sidecar.js +144 -0
  19. package/dist/server/lib/opencode.js +12 -0
  20. package/dist/server/lib/package-bin.js +63 -0
  21. package/dist/server/lib/project-config.js +39 -0
  22. package/dist/server/lib/prompt.js +222 -0
  23. package/dist/server/lib/request-context.js +27 -0
  24. package/dist/server/lib/runtime-tools.js +208 -0
  25. package/dist/server/routes/assets.js +161 -0
  26. package/dist/server/routes/chat.js +356 -0
  27. package/dist/server/routes/compile.js +105 -0
  28. package/dist/server/routes/dot.js +270 -0
  29. package/dist/server/routes/health.js +56 -0
  30. package/dist/server/routes/opencode.js +421 -0
  31. package/dist/server/routes/stages.js +137 -0
  32. package/dist/server/start.js +23 -0
  33. package/dist/server/terminal.js +282 -0
  34. package/dist/shared/mcp-config.js +19 -0
  35. package/dist/shared/model-variants.js +50 -0
  36. package/dist/shared/project-mcp.js +22 -0
  37. package/dist/shared/session-metadata.js +26 -0
  38. package/package.json +103 -0
@@ -0,0 +1,270 @@
1
+ // DOT Integration Routes — .dance-of-tal management
2
+ import { Hono } from 'hono';
3
+ import fs from 'fs/promises';
4
+ import { getDotDir, initRegistry, getPerformer, listLockedPerformerNames, getGlobalDotDir, getGlobalCwd, ensureDotDir } from 'dance-of-tal/lib/registry';
5
+ import { readAgentManifest, writeAgentManifest } from 'dance-of-tal/lib/agents';
6
+ import { installActWithDependencies, installAsset, installPerformerAndLock, searchRegistry } from 'dance-of-tal/lib/installer';
7
+ import { clearDotAuthUser, publishStudioAsset, readDotAuthUser, saveLocalStudioAsset } from '../lib/dot-authoring.js';
8
+ import { startDotLogin } from '../lib/dot-login.js';
9
+ /** Validates that performer URNs follow the 3-part format: kind/@author/name */
10
+ function validatePerformer(performer) {
11
+ const tal = performer.tal ?? null;
12
+ const dances = performer.dance
13
+ ? (Array.isArray(performer.dance) ? performer.dance : [performer.dance])
14
+ : [];
15
+ const act = performer.act ?? null;
16
+ if (!tal && dances.length === 0) {
17
+ throw new Error("Invalid performer: at least one of 'tal' or 'dance' must be present.");
18
+ }
19
+ const validateUrn = (urn, prefix) => {
20
+ const parts = urn.split('/');
21
+ if (parts.length !== 3 || parts[0] !== prefix || !parts[1].startsWith('@') || !parts[2]) {
22
+ throw new Error(`Invalid URN: '${urn}'. Expected: ${prefix}/@<author>/<name>`);
23
+ }
24
+ };
25
+ if (tal)
26
+ validateUrn(tal, 'tal');
27
+ for (const d of dances)
28
+ validateUrn(d, 'dance');
29
+ if (act)
30
+ validateUrn(act, 'act');
31
+ }
32
+ import { invalidate } from '../lib/cache.js';
33
+ import { resolveRequestWorkingDir } from '../lib/request-context.js';
34
+ const dot = new Hono();
35
+ // ── Helpers ─────────────────────────────────────────────
36
+ function resolveCwd(cwd, scope) {
37
+ if (scope === 'global')
38
+ return getGlobalCwd();
39
+ return cwd;
40
+ }
41
+ // Uses ensureDotDir() from lib/registry (auto-inits workspace if missing)
42
+ // ── DOT Status ──────────────────────────────────────────
43
+ dot.get('/api/dot/status', async (c) => {
44
+ const cwd = resolveRequestWorkingDir(c);
45
+ const dotDir = getDotDir(cwd);
46
+ const globalDotDir = getGlobalDotDir();
47
+ try {
48
+ const [stageExists, globalExists] = await Promise.all([
49
+ fs.access(dotDir).then(() => true).catch(() => false),
50
+ fs.access(globalDotDir).then(() => true).catch(() => false),
51
+ ]);
52
+ return c.json({
53
+ initialized: stageExists || globalExists,
54
+ stageInitialized: stageExists,
55
+ globalInitialized: globalExists,
56
+ dotDir,
57
+ globalDotDir,
58
+ projectDir: cwd,
59
+ });
60
+ }
61
+ catch {
62
+ return c.json({ initialized: false, stageInitialized: false, globalInitialized: false, dotDir, globalDotDir, projectDir: cwd });
63
+ }
64
+ });
65
+ // ── DOT Init ────────────────────────────────────────────
66
+ dot.post('/api/dot/init', async (c) => {
67
+ const { scope } = await c.req.json().catch(() => ({ scope: undefined }));
68
+ const cwd = resolveCwd(resolveRequestWorkingDir(c), scope);
69
+ try {
70
+ await initRegistry(cwd);
71
+ return c.json({ ok: true, dotDir: getDotDir(cwd), scope: scope || 'stage' });
72
+ }
73
+ catch (err) {
74
+ return c.json({ error: err.message }, 500);
75
+ }
76
+ });
77
+ // ── Auth User ───────────────────────────────────────────
78
+ dot.get('/api/dot/auth-user', async (c) => {
79
+ try {
80
+ const auth = await readDotAuthUser();
81
+ return c.json({
82
+ authenticated: !!auth,
83
+ username: auth?.username || null,
84
+ });
85
+ }
86
+ catch (err) {
87
+ return c.json({ authenticated: false, username: null, error: err.message }, 500);
88
+ }
89
+ });
90
+ dot.post('/api/dot/login', async (c) => {
91
+ const body = await c.req.json().catch(() => ({}));
92
+ if (!body?.acknowledgedTos) {
93
+ return c.json({
94
+ error: 'Review and accept the Dance of Tal Terms of Service before signing in: https://danceoftal.com/tos',
95
+ }, 400);
96
+ }
97
+ try {
98
+ const result = await startDotLogin();
99
+ return c.json({ ok: true, ...result });
100
+ }
101
+ catch (err) {
102
+ return c.json({ error: err.message || 'Failed to start dot login.' }, 500);
103
+ }
104
+ });
105
+ dot.post('/api/dot/logout', async (c) => {
106
+ try {
107
+ await clearDotAuthUser();
108
+ return c.json({ ok: true });
109
+ }
110
+ catch (err) {
111
+ return c.json({ error: err.message || 'Failed to sign out.' }, 500);
112
+ }
113
+ });
114
+ // ── Performer Lockfiles ─────────────────────────────────
115
+ dot.get('/api/dot/performers', async (c) => {
116
+ const cwd = resolveRequestWorkingDir(c);
117
+ try {
118
+ const result = await listLockedPerformerNames(cwd);
119
+ return c.json(result);
120
+ }
121
+ catch (err) {
122
+ return c.json({ names: [], skipped: [] });
123
+ }
124
+ });
125
+ dot.get('/api/dot/performers/:name', async (c) => {
126
+ const cwd = resolveRequestWorkingDir(c);
127
+ const name = c.req.param('name');
128
+ try {
129
+ const performer = await getPerformer(cwd, name);
130
+ if (!performer)
131
+ return c.json({ error: 'Performer not found' }, 404);
132
+ return c.json({ name, ...performer });
133
+ }
134
+ catch (err) {
135
+ return c.json({ error: err.message }, 500);
136
+ }
137
+ });
138
+ // ── Agent Manifest ──────────────────────────────────────
139
+ dot.get('/api/dot/agents', async (c) => {
140
+ const cwd = resolveRequestWorkingDir(c);
141
+ try {
142
+ const manifest = await readAgentManifest(cwd);
143
+ return c.json(manifest);
144
+ }
145
+ catch (err) {
146
+ return c.json({});
147
+ }
148
+ });
149
+ dot.put('/api/dot/agents', async (c) => {
150
+ const cwd = resolveRequestWorkingDir(c);
151
+ const manifest = await c.req.json();
152
+ try {
153
+ await writeAgentManifest(cwd, manifest);
154
+ return c.json({ ok: true });
155
+ }
156
+ catch (err) {
157
+ return c.json({ error: err.message }, 500);
158
+ }
159
+ });
160
+ // ── Install (with scope: 'global' | 'stage') ────────────
161
+ dot.post('/api/dot/install', async (c) => {
162
+ const { urn, localName, force, scope } = await c.req.json();
163
+ const cwd = resolveCwd(resolveRequestWorkingDir(c), scope);
164
+ try {
165
+ // Ensure .dance-of-tal exists
166
+ await ensureDotDir(cwd);
167
+ // Check if it's a performer (cascading install)
168
+ if (urn.startsWith('performer/')) {
169
+ const result = await installPerformerAndLock(cwd, urn, localName, force);
170
+ invalidate('assets');
171
+ return c.json({ ...result, scope: scope || 'stage' });
172
+ }
173
+ if (urn.startsWith('act/')) {
174
+ const result = await installActWithDependencies(cwd, urn, force);
175
+ invalidate('assets');
176
+ return c.json({ ...result, scope: scope || 'stage' });
177
+ }
178
+ // Single asset install
179
+ const result = await installAsset(cwd, urn, force);
180
+ invalidate('assets');
181
+ return c.json({ ...result, scope: scope || 'stage' });
182
+ }
183
+ catch (err) {
184
+ return c.json({ error: err.message }, 500);
185
+ }
186
+ });
187
+ // ── Local Asset Save ────────────────────────────────────
188
+ dot.put('/api/dot/assets/local', async (c) => {
189
+ const cwd = resolveRequestWorkingDir(c);
190
+ const body = await c.req.json().catch(() => null);
191
+ if (!body?.kind || !body?.slug) {
192
+ return c.json({ error: 'kind and slug are required.' }, 400);
193
+ }
194
+ try {
195
+ const auth = await readDotAuthUser();
196
+ const author = body.author || auth?.username;
197
+ if (!author) {
198
+ return c.json({ error: 'No author available. Sign in with `dot login` first.' }, 400);
199
+ }
200
+ const saved = await saveLocalStudioAsset({
201
+ cwd,
202
+ kind: body.kind,
203
+ author,
204
+ slug: body.slug,
205
+ payload: body.payload,
206
+ });
207
+ invalidate('assets');
208
+ return c.json({ ok: true, ...saved });
209
+ }
210
+ catch (err) {
211
+ return c.json({ error: err.message }, 400);
212
+ }
213
+ });
214
+ // ── Publish Local / Draft Asset ────────────────────────
215
+ dot.post('/api/dot/assets/publish', async (c) => {
216
+ const cwd = resolveRequestWorkingDir(c);
217
+ const body = await c.req.json().catch(() => null);
218
+ if (!body?.kind || !body?.slug) {
219
+ return c.json({ error: 'kind and slug are required.' }, 400);
220
+ }
221
+ if (!body.acknowledgedTos) {
222
+ return c.json({
223
+ error: 'Review and accept the Dance of Tal Terms of Service before publishing: https://danceoftal.com/tos',
224
+ }, 400);
225
+ }
226
+ try {
227
+ const auth = await readDotAuthUser();
228
+ if (!auth) {
229
+ return c.json({ error: 'You are not logged in. Run `dot login` first.' }, 401);
230
+ }
231
+ const result = await publishStudioAsset({
232
+ cwd,
233
+ kind: body.kind,
234
+ slug: body.slug,
235
+ payload: body.payload,
236
+ tags: body.tags,
237
+ auth,
238
+ });
239
+ invalidate('assets');
240
+ return c.json({ ok: true, ...result });
241
+ }
242
+ catch (err) {
243
+ return c.json({ error: err.message }, 400);
244
+ }
245
+ });
246
+ // ── Search Registry ─────────────────────────────────────
247
+ dot.get('/api/dot/search', async (c) => {
248
+ const query = c.req.query('q') || '';
249
+ const kind = c.req.query('kind');
250
+ const limit = parseInt(c.req.query('limit') || '20', 10);
251
+ try {
252
+ const results = await searchRegistry(query, { kind, limit });
253
+ return c.json(results);
254
+ }
255
+ catch (err) {
256
+ return c.json({ error: err.message }, 500);
257
+ }
258
+ });
259
+ // ── Validate Performer ──────────────────────────────────
260
+ dot.post('/api/dot/validate', async (c) => {
261
+ const performer = await c.req.json();
262
+ try {
263
+ validatePerformer(performer);
264
+ return c.json({ valid: true });
265
+ }
266
+ catch (err) {
267
+ return c.json({ valid: false, error: err.message });
268
+ }
269
+ });
270
+ export default dot;
@@ -0,0 +1,56 @@
1
+ // Health, Studio Config & Activation Routes
2
+ import { Hono } from 'hono';
3
+ import { exec } from 'child_process';
4
+ import { promisify } from 'util';
5
+ import fs from 'fs/promises';
6
+ import path from 'path';
7
+ import { getActiveProjectDir, setActiveProjectDir, readStudioConfig, writeStudioConfig, } from '../lib/config.js';
8
+ import { invalidateAll } from '../lib/cache.js';
9
+ import { resolveRequestWorkingDir } from '../lib/request-context.js';
10
+ const execAsync = promisify(exec);
11
+ const health = new Hono();
12
+ // ── Health ───────────────────────────────────────────────
13
+ health.get('/api/health', (c) => c.json({ ok: true, project: resolveRequestWorkingDir(c) }));
14
+ // ── Pick Directory (macOS) ──────────────────────────────
15
+ health.get('/api/studio/pick-directory', async (c) => {
16
+ try {
17
+ const { stdout } = await execAsync(`osascript -e 'POSIX path of (choose folder with prompt "Select Working Directory for Stage")'`);
18
+ return c.json({ path: stdout.trim() });
19
+ }
20
+ catch {
21
+ return c.json({ error: 'Selection cancelled or failed' }, 400);
22
+ }
23
+ });
24
+ // ── Studio Config ───────────────────────────────────────
25
+ health.get('/api/studio/config', async (c) => {
26
+ const config = await readStudioConfig();
27
+ return c.json({ ...config, projectDir: resolveRequestWorkingDir(c) });
28
+ });
29
+ health.put('/api/studio/config', async (c) => {
30
+ const body = await c.req.json();
31
+ const merged = await writeStudioConfig(body);
32
+ return c.json(merged);
33
+ });
34
+ health.post('/api/studio/activate', async (c) => {
35
+ const { workingDir } = await c.req.json();
36
+ if (!workingDir) {
37
+ return c.json({ error: 'workingDir is required' }, 400);
38
+ }
39
+ // Normalize: resolve, trim trailing slashes
40
+ const resolved = path.resolve(workingDir.replace(/\/+$/, ''));
41
+ // Verify directory exists
42
+ try {
43
+ const stat = await fs.stat(resolved);
44
+ if (!stat.isDirectory()) {
45
+ return c.json({ error: 'workingDir is not a directory' }, 400);
46
+ }
47
+ }
48
+ catch {
49
+ return c.json({ error: `Directory not found: ${resolved}` }, 400);
50
+ }
51
+ setActiveProjectDir(resolved);
52
+ invalidateAll(); // flush all caches — assets, models, MCP are project-scoped
53
+ console.log(`🎯 Active project dir switched to: ${getActiveProjectDir()}`);
54
+ return c.json({ ok: true, activeProjectDir: getActiveProjectDir() });
55
+ });
56
+ export default health;