stillmade-sdk 0.1.47 → 0.1.48

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.
Files changed (73) hide show
  1. package/AGENTS.md +6 -2
  2. package/FRAME_VIEWS.md +71 -5
  3. package/INTERFACE_DESIGN.md +65 -2
  4. package/MODULES.md +26 -3
  5. package/PASTE_TO_LLM.md +25 -10
  6. package/README.md +15 -0
  7. package/SDK.md +349 -51
  8. package/blocks/native/sm.board-editor/versions/3.0.0/package.js +921 -0
  9. package/blocks/native/sm.editor/versions/3.0.0/package.js +751 -0
  10. package/blocks/native/sm.panel-strip/versions/3.0.0/package.js +350 -0
  11. package/blocks/native/sm.photo-editor/versions/1.0.0/package.js +6693 -0
  12. package/blocks/native/sm.voiceover/versions/3.0.0/package.js +513 -0
  13. package/docs/build/block-builder-contract.md +2 -2
  14. package/docs/build/custom-interface.md +23 -3
  15. package/docs/build/frame-views.md +71 -5
  16. package/docs/build/hosted-text.md +8 -3
  17. package/docs/build/interface-design.md +65 -2
  18. package/docs/build/media-analysis.md +18 -1
  19. package/docs/build/modules.md +26 -3
  20. package/docs/build/with-your-ai.md +14 -0
  21. package/docs/build/workspaces.md +42 -0
  22. package/docs/reference/authoring-guide.md +4 -4
  23. package/docs/reference/cli.md +8 -3
  24. package/docs/reference/execution.md +13 -5
  25. package/docs/reference/first-party-blocks.md +5 -4
  26. package/docs/reference/generation-models.md +4 -0
  27. package/docs/reference/limits.md +9 -2
  28. package/docs/reference/permissions.md +4 -0
  29. package/docs/reference/view-bridge.md +6 -3
  30. package/docs/reference/view-rules.md +1 -1
  31. package/docs/tools/llm.md +25 -10
  32. package/package.json +1 -1
  33. package/packages/block-cli/cli.js +124 -12
  34. package/packages/block-cli/dev-host/assets/host.js +55 -30
  35. package/packages/block-cli/dev-host/assets/index.css +1 -1
  36. package/packages/block-cli/dev-server.js +74 -23
  37. package/packages/block-cli/format.js +20 -1
  38. package/packages/block-cli/ready-gate.js +34 -2
  39. package/packages/block-cli/templates.js +61 -0
  40. package/packages/block-cli/type-packages.js +7 -4
  41. package/packages/block-cli/workspace-dev.js +124 -0
  42. package/packages/block-cli/workspace-pack.js +62 -0
  43. package/packages/block-cli/workspace.js +304 -0
  44. package/packages/block-platform/block-readiness.js +5 -4
  45. package/packages/block-platform/catalog.js +5 -3
  46. package/packages/block-platform/node-defaults.js +2 -2
  47. package/packages/block-platform/plain-descriptions.js +1 -0
  48. package/packages/block-platform/type-workspace.js +8 -0
  49. package/packages/block-sdk/admission.js +2 -2
  50. package/packages/block-sdk/builder-contract.js +3 -3
  51. package/packages/block-sdk/capabilities.js +32 -12
  52. package/packages/block-sdk/contracts.js +11 -2
  53. package/packages/block-sdk/first-party-identities.js +9 -0
  54. package/packages/block-sdk/first-party-packages.js +9 -1
  55. package/packages/block-sdk/folder-package.js +34 -2
  56. package/packages/block-sdk/generation-catalog.js +7 -0
  57. package/packages/block-sdk/index.d.ts +13 -5
  58. package/packages/block-sdk/index.js +1 -1
  59. package/packages/block-sdk/mock-host.js +7 -0
  60. package/packages/block-sdk/module-hosted.js +11 -1
  61. package/packages/block-sdk/module-media-access.js +5 -3
  62. package/packages/block-sdk/permission-summary.js +1 -1
  63. package/packages/block-sdk/portable-media-actions.d.ts +4 -1
  64. package/packages/block-sdk/portable-media-actions.js +16 -1
  65. package/packages/block-sdk/remix-policy.js +2 -2
  66. package/packages/block-sdk/staged-releases.js +5 -0
  67. package/packages/block-sdk/stillmade-pack.js +64 -0
  68. package/packages/block-sdk/testing.js +2 -2
  69. package/packages/block-sdk/view-bridge.js +6 -3
  70. package/packages/block-sdk/view-design.js +59 -12
  71. package/packages/block-sdk/view.js +47 -9
  72. package/packages/design-system/block-kit.css +6 -0
  73. package/packages/design-system/theme.css +2 -0
