stillmade-sdk 0.1.47 → 0.1.49
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 +6 -2
- package/FRAME_VIEWS.md +71 -5
- package/INTERFACE_DESIGN.md +65 -2
- package/MODULES.md +26 -3
- package/PASTE_TO_LLM.md +25 -10
- package/README.md +15 -0
- package/SDK.md +349 -51
- package/blocks/native/sm.board-editor/versions/3.0.0/package.js +921 -0
- package/blocks/native/sm.editor/versions/3.0.0/package.js +751 -0
- package/blocks/native/sm.panel-strip/versions/3.0.0/package.js +350 -0
- package/blocks/native/sm.photo-editor/versions/1.0.0/package.js +6693 -0
- package/blocks/native/sm.voiceover/versions/3.0.0/package.js +513 -0
- package/docs/build/block-builder-contract.md +2 -2
- package/docs/build/custom-interface.md +23 -3
- package/docs/build/frame-views.md +71 -5
- package/docs/build/hosted-text.md +8 -3
- package/docs/build/interface-design.md +65 -2
- package/docs/build/media-analysis.md +18 -1
- package/docs/build/modules.md +26 -3
- package/docs/build/with-your-ai.md +14 -0
- package/docs/build/workspaces.md +42 -0
- package/docs/reference/authoring-guide.md +4 -4
- package/docs/reference/cli.md +8 -3
- package/docs/reference/execution.md +13 -5
- package/docs/reference/first-party-blocks.md +5 -4
- package/docs/reference/generation-models.md +4 -0
- package/docs/reference/limits.md +9 -2
- package/docs/reference/permissions.md +4 -0
- package/docs/reference/view-bridge.md +6 -3
- package/docs/reference/view-rules.md +1 -1
- package/docs/tools/llm.md +25 -10
- package/package.json +1 -1
- package/packages/block-cli/cli.js +124 -12
- package/packages/block-cli/dev-host/assets/host.js +55 -30
- package/packages/block-cli/dev-host/assets/index.css +1 -1
- package/packages/block-cli/dev-server.js +74 -23
- package/packages/block-cli/format.js +20 -1
- package/packages/block-cli/ready-gate.js +34 -2
- package/packages/block-cli/templates.js +61 -0
- package/packages/block-cli/type-packages.js +7 -4
- package/packages/block-cli/workspace-dev.js +124 -0
- package/packages/block-cli/workspace-pack.js +62 -0
- package/packages/block-cli/workspace.js +304 -0
- package/packages/block-platform/block-readiness.js +5 -4
- package/packages/block-platform/canvas.js +9 -0
- package/packages/block-platform/catalog.js +5 -3
- package/packages/block-platform/listings.js +2 -2
- package/packages/block-platform/node-defaults.js +2 -2
- package/packages/block-platform/plain-descriptions.js +1 -0
- package/packages/block-platform/type-workspace.js +8 -0
- package/packages/block-sdk/admission.js +2 -2
- package/packages/block-sdk/builder-contract.js +3 -3
- package/packages/block-sdk/capabilities.js +32 -12
- package/packages/block-sdk/contracts.js +11 -2
- package/packages/block-sdk/first-party-identities.js +9 -0
- package/packages/block-sdk/first-party-packages.js +9 -1
- package/packages/block-sdk/folder-package.js +34 -2
- package/packages/block-sdk/generation-catalog.js +7 -0
- package/packages/block-sdk/index.d.ts +13 -5
- package/packages/block-sdk/index.js +1 -1
- package/packages/block-sdk/mock-host.js +7 -0
- package/packages/block-sdk/module-hosted.js +11 -1
- package/packages/block-sdk/module-media-access.js +5 -3
- package/packages/block-sdk/permission-summary.js +1 -1
- package/packages/block-sdk/portable-media-actions.d.ts +4 -1
- package/packages/block-sdk/portable-media-actions.js +16 -1
- package/packages/block-sdk/remix-policy.js +2 -2
- package/packages/block-sdk/staged-releases.js +5 -0
- package/packages/block-sdk/stillmade-pack.js +64 -0
- package/packages/block-sdk/testing.js +2 -2
- package/packages/block-sdk/view-bridge.js +6 -3
- package/packages/block-sdk/view-design.js +59 -12
- package/packages/block-sdk/view.js +47 -9
- package/packages/design-system/block-kit.css +6 -0
- 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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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),
|
|
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
|
-
|
|
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';}
|