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.
Files changed (148) hide show
  1. package/README.md +20 -200
  2. package/client/assets/ActFrame-BYOBkLYW.js +1 -0
  3. package/client/assets/ActFrame-C_WEt6bv.css +1 -0
  4. package/client/assets/ActInspectorPanel-C3VlS7tB.js +1 -0
  5. package/client/assets/ActInspectorPanel-CE6s6GYv.css +1 -0
  6. package/client/assets/AssistantChat-BOyW0K79.js +1 -0
  7. package/client/assets/AssistantChat-DoVmHvMJ.css +1 -0
  8. package/client/assets/CanvasTerminalFrame-BC-79q9U.css +1 -0
  9. package/client/assets/CanvasTerminalFrame-DxKbexK6.js +4 -0
  10. package/client/assets/CanvasTrackingFrame-DumxhNwg.js +1 -0
  11. package/client/assets/CanvasTrackingFrame-G4rRrfne.css +1 -0
  12. package/client/assets/CanvasWindowFrame-ziJeVfHG.js +1 -0
  13. package/client/assets/DanceBundleEditorFrame-CH8VDUMK.js +1 -0
  14. package/client/assets/DanceBundleEditorFrame-DaLqMflT.css +1 -0
  15. package/client/assets/MarkdownEditorFrame-DVecIZpZ.css +1 -0
  16. package/client/assets/MarkdownEditorFrame-Dwpgs2GX.js +2 -0
  17. package/client/assets/MarkdownRenderer-Cz8A4AgP.js +1 -0
  18. package/client/assets/PublishModal-DUlHz0fT.js +1 -0
  19. package/client/assets/TodoDock-DcVf7zQG.js +1 -0
  20. package/client/assets/WorkspaceToolbar-CXYi_sMD.js +2 -0
  21. package/client/assets/WorkspaceToolbar-CiQvVocC.css +1 -0
  22. package/client/assets/chat-message-visibility-YwJ-AQno.js +11 -0
  23. package/client/assets/dnd-vendor-CIAZE2P2.js +5 -0
  24. package/client/assets/flow-vendor-BZV40eAE.css +1 -0
  25. package/client/assets/flow-vendor-C868rU-6.js +23 -0
  26. package/client/assets/icon-vendor-I2JVIi1s.js +501 -0
  27. package/client/assets/index-BMY4hrBP.js +3 -0
  28. package/client/assets/index-C-vnj9y3.js +1 -0
  29. package/client/assets/index-C9HTqfZw.css +1 -0
  30. package/client/assets/index-CWrv6O3o.js +64 -0
  31. package/client/assets/index-DMS12-Q2.js +8 -0
  32. package/client/assets/index-Dn7t_Y7G.js +1 -0
  33. package/client/assets/index-p-wk7iGH.css +1 -0
  34. package/client/assets/markdown-vendor-BSTcku12.css +10 -0
  35. package/client/assets/markdown-vendor-DnTJ9hmR.js +35 -0
  36. package/client/assets/participant-labels-Cf3qP3GB.js +1 -0
  37. package/client/assets/queries-Dm1jEHfc.js +1 -0
  38. package/client/assets/query-vendor-_taqgrbn.js +1 -0
  39. package/client/assets/react-vendor-DzpMUNDT.js +49 -0
  40. package/client/assets/settings-utils-l7KCS3Ev.js +1 -0
  41. package/client/assets/terminal-vendor-6GBZ9nXN.css +32 -0
  42. package/client/assets/terminal-vendor-D0xRnmbI.js +112 -0
  43. package/client/index.html +13 -3
  44. package/dist/cli.js +25 -3
  45. package/dist/server/app.js +72 -0
  46. package/dist/server/index.js +2 -62
  47. package/dist/server/lib/act-session-policy.js +31 -0
  48. package/dist/server/lib/chat-session.js +101 -0
  49. package/dist/server/lib/config.js +18 -4
  50. package/dist/server/lib/dot-authoring.js +171 -102
  51. package/dist/server/lib/dot-loader.js +9 -8
  52. package/dist/server/lib/dot-login.js +8 -190
  53. package/dist/server/lib/dot-source.js +11 -0
  54. package/dist/server/lib/model-catalog.js +74 -15
  55. package/dist/server/lib/opencode-auth.js +4 -1
  56. package/dist/server/lib/opencode-errors.js +70 -38
  57. package/dist/server/lib/opencode-sidecar.js +5 -2
  58. package/dist/server/lib/project-config.js +8 -0
  59. package/dist/server/lib/runtime-tools.js +46 -8
  60. package/dist/server/lib/safe-mode.js +410 -0
  61. package/dist/server/lib/session-execution.js +81 -0
  62. package/dist/server/lib/sse.js +22 -0
  63. package/dist/server/routes/act-runtime-threads.js +156 -0
  64. package/dist/server/routes/act-runtime-tools.js +157 -0
  65. package/dist/server/routes/act-runtime.js +7 -0
  66. package/dist/server/routes/adapter.js +32 -0
  67. package/dist/server/routes/assets-collection.js +16 -0
  68. package/dist/server/routes/assets-detail.js +38 -0
  69. package/dist/server/routes/assets.js +4 -158
  70. package/dist/server/routes/chat-messages.js +104 -0
  71. package/dist/server/routes/chat-sessions.js +104 -0
  72. package/dist/server/routes/chat-stream.js +15 -0
  73. package/dist/server/routes/chat.js +6 -353
  74. package/dist/server/routes/compile.js +5 -91
  75. package/dist/server/routes/dot-assets.js +77 -0
  76. package/dist/server/routes/dot-core.js +62 -0
  77. package/dist/server/routes/dot-performer.js +80 -0
  78. package/dist/server/routes/dot.js +6 -267
  79. package/dist/server/routes/drafts-collection.js +40 -0
  80. package/dist/server/routes/drafts-dance-bundle.js +113 -0
  81. package/dist/server/routes/drafts-item.js +86 -0
  82. package/dist/server/routes/drafts.js +9 -0
  83. package/dist/server/routes/health.js +18 -33
  84. package/dist/server/routes/opencode-core.js +120 -0
  85. package/dist/server/routes/opencode-file.js +67 -0
  86. package/dist/server/routes/opencode-mcp.js +74 -0
  87. package/dist/server/routes/opencode-provider.js +41 -0
  88. package/dist/server/routes/opencode.js +8 -418
  89. package/dist/server/routes/route-errors.js +10 -0
  90. package/dist/server/routes/safe-actions.js +60 -0
  91. package/dist/server/routes/safe-summary.js +20 -0
  92. package/dist/server/routes/safe.js +7 -0
  93. package/dist/server/routes/workspaces.js +47 -0
  94. package/dist/server/services/act-runtime/act-context-builder.js +81 -0
  95. package/dist/server/services/act-runtime/act-runtime-service.js +313 -0
  96. package/dist/server/services/act-runtime/act-runtime-utils.js +10 -0
  97. package/dist/server/services/act-runtime/act-tool-projection.js +26 -0
  98. package/dist/server/services/act-runtime/act-tools.js +151 -0
  99. package/dist/server/services/act-runtime/board-persistence.js +38 -0
  100. package/dist/server/services/act-runtime/event-logger.js +73 -0
  101. package/dist/server/services/act-runtime/event-router.js +102 -0
  102. package/dist/server/services/act-runtime/mailbox.js +149 -0
  103. package/dist/server/services/act-runtime/safety-guard.js +162 -0
  104. package/dist/server/services/act-runtime/session-queue.js +114 -0
  105. package/dist/server/services/act-runtime/thread-manager.js +351 -0
  106. package/dist/server/services/act-runtime/wake-cascade.js +306 -0
  107. package/dist/server/services/act-runtime/wake-evaluator.js +43 -0
  108. package/dist/server/services/act-runtime/wake-performer-resolver.js +68 -0
  109. package/dist/server/services/act-runtime/wake-prompt-builder.js +77 -0
  110. package/dist/server/services/adapter-view-service.js +6 -0
  111. package/dist/server/services/asset-service.js +366 -0
  112. package/dist/server/services/chat-event-stream-service.js +157 -0
  113. package/dist/server/services/chat-service.js +207 -0
  114. package/dist/server/services/chat-session-service.js +203 -0
  115. package/dist/server/services/compile-service.js +4 -0
  116. package/dist/server/services/dance-bundle-service.js +222 -0
  117. package/dist/server/services/dot-add-service.js +59 -0
  118. package/dist/server/services/dot-service.js +178 -0
  119. package/dist/server/services/draft-service.js +367 -0
  120. package/dist/server/services/opencode-projection/dance-compiler.js +164 -0
  121. package/dist/server/services/opencode-projection/performer-compiler.js +195 -0
  122. package/dist/server/services/opencode-projection/preview-service.js +31 -0
  123. package/dist/server/services/opencode-projection/projection-manifest.js +98 -0
  124. package/dist/server/services/opencode-projection/stage-projection-service.js +188 -0
  125. package/dist/server/services/opencode-service.js +338 -0
  126. package/dist/server/services/safe-service.js +33 -0
  127. package/dist/server/services/studio-assistant/assistant-service.js +172 -0
  128. package/dist/server/services/studio-service.js +69 -0
  129. package/dist/server/services/workspace-service.js +224 -0
  130. package/dist/server/terminal.js +57 -11
  131. package/dist/shared/act-types.js +4 -0
  132. package/dist/shared/adapter-view.js +1 -0
  133. package/dist/shared/asset-contracts.js +1 -0
  134. package/dist/shared/assistant-actions.js +1 -0
  135. package/dist/shared/chat-contracts.js +1 -0
  136. package/dist/shared/dot-contracts.js +1 -0
  137. package/dist/shared/dot-types.js +4 -0
  138. package/dist/shared/draft-contracts.js +2 -0
  139. package/dist/shared/model-types.js +2 -0
  140. package/dist/shared/performer-mcp-portability.js +10 -0
  141. package/dist/shared/safe-mode.js +1 -0
  142. package/dist/shared/session-metadata.js +4 -3
  143. package/package.json +6 -4
  144. package/client/assets/index-C2eIILoa.css +0 -41
  145. package/client/assets/index-DUPZ_Lw5.js +0 -616
  146. package/dist/server/lib/act-runtime.js +0 -1282
  147. package/dist/server/lib/prompt.js +0 -222
  148. 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
+ }