stillmade-sdk 0.1.38 → 0.1.39

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.
@@ -21,7 +21,7 @@ import {realpathSync} from 'node:fs';
21
21
  import {numberedTakes} from '../block-sdk/state-example.js';
22
22
  import {initialBlockState,validateBlockState} from '../block-sdk/state.js';
23
23
  import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
24
- import { resolve, join, sep, dirname } from 'node:path';
24
+ import { resolve, join, sep, dirname, basename } from 'node:path';
25
25
  import { fileURLToPath } from 'node:url';
26
26
  import { textCleanup, lineArt, javascriptText } from '../block-sdk/examples.js';
27
27
  import {validateViewAppearance} from '../block-sdk/appearance.js';
@@ -84,20 +84,20 @@ const localFileReader=folder=>async name=>{
84
84
  const info=await lstat(full);if(!info.isFile())throw Object.assign(new Error(`${name} is not a file.`),{code:'MODULE_FILE_READ'});
85
85
  return new Uint8Array(await readFile(full));
86
86
  };
87
- async function standIns(path,pkg,requested){
87
+ async function standIns(path,pkg,requested,pickedMedia){
88
88
  let mocks;const folder=await blockFolder(path);
89
89
  if(folder){try{mocks=JSON.parse(await readFile(join(folder,MOCKS_FILE),'utf8'));}catch(error){if(error.code!=='ENOENT')throw Object.assign(new Error(`${MOCKS_FILE}: ${error.message}`),{code:'MOCK_HOST'});}}
90
90
  if(!requested&&mocks===undefined)return null;
91
- return createMockHost({mocks:validateMocks(mocks),readLocalFile:localFileReader(folder),manifest:pkg.manifest});
91
+ return createMockHost({mocks:validateMocks(mocks),readLocalFile:localFileReader(folder),manifest:pkg.manifest,pickedMedia});
92
92
  }
93
93
  // A module fixture's own mocks take the place of tests/mocks.json for that fixture (MODULES.md), with or
94
94
  // without --mock, as ready and StillMade's import review already did (October 27, 2026: test, pack and dev
95
95
  // answered every fixture from tests/mocks.json, or left it unanswered when there was none).
96
96
  // Returns hostFor(mocks), a fresh stand-in host for one fixture, or null for other runtimes.
97
- async function fixtureStandIns(path,pkg){
97
+ async function fixtureStandIns(path,pkg,pickedMedia){
98
98
  if(pkg.manifest?.runtime!=='module')return null;
99
99
  const readLocalFile=localFileReader(await blockFolder(path));
100
- return mocks=>createMockHost({mocks:validateMocks(mocks),readLocalFile,manifest:pkg.manifest});
100
+ return mocks=>createMockHost({mocks:validateMocks(mocks),readLocalFile,manifest:pkg.manifest,pickedMedia});
101
101
  }
102
102
  // A run whose input is a fixture's (dev's Example picker, run without an input file) uses that fixture's mocks.
103
103
  const fixtureFor=(pkg,input)=>pkg.manifest?.runtime==='module'?pkg.tests?.find(test=>test?.mocks&&stableStringify(test.input)===stableStringify(input)):undefined;
@@ -125,13 +125,17 @@ async function dev(path,options){
125
125
  if(!(await lstat(path)).isDirectory())throw Object.assign(new Error('dev previews a Block folder.'),{code:'USAGE'});
126
126
  const {startDevServer}=await import('./dev-server.js');
127
127
  let mock,mocksText,memory,answered;
128
+ // Files the person picks in the preview's own-file mode: kept in this process only, read by the run as themselves
129
+ // (November 1, 2026, owner: try a Block on your own video, not only its example).
130
+ const picked=new Map(),pickedMedia=url=>picked.get(url)||null;
131
+ const pick=(bytes,{mediaType,name})=>{const id=randomBytes(8).toString('hex'),safe=String(name||'file').replace(/[^\w.-]+/g,'-').slice(0,80)||'file',url=`/api/media/picked/${id}/${safe}`;picked.set(url,{bytes,mediaType});return url;};
128
132
  const load=async()=>{
129
133
  let pkg;
130
134
  try{pkg=await readPackage(path);}catch(error){return {problem:`${error.message}${error.code?` (${error.code})`:''}`};}
131
135
  let problem=null;
132
136
  try{validatePackage(pkg);appearanceAdvice(pkg.view);}catch(error){problem=error.message;}
133
137
  let text=null;try{text=await readFile(join(path,MOCKS_FILE),'utf8');}catch(error){if(error.code!=='ENOENT')problem=`${MOCKS_FILE}: ${error.message}`;}
134
- if(!mock||text!==mocksText){try{mock=await standIns(path,pkg,true);mocksText=text;}catch(error){problem=error.message;}}
138
+ if(!mock||text!==mocksText){try{mock=await standIns(path,pkg,true,pickedMedia);mocksText=text;}catch(error){problem=error.message;}}
135
139
  memory=undefined;
136
140
  const files=moduleFiles.get(pkg)||{},filesBySha={};
137
141
  for(const [name,file] of [...Object.entries(pkg.module?.files||{}),...Object.entries(pkg.view?.files?.files||{}).map(([name,file])=>[`view:${name}`,file])])if(files[name])filesBySha[file.sha256]=files[name];
@@ -140,7 +144,7 @@ async function dev(path,options){
140
144
  const run=async(pkg,input)=>{
141
145
  const state=pkg.manifest.state?memory??initialBlockState(pkg.manifest):undefined;
142
146
  // The Example picker sends that example's input: unedited, it runs against the example's own mocks.
143
- const fixture=fixtureFor(pkg,input),own=fixture?(await fixtureStandIns(path,pkg))(fixture.mocks):null;
147
+ const fixture=fixtureFor(pkg,input),own=fixture?(await fixtureStandIns(path,pkg,pickedMedia))(fixture.mocks):null;
144
148
  answered=own?{host:own,files:fixture.mocks.files}:null;
145
149
  const result=await runPackageAsync(pkg,input,{...(state?{state}:{}),...(pkg.manifest.runtime==='module'?{module:moduleExecutor(pkg,(own||mock)?.host)}:{}),...(pkg.manifest.runtime==='capability'&&mock?{capability:mock.capability}:{})});
146
150
  if(pkg.manifest.state)memory=result.state;
@@ -148,7 +152,7 @@ async function dev(path,options){
148
152
  return {outputs:result.outputs,...(result.logs?{logs:result.logs}:{}),...(calls.length?{standIns:calls}:{}),...(result.state!==undefined?{state:result.state}:{})};
149
153
  };
150
154
  const media=async url=>{
151
- const made=answered?.host.file(url)||mock?.file(url);if(made)return made;
155
+ const made=pickedMedia(url)||answered?.host.file(url)||mock?.file(url);if(made)return made;
152
156
  let mocks={};try{mocks=JSON.parse(mocksText||'{}');}catch{}
153
157
  const local=answered?.files?.[url]??mocks.files?.[url];if(typeof local!=='string')return null;
154
158
  const full=resolve(path,local);if(!full.startsWith(path+sep))return null;
@@ -159,7 +163,7 @@ async function dev(path,options){
159
163
  ?{fileName:`${pkg.manifest.id}-${pkg.manifest.version}${MODULE_BUNDLE_SUFFIX}`,source:JSON.stringify(createModuleBundle(pkg,moduleFiles.get(pkg)))}
160
164
  :{fileName:`${pkg.manifest.id}-${pkg.manifest.version}.stillmade.json`,source:JSON.stringify(pkg)};
161
165
  const link=live?{origin:liveSite(site),key:randomBytes(24).toString('base64url')}:undefined;
162
- const server=await startDevServer({folder:path,port,build,load,run,media,live:link,bundle});
166
+ const server=await startDevServer({folder:path,port,build,load,run,media,pick,live:link,bundle});
163
167
  const liveUrl=link&&`${link.origin}/create/import?live=${encodeURIComponent(`${server.url}/__live/${link.key}`)}`;
164
168
  return {dev:server.url,folder:path,...(build?{build}:{}),...(liveUrl?{live:liveUrl}:{}),close:server.close};
165
169
  }
@@ -205,7 +209,11 @@ export async function main(rawArgs) {
205
209
  if(args.length<2)throw Object.assign(new Error('Name the folder to create: stillmade-block create my-block [template]'),{code:'USAGE'});
206
210
  const template=templateNamed(extra),files=await template.files({folder:path});
207
211
  // Every new Block starts with a feature map (one entry from its name and description) for the builder to rewrite as it grows.
208
- {const created=JSON.parse(files['stillmade.block.json']);if(!created.features&&!created.stages){created.features=blockFeatures(created).map(({source,...feature})=>feature);files['stillmade.block.json']=`${JSON.stringify(created,null,2)}\n`;}}
212
+ {const created=JSON.parse(files['stillmade.block.json']);if(!created.features&&!created.stages){created.features=blockFeatures(created).map(({source,...feature})=>feature);files['stillmade.block.json']=`${JSON.stringify(created,null,2)}\n`;}
213
+ // The new Block is named for its folder, never the example it started from (November 1, 2026: a no-context agent's
214
+ // social-media-clip-maker kept the id example.poster-from-idea).
215
+ const slug=basename(path).toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^[^a-z]+|-+$/g,'').slice(0,60);
216
+ if(!created.stages&&typeof created.id==='string'&&created.id.startsWith('example.')&&slug){created.id=`creator.${slug}`;files['stillmade.block.json']=`${JSON.stringify(created,null,2)}\n`;}}
209
217
  // Editors pick up the generated types for the view, src/run.js and module code.
210
218
  files['stillmade-env.d.ts']=blockTypeDeclarations(JSON.parse(files['stillmade.block.json']));
211
219
  files['jsconfig.json']=`${JSON.stringify({compilerOptions:{target:'ES2022',module:'ESNext',moduleResolution:'Bundler',lib:['ES2022','DOM'],jsx:'react-jsx',checkJs:false},include:['src','module','stillmade-env.d.ts']},null,2)}\n`;
@@ -295,14 +303,14 @@ export async function main(rawArgs) {
295
303
  if(!scan.passed)throw Object.assign(new Error(`${dependency.manifest.name}: Static validation failed`),{report:scan});
296
304
  }
297
305
  const output=jsonOutput(extra,`${pkg.id}-${pkg.version}.stillmade.json`);
298
- await writeFile(output,JSON.stringify(pkg,null,2),{flag:'wx'});
306
+ await writeFile(output,JSON.stringify(pkg,null,2),{flag:packFlag(extra)});
299
307
  return {file:output,digest:await digest(pkg),tests:0,...review,message:'Project Type source packaged without execution. In StillMade, review every embedded hosted Block in StillMade (including ComfyUI backend fixtures and samples), then confirm the import after local admission checks. The connected workflow has not run.'};
300
308
  }
301
309
  const report = await testProjectType(pkg);
302
310
  if (!report.passed) throw Object.assign(new Error('Project Type validation or execution checks need attention'), { report:{...report,...(hosted ? {reviewRequired:true,liveVerified:false} : {})} });
303
311
  if (command === 'test') return report;
304
312
  const output = jsonOutput(extra,`${pkg.id}-${pkg.version}.stillmade.json`);
305
- await writeFile(output, JSON.stringify(pkg, null, 2), { flag: 'wx' });
313
+ await writeFile(output, JSON.stringify(pkg, null, 2), { flag: packFlag(extra) });
306
314
  return { file: output, digest: report.digest, tests: (pkg.packages || []).reduce((count,dependency)=>count+dependency.tests.length,0), workflowComplete:report.workflowComplete, workflowStatus:report.workflowStatus };
307
315
  }
308
316
  // permissions: what the Block can do in plain words, and what its source uses that
@@ -330,7 +338,7 @@ export async function main(rawArgs) {
330
338
  const report=await admitLocalPackage(pkg,{files:moduleFiles.get(pkg),moduleExecution:true});if(!report.passed)throw Object.assign(new Error('Portable admission failed'),{report});
331
339
  const output=resolve(extra||`${pkg.manifest.id}-${pkg.manifest.version}.stillmade-block`);
332
340
  if(!output.endsWith('.stillmade-block'))throw new Error('Canonical output must end in .stillmade-block');
333
- await writeFile(output,writePortableArchive(pkg,archiveCodec()),{flag:'wx'});return {file:output,digest:report.digest,tests:hosted?0:pkg.tests.length,format:pkg.portable.format,admission:report,repositoryEvidence:evidence,...(hosted?{fixtureContracts:pkg.tests.length,execution:'not-run',liveVerified:false,reviewRequired:true}:{})};
341
+ await writeFile(output,writePortableArchive(pkg,archiveCodec()),{flag:packFlag(extra)});return {file:output,digest:report.digest,tests:hosted?0:pkg.tests.length,format:pkg.portable.format,admission:report,repositoryEvidence:evidence,...(hosted?{fixtureContracts:pkg.tests.length,execution:'not-run',liveVerified:false,reviewRequired:true}:{})};
334
342
  }
335
343
  }
336
344
  const advice=command !== 'preview'?appearanceAdvice(pkg.view):null;
@@ -367,14 +375,14 @@ export async function main(rawArgs) {
367
375
  const report=await admitLocalPackage(pkg);if(!report.passed)throw Object.assign(new Error('Desktop source admission failed'),{report});
368
376
  if(command==='test')return report;
369
377
  const output=jsonOutput(extra,`${pkg.manifest.id}-${pkg.manifest.version}.stillmade.json`);
370
- await writeFile(output,JSON.stringify(pkg,null,2),{flag:'wx'});
378
+ await writeFile(output,JSON.stringify(pkg,null,2),{flag:packFlag(extra)});
371
379
  return {file:output,digest:report.digest,tests:0,fixtureContracts:pkg.tests.length,execution:'not-run',liveVerified:false,reviewRequired:true,admission:report,message:'Source packaged without native execution. Run and review the declared action through its approved StillMade Desktop adapter.'};
372
380
  }
373
381
  if(command === 'pack' && ['comfyui','capability'].includes(pkg.manifest.runtime)) {
374
382
  const scan=scanPackage(pkg);
375
383
  if(!scan.passed)throw Object.assign(new Error('Static validation failed'),{report:scan});
376
384
  const output=jsonOutput(extra,`${pkg.manifest.id}-${pkg.manifest.version}.stillmade.json`);
377
- await writeFile(output,JSON.stringify(pkg,null,2),{flag:'wx'});
385
+ await writeFile(output,JSON.stringify(pkg,null,2),{flag:packFlag(extra)});
378
386
  return {file:output,digest:await digest(pkg),tests:0,reviewRequired:true,...(pkg.manifest.runtime==='capability'?{liveVerified:false}:{}),message:pkg.manifest.runtime==='capability'?'Source packaged without provider execution. Import into StillMade, choose a model and payment method, and approve the fixtures and sample before confirming installation.':'Source packaged. Import into StillMade to run all fixtures and preview on your connected ComfyUI backend before confirming installation.'};
379
387
  }
380
388
  const mock=['module','capability'].includes(pkg.manifest.runtime)?await standIns(path,pkg,mockRequested):null,hostFor=await fixtureStandIns(path,pkg),own=[];
@@ -394,13 +402,16 @@ export async function main(rawArgs) {
394
402
  if (hasDigestFiles(pkg)) {
395
403
  const files=moduleFiles.get(pkg),output=resolve(extra || `${pkg.manifest.id}-${pkg.manifest.version}${MODULE_BUNDLE_SUFFIX}`);
396
404
  if(!output.endsWith(MODULE_BUNDLE_SUFFIX))throw new Error(`Module output must end in ${MODULE_BUNDLE_SUFFIX}`);
397
- await writeFile(output,JSON.stringify(createModuleBundle(pkg,files)),{flag:'wx'});
405
+ await writeFile(output,JSON.stringify(createModuleBundle(pkg,files)),{flag:packFlag(extra)});
398
406
  return {file:output,digest:report.digest,tests:report.results.length,files:Object.keys(pkg.module?.files||{}).length+Object.keys(pkg.view?.files?.files||{}).length,...standInNote(mock,own),message:'Import this bundle in StillMade (Create, then Import). StillMade checks every file digest and uploads the files once before saving the Block.'};
399
407
  }
400
408
  const output = jsonOutput(extra,`${pkg.manifest.id}-${pkg.manifest.version}.stillmade.json`);
401
- await writeFile(output, JSON.stringify(pkg, null, 2), { flag: 'wx' });
409
+ await writeFile(output, JSON.stringify(pkg, null, 2), { flag: packFlag(extra) });
402
410
  return { file: output, digest: report.digest, tests: report.results.length };
403
411
  }
412
+ // pack replaces the file it wrote before under its own name, so packing again after a fix just works; an output path the
413
+ // person names is never overwritten (November 1, 2026: a no-context agent's second pack failed with EEXIST).
414
+ function packFlag(output){return output?'wx':'w';}
404
415
  // How many fixtures admission ran and passed, for the ready and gate reports.
405
416
  function fixtureCounts(pkg,admission){
406
417
  const rows=(admission.results||[]).filter(row=>/ contract test: /.test(row.name)),failed=rows.filter(row=>!row.passed);