dsh-artcraft-plugin-soundcraft 0.1.2 → 0.2.0
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/package.json +3 -4
- package/src/index.js +38 -0
- package/src/kit/catalog.js +48 -3
- package/src/kit/discover.js +57 -8
- package/src/kit/exec.js +31 -3
- package/src/kit/mcp.js +6 -1
- package/src/kit/profiles.mjs +57 -0
- package/src/kit/tools.js +46 -1
- package/src/kit/types.js +1 -1
- package/src/root.js +13 -0
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-artcraft-plugin-soundcraft",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "DSH plugin: drive SoundCraft (audio production) headlessly through its own CLI and MCP server.",
|
|
7
7
|
"license": "MIT OR Apache-2.0",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./src/index.
|
|
9
|
+
".": "./src/index.js",
|
|
10
10
|
"./package.json": "./package.json"
|
|
11
11
|
},
|
|
12
12
|
"peerDependencies": {
|
|
@@ -23,8 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"src",
|
|
26
|
-
"cordis.patch.yml"
|
|
27
|
-
"README.md"
|
|
26
|
+
"cordis.patch.yml"
|
|
28
27
|
],
|
|
29
28
|
"dsh": {
|
|
30
29
|
"bundle": {
|
package/src/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SoundCraft as a DSH plugin.
|
|
3
|
+
*
|
|
4
|
+
* Registers model-callable tools that drive the `soundcraft-cli` binary this machine
|
|
5
|
+
* already has — both its generic `--cmd` surface and its own MCP server. The
|
|
6
|
+
* SoundCraft source tree is never touched: the CLI is an external process, located
|
|
7
|
+
* at call time so installing it mid-session works.
|
|
8
|
+
*
|
|
9
|
+
* The kernel under `./kit/` is generated from `dsh-plugins/craft-kit/src` by
|
|
10
|
+
* `scripts/build-kit.mjs` and inlined so this package installs standalone.
|
|
11
|
+
* @module dsh-artcraft-plugin-soundcraft
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
15
|
+
|
|
16
|
+
import { applyCraftPlugin, profileOf } from './kit/index.js'
|
|
17
|
+
|
|
18
|
+
import { PLUGIN_ROOT } from './root.js'
|
|
19
|
+
|
|
20
|
+
export const name = 'dsh-artcraft-soundcraft'
|
|
21
|
+
export const inject = ['tools', 'systemPrompt']
|
|
22
|
+
|
|
23
|
+
/** The craft this plugin exposes. */
|
|
24
|
+
export const craft = profileOf('soundcraft')
|
|
25
|
+
|
|
26
|
+
/** Configuration is the shared craft config. */
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
export function apply(ctx , config = {}) {
|
|
30
|
+
if (craft === undefined) throw new Error('dsh-artcraft-soundcraft: profile "soundcraft" is missing from the inlined kernel')
|
|
31
|
+
// `ctx.effect` ties the registration and the MCP server to this plugin's fiber.
|
|
32
|
+
ctx.effect(() => applyCraftPlugin(ctx, {
|
|
33
|
+
profile: craft,
|
|
34
|
+
packageRoot: PLUGIN_ROOT,
|
|
35
|
+
config,
|
|
36
|
+
defineTool: defineTool ,
|
|
37
|
+
}))
|
|
38
|
+
}
|
package/src/kit/catalog.js
CHANGED
|
@@ -92,9 +92,44 @@ export function parseCatalog(stdout , json ) {
|
|
|
92
92
|
// Fall through to the table parser: a CLI may ignore an unknown --json.
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
+
const emDash = parseEmDash(text)
|
|
96
|
+
if (emDash.length > 0) return emDash
|
|
95
97
|
return parseTable(text)
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Parse an `id — Label: {params}` catalogue (DeckCraft 0.14+).
|
|
102
|
+
*
|
|
103
|
+
* One command per line, single-spaced, with an em dash between the id and its
|
|
104
|
+
* label. `: {…}` introduces the parameter grammar and ` → …` describes what the
|
|
105
|
+
* command answers, which is not part of the grammar.
|
|
106
|
+
* @param text - raw `commands` output.
|
|
107
|
+
* @returns every command whose line has the `id — label` shape.
|
|
108
|
+
*/
|
|
109
|
+
export function parseEmDash(text ) {
|
|
110
|
+
const out = []
|
|
111
|
+
for (const raw of text.split('\n')) {
|
|
112
|
+
const line = raw.trimEnd()
|
|
113
|
+
if (line.length === 0 || line.startsWith('#')) continue
|
|
114
|
+
const match = /^([\w.-]+)\s+—\s+(.*)$/.exec(line)
|
|
115
|
+
if (match === null) continue
|
|
116
|
+
const id = match[1]
|
|
117
|
+
const rest = match[2]
|
|
118
|
+
const returnIndex = rest.indexOf(' → ')
|
|
119
|
+
const beforeReturn = returnIndex === -1 ? rest : rest.slice(0, returnIndex)
|
|
120
|
+
const brace = beforeReturn.indexOf(': {')
|
|
121
|
+
const label = brace === -1 ? beforeReturn.trim() : beforeReturn.slice(0, brace).trim()
|
|
122
|
+
const params = brace === -1 ? undefined : beforeReturn.slice(brace + 2).trim()
|
|
123
|
+
out.push({
|
|
124
|
+
id,
|
|
125
|
+
...label.length > 0 ? { label } : {},
|
|
126
|
+
...params !== undefined && params.length > 0 ? { params } : {},
|
|
127
|
+
group: groupOf(id),
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
return out
|
|
131
|
+
}
|
|
132
|
+
|
|
98
133
|
/**
|
|
99
134
|
* Parse a craft's fixed-column `commands` table.
|
|
100
135
|
*
|
|
@@ -111,7 +146,14 @@ export function parseTable(text ) {
|
|
|
111
146
|
const flush = () => {
|
|
112
147
|
if (current === undefined) return
|
|
113
148
|
const { id, columns } = current
|
|
114
|
-
|
|
149
|
+
let rest = columns.join(' ').trim()
|
|
150
|
+
// Some tables end with the grammar as its own column (`{name?, color?}`),
|
|
151
|
+
// with no `params:` label; take the last column when it is only an object.
|
|
152
|
+
let columnParams
|
|
153
|
+
if (columns.length > 1 && /^\{.*\}$/.test(columns[columns.length - 1].trim())) {
|
|
154
|
+
columnParams = columns[columns.length - 1].trim()
|
|
155
|
+
rest = columns.slice(0, -1).join(' ').trim()
|
|
156
|
+
}
|
|
115
157
|
const paramsMatch = /\bparams:\s*(\{.*)$/.exec(rest)
|
|
116
158
|
const labelPart = paramsMatch === null ? rest : rest.slice(0, paramsMatch.index).trim()
|
|
117
159
|
// Trailing `label [Shortcut]` and an interleaved menu column.
|
|
@@ -120,7 +162,9 @@ export function parseTable(text ) {
|
|
|
120
162
|
out.push({
|
|
121
163
|
id,
|
|
122
164
|
...label.length > 0 ? { label } : {},
|
|
123
|
-
...
|
|
165
|
+
...columnParams !== undefined
|
|
166
|
+
? { params: columnParams.replace(/\s+/g, ' ') }
|
|
167
|
+
: paramsMatch !== null ? { params: paramsMatch[1].replace(/\s+/g, ' ').trim() } : {},
|
|
124
168
|
...shortcutMatch !== null ? { shortcut: shortcutMatch[1] } : {},
|
|
125
169
|
group: groupOf(id),
|
|
126
170
|
})
|
|
@@ -136,7 +180,8 @@ export function parseTable(text ) {
|
|
|
136
180
|
}
|
|
137
181
|
flush()
|
|
138
182
|
const columns = raw.trimEnd().split(/\s{2,}/).map(part => part.trim()).filter(part => part.length > 0)
|
|
139
|
-
|
|
183
|
+
// Rust CLIs mark undoable commands with a trailing `*`; it is not the id.
|
|
184
|
+
const id = (columns[0] ?? '').replace(/\*$/, '')
|
|
140
185
|
if (id.length === 0 || /\s/.test(id) || !/^[\w.-]+$/.test(id)) continue
|
|
141
186
|
current = { id, columns: columns.slice(1) }
|
|
142
187
|
}
|
package/src/kit/discover.js
CHANGED
|
@@ -20,20 +20,36 @@ import { execFileSync } from 'node:child_process'
|
|
|
20
20
|
import { fileURLToPath } from 'node:url'
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* True when `path` is an executable regular file (or a symlink to one).
|
|
25
|
+
*
|
|
26
|
+
* `X_OK` is a no-op on Windows — it succeeds for any file that exists — so on
|
|
27
|
+
* that platform the name is additionally required to carry an executable
|
|
28
|
+
* extension (`PATHEXT`), which stops a stray text file called
|
|
29
|
+
* `gridcraft-cli` from being adopted as the binary.
|
|
30
|
+
*/
|
|
24
31
|
export function isExecutable(path ) {
|
|
25
32
|
try {
|
|
26
33
|
if (!statSync(path).isFile()) return false
|
|
27
34
|
accessSync(path, constants.X_OK)
|
|
35
|
+
if (process.platform === 'win32') {
|
|
36
|
+
const extensions = (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD')
|
|
37
|
+
.split(';').map(extension => extension.trim().toLowerCase()).filter(extension => extension.length > 0)
|
|
38
|
+
const lower = path.toLowerCase()
|
|
39
|
+
return extensions.some(extension => lower.endsWith(extension))
|
|
40
|
+
}
|
|
28
41
|
return true
|
|
29
42
|
} catch {
|
|
30
43
|
return false
|
|
31
44
|
}
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
/** Every directory on `PATH`, in order. */
|
|
47
|
+
/** Every directory on `PATH`, in order, with quoting removed. */
|
|
35
48
|
function pathDirs() {
|
|
36
|
-
return (process.env.PATH ?? '')
|
|
49
|
+
return (process.env.PATH ?? '')
|
|
50
|
+
.split(delimiter)
|
|
51
|
+
.map(entry => entry.trim().replace(/^"(.*)"$/, '$1'))
|
|
52
|
+
.filter(entry => entry.length > 0)
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
/** Directory holding this kernel (`<workspace>/dsh-plugins/craft-kit/src`). */
|
|
@@ -111,16 +127,39 @@ export function candidates(profile , configured , packageRo
|
|
|
111
127
|
const resolved = isAbsolute(dir) ? dir : resolve(base, dir)
|
|
112
128
|
if (seen.has(resolved)) continue
|
|
113
129
|
seen.add(resolved)
|
|
114
|
-
|
|
130
|
+
// Windows installs the CLI as `<craft>-cli.exe`, so the platform's own
|
|
131
|
+
// name has to be tried before the bare one; otherwise a workspace
|
|
132
|
+
// `crafts-bin/` copy is never found there.
|
|
133
|
+
for (const name of candidateNames(profile)) {
|
|
134
|
+
out.push({ path: join(resolved, name), source: `search-dir:${resolved}` })
|
|
135
|
+
}
|
|
115
136
|
}
|
|
116
137
|
}
|
|
117
138
|
for (const dir of pathDirs()) {
|
|
118
|
-
|
|
119
|
-
|
|
139
|
+
for (const name of candidateNames(profile)) {
|
|
140
|
+
out.push({ path: join(dir, name), source: 'PATH' })
|
|
141
|
+
}
|
|
120
142
|
}
|
|
121
143
|
return out
|
|
122
144
|
}
|
|
123
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Names a craft CLI may have on this platform, most likely first.
|
|
148
|
+
*
|
|
149
|
+
* Windows installs `<craft>-cli.exe`, so the extension is what a `PATH` or
|
|
150
|
+
* `crafts-bin/` lookup has to try; `PATHEXT` is honoured rather than assumed
|
|
151
|
+
* because a machine may have narrowed it. The bare name comes last, and only on
|
|
152
|
+
* Windows, for a CLI a user renamed themselves.
|
|
153
|
+
* @param profile - the craft being located.
|
|
154
|
+
* @returns candidate file names, deduplicated.
|
|
155
|
+
*/
|
|
156
|
+
function candidateNames(profile ) {
|
|
157
|
+
if (process.platform !== 'win32') return [profile.bin]
|
|
158
|
+
const extensions = (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD')
|
|
159
|
+
.split(';').map(extension => extension.trim().toLowerCase()).filter(extension => extension.length > 0)
|
|
160
|
+
return [...new Set([platformBinaryName(profile), ...extensions.map(extension => `${profile.bin}${extension}`), profile.bin])]
|
|
161
|
+
}
|
|
162
|
+
|
|
124
163
|
/** The CLI's executable name on this platform (`-cli.exe` on Windows). */
|
|
125
164
|
export function platformBinaryName(profile ) {
|
|
126
165
|
return process.platform === 'win32' ? `${profile.bin}.exe` : profile.bin
|
|
@@ -178,6 +217,13 @@ export function craftVersion(path , timeoutMs = 5000)
|
|
|
178
217
|
*/
|
|
179
218
|
export function missingBinaryMessage(profile , configured , packageRoot ) {
|
|
180
219
|
const tried = candidates(profile, configured, packageRoot)
|
|
220
|
+
const executable = `${profile.bin}${process.platform === 'win32' ? '.exe' : ''}`
|
|
221
|
+
const example = process.platform === 'win32'
|
|
222
|
+
? `C:\\path\\to\\${executable}`
|
|
223
|
+
: `/absolute/path/to/${executable}`
|
|
224
|
+
const setEnv = process.platform === 'win32'
|
|
225
|
+
? `$env:${profile.envVar}="${example}" (PowerShell; set ${profile.envVar}=${example} in cmd.exe)`
|
|
226
|
+
: `export ${profile.envVar}=${example}`
|
|
181
227
|
const lines = [
|
|
182
228
|
`${profile.product}: '${profile.bin}' was not found, so the ${profile.id} tools cannot run.`,
|
|
183
229
|
`Searched, in order:`,
|
|
@@ -186,9 +232,12 @@ export function missingBinaryMessage(profile , configured ,
|
|
|
186
232
|
lines.push(
|
|
187
233
|
`Fix it by any of:`,
|
|
188
234
|
` - run \`node scripts/install-craft.mjs ${profile.id}\` (macOS, Windows and Linux)`,
|
|
189
|
-
` - set ${
|
|
235
|
+
` - set ${setEnv}`,
|
|
190
236
|
` - download the CLI from https://github.com/storytold/${profile.id}/releases/latest and put it on PATH`,
|
|
191
|
-
` - pass { "path": "
|
|
237
|
+
` - pass { "path": "${example.replace(/\\/g, '\\\\')}" } in this plugin's config`,
|
|
192
238
|
)
|
|
239
|
+
if (process.platform === 'win32') {
|
|
240
|
+
lines.push(`The Windows build is the portable archive's \`${executable}\`; no Rust toolchain is needed.`)
|
|
241
|
+
}
|
|
193
242
|
return lines.join('\n')
|
|
194
243
|
}
|
package/src/kit/exec.js
CHANGED
|
@@ -7,10 +7,36 @@
|
|
|
7
7
|
* @module @dsh-artcraft/craft-kit/exec
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { spawn } from 'node:child_process'
|
|
10
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
11
|
+
|
|
11
12
|
import { appendFileSync } from 'node:fs'
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Terminate a craft process and anything it started.
|
|
17
|
+
*
|
|
18
|
+
* Windows has no process groups and no signals, so `child.kill` reaches only
|
|
19
|
+
* the direct child: a craft engine that spawned helpers would leave them
|
|
20
|
+
* running. `taskkill /T` walks the child tree instead. Both steps are
|
|
21
|
+
* best-effort, because by the time this runs the process may already be gone.
|
|
22
|
+
* @param child - the process to terminate.
|
|
23
|
+
*/
|
|
24
|
+
export function killTree(child ) {
|
|
25
|
+
if (child.exitCode !== null || child.signalCode !== null) return
|
|
26
|
+
try {
|
|
27
|
+
child.kill('SIGKILL')
|
|
28
|
+
} catch {
|
|
29
|
+
// Already exited.
|
|
30
|
+
}
|
|
31
|
+
// Re-check before taskkill: a recycled pid would otherwise be killed.
|
|
32
|
+
if (process.platform !== 'win32' || child.pid === undefined || child.exitCode !== null) return
|
|
33
|
+
try {
|
|
34
|
+
spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' })
|
|
35
|
+
} catch {
|
|
36
|
+
// taskkill is absent on some minimal images; the direct kill still ran.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
14
40
|
/** Default cap on captured stdout+stderr, so a runaway export cannot flood the model. */
|
|
15
41
|
export const MAX_CAPTURE_BYTES = 256 * 1024
|
|
16
42
|
|
|
@@ -63,6 +89,8 @@ export async function runCraft(options )
|
|
|
63
89
|
cwd,
|
|
64
90
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
65
91
|
env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
|
|
92
|
+
// Without this Windows flashes a console window for every craft run.
|
|
93
|
+
windowsHide: true,
|
|
66
94
|
})
|
|
67
95
|
|
|
68
96
|
const stdout = []
|
|
@@ -89,10 +117,10 @@ export async function runCraft(options )
|
|
|
89
117
|
let timedOut = false
|
|
90
118
|
const timer = setTimeout(() => {
|
|
91
119
|
timedOut = true
|
|
92
|
-
child
|
|
120
|
+
killTree(child)
|
|
93
121
|
}, timeoutMs)
|
|
94
122
|
|
|
95
|
-
const onAbort = () => { child
|
|
123
|
+
const onAbort = () => { killTree(child) }
|
|
96
124
|
options.signal?.addEventListener('abort', onAbort, { once: true })
|
|
97
125
|
|
|
98
126
|
const cleanup = () => {
|
package/src/kit/mcp.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { spawn } from 'node:child_process'
|
|
16
16
|
|
|
17
|
+
import { killTree } from './exec.js'
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
/** One tool as the MCP server declares it. */
|
|
@@ -81,6 +82,8 @@ export class CraftMcpClient {
|
|
|
81
82
|
const child = spawn(this.#binary.path, [...(this.#profile.mcpArgs ?? ['mcp']), ...this.#args], {
|
|
82
83
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
83
84
|
env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
|
|
85
|
+
// Keeps the long-lived server from flashing a console window on Windows.
|
|
86
|
+
windowsHide: true,
|
|
84
87
|
})
|
|
85
88
|
this.#child = child
|
|
86
89
|
this.#buffer = ''
|
|
@@ -153,7 +156,9 @@ export class CraftMcpClient {
|
|
|
153
156
|
if (this.#idleTimer !== undefined) clearTimeout(this.#idleTimer)
|
|
154
157
|
const child = this.#child
|
|
155
158
|
this.#child = undefined
|
|
156
|
-
|
|
159
|
+
// `killTree` rather than `child.kill`: on Windows the craft server's own
|
|
160
|
+
// helper processes are not in a process group and would otherwise survive.
|
|
161
|
+
if (child !== undefined && child.exitCode === null) killTree(child)
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
/** Buffer stdout and resolve one response per complete line. */
|
package/src/kit/profiles.mjs
CHANGED
|
@@ -359,6 +359,63 @@ export const CRAFT_PROFILES = [
|
|
|
359
359
|
catalogArgs: ['commands'],
|
|
360
360
|
mcpArgs: ['mcp'],
|
|
361
361
|
},
|
|
362
|
+
{
|
|
363
|
+
id: 'figcraft',
|
|
364
|
+
product: 'FigCraft',
|
|
365
|
+
bin: 'figcraft-cli',
|
|
366
|
+
envVar: 'FIGCRAFT_CLI',
|
|
367
|
+
searchDirs: [CRAFTS_BIN_DIR],
|
|
368
|
+
extensions: ['.figcraft', '.penpot', '.fig', '.sketch', '.svg'],
|
|
369
|
+
blurb: 'Interface design and prototyping (Figma-style): frames, components, auto layout, variables, prototyping and code export.',
|
|
370
|
+
run: {
|
|
371
|
+
verb: 'run',
|
|
372
|
+
inputMode: 'flag',
|
|
373
|
+
inFlag: '--in',
|
|
374
|
+
sampleFlag: '',
|
|
375
|
+
cmdFlag: '',
|
|
376
|
+
cmdSeparator: ' ',
|
|
377
|
+
// `run script.json` takes a generated script, one step per command.
|
|
378
|
+
scriptFile: { encoding: 'json-array', commandKey: 'command', paramsKey: 'params', inputFlag: '--in', saveFlag: '--out' },
|
|
379
|
+
saveFlag: '--out',
|
|
380
|
+
},
|
|
381
|
+
reads: [
|
|
382
|
+
{ name: 'inspect', verb: 'info', summary: 'Summarize a design file: pages, node counts, top-level frames, style and variable counts.' },
|
|
383
|
+
{ name: 'convert', verb: 'convert', summary: 'Convert a design file between .figcraft, .json, .svg, .png, .pdf and more.' },
|
|
384
|
+
],
|
|
385
|
+
catalogJson: true,
|
|
386
|
+
catalogArgs: ['commands', '--json'],
|
|
387
|
+
mcpArgs: ['mcp'],
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
id: 'texturecraft',
|
|
391
|
+
product: 'TextureCraft',
|
|
392
|
+
bin: 'texturecraft-cli',
|
|
393
|
+
envVar: 'TEXTURECRAFT_CLI',
|
|
394
|
+
searchDirs: [CRAFTS_BIN_DIR],
|
|
395
|
+
extensions: ['.tcraft'],
|
|
396
|
+
blurb: '3D texture painting: texture sets and layers over a mesh, baking, material channels and PBR export.',
|
|
397
|
+
run: {
|
|
398
|
+
verb: 'batch',
|
|
399
|
+
inputMode: 'flag',
|
|
400
|
+
inFlag: '',
|
|
401
|
+
sampleFlag: '',
|
|
402
|
+
cmdFlag: '',
|
|
403
|
+
cmdSeparator: ' ',
|
|
404
|
+
// `batch script.json` carries `{ open, steps, save }` in one file.
|
|
405
|
+
scriptFile: { encoding: 'json-array', commandKey: 'id', paramsKey: 'params', batch: { openKey: 'open', stepsKey: 'steps', saveKey: 'save' } },
|
|
406
|
+
saveMode: 'script',
|
|
407
|
+
saveFlag: '',
|
|
408
|
+
// A project is required before any command, so a bare run makes one.
|
|
409
|
+
sampleCreate: { verb: 'new', args: ['builtin:crate'], extension: '.tcraft' },
|
|
410
|
+
},
|
|
411
|
+
reads: [
|
|
412
|
+
{ name: 'inspect', verb: 'info', summary: 'Summarize a texture project: texture sets, layers, triangles, materials and the UV report.' },
|
|
413
|
+
{ name: 'convert', verb: 'export', summary: 'Export textures from a project: presets, formats, padding and size.' },
|
|
414
|
+
],
|
|
415
|
+
catalogJson: false,
|
|
416
|
+
catalogArgs: ['commands'],
|
|
417
|
+
mcpArgs: ['mcp'],
|
|
418
|
+
},
|
|
362
419
|
]
|
|
363
420
|
|
|
364
421
|
/** Profile lookup by craft id. */
|
package/src/kit/tools.js
CHANGED
|
@@ -39,6 +39,9 @@ function resolveSample(profile , args )
|
|
|
39
39
|
const requested = args.sample
|
|
40
40
|
if (typeof requested === 'string' && requested.length > 0) return requested
|
|
41
41
|
if (requested === true) {
|
|
42
|
+
// A craft that starts a new document by default, or creates one itself,
|
|
43
|
+
// has nothing to select; asking for the sample is a harmless no-op.
|
|
44
|
+
if (profile.run.sampleFlag.length === 0 && (profile.run.sampleCreate !== undefined || profile.run.scriptFile !== undefined)) return undefined
|
|
42
45
|
if (profile.run.sampleFlag.length === 0) {
|
|
43
46
|
throw new Error(
|
|
44
47
|
`${profile.product} has no "new document" flag: pass \`input\` to open an existing document, `
|
|
@@ -361,7 +364,7 @@ export function createToolSet(runtime ) {
|
|
|
361
364
|
description: 'Ordered commands to apply. Each is { "id": "cell.set", "json": { "cell": "A1", "input": "x" } }. An entry may also be a raw CLI string.',
|
|
362
365
|
items: { type: 'json' },
|
|
363
366
|
},
|
|
364
|
-
...(profile.run.saveMode ?? 'flag')
|
|
367
|
+
...(profile.run.saveMode ?? 'flag') !== 'none'
|
|
365
368
|
? { save: { type: 'string', description: `Path to write the edited document to (${profile.extensions.join('/')}).` } }
|
|
366
369
|
: {},
|
|
367
370
|
// `export` is always advertised when the craft can render one. Where the
|
|
@@ -410,6 +413,48 @@ export function createToolSet(runtime ) {
|
|
|
410
413
|
// Crafts whose headless entry point only accepts a script file get one
|
|
411
414
|
// written for this call; the file names the same commands the inline
|
|
412
415
|
// style would have carried.
|
|
416
|
+
// Crafts whose headless verb takes a generated script file instead of
|
|
417
|
+
// inline command flags: FigCraft's `run script.json` and TextureCraft's
|
|
418
|
+
// `batch script.json`. The kernel writes it, so the model still supplies
|
|
419
|
+
// an ordered list and never has to know the file format.
|
|
420
|
+
if (profile.run.scriptFile !== undefined) {
|
|
421
|
+
const spec = profile.run.scriptFile
|
|
422
|
+
const savePath = typeof args.save === 'string' ? args.save : undefined
|
|
423
|
+
let effectiveInput = input
|
|
424
|
+
if (effectiveInput === undefined && profile.run.sampleCreate !== undefined) {
|
|
425
|
+
const created = join(craftWorkDir(profile), `sample-${Date.now()}${profile.run.sampleCreate.extension}`)
|
|
426
|
+
mkdirSync(dirname(created), { recursive: true })
|
|
427
|
+
const createdResult = await runCraft({
|
|
428
|
+
profile, binary,
|
|
429
|
+
argv: [profile.run.sampleCreate.verb, ...profile.run.sampleCreate.args, created],
|
|
430
|
+
cwd: process.cwd(), timeoutMs,
|
|
431
|
+
})
|
|
432
|
+
if (!createdResult.ok) return renderRun(createdResult, profile)
|
|
433
|
+
effectiveInput = created
|
|
434
|
+
}
|
|
435
|
+
const steps = commands.map(command => ({ [spec.commandKey]: command.id, [spec.paramsKey]: command.json ?? {} }))
|
|
436
|
+
const scriptPath = join(craftWorkDir(profile), `run-${Date.now()}.${spec.encoding === 'jsonl' ? 'jsonl' : 'json'}`)
|
|
437
|
+
mkdirSync(dirname(scriptPath), { recursive: true })
|
|
438
|
+
const argv = [profile.run.verb]
|
|
439
|
+
let body
|
|
440
|
+
if (spec.batch !== undefined) {
|
|
441
|
+
body = {
|
|
442
|
+
...effectiveInput !== undefined ? { [spec.batch.openKey]: effectiveInput } : {},
|
|
443
|
+
[spec.batch.stepsKey]: steps,
|
|
444
|
+
...savePath !== undefined ? { [spec.batch.saveKey]: savePath } : {},
|
|
445
|
+
}
|
|
446
|
+
argv.push(scriptPath)
|
|
447
|
+
} else {
|
|
448
|
+
body = spec.encoding === 'jsonl' ? steps.map(step => JSON.stringify(step)).join('\n') + '\n' : steps
|
|
449
|
+
argv.push(scriptPath)
|
|
450
|
+
if (effectiveInput !== undefined && spec.inputFlag !== undefined) argv.push(spec.inputFlag, effectiveInput)
|
|
451
|
+
if (savePath !== undefined && spec.saveFlag !== undefined) argv.push(spec.saveFlag, savePath)
|
|
452
|
+
}
|
|
453
|
+
writeFileSync(scriptPath, typeof body === 'string' ? body : JSON.stringify(body, null, 2) + '\n')
|
|
454
|
+
const extra = Array.isArray(args.extraArgs) ? args.extraArgs.filter((a) => typeof a === 'string') : []
|
|
455
|
+
const scriptResult = await runCraft({ profile, binary, argv: [...argv, ...extra], cwd: process.cwd(), timeoutMs })
|
|
456
|
+
return renderRun(scriptResult, profile)
|
|
457
|
+
}
|
|
413
458
|
if (profile.run.scriptFlag !== undefined) {
|
|
414
459
|
const scriptPath = join(craftWorkDir(profile), `steps-${Date.now()}.json`)
|
|
415
460
|
mkdirSync(dirname(scriptPath), { recursive: true })
|
package/src/root.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve this plugin package's root directory at runtime.
|
|
3
|
+
*
|
|
4
|
+
* Kept in its own module so every craft plugin can share the same two lines
|
|
5
|
+
* while the kernel stays free of `import.meta` path arithmetic.
|
|
6
|
+
* @module @dsh-artcraft/plugin-soundcraft/root
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { dirname } from 'node:path'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
|
|
12
|
+
/** Absolute path of this plugin package (`<root>/src/root.ts` → `<root>`). */
|
|
13
|
+
export const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|