stillmade-sdk 0.1.36 → 0.1.38
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/AGENTS.md +8 -8
- package/IMAGE_DESCRIPTION.md +2 -0
- package/MODULES.md +68 -10
- package/NATIVE_MCP.md +18 -7
- package/PASTE_TO_LLM.md +121 -50
- package/PORTABLE.md +7 -3
- package/README.md +19 -6
- package/REMIX.md +6 -0
- package/SDK.md +226 -103
- package/TRANSCRIPTION.md +4 -2
- package/UI_AESTHETIC.md +2 -0
- package/api-index.json +1 -1
- package/blocks/narration/package.js +1 -1
- package/blocks/narration/versions/1.0.1/package.js +221 -0
- package/blocks/native/sm.animated-scene/versions/2.3.9/package.js +769 -0
- package/blocks/native/sm.board-editor/versions/2.2.11/package.js +551 -0
- package/blocks/native/sm.describe-media/versions/2.1.0/package.js +140 -0
- package/blocks/native/sm.panel-strip/versions/2.4.2/package.js +1316 -0
- package/blocks/native/sm.photo-notes/versions/1.1.0/package.js +170 -0
- package/blocks/native/sm.pipeline/versions/2.2.6/package.js +1095 -0
- package/blocks/screen-recorder/package.js +1 -1
- package/blocks/screen-recorder/versions/1.0.1/package.js +223 -0
- package/package.json +1 -1
- package/packages/block-cli/cli.js +91 -35
- package/packages/block-cli/dev-host/assets/host.js +35 -32
- package/packages/block-cli/dev-host/assets/index.css +1 -1
- package/packages/block-cli/dev-server.js +4 -0
- package/packages/block-cli/ready-gate.js +19 -4
- package/packages/block-cli/templates.js +33 -21
- package/packages/block-platform/block-readiness.js +10 -1
- package/packages/block-platform/catalog.js +14 -3
- package/packages/block-platform/chat-packages.js +6 -0
- package/packages/block-platform/flow-inspection.js +3 -1
- package/packages/block-platform/project-target.js +57 -0
- package/packages/block-platform/project-type-composer.js +4 -3
- package/packages/block-platform/project-types.js +5 -2
- package/packages/block-sdk/admission.js +4 -1
- package/packages/block-sdk/block-tier.js +23 -0
- package/packages/block-sdk/builder-contract.js +12 -10
- package/packages/block-sdk/capabilities.js +23 -10
- package/packages/block-sdk/collaboration-admission.js +3 -0
- package/packages/block-sdk/contracts.js +13 -1
- package/packages/block-sdk/example-media-data.js +16 -0
- package/packages/block-sdk/example-media.js +42 -0
- package/packages/block-sdk/first-party-identities.js +8 -0
- package/packages/block-sdk/first-party-packages.js +11 -1
- package/packages/block-sdk/index.d.ts +6 -2
- package/packages/block-sdk/local-admission.js +6 -5
- package/packages/block-sdk/media-transform.js +59 -3
- package/packages/block-sdk/mock-host.js +23 -2
- package/packages/block-sdk/mocks.js +4 -1
- package/packages/block-sdk/module-hosted.js +12 -2
- package/packages/block-sdk/port-types.d.ts +6 -0
- package/packages/block-sdk/port-types.js +32 -10
- package/packages/block-sdk/shared-agent-controls.js +25 -2
- package/packages/block-sdk/shared-controls.js +17 -3
- package/packages/block-sdk/testing.js +46 -3
- package/packages/block-sdk/view-design.js +67 -0
- package/packages/block-sdk/view-renderer.js +50 -8
- package/packages/design-system/block-kit.css +6 -4
|
@@ -36,7 +36,7 @@ import {textGenerate} from '../block-sdk/capability-example.js';
|
|
|
36
36
|
import {scanPackage} from '../block-sdk/admission.js';
|
|
37
37
|
import {admitLocalPackage} from '../block-sdk/local-admission.js';
|
|
38
38
|
import {requiresDesktopExecution} from '../block-sdk/desktop-execution.js';
|
|
39
|
-
import {digest,validateManifest} from '../block-sdk/contracts.js';
|
|
39
|
+
import {assertStorableText,digest,validateManifest,stableStringify} from '../block-sdk/contracts.js';
|
|
40
40
|
import { testPackage, validatePackage } from '../block-sdk/testing.js';
|
|
41
41
|
import { runPackageAsync } from '../block-sdk/runtime.js';
|
|
42
42
|
import { validateProjectType } from '../block-platform/project-types.js';
|
|
@@ -76,20 +76,31 @@ const moduleExecutor=(pkg,host)=>{const files=moduleFiles.get(pkg);return files?
|
|
|
76
76
|
// Stand-ins for host calls (hosted operations, files, network, connections,
|
|
77
77
|
// storage, project reads): on with --mock, or whenever the folder has
|
|
78
78
|
// tests/mocks.json. They never contact a provider or spend credits.
|
|
79
|
+
const blockFolder=async path=>{try{if((await lstat(path)).isDirectory())return path;}catch{}return null;};
|
|
80
|
+
const localFileReader=folder=>async name=>{
|
|
81
|
+
if(!folder)throw Object.assign(new Error('Local files are read from a Block folder.'),{code:'MODULE_FILE_READ'});
|
|
82
|
+
const full=resolve(folder,name);
|
|
83
|
+
if(!full.startsWith(folder+sep))throw Object.assign(new Error(`${name} is outside the Block folder.`),{code:'MODULE_FILE_READ'});
|
|
84
|
+
const info=await lstat(full);if(!info.isFile())throw Object.assign(new Error(`${name} is not a file.`),{code:'MODULE_FILE_READ'});
|
|
85
|
+
return new Uint8Array(await readFile(full));
|
|
86
|
+
};
|
|
79
87
|
async function standIns(path,pkg,requested){
|
|
80
|
-
let mocks
|
|
81
|
-
try{if((await lstat(path)).isDirectory())folder=path;}catch{}
|
|
88
|
+
let mocks;const folder=await blockFolder(path);
|
|
82
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'});}}
|
|
83
90
|
if(!requested&&mocks===undefined)return null;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
return createMockHost({mocks:validateMocks(mocks),readLocalFile:localFileReader(folder),manifest:pkg.manifest});
|
|
92
|
+
}
|
|
93
|
+
// A module fixture's own mocks take the place of tests/mocks.json for that fixture (MODULES.md), with or
|
|
94
|
+
// without --mock, as ready and StillMade's import review already did (October 27, 2026: test, pack and dev
|
|
95
|
+
// answered every fixture from tests/mocks.json, or left it unanswered when there was none).
|
|
96
|
+
// Returns hostFor(mocks), a fresh stand-in host for one fixture, or null for other runtimes.
|
|
97
|
+
async function fixtureStandIns(path,pkg){
|
|
98
|
+
if(pkg.manifest?.runtime!=='module')return null;
|
|
99
|
+
const readLocalFile=localFileReader(await blockFolder(path));
|
|
100
|
+
return mocks=>createMockHost({mocks:validateMocks(mocks),readLocalFile,manifest:pkg.manifest});
|
|
92
101
|
}
|
|
102
|
+
// A run whose input is a fixture's (dev's Example picker, run without an input file) uses that fixture's mocks.
|
|
103
|
+
const fixtureFor=(pkg,input)=>pkg.manifest?.runtime==='module'?pkg.tests?.find(test=>test?.mocks&&stableStringify(test.input)===stableStringify(input)):undefined;
|
|
93
104
|
// `dev`: preview the folder in StillMade's view host with hot reload. Hosted
|
|
94
105
|
// calls always use stand-ins here; the page runs only on this machine.
|
|
95
106
|
// --live also lets the StillMade site (--site, default STILLMADE_SITE_URL or
|
|
@@ -113,7 +124,7 @@ async function dev(path,options){
|
|
|
113
124
|
}
|
|
114
125
|
if(!(await lstat(path)).isDirectory())throw Object.assign(new Error('dev previews a Block folder.'),{code:'USAGE'});
|
|
115
126
|
const {startDevServer}=await import('./dev-server.js');
|
|
116
|
-
let mock,mocksText,memory;
|
|
127
|
+
let mock,mocksText,memory,answered;
|
|
117
128
|
const load=async()=>{
|
|
118
129
|
let pkg;
|
|
119
130
|
try{pkg=await readPackage(path);}catch(error){return {problem:`${error.message}${error.code?` (${error.code})`:''}`};}
|
|
@@ -128,15 +139,18 @@ async function dev(path,options){
|
|
|
128
139
|
};
|
|
129
140
|
const run=async(pkg,input)=>{
|
|
130
141
|
const state=pkg.manifest.state?memory??initialBlockState(pkg.manifest):undefined;
|
|
131
|
-
|
|
142
|
+
// 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;
|
|
144
|
+
answered=own?{host:own,files:fixture.mocks.files}:null;
|
|
145
|
+
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}:{})});
|
|
132
146
|
if(pkg.manifest.state)memory=result.state;
|
|
133
|
-
const calls=mock?.calls.splice(0)||[];
|
|
147
|
+
const calls=own?own.calls.map(call=>({...call,source:call.source==='mocks'?'fixture mocks':call.source})):mock?.calls.splice(0)||[];
|
|
134
148
|
return {outputs:result.outputs,...(result.logs?{logs:result.logs}:{}),...(calls.length?{standIns:calls}:{}),...(result.state!==undefined?{state:result.state}:{})};
|
|
135
149
|
};
|
|
136
150
|
const media=async url=>{
|
|
137
|
-
const made=mock?.file(url);if(made)return made;
|
|
151
|
+
const made=answered?.host.file(url)||mock?.file(url);if(made)return made;
|
|
138
152
|
let mocks={};try{mocks=JSON.parse(mocksText||'{}');}catch{}
|
|
139
|
-
const local=mocks.files?.[url];if(typeof local!=='string')return null;
|
|
153
|
+
const local=answered?.files?.[url]??mocks.files?.[url];if(typeof local!=='string')return null;
|
|
140
154
|
const full=resolve(path,local);if(!full.startsWith(path+sep))return null;
|
|
141
155
|
try{const bytes=await readFile(full);return {bytes,mediaType:{png:'image/png',jpg:'image/jpeg',jpeg:'image/jpeg',webp:'image/webp',gif:'image/gif',mp4:'video/mp4',webm:'video/webm',mp3:'audio/mpeg',wav:'audio/wav',m4a:'audio/mp4',ogg:'audio/ogg'}[full.split('.').pop().toLowerCase()]||'application/octet-stream'};}catch{return null;}
|
|
142
156
|
};
|
|
@@ -149,7 +163,8 @@ async function dev(path,options){
|
|
|
149
163
|
const liveUrl=link&&`${link.origin}/create/import?live=${encodeURIComponent(`${server.url}/__live/${link.key}`)}`;
|
|
150
164
|
return {dev:server.url,folder:path,...(build?{build}:{}),...(liveUrl?{live:liveUrl}:{}),close:server.close};
|
|
151
165
|
}
|
|
152
|
-
|
|
166
|
+
// own: the hosts that answered fixtures from their own mocks.
|
|
167
|
+
const standInNote=(mock,own=[])=>{const calls=[...(mock?.calls||[]).map(call=>[call,MOCKS_FILE]),...own.flatMap(host=>host.calls.map(call=>[call,'fixture mocks']))];return calls.length?{standIns:calls.map(([call,file])=>({api:call.api,name:call.name,source:call.source==='mocks'?file:'recorded stand-in'})),liveVerified:false}:{};};
|
|
153
168
|
const mobileProfile=()=>({policy:PLATFORM_CHECK_POLICY,targets:PLATFORM_CHECK_TARGETS,themes:PLATFORM_CHECK_THEMES,passed:null,message:'Static SDK validation passed. Import this exact source or run the repository platform runner to complete the trusted embedded UI profile.'});
|
|
154
169
|
export async function readPackage(path) {
|
|
155
170
|
const info = await lstat(path);
|
|
@@ -188,7 +203,7 @@ export async function main(rawArgs) {
|
|
|
188
203
|
}
|
|
189
204
|
if (command === 'create') {
|
|
190
205
|
if(args.length<2)throw Object.assign(new Error('Name the folder to create: stillmade-block create my-block [template]'),{code:'USAGE'});
|
|
191
|
-
const template=templateNamed(extra),files=await template.files();
|
|
206
|
+
const template=templateNamed(extra),files=await template.files({folder:path});
|
|
192
207
|
// Every new Block starts with a feature map (one entry from its name and description) for the builder to rewrite as it grows.
|
|
193
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`;}}
|
|
194
209
|
// Editors pick up the generated types for the view, src/run.js and module code.
|
|
@@ -198,15 +213,18 @@ export async function main(rawArgs) {
|
|
|
198
213
|
await mkdir(path,{recursive:true});
|
|
199
214
|
const names=['stillmade.block.json',...Object.keys(files).filter(name=>name!=='stillmade.block.json')];
|
|
200
215
|
for(const name of names){await mkdir(dirname(join(path,name)),{recursive:true});await writeFile(join(path,name),files[name],{flag:'wx'});}
|
|
201
|
-
// Coding agents read command output: restate the readiness rule where they will see it
|
|
202
|
-
|
|
216
|
+
// Coding agents read command output: restate the readiness rule where they will see it, with the folder as it
|
|
217
|
+
// was typed (October 27, 2026: `gate .` in the folder holding an extracted SDK judged the SDK's examples too).
|
|
218
|
+
return { created: path, sdkVersion: '0.1.0', template:template.name, runtime:template.runtime, files:names, next: `Build the Block and write its feature map (\`manifest.features\`: one entry per thing it can do), then run \`stillmade-block gate ${/\s/.test(target)?`"${target}"`:target}\`. Do not call the Block ready until the gate passes; report the status it prints.` };
|
|
203
219
|
}
|
|
204
220
|
if (command === 'gate') {
|
|
205
221
|
// Every new or edited Block in a folder must pass readiness before anyone calls it ready.
|
|
206
222
|
const {runReadyGate,gateMessage}=await import('./ready-gate.js');
|
|
207
|
-
|
|
208
|
-
if(
|
|
209
|
-
|
|
223
|
+
// Each ready this gate runs says how many fixtures ran and passed (a verdict reused for unchanged content has no count).
|
|
224
|
+
const counts=new Map(),judge=async gateArgs=>{try{const result=await main(gateArgs);if(gateArgs[0]==='ready')counts.set(gateArgs[1],result?.fixtures);return result;}catch(error){if(gateArgs[0]==='ready')counts.set(gateArgs[1],error.report?.fixtures);throw error;}};
|
|
225
|
+
const verdict=await runReadyGate({root:path,main:judge}),counted=result=>{const fixtures=counts.get(resolve(path,result.package));return fixtures?{fixtures}:{};};
|
|
226
|
+
if(!verdict.passed)throw Object.assign(new Error(gateMessage(verdict)),{code:'READINESS_GATE_FAILED',report:{results:verdict.results.map(result=>({...result,...counted(result)}))}});
|
|
227
|
+
return {passed:true,checked:verdict.checked,results:verdict.results.map(({package:name,status,limitations})=>({package:name,status,limitations,...counted({package:name})}))};
|
|
210
228
|
}
|
|
211
229
|
if(command==='run')return main(['preview',...args.slice(1),...(mockRequested?['--mock']:[])]);
|
|
212
230
|
if(command==='dev')return dev(path,args.slice(2));
|
|
@@ -237,10 +255,15 @@ export async function main(rawArgs) {
|
|
|
237
255
|
// 2026: ready refused every module Block at DOD-01, so none could pass the gate from the SDK).
|
|
238
256
|
const module=pkg.manifest?.runtime==='module';
|
|
239
257
|
if(module&&noModuleExecution)throw Object.assign(new Error(MODULE_EXECUTION_REQUIRED_MESSAGE),{code:'MODULE_EXECUTION_REQUIRED'});
|
|
240
|
-
const mock=module?await standIns(path,pkg,true):null;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
258
|
+
const mock=module?await standIns(path,pkg,true):null,hostFor=await fixtureStandIns(path,pkg),files=moduleFiles.get(pkg);
|
|
259
|
+
// Admission runs first, exactly as readiness would run it, so the report can say how many fixtures ran
|
|
260
|
+
// and passed (October 27, 2026); readiness keeps its own budget (20 s by default) for both.
|
|
261
|
+
const started=Date.now();let admission;
|
|
262
|
+
try{admission=await admitLocalPackage(pkg,{moduleExecution:true,files,...(mock?{host:mock.host}:{}),...(hostFor?{hostFor:mocks=>hostFor(mocks).host}:{})});}catch{}
|
|
263
|
+
const readiness=await assessBlockReadiness(pkg,{moduleExecution:true,files,...(mock?{host:mock.host}:{}),...(admission?{admission,budgetMs:Math.max(1000,20000-(Date.now()-started))}:{})});
|
|
264
|
+
const report={...readiness,...(admission?{fixtures:fixtureCounts(pkg,admission)}:{})};
|
|
265
|
+
if(['FAILED','BLOCKED BY PLATFORM CAPABILITY'].includes(readiness.status))throw Object.assign(new Error(`Block readiness: ${readiness.status}`),{report});
|
|
266
|
+
return report;
|
|
244
267
|
}
|
|
245
268
|
if(command==='remix'){
|
|
246
269
|
const result=await remixBlock(pkg);
|
|
@@ -252,6 +275,8 @@ export async function main(rawArgs) {
|
|
|
252
275
|
return {file:output,parent:result.parent,digest:await digest(result.content),draft:true,validated:false,message:'Editable remix created. Run validate and test before packaging a release. No source code was executed by remix.'};
|
|
253
276
|
}
|
|
254
277
|
|
|
278
|
+
// StillMade cannot store text with a NUL character; say where it is before the author imports the package.
|
|
279
|
+
if(['validate','test','pack','ready'].includes(command))assertStorableText(pkg);
|
|
255
280
|
if (pkg.stages) {
|
|
256
281
|
const valid = validateProjectType(pkg, BUILTIN_BLOCKS);
|
|
257
282
|
if (!valid.valid) throw new Error(valid.errors.map(e => e.message).join('. '));
|
|
@@ -332,10 +357,11 @@ export async function main(rawArgs) {
|
|
|
332
357
|
if(saved.packageDigest!==sourceDigest)throw Error('Saved memory belongs to another Block build');
|
|
333
358
|
state=validateBlockState(pkg.manifest,saved.state);
|
|
334
359
|
}
|
|
335
|
-
const mock=await standIns(path,pkg,mockRequested);
|
|
336
|
-
const
|
|
360
|
+
const mock=await standIns(path,pkg,mockRequested),input=inputPath?JSON.parse(await readFile(resolve(inputPath),'utf8')):pkg.tests[0].input;
|
|
361
|
+
const fixture=fixtureFor(pkg,input),own=fixture?(await fixtureStandIns(path,pkg))(fixture.mocks):null;
|
|
362
|
+
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}:{})});
|
|
337
363
|
if(savePath)await writeFile(resolve(savePath),JSON.stringify({packageDigest:sourceDigest,state:validateBlockState(pkg.manifest,result.state)},null,2),{flag:'wx'});
|
|
338
|
-
return {...result,...standInNote(mock)};
|
|
364
|
+
return {...result,...(own?standInNote(null,[own]):standInNote(mock))};
|
|
339
365
|
}
|
|
340
366
|
if(requiresDesktopExecution(pkg)&&['test','pack'].includes(command)){
|
|
341
367
|
const report=await admitLocalPackage(pkg);if(!report.passed)throw Object.assign(new Error('Desktop source admission failed'),{report});
|
|
@@ -351,29 +377,59 @@ export async function main(rawArgs) {
|
|
|
351
377
|
await writeFile(output,JSON.stringify(pkg,null,2),{flag:'wx'});
|
|
352
378
|
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.'};
|
|
353
379
|
}
|
|
354
|
-
const mock=['module','capability'].includes(pkg.manifest.runtime)?await standIns(path,pkg,mockRequested):null;
|
|
380
|
+
const mock=['module','capability'].includes(pkg.manifest.runtime)?await standIns(path,pkg,mockRequested):null,hostFor=await fixtureStandIns(path,pkg),own=[];
|
|
355
381
|
let report;
|
|
356
382
|
if(pkg.manifest.runtime==='capability'&&mock){
|
|
357
383
|
// Checks on the model's words are listed rather than run against a stand-in.
|
|
358
384
|
const {pkg:checked,notChecked}=standInFixtures(pkg);
|
|
359
385
|
report={...await testPackage(checked,{capability:mock.capability}),digest:await digest(pkg),execution:'stand-ins',liveVerified:false,reviewRequired:true,...(notChecked.length?{notChecked}:{})};
|
|
360
|
-
}else
|
|
361
|
-
|
|
386
|
+
}else if(pkg.manifest.runtime==='module'){
|
|
387
|
+
const module=moduleExecutor(pkg,mock?.host);
|
|
388
|
+
const moduleFor=module&&(test=>{if(!test?.mocks)return module;const host=hostFor(test.mocks);own.push(host);return moduleExecutor(pkg,host.host);});
|
|
389
|
+
report=await testPackage(pkg,{module,...(moduleFor?{moduleFor}:{}),timeoutMs:MODULE_LIMITS.wallClockMs});
|
|
390
|
+
}else report = await testPackage(pkg,{});
|
|
391
|
+
Object.assign(report,standInNote(mock,own));
|
|
362
392
|
if (!report.passed) throw Object.assign(new Error('Fixture tests failed'), { report });
|
|
363
393
|
if (command === 'test') return report;
|
|
364
394
|
if (hasDigestFiles(pkg)) {
|
|
365
395
|
const files=moduleFiles.get(pkg),output=resolve(extra || `${pkg.manifest.id}-${pkg.manifest.version}${MODULE_BUNDLE_SUFFIX}`);
|
|
366
396
|
if(!output.endsWith(MODULE_BUNDLE_SUFFIX))throw new Error(`Module output must end in ${MODULE_BUNDLE_SUFFIX}`);
|
|
367
397
|
await writeFile(output,JSON.stringify(createModuleBundle(pkg,files)),{flag:'wx'});
|
|
368
|
-
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),message:'Import this bundle in StillMade (Create, then Import). StillMade checks every file digest and uploads the files once before saving the Block.'};
|
|
398
|
+
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.'};
|
|
369
399
|
}
|
|
370
400
|
const output = jsonOutput(extra,`${pkg.manifest.id}-${pkg.manifest.version}.stillmade.json`);
|
|
371
401
|
await writeFile(output, JSON.stringify(pkg, null, 2), { flag: 'wx' });
|
|
372
402
|
return { file: output, digest: report.digest, tests: report.results.length };
|
|
373
403
|
}
|
|
404
|
+
// How many fixtures admission ran and passed, for the ready and gate reports.
|
|
405
|
+
function fixtureCounts(pkg,admission){
|
|
406
|
+
const rows=(admission.results||[]).filter(row=>/ contract test: /.test(row.name)),failed=rows.filter(row=>!row.passed);
|
|
407
|
+
return {total:pkg.tests?.length||0,ran:rows.length,passed:rows.length-failed.length,...(admission.execution?{execution:admission.execution}:{}),...(failed.length?{failed:failed.slice(0,10).map(row=>({name:row.name.replace(/^.*? contract test: /,''),message:String(row.message||row.code||'failed').slice(0,300)}))}:{})};
|
|
408
|
+
}
|
|
409
|
+
// Readable ready and gate results, which format.js prints as JSON (October 27, 2026: --text printed JSON): the
|
|
410
|
+
// verdict, how many fixtures ran and passed, then what failed and what is not verified.
|
|
411
|
+
const plural=(count,word)=>`${count} ${word}${count===1?'':'s'}`;
|
|
412
|
+
const fixtureText=fixtures=>fixtures.ran?`${fixtures.passed} of ${plural(fixtures.total,'fixture')} passed`:`${plural(fixtures.total,'fixture')} not run here`;
|
|
413
|
+
const readinessLines=report=>{
|
|
414
|
+
const failed=(report.items||[]).filter(item=>['failed','blocked'].includes(item.state));
|
|
415
|
+
return [report.fixtures?`${fixtureText(report.fixtures)}.`:null,...(report.fixtures?.failed||[]).map(item=>` FAIL ${item.name}: ${item.message}`),report.fixtures?.execution==='local-stand-ins'?'Hosted calls were answered by each fixture\'s own mocks, else tests/mocks.json, else stand-ins. No provider was called.':null,
|
|
416
|
+
failed.length?'Failed:':null,...failed.map(item=>` ${item.id} ${item.title}: ${item.reason}`),report.limitations?.length?`Not verified (${report.limitations.length}):`:null,...(report.limitations||[]).map(line=>` ${line}`)].filter(Boolean);
|
|
417
|
+
};
|
|
418
|
+
const gateLines=results=>results.map(result=>` ${result.package}: ${result.status}${result.fixtures?`, ${fixtureText(result.fixtures)}`:''}${result.limitations?`, ${result.limitations} not verified`:''}`);
|
|
419
|
+
export function readinessText(command,result){
|
|
420
|
+
if(command==='ready'&&Array.isArray(result?.items))return `${[`Readiness: ${result.status}`,...readinessLines(result)].join('\n')}\n`;
|
|
421
|
+
if(command==='gate'&&Array.isArray(result?.results))return `${[result.checked?`Readiness gate passed: ${plural(result.checked,'Block')} checked.`:'Readiness gate passed: no new or edited Blocks to check.',...gateLines(result.results)].join('\n')}\n`;
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
export function readinessErrorText(command,error){
|
|
425
|
+
const report=error?.report,head=`Error: ${error?.message||error}${error?.code?` (${error.code})`:''}`;
|
|
426
|
+
if(command==='ready'&&Array.isArray(report?.items))return `${[head,...readinessLines(report)].join('\n')}\n`;
|
|
427
|
+
if(command==='gate'&&Array.isArray(report?.results))return `${[head,'Checked:',...gateLines(report.results)].join('\n')}\n`;
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
374
430
|
if (process.argv[1] && (()=>{try{return realpathSync(process.argv[1])===fileURLToPath(import.meta.url);}catch{return false;}})()) {
|
|
375
431
|
// Readable text in a terminal; the JSON result object when piped (scripts,
|
|
376
432
|
// coding agents) or with --json. --text forces readable text.
|
|
377
433
|
const argv=process.argv.slice(2),asJson=argv.includes('--json')||!process.stdout.isTTY&&!argv.includes('--text'),args=argv.filter(value=>!['--json','--text'].includes(value));
|
|
378
|
-
main(args).then(result => process.stdout.write(asJson?`${JSON.stringify(result, null, 2)}\n`:formatResult(args,result))).catch(error => { process.stderr.write(asJson?`${JSON.stringify({ error: error.message, code: error.code, report: error.report })}\n`:formatError(error)); process.exitCode = 1; });
|
|
434
|
+
main(args).then(result => process.stdout.write(asJson?`${JSON.stringify(result, null, 2)}\n`:readinessText(args[0],result)??formatResult(args,result))).catch(error => { process.stderr.write(asJson?`${JSON.stringify({ error: error.message, code: error.code, report: error.report })}\n`:readinessErrorText(args[0],error)??formatError(error)); process.exitCode = 1; });
|
|
379
435
|
}
|