dsh-artcraft-plugin-soundcraft 0.1.3 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-artcraft-plugin-soundcraft",
3
- "version": "0.1.3",
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.",
@@ -43,4 +43,4 @@
43
43
  "publishConfig": {
44
44
  "access": "public"
45
45
  }
46
- }
46
+ }
@@ -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
- const rest = columns.join(' ').trim()
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
- ...paramsMatch !== null ? { params: paramsMatch[1].replace(/\s+/g, ' ').trim() } : {},
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
- const id = columns[0] ?? ''
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
  }
@@ -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') === '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/kit/types.js CHANGED
@@ -69,7 +69,7 @@
69
69
 
70
70
   
71
71
 
72
-
72
+
73
73
 
74
74
 
75
75