@@ -78,7 +78,7 @@ const moduleExecutor=(pkg,host)=>{const files=moduleFiles.get(pkg);return files?
78
78
  // storage, project reads): on with --mock, or whenever the folder has
79
79
  // tests/mocks.json. They never contact a provider or spend credits.
80
80
  const blockFolder=async path=>{try{if((await lstat(path)).isDirectory())return path;}catch{}return null;};
81
- const localFileReader=folder=>async name=>{
81
+ export const localFileReader=folder=>async name=>{
82
82
  if(!folder)throw Object.assign(new Error('Local files are read from a Block folder.'),{code:'MODULE_FILE_READ'});
83
83
  const full=resolve(folder,name);
84
84
  if(!full.startsWith(folder+sep))throw Object.assign(new Error(`${name} is outside the Block folder.`),{code:'MODULE_FILE_READ'});
@@ -125,11 +125,23 @@ async function dev(path,options){
125
125
  }
126
126
  if(!(await lstat(path)).isDirectory())throw Object.assign(new Error('dev previews a Block folder.'),{code:'USAGE'});
127
127
  const {startDevServer}=await import('./dev-server.js');
128
- let mock,mocksText,memory,answered;
129
- // Files the person picks in the preview's own-file mode: kept in this process only, read by the run as themselves
130
- // (November 1, 2026, owner: try a Block on your own video, not only its example).
128
+ const picks=pickedFiles(),session=blockSession(path,picks);
129
+ const link=live?{origin:liveSite(site),key:randomBytes(24).toString('base64url')}:undefined;
130
+ const server=await startDevServer({folder:path,port,build,...session,pick:picks.pick,live:link});
131
+ const liveUrl=link&&`${link.origin}/create/import?live=${encodeURIComponent(`${server.url}/__live/${link.key}`)}`;
132
+ return {dev:server.url,folder:path,...(build?{build}:{}),...(liveUrl?{live:liveUrl}:{}),close:server.close};
133
+ }
134
+ // Files the person picks in the preview's own-file mode: kept in this process only, read by the run as themselves
135
+ // (November 1, 2026, owner: try a Block on your own video, not only its example).
136
+ export function pickedFiles(){
131
137
  const picked=new Map(),pickedMedia=url=>picked.get(url)||null;
132
138
  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;};
139
+ return {pick,pickedMedia};
140
+ }
141
+ // One Block folder in the preview: load the current source, run it with stand-ins, serve its media, pack it.
142
+ // `dev` on a Block folder has one; `dev` on a workspace has one per Block.
143
+ export function blockSession(path,{pickedMedia}){
144
+ let mock,mocksText,memory,answered;
133
145
  const load=async()=>{
134
146
  let pkg;
135
147
  try{pkg=await readPackage(path);}catch(error){return {problem:`${error.message}${error.code?` (${error.code})`:''}`};}
@@ -163,10 +175,7 @@ async function dev(path,options){
163
175
  const bundle=async({pkg})=>hasDigestFiles(pkg)
164
176
  ?{fileName:`${pkg.manifest.id}-${pkg.manifest.version}${MODULE_BUNDLE_SUFFIX}`,source:JSON.stringify(createModuleBundle(pkg,moduleFiles.get(pkg)))}
165
177
  :{fileName:`${pkg.manifest.id}-${pkg.manifest.version}.stillmade.json`,source:JSON.stringify(pkg)};
166
- const link=live?{origin:liveSite(site),key:randomBytes(24).toString('base64url')}:undefined;
167
- const server=await startDevServer({folder:path,port,build,load,run,media,pick,live:link,bundle});
168
- const liveUrl=link&&`${link.origin}/create/import?live=${encodeURIComponent(`${server.url}/__live/${link.key}`)}`;
169
- return {dev:server.url,folder:path,...(build?{build}:{}),...(liveUrl?{live:liveUrl}:{}),close:server.close};
178
+ return {load,run,media,bundle};
170
179
  }
171
180
  // own: the hosts that answered fixtures from their own mocks.
172
181
  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}:{};};
@@ -194,6 +203,20 @@ export async function readPackage(path) {
194
203
  if(files)moduleFiles.set(pkg,files);
195
204
  return pkg;
196
205
  }
206
+ // A Project Type file ready to check or run: in a workspace its Steps may name a Block by folder (workspace.js
207
+ // compileWorkflow), and its packages may name a packed Block or a Block folder, or embed a packed module bundle
208
+ // (type-packages.js).
209
+ const typeMocks=new WeakMap();
210
+ async function projectTypeFrom(type,path){
211
+ const {findWorkspace,compileWorkflow,workspaceMocks}=await import('./workspace.js');
212
+ const found=await findWorkspace(path);if(found)type=await compileWorkflow(type,{root:found.root,file:path});
213
+ const result=await embedTypePackages(type,{folder:dirname(path),resolvePath:resolve,read:async file=>{const block=await readPackage(file);return {pkg:block,files:moduleFiles.get(block)};}});
214
+ if(found)typeMocks.set(result,await workspaceMocks(found.root));
215
+ return result;
216
+ }
217
+ // The recorded answers a Type's Block brings from its workspace folder, for typeModuleRunner.
218
+ export const typeMocksFor=type=>{const mocks=typeMocks.get(type);return mocks?pkg=>mocks.get(`${pkg?.manifest?.id}@${pkg?.manifest?.version}`):undefined;};
219
+ export async function loadProjectType(path){const type=await readPackage(path);if(!type?.stages)throw new Error(`${basename(path)} is not a Project Type.`);return projectTypeFrom(type,path);}
197
220
  export const MODULE_EXECUTION_REQUIRED_MESSAGE='This Block runs code, so the gate a hook starts does not run it. Run `stillmade-block gate .` (`npx stillmade-block gate .` in an npm project) yourself and report the status it prints.';
198
221
  export async function main(rawArgs) {
199
222
  // --no-module-execution: what the gate a hook starts passes to ready, so a module Block is refused
@@ -201,14 +224,62 @@ export async function main(rawArgs) {
201
224
  const mockRequested=rawArgs.includes('--mock'),noModuleExecution=rawArgs.includes('--no-module-execution'),args=rawArgs.filter(value=>value!=='--mock'&&value!=='--no-module-execution');
202
225
  if(!args.length||['help','--help','-h'].includes(args[0])||args.includes('--help')||args.includes('-h'))return {help:HELP};
203
226
  if(args[0]==='templates'||args[0]==='create'&&args.includes('--list'))return {templates:Object.entries(TEMPLATES).map(([name,template])=>({name,runtime:template.runtime,description:template.description}))};
227
+ // A command that works on a folder may start with its options; the folder is then this one (`dev --port 4811`).
228
+ if(['dev','test','validate','gate','pack','build','ready','inspect','permissions'].includes(args[0])&&args[1]?.startsWith('--'))args.splice(1,0,'.');
229
+ // In a workspace a Block may be named by its folder alone (`test listing-facts` for blocks/listing-facts; November 6,
230
+ // 2026: a coding agent's `test listing-facts` from the workspace folder failed with ENOENT).
231
+ if(['validate','test','run','preview','pack','ready','inspect','permissions','types','dev'].includes(args[0])&&args[1]&&!args[1].startsWith('--')){
232
+ const named=args[1];let missing=false;try{await lstat(resolve(named));}catch{missing=true;}
233
+ if(missing&&/^[a-z][a-z0-9-]{0,59}$/.test(named)){const {findWorkspace,BLOCKS_DIR,WORKFLOWS_DIR}=await import('./workspace.js'),found=await findWorkspace(process.cwd());
234
+ if(found){for(const candidate of [join(found.root,BLOCKS_DIR,named),join(found.root,WORKFLOWS_DIR,`${named}.json`)]){try{await lstat(candidate);args[1]=candidate;break;}catch{}}}}
235
+ }
204
236
  const [command, target = '.', extra] = args, path = resolve(target);
237
+ // A workspace: several Blocks, shared code and workflows in one folder (workspace.js).
238
+ if(command==='init'){
239
+ if(args.length<2||target.startsWith('--'))throw Object.assign(new Error('Name the folder to create: stillmade-block init my-blocks [--name "Listing kit"]'),{code:'USAGE'});
240
+ const {initWorkspace}=await import('./workspace.js');
241
+ return initWorkspace(path,{name:optionValue(args,'--name')});
242
+ }
243
+ if(command==='add'){
244
+ const {requireWorkspace,folderName,adoptWorkspaceBlock,BLOCKS_DIR,isWorkspaceRoot}=await import('./workspace.js');
245
+ if(args.length<2||target.startsWith('--'))throw Object.assign(new Error('Name the Block: stillmade-block add price-drop-alert [template]'),{code:'USAGE'});
246
+ const {root}=await requireWorkspace(process.cwd(),'add'),name=folderName(target),folder=join(root,BLOCKS_DIR,name);
247
+ if(await isWorkspaceRoot(folder))throw Object.assign(new Error(`${name} is a workspace.`),{code:'USAGE'});
248
+ const template=extra&&!extra.startsWith('--')?extra:undefined;
249
+ const app=optionValue(args,'--app'),created=await main(['create',folder,...(template?[template]:[]),...(app?['--app',app]:[])]);
250
+ const adopted=await adoptWorkspaceBlock(folder);
251
+ return {...created,workspace:root,block:name,...(adopted.source?{source:adopted.source}:{}),next:`Build the Block in ${BLOCKS_DIR}/${name}${adopted.source?` (edit its code in ${BLOCKS_DIR}/${name}/src/main.js only: module/main.js is a copy that \`stillmade-block build\`, test, gate and pack write from it)`:''}, replace its placeholder \`description\` and the copyright holder in \`provenance.notice\`, write its feature map (\`manifest.features\`), then run \`stillmade-block gate\` from the workspace folder. Do not call the Block ready until the gate passes for it.`};
252
+ }
253
+ if(command==='add-type'){
254
+ const {requireWorkspace,addWorkflow}=await import('./workspace.js');
255
+ if(args.length<2||target.startsWith('--'))throw Object.assign(new Error('Name the workflow: stillmade-block add-type listing-video [block ...] [--when schedule]'),{code:'USAGE'});
256
+ const {root}=await requireWorkspace(process.cwd(),'add-type');
257
+ const when=optionValue(args,'--when'),blocks=args.slice(2).filter((value,index,list)=>!value.startsWith('--')&&list[index-1]!=='--when');
258
+ return addWorkflow(root,target,{blocks,...(when?{config:{first:await whenStage(when)}}:{})});
259
+ }
260
+ if(command==='build'){
261
+ const {requireWorkspace,buildWorkspace}=await import('./workspace.js');
262
+ const {root}=await requireWorkspace(path,'build'),report=await buildWorkspace(root);
263
+ if(report.failed.length)throw Object.assign(new Error(`Build failed for ${report.failed.map(item=>item.block).join(', ')}`),{code:'BUILD',report});
264
+ return {workspace:root,...report};
265
+ }
266
+ if(['test','validate','pack','dev'].includes(command)){
267
+ const {isWorkspaceRoot}=await import('./workspace.js');
268
+ if(await isWorkspaceRoot(path)){
269
+ if(command==='dev')return workspaceDev(path,args.slice(2));
270
+ if(command==='pack'){const {packWorkspace}=await import('./workspace-pack.js');return packWorkspace(path,{main,output:extra&&!extra.startsWith('--')?resolve(extra):undefined});}
271
+ return workspaceCheck(command,path,args.slice(2),mockRequested);
272
+ }
273
+ }
205
274
  if (command === 'create-type') {
206
275
  await writeFile(path, JSON.stringify(TYPE_SCAFFOLD, null, 2), { flag: 'wx' });
207
276
  return { created: path, kind: 'type' };
208
277
  }
209
278
  if (command === 'create') {
210
279
  if(args.length<2)throw Object.assign(new Error('Name the folder to create: stillmade-block create my-block [template]'),{code:'USAGE'});
211
- const template=templateNamed(extra),files=await template.files({folder:path});
280
+ const template=templateNamed(extra&&!extra.startsWith('--')?extra:undefined),app=optionValue(args,'--app');
281
+ if(app&&template.name!=='connector')throw Object.assign(new Error('--app goes with the connector template: create <folder> connector --app gmail'),{code:'USAGE'});
282
+ const files=await template.files({folder:path,...(app?{app}:{})});
212
283
  // Every new Block starts with a feature map (one entry from its name and description) for the builder to rewrite as it grows.
213
284
  {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`;}
214
285
  // The new Block is named for its folder, never the example it started from (November 1, 2026: a no-context agent's
@@ -246,8 +317,7 @@ export async function main(rawArgs) {
246
317
  }
247
318
  if (!['validate', 'test', 'preview', 'pack', 'inspect', 'remix', 'ready', 'permissions'].includes(command)) throw Object.assign(new Error(`Unknown command ${command}. Run stillmade-block --help for the commands.`),{code:'USAGE'});
248
319
  let pkg = await readPackage(path);
249
- // A Type's packages may name a packed Block or a Block folder, or embed a packed module bundle (type-packages.js).
250
- if(pkg?.stages)pkg=await embedTypePackages(pkg,{folder:dirname(path),resolvePath:resolve,read:async file=>{const block=await readPackage(file);return {pkg:block,files:moduleFiles.get(block)};}});
320
+ if(pkg?.stages)pkg=await projectTypeFrom(pkg,path);
251
321
  if(args.includes('--original')){
252
322
  if(command!=='pack'||!extra||extra.startsWith('--'))throw new Error('Use pack <source> <output.stillmade-block> --original only for your own original implementation.');
253
323
  pkg=withOriginalSource(pkg);
@@ -299,7 +369,7 @@ export async function main(rawArgs) {
299
369
  if (command === 'inspect') return { id: pkg.id, version: pkg.version, stages: pkg.stages, workspace: typeWorkspaceSupport(pkg) };
300
370
  if(command==='permissions')return {id:pkg.id,blocks:(pkg.packages||[]).map(block=>({id:block.manifest.id,summary:summarizePermissions(block.manifest)}))};
301
371
  // Module Steps run in the SDK's local module runner, as `test` and `preview` run a module Block (type-packages.js).
302
- const runner=typeModuleRunner({mock:mockRequested,readLocalFile:localFileReader(dirname(path))});
372
+ const runner=typeModuleRunner({mock:mockRequested,readLocalFile:localFileReader(dirname(path)),...(typeMocksFor(pkg)?{mocksFor:typeMocksFor(pkg)}:{})});
303
373
  const standIns=()=>{const calls=runner.calls();return calls.length?{standIns:calls,liveVerified:false}:{};};
304
374
  if (command === 'preview') {const preview=await previewProjectType(pkg,{inputs:extra ? JSON.parse(await readFile(resolve(extra),'utf8')) : {},run:runner.run});return {...preview,...standIns()};}
305
375
  // Export source without executing local or hosted code. This is not an
@@ -416,6 +486,48 @@ export async function main(rawArgs) {
416
486
  await writeFile(output, JSON.stringify(pkg, null, 2), { flag: packFlag(extra) });
417
487
  return { file: output, digest: report.digest, tests: report.results.length };
418
488
  }
489
+ async function workspaceDev(root,options){
490
+ let port=4800;
491
+ for(let index=0;index<options.length;index++){
492
+ const value=options[index];
493
+ if(value==='--port'){port=Number(options[++index]);if(!Number.isInteger(port)||port<0||port>65535)throw Object.assign(new Error('--port takes a port number'),{code:'USAGE'});}
494
+ else throw Object.assign(new Error(`dev on a workspace does not take ${value}. --live and --build work on one Block folder (dev blocks/<name>).`),{code:'USAGE'});
495
+ }
496
+ const [{startDevServer},{workspaceProvider}]=await Promise.all([import('./dev-server.js'),import('./workspace-dev.js')]);
497
+ const picks=pickedFiles(),workspace=await workspaceProvider(root,{main,picks});
498
+ const server=await startDevServer({folder:root,port,workspace,pick:picks.pick});
499
+ return {dev:server.url,folder:root,workspace:true,close:server.close};
500
+ }
501
+ // The When Step that starts an automation workflow (StillMade's sm.when, its newest version): what starts each run is
502
+ // turned on in the project's Automation page; an app event here is the start it suggests there.
503
+ async function whenStage(kind){
504
+ const latest=BUILTIN_BLOCKS.filter(block=>block.manifest?.id==='sm.when').map(block=>block.manifest.version).sort((a,b)=>{const x=a.split('.').map(Number),y=b.split('.').map(Number);for(let i=0;i<3;i++)if(x[i]!==y[i])return x[i]-y[i];return 0;}).pop();
505
+ if(!latest)throw Object.assign(new Error('This SDK has no When Step.'),{code:'USAGE'});
506
+ const app=/^app:([a-z0-9][a-z0-9-]{0,79})\/([A-Za-z0-9_]{1,120})$/.exec(kind);
507
+ if(!['schedule','webhook'].includes(kind)&&!app)throw Object.assign(new Error('--when takes schedule, webhook, or an app event such as app:gmail/GMAIL_NEW_GMAIL_MESSAGE'),{code:'USAGE'});
508
+ return {id:'when',blockId:'sm.when',version:latest,label:kind==='schedule'?'On a schedule':kind==='webhook'?'When a webhook is called':'When the app has something new',...(app?{config:{start:{app:`app:${app[1]}`,event:app[2]}}}:{})};
509
+ }
510
+ // The value after a --flag, or undefined.
511
+ function optionValue(args,flag){const index=args.indexOf(flag);if(index<0)return undefined;const value=args[index+1];if(!value||value.startsWith('--'))throw Object.assign(new Error(`${flag} takes a value`),{code:'USAGE'});return value;}
512
+ // test or validate at a workspace's top: every Block, then every workflow, each judged as it would be alone.
513
+ async function workspaceCheck(command,root,options,mock){
514
+ const {workspaceItems,buildWorkspace,unbundledImport,BLOCKS_DIR,WORKFLOWS_DIR}=await import('./workspace.js');
515
+ const build=await buildWorkspace(root);
516
+ const {blocks,workflows}=await workspaceItems(root),results=[];
517
+ const one=async(item,label)=>{
518
+ if(item.problem)return {item:label,kind:item.kind,passed:false,message:item.problem};
519
+ const failed=build.failed.find(entry=>entry.block===item.name);if(failed)return {item:label,kind:item.kind,passed:false,message:`Build failed: ${failed.message}`};
520
+ if(item.kind==='block'){const loose=await unbundledImport(item.path);if(loose)return {item:label,kind:item.kind,passed:false,message:`${loose.file} imports ${loose.specifier}. A Block runs one file: keep the code in src/main.js and run \`stillmade-block build\`.`};}
521
+ try{const result=await main([command,item.path,...options,...(mock?['--mock']:[])]);return {item:label,kind:item.kind,passed:true,...(Array.isArray(result?.results)?{tests:result.results.length}:{})};}
522
+ catch(error){const failing=(error.report?.results||[]).filter(entry=>entry&&entry.passed===false).slice(0,5).map(entry=>`${entry.name}: ${entry.message||'failed'}`);return {item:label,kind:item.kind,passed:false,message:[error.message,...failing].join('\n')};}
523
+ };
524
+ for(const item of blocks)results.push(await one(item,`${BLOCKS_DIR}/${item.name}`));
525
+ for(const item of workflows)results.push(await one(item,`${WORKFLOWS_DIR}/${item.name}.json`));
526
+ const report={workspace:root,command,passed:results.every(item=>item.passed),results,...(build.built.length?{built:build.built}:{})};
527
+ if(!results.length)throw Object.assign(new Error('This workspace has no Blocks yet. Add one with `stillmade-block add <name> [template]`.'),{code:'USAGE'});
528
+ if(!report.passed)throw Object.assign(new Error(`${results.filter(item=>!item.passed).length} of ${results.length} items need attention`),{report});
529
+ return report;
530
+ }
419
531
  // pack replaces the file it wrote before under its own name, so packing again after a fix just works; an output path the
420
532
  // person names is never overwritten (November 1, 2026: a no-context agent's second pack failed with EEXIST).
421
533
  function packFlag(output){return output?'wx':'w';}