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.
- package/MODULES.md +29 -11
- package/PASTE_TO_LLM.md +7 -6
- package/SDK.md +10 -9
- package/UI_AESTHETIC.md +6 -0
- package/package.json +1 -1
- package/packages/block-cli/cli.js +28 -17
- package/packages/block-cli/dev-host/assets/host.js +16 -16
- package/packages/block-cli/dev-host/assets/index.css +1 -1
- package/packages/block-cli/dev-server.js +14 -2
- package/packages/block-cli/ready-gate.js +3 -2
- package/packages/block-platform/motion-starters.js +14 -0
- package/packages/block-platform/project-type-starters.js +20 -7
- package/packages/block-sdk/capabilities.js +1 -1
- package/packages/block-sdk/example-media-data.js +2 -1
- package/packages/block-sdk/example-media.js +1 -0
- package/packages/block-sdk/media-transform.js +17 -4
- package/packages/block-sdk/mock-host.js +24 -5
- package/packages/block-sdk/module-hosted.js +12 -4
- package/packages/block-sdk/semantic-relationships.js +6 -0
- package/packages/block-sdk/testing.js +5 -3
- package/packages/block-sdk/view-design.js +1 -1
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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);
|