dot-studio 0.0.1 → 0.0.2
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/README.md +20 -200
- package/client/assets/ActFrame-BYOBkLYW.js +1 -0
- package/client/assets/ActFrame-C_WEt6bv.css +1 -0
- package/client/assets/ActInspectorPanel-C3VlS7tB.js +1 -0
- package/client/assets/ActInspectorPanel-CE6s6GYv.css +1 -0
- package/client/assets/AssistantChat-BOyW0K79.js +1 -0
- package/client/assets/AssistantChat-DoVmHvMJ.css +1 -0
- package/client/assets/CanvasTerminalFrame-BC-79q9U.css +1 -0
- package/client/assets/CanvasTerminalFrame-DxKbexK6.js +4 -0
- package/client/assets/CanvasTrackingFrame-DumxhNwg.js +1 -0
- package/client/assets/CanvasTrackingFrame-G4rRrfne.css +1 -0
- package/client/assets/CanvasWindowFrame-ziJeVfHG.js +1 -0
- package/client/assets/DanceBundleEditorFrame-CH8VDUMK.js +1 -0
- package/client/assets/DanceBundleEditorFrame-DaLqMflT.css +1 -0
- package/client/assets/MarkdownEditorFrame-DVecIZpZ.css +1 -0
- package/client/assets/MarkdownEditorFrame-Dwpgs2GX.js +2 -0
- package/client/assets/MarkdownRenderer-Cz8A4AgP.js +1 -0
- package/client/assets/PublishModal-DUlHz0fT.js +1 -0
- package/client/assets/TodoDock-DcVf7zQG.js +1 -0
- package/client/assets/WorkspaceToolbar-CXYi_sMD.js +2 -0
- package/client/assets/WorkspaceToolbar-CiQvVocC.css +1 -0
- package/client/assets/chat-message-visibility-YwJ-AQno.js +11 -0
- package/client/assets/dnd-vendor-CIAZE2P2.js +5 -0
- package/client/assets/flow-vendor-BZV40eAE.css +1 -0
- package/client/assets/flow-vendor-C868rU-6.js +23 -0
- package/client/assets/icon-vendor-I2JVIi1s.js +501 -0
- package/client/assets/index-BMY4hrBP.js +3 -0
- package/client/assets/index-C-vnj9y3.js +1 -0
- package/client/assets/index-C9HTqfZw.css +1 -0
- package/client/assets/index-CWrv6O3o.js +64 -0
- package/client/assets/index-DMS12-Q2.js +8 -0
- package/client/assets/index-Dn7t_Y7G.js +1 -0
- package/client/assets/index-p-wk7iGH.css +1 -0
- package/client/assets/markdown-vendor-BSTcku12.css +10 -0
- package/client/assets/markdown-vendor-DnTJ9hmR.js +35 -0
- package/client/assets/participant-labels-Cf3qP3GB.js +1 -0
- package/client/assets/queries-Dm1jEHfc.js +1 -0
- package/client/assets/query-vendor-_taqgrbn.js +1 -0
- package/client/assets/react-vendor-DzpMUNDT.js +49 -0
- package/client/assets/settings-utils-l7KCS3Ev.js +1 -0
- package/client/assets/terminal-vendor-6GBZ9nXN.css +32 -0
- package/client/assets/terminal-vendor-D0xRnmbI.js +112 -0
- package/client/index.html +13 -3
- package/dist/cli.js +25 -3
- package/dist/server/app.js +72 -0
- package/dist/server/index.js +2 -62
- package/dist/server/lib/act-session-policy.js +31 -0
- package/dist/server/lib/chat-session.js +101 -0
- package/dist/server/lib/config.js +18 -4
- package/dist/server/lib/dot-authoring.js +171 -102
- package/dist/server/lib/dot-loader.js +9 -8
- package/dist/server/lib/dot-login.js +8 -190
- package/dist/server/lib/dot-source.js +11 -0
- package/dist/server/lib/model-catalog.js +74 -15
- package/dist/server/lib/opencode-auth.js +4 -1
- package/dist/server/lib/opencode-errors.js +70 -38
- package/dist/server/lib/opencode-sidecar.js +5 -2
- package/dist/server/lib/project-config.js +8 -0
- package/dist/server/lib/runtime-tools.js +46 -8
- package/dist/server/lib/safe-mode.js +410 -0
- package/dist/server/lib/session-execution.js +81 -0
- package/dist/server/lib/sse.js +22 -0
- package/dist/server/routes/act-runtime-threads.js +156 -0
- package/dist/server/routes/act-runtime-tools.js +157 -0
- package/dist/server/routes/act-runtime.js +7 -0
- package/dist/server/routes/adapter.js +32 -0
- package/dist/server/routes/assets-collection.js +16 -0
- package/dist/server/routes/assets-detail.js +38 -0
- package/dist/server/routes/assets.js +4 -158
- package/dist/server/routes/chat-messages.js +104 -0
- package/dist/server/routes/chat-sessions.js +104 -0
- package/dist/server/routes/chat-stream.js +15 -0
- package/dist/server/routes/chat.js +6 -353
- package/dist/server/routes/compile.js +5 -91
- package/dist/server/routes/dot-assets.js +77 -0
- package/dist/server/routes/dot-core.js +62 -0
- package/dist/server/routes/dot-performer.js +80 -0
- package/dist/server/routes/dot.js +6 -267
- package/dist/server/routes/drafts-collection.js +40 -0
- package/dist/server/routes/drafts-dance-bundle.js +113 -0
- package/dist/server/routes/drafts-item.js +86 -0
- package/dist/server/routes/drafts.js +9 -0
- package/dist/server/routes/health.js +18 -33
- package/dist/server/routes/opencode-core.js +120 -0
- package/dist/server/routes/opencode-file.js +67 -0
- package/dist/server/routes/opencode-mcp.js +74 -0
- package/dist/server/routes/opencode-provider.js +41 -0
- package/dist/server/routes/opencode.js +8 -418
- package/dist/server/routes/route-errors.js +10 -0
- package/dist/server/routes/safe-actions.js +60 -0
- package/dist/server/routes/safe-summary.js +20 -0
- package/dist/server/routes/safe.js +7 -0
- package/dist/server/routes/workspaces.js +47 -0
- package/dist/server/services/act-runtime/act-context-builder.js +81 -0
- package/dist/server/services/act-runtime/act-runtime-service.js +313 -0
- package/dist/server/services/act-runtime/act-runtime-utils.js +10 -0
- package/dist/server/services/act-runtime/act-tool-projection.js +26 -0
- package/dist/server/services/act-runtime/act-tools.js +151 -0
- package/dist/server/services/act-runtime/board-persistence.js +38 -0
- package/dist/server/services/act-runtime/event-logger.js +73 -0
- package/dist/server/services/act-runtime/event-router.js +102 -0
- package/dist/server/services/act-runtime/mailbox.js +149 -0
- package/dist/server/services/act-runtime/safety-guard.js +162 -0
- package/dist/server/services/act-runtime/session-queue.js +114 -0
- package/dist/server/services/act-runtime/thread-manager.js +351 -0
- package/dist/server/services/act-runtime/wake-cascade.js +306 -0
- package/dist/server/services/act-runtime/wake-evaluator.js +43 -0
- package/dist/server/services/act-runtime/wake-performer-resolver.js +68 -0
- package/dist/server/services/act-runtime/wake-prompt-builder.js +77 -0
- package/dist/server/services/adapter-view-service.js +6 -0
- package/dist/server/services/asset-service.js +366 -0
- package/dist/server/services/chat-event-stream-service.js +157 -0
- package/dist/server/services/chat-service.js +207 -0
- package/dist/server/services/chat-session-service.js +203 -0
- package/dist/server/services/compile-service.js +4 -0
- package/dist/server/services/dance-bundle-service.js +222 -0
- package/dist/server/services/dot-add-service.js +59 -0
- package/dist/server/services/dot-service.js +178 -0
- package/dist/server/services/draft-service.js +367 -0
- package/dist/server/services/opencode-projection/dance-compiler.js +164 -0
- package/dist/server/services/opencode-projection/performer-compiler.js +195 -0
- package/dist/server/services/opencode-projection/preview-service.js +31 -0
- package/dist/server/services/opencode-projection/projection-manifest.js +98 -0
- package/dist/server/services/opencode-projection/stage-projection-service.js +188 -0
- package/dist/server/services/opencode-service.js +338 -0
- package/dist/server/services/safe-service.js +33 -0
- package/dist/server/services/studio-assistant/assistant-service.js +172 -0
- package/dist/server/services/studio-service.js +69 -0
- package/dist/server/services/workspace-service.js +224 -0
- package/dist/server/terminal.js +57 -11
- package/dist/shared/act-types.js +4 -0
- package/dist/shared/adapter-view.js +1 -0
- package/dist/shared/asset-contracts.js +1 -0
- package/dist/shared/assistant-actions.js +1 -0
- package/dist/shared/chat-contracts.js +1 -0
- package/dist/shared/dot-contracts.js +1 -0
- package/dist/shared/dot-types.js +4 -0
- package/dist/shared/draft-contracts.js +2 -0
- package/dist/shared/model-types.js +2 -0
- package/dist/shared/performer-mcp-portability.js +10 -0
- package/dist/shared/safe-mode.js +1 -0
- package/dist/shared/session-metadata.js +4 -3
- package/package.json +6 -4
- package/client/assets/index-C2eIILoa.css +0 -41
- package/client/assets/index-DUPZ_Lw5.js +0 -616
- package/dist/server/lib/act-runtime.js +0 -1282
- package/dist/server/lib/prompt.js +0 -222
- package/dist/server/routes/stages.js +0 -137
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* draft-service.ts — Filesystem CRUD for `.dance-of-tal/drafts/`
|
|
3
|
+
*
|
|
4
|
+
* Tal / Performer / Act: .dance-of-tal/drafts/<kind>/<id>.json
|
|
5
|
+
* Dance (bundle): .dance-of-tal/drafts/dance/<id>/draft.json + SKILL.md + sibling dirs
|
|
6
|
+
* Dance (legacy): .dance-of-tal/drafts/dance/<id>.json (lazily migrated to bundle)
|
|
7
|
+
* Project-local only — no global scope.
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import crypto from 'crypto';
|
|
12
|
+
import { getDotDir, ensureDotDir } from '../lib/dot-source.js';
|
|
13
|
+
import { danceBundleDir, isDanceBundleDraft, scaffoldDanceBundle, readBundleSkillContent, writeBundleSkillContent, } from './dance-bundle-service.js';
|
|
14
|
+
const DRAFT_KINDS = ['tal', 'dance', 'performer', 'act'];
|
|
15
|
+
function isErrnoException(error) {
|
|
16
|
+
return error instanceof Error;
|
|
17
|
+
}
|
|
18
|
+
function isPerformerDraftFile(draft) {
|
|
19
|
+
return draft.kind === 'performer' && !!draft.content && typeof draft.content === 'object';
|
|
20
|
+
}
|
|
21
|
+
function isActDraftFile(draft) {
|
|
22
|
+
return draft.kind === 'act' && !!draft.content && typeof draft.content === 'object';
|
|
23
|
+
}
|
|
24
|
+
function draftsDir(cwd) {
|
|
25
|
+
return path.join(getDotDir(cwd), 'drafts');
|
|
26
|
+
}
|
|
27
|
+
function kindDir(cwd, kind) {
|
|
28
|
+
return path.join(draftsDir(cwd), kind);
|
|
29
|
+
}
|
|
30
|
+
function draftFilePath(cwd, kind, id) {
|
|
31
|
+
return path.join(kindDir(cwd, kind), `${id}.json`);
|
|
32
|
+
}
|
|
33
|
+
function generateDraftId() {
|
|
34
|
+
const timestamp = Date.now().toString(36);
|
|
35
|
+
const random = crypto.randomBytes(4).toString('hex');
|
|
36
|
+
return `draft-${timestamp}-${random}`;
|
|
37
|
+
}
|
|
38
|
+
async function ensureDraftsDir(cwd, kind) {
|
|
39
|
+
await ensureDotDir(cwd);
|
|
40
|
+
await fs.mkdir(kindDir(cwd, kind), { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
// ── Create ──────────────────────────────────────────────
|
|
43
|
+
export async function createDraft(cwd, input) {
|
|
44
|
+
const id = input.id || generateDraftId();
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
// Dance drafts use bundle format
|
|
47
|
+
if (input.kind === 'dance') {
|
|
48
|
+
const skillContent = typeof input.content === 'string' ? input.content : '';
|
|
49
|
+
const draft = {
|
|
50
|
+
id,
|
|
51
|
+
kind: input.kind,
|
|
52
|
+
name: input.name,
|
|
53
|
+
content: skillContent,
|
|
54
|
+
slug: input.slug,
|
|
55
|
+
description: input.description,
|
|
56
|
+
tags: input.tags || [],
|
|
57
|
+
derivedFrom: input.derivedFrom || null,
|
|
58
|
+
createdAt: now,
|
|
59
|
+
updatedAt: now,
|
|
60
|
+
formatVersion: 2,
|
|
61
|
+
};
|
|
62
|
+
await ensureDraftsDir(cwd, input.kind);
|
|
63
|
+
await scaffoldDanceBundle(cwd, id, skillContent);
|
|
64
|
+
// Write draft.json metadata (content is stored in SKILL.md, not in draft.json)
|
|
65
|
+
const metaOnly = { ...draft, content: undefined };
|
|
66
|
+
await fs.writeFile(path.join(danceBundleDir(cwd, id), 'draft.json'), JSON.stringify(metaOnly, null, 2), 'utf-8');
|
|
67
|
+
return draft;
|
|
68
|
+
}
|
|
69
|
+
// Tal / Performer / Act — legacy JSON single-file
|
|
70
|
+
const draft = {
|
|
71
|
+
id,
|
|
72
|
+
kind: input.kind,
|
|
73
|
+
name: input.name,
|
|
74
|
+
content: input.content,
|
|
75
|
+
slug: input.slug,
|
|
76
|
+
description: input.description,
|
|
77
|
+
tags: input.tags || [],
|
|
78
|
+
derivedFrom: input.derivedFrom || null,
|
|
79
|
+
createdAt: now,
|
|
80
|
+
updatedAt: now,
|
|
81
|
+
formatVersion: 1,
|
|
82
|
+
};
|
|
83
|
+
await ensureDraftsDir(cwd, input.kind);
|
|
84
|
+
await fs.writeFile(draftFilePath(cwd, input.kind, id), JSON.stringify(draft, null, 2), 'utf-8');
|
|
85
|
+
return draft;
|
|
86
|
+
}
|
|
87
|
+
// ── Read ────────────────────────────────────────────────
|
|
88
|
+
export async function readDraft(cwd, kind, id) {
|
|
89
|
+
// Dance: try bundle format first
|
|
90
|
+
if (kind === 'dance') {
|
|
91
|
+
if (await isDanceBundleDraft(cwd, id)) {
|
|
92
|
+
return readDanceBundleDraft(cwd, id);
|
|
93
|
+
}
|
|
94
|
+
// Try legacy JSON, then lazily migrate
|
|
95
|
+
const legacy = await readLegacyJsonDraft(cwd, kind, id);
|
|
96
|
+
if (legacy) {
|
|
97
|
+
return migrateLegacyDanceDraft(cwd, legacy);
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
// Tal / Performer / Act — legacy JSON
|
|
102
|
+
return readLegacyJsonDraft(cwd, kind, id);
|
|
103
|
+
}
|
|
104
|
+
async function readLegacyJsonDraft(cwd, kind, id) {
|
|
105
|
+
try {
|
|
106
|
+
const raw = await fs.readFile(draftFilePath(cwd, kind, id), 'utf-8');
|
|
107
|
+
return JSON.parse(raw);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (isErrnoException(error) && error.code === 'ENOENT')
|
|
111
|
+
return null;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function readDanceBundleDraft(cwd, id) {
|
|
116
|
+
try {
|
|
117
|
+
const metaRaw = await fs.readFile(path.join(danceBundleDir(cwd, id), 'draft.json'), 'utf-8');
|
|
118
|
+
const meta = JSON.parse(metaRaw);
|
|
119
|
+
const skillContent = await readBundleSkillContent(cwd, id);
|
|
120
|
+
return {
|
|
121
|
+
...meta,
|
|
122
|
+
content: skillContent || '',
|
|
123
|
+
formatVersion: 2,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (isErrnoException(error) && error.code === 'ENOENT')
|
|
128
|
+
return null;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Lazily migrate a legacy Dance JSON draft to bundle format.
|
|
134
|
+
* Creates the bundle directory, writes SKILL.md, writes draft.json, removes the old file.
|
|
135
|
+
*/
|
|
136
|
+
async function migrateLegacyDanceDraft(cwd, legacy) {
|
|
137
|
+
const skillContent = typeof legacy.content === 'string' ? legacy.content : '';
|
|
138
|
+
await scaffoldDanceBundle(cwd, legacy.id, skillContent);
|
|
139
|
+
const migrated = {
|
|
140
|
+
...legacy,
|
|
141
|
+
formatVersion: 2,
|
|
142
|
+
};
|
|
143
|
+
const metaOnly = { ...migrated, content: undefined };
|
|
144
|
+
await fs.writeFile(path.join(danceBundleDir(cwd, legacy.id), 'draft.json'), JSON.stringify(metaOnly, null, 2), 'utf-8');
|
|
145
|
+
// Remove legacy file (best effort)
|
|
146
|
+
try {
|
|
147
|
+
await fs.unlink(draftFilePath(cwd, 'dance', legacy.id));
|
|
148
|
+
}
|
|
149
|
+
catch { /* ignore if already gone */ }
|
|
150
|
+
return migrated;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Read just the content field from a draft — used by compilers.
|
|
154
|
+
* Returns the text content for tal/dance, or the full content object for performer/act.
|
|
155
|
+
*/
|
|
156
|
+
export async function readDraftContent(cwd, kind, id) {
|
|
157
|
+
const draft = await readDraft(cwd, kind, id);
|
|
158
|
+
if (!draft)
|
|
159
|
+
return null;
|
|
160
|
+
return draft.content;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Read the text content from a draft — convenience for tal/dance.
|
|
164
|
+
* Returns null if not found or content is not a string.
|
|
165
|
+
*/
|
|
166
|
+
export async function readDraftTextContent(cwd, kind, id) {
|
|
167
|
+
const content = await readDraftContent(cwd, kind, id);
|
|
168
|
+
return typeof content === 'string' ? content : null;
|
|
169
|
+
}
|
|
170
|
+
// ── List ────────────────────────────────────────────────
|
|
171
|
+
export async function listDrafts(cwd, kind) {
|
|
172
|
+
const kinds = kind ? [kind] : [...DRAFT_KINDS];
|
|
173
|
+
const drafts = [];
|
|
174
|
+
const seenIds = new Set();
|
|
175
|
+
for (const k of kinds) {
|
|
176
|
+
const dir = kindDir(cwd, k);
|
|
177
|
+
let entries;
|
|
178
|
+
try {
|
|
179
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (isErrnoException(error) && error.code === 'ENOENT')
|
|
183
|
+
continue;
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
// Dance bundle directories
|
|
188
|
+
if (k === 'dance' && entry.isDirectory()) {
|
|
189
|
+
try {
|
|
190
|
+
const metaPath = path.join(dir, entry.name, 'draft.json');
|
|
191
|
+
const raw = await fs.readFile(metaPath, 'utf-8');
|
|
192
|
+
const meta = JSON.parse(raw);
|
|
193
|
+
const skillContent = await readBundleSkillContent(cwd, entry.name);
|
|
194
|
+
drafts.push({
|
|
195
|
+
...meta,
|
|
196
|
+
content: skillContent || '',
|
|
197
|
+
formatVersion: 2,
|
|
198
|
+
});
|
|
199
|
+
seenIds.add(meta.id);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Skip malformed bundle
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
// Legacy JSON files
|
|
207
|
+
if (!entry.isFile() || !entry.name.endsWith('.json'))
|
|
208
|
+
continue;
|
|
209
|
+
try {
|
|
210
|
+
const raw = await fs.readFile(path.join(dir, entry.name), 'utf-8');
|
|
211
|
+
const draft = JSON.parse(raw);
|
|
212
|
+
// Skip if already seen as a bundle draft (shouldn't happen, but safety)
|
|
213
|
+
if (seenIds.has(draft.id))
|
|
214
|
+
continue;
|
|
215
|
+
drafts.push(draft);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// Skip malformed files
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return drafts.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
223
|
+
}
|
|
224
|
+
// ── Update ──────────────────────────────────────────────
|
|
225
|
+
export async function updateDraft(cwd, kind, id, patch) {
|
|
226
|
+
const existing = await readDraft(cwd, kind, id);
|
|
227
|
+
if (!existing)
|
|
228
|
+
return null;
|
|
229
|
+
const updated = {
|
|
230
|
+
...existing,
|
|
231
|
+
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
232
|
+
...(patch.content !== undefined ? { content: patch.content } : {}),
|
|
233
|
+
...(patch.slug !== undefined ? { slug: patch.slug } : {}),
|
|
234
|
+
...(patch.description !== undefined ? { description: patch.description } : {}),
|
|
235
|
+
...(patch.tags !== undefined ? { tags: patch.tags } : {}),
|
|
236
|
+
...(patch.derivedFrom !== undefined ? { derivedFrom: patch.derivedFrom } : {}),
|
|
237
|
+
updatedAt: Date.now(),
|
|
238
|
+
};
|
|
239
|
+
// Dance bundle: write SKILL.md content + metadata to draft.json
|
|
240
|
+
if (kind === 'dance' && (existing.formatVersion === 2 || await isDanceBundleDraft(cwd, id))) {
|
|
241
|
+
updated.formatVersion = 2;
|
|
242
|
+
// Write SKILL.md if content was patched
|
|
243
|
+
if (patch.content !== undefined && typeof patch.content === 'string') {
|
|
244
|
+
await writeBundleSkillContent(cwd, id, patch.content);
|
|
245
|
+
}
|
|
246
|
+
// Write metadata to draft.json (content excluded from metadata file)
|
|
247
|
+
const metaOnly = { ...updated, content: undefined };
|
|
248
|
+
await fs.writeFile(path.join(danceBundleDir(cwd, id), 'draft.json'), JSON.stringify(metaOnly, null, 2), 'utf-8');
|
|
249
|
+
return updated;
|
|
250
|
+
}
|
|
251
|
+
// Legacy JSON
|
|
252
|
+
await fs.writeFile(draftFilePath(cwd, kind, id), JSON.stringify(updated, null, 2), 'utf-8');
|
|
253
|
+
return updated;
|
|
254
|
+
}
|
|
255
|
+
function extractReferencedDrafts(draft) {
|
|
256
|
+
const refs = [];
|
|
257
|
+
if (isPerformerDraftFile(draft)) {
|
|
258
|
+
const content = draft.content;
|
|
259
|
+
if (content.talRef?.kind === 'draft' && typeof content.talRef.draftId === 'string') {
|
|
260
|
+
refs.push({ kind: 'tal', draftId: content.talRef.draftId });
|
|
261
|
+
}
|
|
262
|
+
if (Array.isArray(content.danceRefs)) {
|
|
263
|
+
for (const ref of content.danceRefs) {
|
|
264
|
+
if (ref?.kind === 'draft' && typeof ref.draftId === 'string') {
|
|
265
|
+
refs.push({ kind: 'dance', draftId: ref.draftId });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (isActDraftFile(draft)) {
|
|
271
|
+
const content = draft.content;
|
|
272
|
+
for (const key of Object.keys(content.participants)) {
|
|
273
|
+
const participant = content.participants[key];
|
|
274
|
+
if (participant?.performerRef?.kind === 'draft' && typeof participant.performerRef.draftId === 'string') {
|
|
275
|
+
refs.push({ kind: 'performer', draftId: participant.performerRef.draftId });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return refs;
|
|
280
|
+
}
|
|
281
|
+
export async function findDraftDependents(cwd, targetKind, targetId) {
|
|
282
|
+
const allDrafts = await listDrafts(cwd);
|
|
283
|
+
const targetDraft = allDrafts.find((d) => d.kind === targetKind && d.id === targetId);
|
|
284
|
+
if (!targetDraft) {
|
|
285
|
+
throw new Error(`Draft not found: ${targetKind}/${targetId}`);
|
|
286
|
+
}
|
|
287
|
+
const dependents = [];
|
|
288
|
+
const processedIds = new Set([targetId]);
|
|
289
|
+
const queue = [targetId];
|
|
290
|
+
while (queue.length > 0) {
|
|
291
|
+
const currentId = queue.shift();
|
|
292
|
+
for (const draft of allDrafts) {
|
|
293
|
+
if (processedIds.has(draft.id))
|
|
294
|
+
continue;
|
|
295
|
+
const refs = extractReferencedDrafts(draft);
|
|
296
|
+
if (refs.some((r) => r.draftId === currentId)) {
|
|
297
|
+
processedIds.add(draft.id);
|
|
298
|
+
const reason = draft.kind === 'performer'
|
|
299
|
+
? `References ${targetKind} draft`
|
|
300
|
+
: `Contains performer referencing ${targetKind} draft`;
|
|
301
|
+
dependents.push({
|
|
302
|
+
draftId: draft.id,
|
|
303
|
+
kind: draft.kind,
|
|
304
|
+
name: draft.name,
|
|
305
|
+
source: 'draft',
|
|
306
|
+
reason,
|
|
307
|
+
});
|
|
308
|
+
queue.push(draft.id);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
target: {
|
|
314
|
+
draftId: targetDraft.id,
|
|
315
|
+
kind: targetDraft.kind,
|
|
316
|
+
name: targetDraft.name,
|
|
317
|
+
source: 'draft',
|
|
318
|
+
reason: 'Target',
|
|
319
|
+
},
|
|
320
|
+
dependents,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
async function deleteSingleDraft(cwd, kind, id) {
|
|
324
|
+
// Dance: try bundle first
|
|
325
|
+
if (kind === 'dance' && await isDanceBundleDraft(cwd, id)) {
|
|
326
|
+
await fs.rm(danceBundleDir(cwd, id), { recursive: true, force: true });
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
// Legacy JSON
|
|
330
|
+
try {
|
|
331
|
+
await fs.unlink(draftFilePath(cwd, kind, id));
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
if (isErrnoException(error) && error.code === 'ENOENT')
|
|
336
|
+
return false;
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
export async function deleteDraft(cwd, kind, id, cascade = false) {
|
|
341
|
+
const deletedIds = [];
|
|
342
|
+
if (cascade) {
|
|
343
|
+
const plan = await findDraftDependents(cwd, kind, id);
|
|
344
|
+
// Delete dependents first (bottom-up: acts before performers)
|
|
345
|
+
const sortedDependents = [...plan.dependents].sort((a, b) => {
|
|
346
|
+
const order = { act: 0, performer: 1, dance: 2, tal: 3 };
|
|
347
|
+
return (order[a.kind] ?? 9) - (order[b.kind] ?? 9);
|
|
348
|
+
});
|
|
349
|
+
for (const dep of sortedDependents) {
|
|
350
|
+
try {
|
|
351
|
+
const deleted = await deleteSingleDraft(cwd, dep.kind, dep.draftId);
|
|
352
|
+
if (deleted)
|
|
353
|
+
deletedIds.push(dep.draftId);
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
if (!isErrnoException(error) || error.code !== 'ENOENT')
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const deleted = await deleteSingleDraft(cwd, kind, id);
|
|
362
|
+
if (deleted) {
|
|
363
|
+
deletedIds.push(id);
|
|
364
|
+
return { ok: true, deletedIds };
|
|
365
|
+
}
|
|
366
|
+
return { ok: false, deletedIds };
|
|
367
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import { getAssetPayload, readAsset, danceAssetDir } from '../../lib/dot-source.js';
|
|
4
|
+
import { localSkillProjectionDir, toRelativePath } from './projection-manifest.js';
|
|
5
|
+
import { readDraft } from '../draft-service.js';
|
|
6
|
+
import { isDanceBundleDraft, danceBundleDir, readBundleSkillContent, } from '../dance-bundle-service.js';
|
|
7
|
+
function sanitizeSegment(value) {
|
|
8
|
+
return value
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
12
|
+
.replace(/-{2,}/g, '-')
|
|
13
|
+
.replace(/^-+|-+$/g, '');
|
|
14
|
+
}
|
|
15
|
+
function extractDraftDescription(draft) {
|
|
16
|
+
if (!draft) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
if (typeof draft.description === 'string') {
|
|
20
|
+
return draft.description;
|
|
21
|
+
}
|
|
22
|
+
if (draft.content && typeof draft.content === 'object') {
|
|
23
|
+
const content = draft.content;
|
|
24
|
+
if (typeof content.description === 'string') {
|
|
25
|
+
return content.description;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
function parseUrn(urn) {
|
|
31
|
+
// URN is 4-segment: kind/@owner/stage/name
|
|
32
|
+
const parts = urn.split('/');
|
|
33
|
+
const kind = parts[0] ?? '';
|
|
34
|
+
const ownerWithAt = parts[1] ?? '';
|
|
35
|
+
const stage = parts[2] ?? '';
|
|
36
|
+
const name = parts[3] ?? '';
|
|
37
|
+
return {
|
|
38
|
+
kind,
|
|
39
|
+
author: sanitizeSegment(ownerWithAt.replace(/^@/, '')),
|
|
40
|
+
stage: sanitizeSegment(stage),
|
|
41
|
+
slug: sanitizeSegment(name),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function buildFrontmatter(name, description) {
|
|
45
|
+
return [
|
|
46
|
+
'---',
|
|
47
|
+
`name: ${JSON.stringify(name)}`,
|
|
48
|
+
`description: ${JSON.stringify(description || 'Generated skill')}`,
|
|
49
|
+
'---',
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
export async function compileDance(cwd, ref, stageHash, performerId, executionDir, scope = 'workspace', actId) {
|
|
53
|
+
if (ref.kind === 'registry') {
|
|
54
|
+
const asset = await readAsset(cwd, ref.urn);
|
|
55
|
+
const body = await getAssetPayload(cwd, ref.urn);
|
|
56
|
+
if (!body) {
|
|
57
|
+
throw new Error(`Dance '${ref.urn}' was not found or has no content.`);
|
|
58
|
+
}
|
|
59
|
+
const parsed = parseUrn(ref.urn);
|
|
60
|
+
const logicalName = parsed.slug;
|
|
61
|
+
const description = typeof asset?.description === 'string' ? asset.description : parsed.slug;
|
|
62
|
+
const skillDir = path.join(localSkillProjectionDir(executionDir, stageHash, performerId, scope, actId), logicalName);
|
|
63
|
+
const filePath = path.join(skillDir, 'SKILL.md');
|
|
64
|
+
const content = `${buildFrontmatter(logicalName, description)}\n\n${body}`;
|
|
65
|
+
// Copy bundle sibling dirs (scripts/, references/, assets/) from locally installed dance
|
|
66
|
+
const bundleDir = danceAssetDir(cwd, ref.urn);
|
|
67
|
+
const additionalFiles = await copyBundleSiblings(bundleDir, skillDir);
|
|
68
|
+
return {
|
|
69
|
+
logicalName,
|
|
70
|
+
description,
|
|
71
|
+
filePath,
|
|
72
|
+
relativePath: toRelativePath(executionDir, filePath),
|
|
73
|
+
content,
|
|
74
|
+
additionalFiles,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// ── Draft ref: check if bundle-backed ─────────────────
|
|
78
|
+
const isBundle = await isDanceBundleDraft(cwd, ref.draftId);
|
|
79
|
+
if (isBundle) {
|
|
80
|
+
const body = await readBundleSkillContent(cwd, ref.draftId);
|
|
81
|
+
if (!body) {
|
|
82
|
+
throw new Error(`Dance draft '${ref.draftId}' is missing SKILL.md.`);
|
|
83
|
+
}
|
|
84
|
+
const draft = await readDraft(cwd, 'dance', ref.draftId);
|
|
85
|
+
const logicalName = sanitizeSegment(draft?.name || ref.draftId);
|
|
86
|
+
const description = extractDraftDescription(draft) || draft?.name || 'Draft skill';
|
|
87
|
+
const skillDir = path.join(localSkillProjectionDir(executionDir, stageHash, performerId, scope, actId), logicalName);
|
|
88
|
+
const filePath = path.join(skillDir, 'SKILL.md');
|
|
89
|
+
const content = `${buildFrontmatter(logicalName, description)}\n\n${body}`;
|
|
90
|
+
// Copy bundle sibling directories into projection
|
|
91
|
+
const bundleRoot = danceBundleDir(cwd, ref.draftId);
|
|
92
|
+
const additionalFiles = await copyBundleSiblings(bundleRoot, skillDir);
|
|
93
|
+
return {
|
|
94
|
+
logicalName,
|
|
95
|
+
description,
|
|
96
|
+
filePath,
|
|
97
|
+
relativePath: toRelativePath(executionDir, filePath),
|
|
98
|
+
content,
|
|
99
|
+
additionalFiles,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const draft = await readDraft(cwd, 'dance', ref.draftId);
|
|
103
|
+
const body = draft ? (typeof draft.content === 'string' ? draft.content : null) : null;
|
|
104
|
+
if (!draft || !body) {
|
|
105
|
+
throw new Error(`Dance draft '${ref.draftId}' was not found or has no content.`);
|
|
106
|
+
}
|
|
107
|
+
const logicalName = sanitizeSegment(draft.name || ref.draftId);
|
|
108
|
+
const description = extractDraftDescription(draft) || draft.name || 'Draft skill';
|
|
109
|
+
const filePath = path.join(localSkillProjectionDir(executionDir, stageHash, performerId, scope, actId), logicalName, 'SKILL.md');
|
|
110
|
+
const content = `${buildFrontmatter(logicalName, description)}\n\n${body}`;
|
|
111
|
+
return {
|
|
112
|
+
logicalName,
|
|
113
|
+
description,
|
|
114
|
+
filePath,
|
|
115
|
+
relativePath: toRelativePath(executionDir, filePath),
|
|
116
|
+
content,
|
|
117
|
+
additionalFiles: [],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Copy all bundle contents (except SKILL.md itself) from the source bundle directory
|
|
122
|
+
* into the target projection directory. SKILL.md is handled separately via writeIfChanged.
|
|
123
|
+
* Returns absolute paths of all copied files.
|
|
124
|
+
*/
|
|
125
|
+
async function copyBundleSiblings(bundleRoot, targetDir) {
|
|
126
|
+
const copiedFiles = [];
|
|
127
|
+
let entries;
|
|
128
|
+
try {
|
|
129
|
+
entries = await fs.readdir(bundleRoot, { withFileTypes: true, encoding: 'utf-8' });
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return copiedFiles; // bundle dir doesn't exist (e.g. SKILL.md-only install)
|
|
133
|
+
}
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
if (entry.name === 'SKILL.md')
|
|
136
|
+
continue; // handled via writeIfChanged
|
|
137
|
+
const srcPath = path.join(bundleRoot, entry.name);
|
|
138
|
+
const destPath = path.join(targetDir, entry.name);
|
|
139
|
+
if (entry.isDirectory()) {
|
|
140
|
+
await copyDirRecursive(srcPath, destPath, copiedFiles);
|
|
141
|
+
}
|
|
142
|
+
else if (entry.isFile()) {
|
|
143
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
144
|
+
await fs.copyFile(srcPath, destPath);
|
|
145
|
+
copiedFiles.push(destPath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return copiedFiles;
|
|
149
|
+
}
|
|
150
|
+
async function copyDirRecursive(src, dest, copiedFiles) {
|
|
151
|
+
await fs.mkdir(dest, { recursive: true });
|
|
152
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
const srcPath = path.join(src, entry.name);
|
|
155
|
+
const destPath = path.join(dest, entry.name);
|
|
156
|
+
if (entry.isDirectory()) {
|
|
157
|
+
await copyDirRecursive(srcPath, destPath, copiedFiles);
|
|
158
|
+
}
|
|
159
|
+
else if (entry.isFile()) {
|
|
160
|
+
await fs.copyFile(srcPath, destPath);
|
|
161
|
+
copiedFiles.push(destPath);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|