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.
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/client/assets/index-C2eIILoa.css +41 -0
- package/client/assets/index-DUPZ_Lw5.js +616 -0
- package/client/assets/index.es-Btlrnc3g.js +1 -0
- package/client/index.html +14 -0
- package/dist/cli.js +196 -0
- package/dist/server/index.js +79 -0
- package/dist/server/lib/act-runtime.js +1282 -0
- package/dist/server/lib/cache.js +31 -0
- package/dist/server/lib/config.js +53 -0
- package/dist/server/lib/dot-authoring.js +245 -0
- package/dist/server/lib/dot-loader.js +61 -0
- package/dist/server/lib/dot-login.js +190 -0
- package/dist/server/lib/model-catalog.js +111 -0
- package/dist/server/lib/opencode-auth.js +69 -0
- package/dist/server/lib/opencode-errors.js +220 -0
- package/dist/server/lib/opencode-sidecar.js +144 -0
- package/dist/server/lib/opencode.js +12 -0
- package/dist/server/lib/package-bin.js +63 -0
- package/dist/server/lib/project-config.js +39 -0
- package/dist/server/lib/prompt.js +222 -0
- package/dist/server/lib/request-context.js +27 -0
- package/dist/server/lib/runtime-tools.js +208 -0
- package/dist/server/routes/assets.js +161 -0
- package/dist/server/routes/chat.js +356 -0
- package/dist/server/routes/compile.js +105 -0
- package/dist/server/routes/dot.js +270 -0
- package/dist/server/routes/health.js +56 -0
- package/dist/server/routes/opencode.js +421 -0
- package/dist/server/routes/stages.js +137 -0
- package/dist/server/start.js +23 -0
- package/dist/server/terminal.js +282 -0
- package/dist/shared/mcp-config.js +19 -0
- package/dist/shared/model-variants.js +50 -0
- package/dist/shared/project-mcp.js +22 -0
- package/dist/shared/session-metadata.js +26 -0
- package/package.json +103 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>DOT Studio</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DUPZ_Lw5.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C2eIILoa.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// DOT Studio CLI — npx dot-studio [projectDir] [--no-open]
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import { resolve, basename, dirname, join } from 'path';
|
|
5
|
+
import net from 'net';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import readline from 'readline/promises';
|
|
9
|
+
function parseCliArgs(argv) {
|
|
10
|
+
let openBrowser = true;
|
|
11
|
+
let projectDir = null;
|
|
12
|
+
let port = null;
|
|
13
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
14
|
+
const arg = argv[index];
|
|
15
|
+
if (arg === '--no-open') {
|
|
16
|
+
openBrowser = false;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (arg === '--open') {
|
|
20
|
+
openBrowser = true;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (arg === '--port' || arg === '-p') {
|
|
24
|
+
const value = argv[index + 1];
|
|
25
|
+
if (!value) {
|
|
26
|
+
console.error(`Missing value for ${arg}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const parsed = Number.parseInt(value, 10);
|
|
30
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
31
|
+
console.error(`Invalid port: ${value}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
port = parsed;
|
|
35
|
+
index += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg.startsWith('-')) {
|
|
39
|
+
console.error(`Unknown option: ${arg}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
if (projectDir) {
|
|
43
|
+
console.error(`Unexpected extra argument: ${arg}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
projectDir = arg;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
openBrowser,
|
|
50
|
+
projectDir: resolve(projectDir || process.cwd()),
|
|
51
|
+
port,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function canListenOnPort(port) {
|
|
55
|
+
return new Promise((resolvePromise) => {
|
|
56
|
+
const server = net.createServer();
|
|
57
|
+
server.once('error', () => {
|
|
58
|
+
resolvePromise(false);
|
|
59
|
+
});
|
|
60
|
+
server.once('listening', () => {
|
|
61
|
+
server.close(() => resolvePromise(true));
|
|
62
|
+
});
|
|
63
|
+
server.listen({
|
|
64
|
+
port,
|
|
65
|
+
host: '::',
|
|
66
|
+
exclusive: true,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function compareSemver(left, right) {
|
|
71
|
+
const normalize = (value) => value
|
|
72
|
+
.split('-')[0]
|
|
73
|
+
.split('.')
|
|
74
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
75
|
+
const leftParts = normalize(left);
|
|
76
|
+
const rightParts = normalize(right);
|
|
77
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
78
|
+
for (let index = 0; index < length; index += 1) {
|
|
79
|
+
const leftValue = leftParts[index] || 0;
|
|
80
|
+
const rightValue = rightParts[index] || 0;
|
|
81
|
+
if (leftValue === rightValue) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
return leftValue - rightValue;
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
async function readStudioPackageMeta() {
|
|
89
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
90
|
+
const currentDir = dirname(currentFile);
|
|
91
|
+
const packageRoot = basename(currentDir) === 'dist' ? dirname(currentDir) : currentDir;
|
|
92
|
+
const raw = await fs.readFile(join(packageRoot, 'package.json'), 'utf-8');
|
|
93
|
+
return JSON.parse(raw);
|
|
94
|
+
}
|
|
95
|
+
async function fetchLatestVersion(packageName) {
|
|
96
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, {
|
|
97
|
+
headers: {
|
|
98
|
+
accept: 'application/json',
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error(`Failed to fetch npm metadata for ${packageName}.`);
|
|
103
|
+
}
|
|
104
|
+
const payload = await response.json();
|
|
105
|
+
return payload.version || null;
|
|
106
|
+
}
|
|
107
|
+
async function promptForNpmUpdate(packageMeta) {
|
|
108
|
+
let latestVersion = null;
|
|
109
|
+
try {
|
|
110
|
+
latestVersion = await fetchLatestVersion(packageMeta.name);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (!latestVersion || compareSemver(latestVersion, packageMeta.version) <= 0) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
const message = `A newer ${packageMeta.name} version is available on npm (${packageMeta.version} -> ${latestVersion}). Update now? [Y/n] `;
|
|
119
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
120
|
+
if (!isInteractive) {
|
|
121
|
+
console.log(`${message.trim()} Run npm install -g ${packageMeta.name}@latest to update.`);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
const rl = readline.createInterface({
|
|
125
|
+
input: process.stdin,
|
|
126
|
+
output: process.stdout,
|
|
127
|
+
});
|
|
128
|
+
try {
|
|
129
|
+
const answer = (await rl.question(message)).trim().toLowerCase();
|
|
130
|
+
if (answer && answer !== 'y' && answer !== 'yes') {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
rl.close();
|
|
136
|
+
}
|
|
137
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
138
|
+
await new Promise((resolvePromise, reject) => {
|
|
139
|
+
const child = spawn(npmCommand, ['install', '-g', `${packageMeta.name}@latest`], {
|
|
140
|
+
stdio: 'inherit',
|
|
141
|
+
});
|
|
142
|
+
child.on('exit', (code) => {
|
|
143
|
+
if (code === 0) {
|
|
144
|
+
resolvePromise();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
reject(new Error(`npm install exited with code ${code ?? 1}.`));
|
|
148
|
+
});
|
|
149
|
+
child.on('error', reject);
|
|
150
|
+
});
|
|
151
|
+
console.log(`Updated ${packageMeta.name} to ${latestVersion}. Run dot-studio again to start the new version.`);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
const startServer = async () => {
|
|
155
|
+
try {
|
|
156
|
+
const packageMeta = await readStudioPackageMeta();
|
|
157
|
+
const updated = await promptForNpmUpdate(packageMeta);
|
|
158
|
+
if (updated) {
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
const { openBrowser, projectDir, port: requestedPort } = parseCliArgs(process.argv.slice(2));
|
|
162
|
+
const basePort = requestedPort || Number.parseInt(process.env.PORT || '3001', 10) || 3001;
|
|
163
|
+
let resolvedPort = basePort;
|
|
164
|
+
if (requestedPort) {
|
|
165
|
+
if (!(await canListenOnPort(requestedPort))) {
|
|
166
|
+
console.error(`Port ${requestedPort} is already in use.`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
while (!(await canListenOnPort(resolvedPort))) {
|
|
172
|
+
resolvedPort += 1;
|
|
173
|
+
if (resolvedPort - basePort > 20) {
|
|
174
|
+
console.error(`Could not find an open port starting from ${basePort}.`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
process.env.PROJECT_DIR = projectDir;
|
|
180
|
+
process.env.DOT_STUDIO_PRODUCTION = '1';
|
|
181
|
+
process.env.PORT = String(resolvedPort);
|
|
182
|
+
const studioUrl = `http://localhost:${resolvedPort}`;
|
|
183
|
+
// Dynamic import to let env vars take effect before config.ts loads
|
|
184
|
+
await import('./server/index.js');
|
|
185
|
+
console.log(`DOT Studio running at ${studioUrl}`);
|
|
186
|
+
if (openBrowser) {
|
|
187
|
+
const open = await import('open');
|
|
188
|
+
await open.default(studioUrl);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
console.error('Failed to start DOT Studio:', err);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
startServer();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// DOT Studio — Hono API Server (Entry Point)
|
|
2
|
+
import { serve } from '@hono/node-server';
|
|
3
|
+
import { serveStatic } from '@hono/node-server/serve-static';
|
|
4
|
+
import { Hono } from 'hono';
|
|
5
|
+
import { cors } from 'hono/cors';
|
|
6
|
+
import { setupTerminalWs } from './terminal.js';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
// Route Modules
|
|
11
|
+
import healthRoutes from './routes/health.js';
|
|
12
|
+
import assetRoutes from './routes/assets.js';
|
|
13
|
+
import stageRoutes from './routes/stages.js';
|
|
14
|
+
import chatRoutes from './routes/chat.js';
|
|
15
|
+
import opencodeRoutes from './routes/opencode.js';
|
|
16
|
+
import compileRoutes from './routes/compile.js';
|
|
17
|
+
import dotRoutes from './routes/dot.js';
|
|
18
|
+
// Config
|
|
19
|
+
import { PORT, OPENCODE_URL, STUDIO_DIR, IS_PRODUCTION, getActiveProjectDir } from './lib/config.js';
|
|
20
|
+
import { ensureOpencodeSidecar, isManagedOpencode } from './lib/opencode-sidecar.js';
|
|
21
|
+
const app = new Hono();
|
|
22
|
+
function resolveClientDir() {
|
|
23
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const candidates = [
|
|
25
|
+
path.resolve(__dirname, '..', '..', 'client'),
|
|
26
|
+
path.resolve(__dirname, '..', 'client'),
|
|
27
|
+
];
|
|
28
|
+
return candidates.find((dir) => fs.existsSync(path.join(dir, 'index.html'))) || candidates[0];
|
|
29
|
+
}
|
|
30
|
+
if (IS_PRODUCTION) {
|
|
31
|
+
// Production: Hono serves built frontend from client/ directory
|
|
32
|
+
const clientDir = resolveClientDir();
|
|
33
|
+
// API routes first, then static files
|
|
34
|
+
app.route('/', healthRoutes);
|
|
35
|
+
app.route('/', assetRoutes);
|
|
36
|
+
app.route('/', stageRoutes);
|
|
37
|
+
app.route('/', chatRoutes);
|
|
38
|
+
app.route('/', opencodeRoutes);
|
|
39
|
+
app.route('/', compileRoutes);
|
|
40
|
+
app.route('/', dotRoutes);
|
|
41
|
+
// Serve static assets
|
|
42
|
+
app.use('/assets/*', serveStatic({ root: clientDir }));
|
|
43
|
+
// SPA fallback: serve index.html for all non-API, non-asset routes
|
|
44
|
+
app.get('*', async (c) => {
|
|
45
|
+
const indexPath = path.join(clientDir, 'index.html');
|
|
46
|
+
if (fs.existsSync(indexPath)) {
|
|
47
|
+
const html = fs.readFileSync(indexPath, 'utf-8');
|
|
48
|
+
return c.html(html);
|
|
49
|
+
}
|
|
50
|
+
return c.text('Not found', 404);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// Dev: allow localhost Vite ports so API calls keep working when Vite auto-increments.
|
|
55
|
+
app.use('*', cors({
|
|
56
|
+
origin: (origin) => (/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)
|
|
57
|
+
? origin
|
|
58
|
+
: null),
|
|
59
|
+
}));
|
|
60
|
+
// ── Mount Route Modules ─────────────────────────────────
|
|
61
|
+
app.route('/', healthRoutes);
|
|
62
|
+
app.route('/', assetRoutes);
|
|
63
|
+
app.route('/', stageRoutes);
|
|
64
|
+
app.route('/', chatRoutes);
|
|
65
|
+
app.route('/', opencodeRoutes);
|
|
66
|
+
app.route('/', compileRoutes);
|
|
67
|
+
app.route('/', dotRoutes);
|
|
68
|
+
}
|
|
69
|
+
// ── Start Server ────────────────────────────────────────
|
|
70
|
+
await ensureOpencodeSidecar().catch((err) => {
|
|
71
|
+
console.warn(`OpenCode sidecar is not ready yet: ${err instanceof Error ? err.message : String(err)}`);
|
|
72
|
+
});
|
|
73
|
+
console.log(`\n🎪 DOT Studio Server${IS_PRODUCTION ? ' (production)' : ' (dev)'}`);
|
|
74
|
+
console.log(` API: http://localhost:${PORT}`);
|
|
75
|
+
console.log(` OpenCode: ${OPENCODE_URL} (${isManagedOpencode() ? 'managed sidecar' : 'external'})`);
|
|
76
|
+
console.log(` Project: ${getActiveProjectDir()}`);
|
|
77
|
+
console.log(` Data: ${STUDIO_DIR}\n`);
|
|
78
|
+
const server = serve({ fetch: app.fetch, port: PORT });
|
|
79
|
+
setupTerminalWs(server, () => getActiveProjectDir());
|