stillmade-sdk 0.1.35 → 0.1.36
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 -4
- package/DESKTOP_CAPABILITIES.md +81 -6
- package/FRAME_VIEWS.md +5 -0
- package/MODULES.md +104 -11
- package/PASTE_TO_LLM.md +14 -6
- package/SDK.md +245 -50
- package/blocks/connected-app/versions/1.6.0/view.js +27 -0
- package/blocks/gmail/versions/1.1.0/workspace.js +15 -0
- package/blocks/native/sm.when/versions/1.1.0/package.js +216 -0
- package/package.json +1 -1
- package/packages/block-cli/cli.js +27 -32
- package/packages/block-cli/dev-host/assets/host.js +38 -37
- package/packages/block-cli/dev-host/assets/index.css +1 -1
- package/packages/block-cli/dev-host/index.html +1 -0
- package/packages/block-cli/format.js +8 -0
- package/packages/block-platform/catalog.js +17 -1
- package/packages/block-platform/segment-runner.js +10 -4
- package/packages/block-platform/steps.js +6 -0
- package/packages/block-sdk/admission.js +11 -4
- package/packages/block-sdk/builder-contract.js +10 -5
- package/packages/block-sdk/capabilities.js +3 -0
- package/packages/block-sdk/connected-app.js +1 -1
- package/packages/block-sdk/contracts.js +1 -0
- package/packages/block-sdk/desktop-permissions.js +1 -1
- package/packages/block-sdk/first-party-identities.js +1 -0
- package/packages/block-sdk/first-party-packages.js +2 -1
- package/packages/block-sdk/folder-package.js +75 -0
- package/packages/block-sdk/index.d.ts +32 -6
- package/packages/block-sdk/index.js +4 -0
- package/packages/block-sdk/local-admission.js +4 -2
- package/packages/block-sdk/media-transform.js +42 -2
- package/packages/block-sdk/mock-host.js +3 -16
- package/packages/block-sdk/mocks.js +20 -0
- package/packages/block-sdk/module-host-calls.js +23 -0
- package/packages/block-sdk/module-hosted.js +22 -1
- package/packages/block-sdk/module-node.js +15 -20
- package/packages/block-sdk/module-runtime.js +7 -4
- package/packages/block-sdk/module-sockets.js +66 -0
- package/packages/block-sdk/module-worker.js +38 -1
- package/packages/block-sdk/network.js +20 -0
- package/packages/block-sdk/permission-summary.js +97 -0
- package/packages/block-sdk/permission-usage.js +72 -0
- package/packages/block-sdk/port-types.js +1 -1
- package/packages/block-sdk/schema-fields.js +11 -4
- package/packages/block-sdk/testing.js +20 -7
- package/packages/block-sdk/view-bridge.js +2 -1
- package/blocks/connected-app/versions/1.5.0/view.js +0 -18
- package/blocks/gmail/versions/1.0.0/workspace.js +0 -6
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// 1.6.0 (October 24, 2026): an app's workspace note is an (i) beside the Task heading, not a
|
|
2
|
+
// heading and paragraph; its developer settings are not shown and rare ones fold into More options.
|
|
3
|
+
import {taskSelectionView} from '../1.5.0/task-selection.js';
|
|
4
|
+
import {connectedAppView as originalView} from '../1.0.0/view.js';
|
|
5
|
+
import {metaTestView,metaTestContract} from '../../../meta-ads/versions/1.2.0/workspace.js';
|
|
6
|
+
import {gmailWorkspaceView} from '../../../gmail/versions/1.1.0/workspace.js';
|
|
7
|
+
const DEVELOPER_FIELDS=/^(page_?token|next_?page_?token|cursor|verbose|include_?payload|ids_?only)$/i;
|
|
8
|
+
// Result tables from the shared preview: scroll sideways instead of squeezing.
|
|
9
|
+
const RESULT_CSS='.connection-task-head{display:flex;align-items:center;gap:4px}.connection-task-head h3{margin:0}.sm-field:has(>input[type="checkbox"]){display:flex;align-items:center;gap:8px}.sm-field>input[type="checkbox"]{width:20px;height:20px;margin:0;flex:none}.connection-table{overflow-x:auto;display:grid;gap:4px}#connection-result table{border-collapse:collapse;width:100%;font-size:14px}#connection-result th,#connection-result td{text-align:left;vertical-align:top;padding:6px 8px;border-bottom:1px solid var(--border);overflow-wrap:break-word}#connection-result thead th{font-weight:600;white-space:nowrap}.connection-fields th{width:35%;font-weight:500}#connection-result details{border-top:1px solid var(--border);padding:6px 0}#connection-result summary{cursor:pointer;font-weight:500}';
|
|
10
|
+
export function connectedAppWorkspace(appId,schema,bindings){
|
|
11
|
+
if(appId==='app:meta-ads')return {sharedWork:metaTestContract,view:metaTestView()};
|
|
12
|
+
if(appId==='app:gmail')return gmailWorkspaceView(schema,bindings);
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
export function connectedAppView(schema,bindings,{appId}={}){
|
|
16
|
+
const workspace=connectedAppWorkspace(appId,schema,bindings),view=originalView(workspace.schema||schema,bindings);
|
|
17
|
+
view.javascript=view.javascript.replace('canEdit=true','canEdit=false').replace("[{tag:'h3',text:'Result'},{tag:'output',className:'connection-result',text:JSON.stringify(shared.outputs.result,null,2).slice(0,10000)}]",'StillMade.schemaFields.preview(shared.outputs.result)');
|
|
18
|
+
if(workspace.view){view.html=workspace.view.html+view.html;view.css=workspace.view.css+'\n'+view.css;view.javascript+='\n'+workspace.view.javascript;}
|
|
19
|
+
// An app's workspace note is an (i) beside the Task heading.
|
|
20
|
+
const note=workspace.heading?'<details class="sm-hint"><summary aria-label="About this '+workspace.heading.toLowerCase()+' task" title="About this '+workspace.heading.toLowerCase()+' task"></summary><p>'+workspace.description+'</p></details>':'';
|
|
21
|
+
if(workspace.heading)view.html=view.html.replace('Review and run task',workspace.runLabel);
|
|
22
|
+
// Paging and debugging settings of any app are for developers: they keep the app's default.
|
|
23
|
+
const fields=Object.keys((workspace.schema||schema).properties||{}),required=new Set((workspace.schema||schema).required||[]);
|
|
24
|
+
const hide=[...new Set([...(workspace.hide||[]),...fields.filter(key=>!required.has(key)&&DEVELOPER_FIELDS.test(key))])],more=(workspace.more||[]).filter(key=>!hide.includes(key));
|
|
25
|
+
if(hide.length||more.length)view.javascript=view.javascript.replace('StillMade.schemaFields.controls(schema,bindings,draft,{canEdit,busy})','StillMade.schemaFields.controls(schema,bindings,draft,{canEdit,busy,more:'+JSON.stringify(more)+',hide:'+JSON.stringify(hide)+'})');
|
|
26
|
+
const tasks=taskSelectionView();view.javascript=view.javascript.replace('if(busy||!canEdit)return;try{draft=StillMade.schemaFields.change','if(busy||!canEdit||!actions.some(item=>item.id===event.action))return;try{draft=StillMade.schemaFields.change');return {html:(note?tasks.html.replace('<h3>Task</h3>','<div class="sm-label connection-task-head"><h3>Task</h3>'+note+'</div>'):tasks.html)+view.html,css:tasks.css+'\n'+view.css+'\n'+RESULT_CSS,javascript:view.javascript+'\n'+tasks.javascript};
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// 1.1.0 (October 24, 2026, owner: "so much text… this better not be what regular users see"): the
|
|
2
|
+
// workspace note is one sentence behind an (i); Gmail's API settings (user id, page token, payload,
|
|
3
|
+
// label ids, format, verbose) are not shown and keep Gmail's defaults; a rarely changed choice folds
|
|
4
|
+
// into More options. Draft controls remain in the existing project-local view state. Reusing a
|
|
5
|
+
// mailbox grants neither cross-project draft access nor permission to send.
|
|
6
|
+
const TITLES={to:'To',recipient:'To',recipients:'To',recipient_email:'To',extra_recipients:'More recipients',cc:'Cc',bcc:'Bcc',subject:'Subject',body:'Message',message_body:'Message',text_body:'Message',html_body:'HTML message',is_html:'Send as HTML',attachment:'Attachment',thread_id:'Selected thread',threadId:'Selected thread',message_id:'Email',messageId:'Email',query:'Search',max_results:'How many',include_spam_trash:'Spam and trash too'};
|
|
7
|
+
const KNOWN=['to','recipient','recipients','recipient_email','subject','body','message_body','text_body','html_body','thread_id','threadId'];
|
|
8
|
+
const MAIN=new Set([...KNOWN,'cc','bcc','attachment','query','max_results','message_id','messageId']);
|
|
9
|
+
// Settings for developers, never for people: Gmail's own defaults are right.
|
|
10
|
+
const HIDDEN=new Set(['user_id','userId','page_token','pageToken','include_payload','verbose','ids_only','format','metadata_headers','label_ids','labelIds']);
|
|
11
|
+
export function gmailWorkspaceView(schema,bindings){
|
|
12
|
+
const fieldNames=Object.keys(schema.properties||{}),required=new Set(schema.required||[]),ordered=[...KNOWN.filter(key=>fieldNames.includes(key)),...fieldNames.filter(key=>!KNOWN.includes(key))],copy={...schema,properties:Object.fromEntries(ordered.map(key=>[key,{...schema.properties[key],title:TITLES[key]||schema.properties[key].title||key}]))};
|
|
13
|
+
const hide=ordered.filter(key=>!required.has(key)&&HIDDEN.has(key));
|
|
14
|
+
return {schema:copy,heading:'Email',description:'Reading or attaching a thread never lets this Step send, and each project keeps its own draft.',runLabel:'Review email task',hide,more:ordered.filter(key=>!required.has(key)&&!MAIN.has(key)&&!HIDDEN.has(key))};
|
|
15
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Generated by scripts/build-owned-blocks.mjs. Edit the owned source files.
|
|
2
|
+
export default {
|
|
3
|
+
"manifest": {
|
|
4
|
+
"schemaVersion": 1,
|
|
5
|
+
"sdkVersion": "0.1.0",
|
|
6
|
+
"id": "sm.when",
|
|
7
|
+
"version": "1.1.0",
|
|
8
|
+
"name": "Automation",
|
|
9
|
+
"description": "Start an automation and choose which items go through: a schedule, a new item in a connected app, a webhook, another project finishing or only by hand starts each run, and Only if rules (a quick check on the item, or an AI check in plain words) let through only the items you want. The item that started a run goes to the next Step.",
|
|
10
|
+
"kind": "task",
|
|
11
|
+
"runtime": "javascript",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"provenance": {
|
|
14
|
+
"notice": "Original source (authorship declared by the packager)\nLicense: MIT\nChanges: Original implementation declared by the packager; no reused repository or dependency code.\nFiles: LICENSE, src/run.js\n\nMIT License\n\nCopyright (c) 2026 StillMade contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
|
15
|
+
"portableSources": [
|
|
16
|
+
{
|
|
17
|
+
"kind": "original",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"paths": [
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"src/run.js"
|
|
22
|
+
],
|
|
23
|
+
"evidence": [
|
|
24
|
+
"upstream/0/LICENSE",
|
|
25
|
+
"upstream/0/src/run.js"
|
|
26
|
+
],
|
|
27
|
+
"changes": "Original implementation declared by the packager; no reused repository or dependency code."
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"inputs": {
|
|
32
|
+
"event": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"semantic": "workflow_event",
|
|
35
|
+
"schema": {
|
|
36
|
+
"id": "string",
|
|
37
|
+
"kind": "string",
|
|
38
|
+
"occurredAt": "string",
|
|
39
|
+
"preview": "boolean",
|
|
40
|
+
"data": "object"
|
|
41
|
+
},
|
|
42
|
+
"sources": [
|
|
43
|
+
"project"
|
|
44
|
+
],
|
|
45
|
+
"description": "The schedule time, app event, webhook call or finished project that started this run. Absent when you run the step by hand.",
|
|
46
|
+
"required": false
|
|
47
|
+
},
|
|
48
|
+
"start": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"schema": "object",
|
|
51
|
+
"semantic": "automation_start",
|
|
52
|
+
"required": false,
|
|
53
|
+
"description": "A suggested start for the Automation page, such as {\"app\":\"app:gmail\",\"event\":\"GMAIL_NEW_GMAIL_MESSAGE\"}. What starts a run is what you turn on there."
|
|
54
|
+
},
|
|
55
|
+
"only_if": {
|
|
56
|
+
"type": "text",
|
|
57
|
+
"semantic": "automation_condition",
|
|
58
|
+
"required": false,
|
|
59
|
+
"description": "A suggested AI check in plain words for the Automation page, for example: Is it a sales lead? What runs is what you turn on there."
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"outputs": {
|
|
63
|
+
"item": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"primary": true,
|
|
66
|
+
"semantic": "external_data",
|
|
67
|
+
"schema": "object",
|
|
68
|
+
"required": false,
|
|
69
|
+
"description": "The item that started this run, such as a new email, lead or order. Missing for a schedule, a run by hand, or an item the AI check turned down."
|
|
70
|
+
},
|
|
71
|
+
"trigger": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"semantic": "automation_trigger",
|
|
74
|
+
"schema": {
|
|
75
|
+
"kind": "string",
|
|
76
|
+
"at": "string",
|
|
77
|
+
"source": "string"
|
|
78
|
+
},
|
|
79
|
+
"description": "What started this run and when: a schedule time, an app event, or a manual run."
|
|
80
|
+
},
|
|
81
|
+
"check": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"semantic": "ai_decision",
|
|
84
|
+
"schema": {
|
|
85
|
+
"yes": "boolean",
|
|
86
|
+
"score": "integer",
|
|
87
|
+
"label": "string",
|
|
88
|
+
"reason": "string"
|
|
89
|
+
},
|
|
90
|
+
"required": false,
|
|
91
|
+
"description": "The AI check’s answer about the item: yes or no, a score from 0 to 100, a short label and the reason."
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"permissions": {
|
|
95
|
+
"project": []
|
|
96
|
+
},
|
|
97
|
+
"ui": []
|
|
98
|
+
},
|
|
99
|
+
"code": "const e = input.event;\nif (!e) return { trigger: { kind: 'manual', at: '', source: 'manual' } };\nconst data = e.data && typeof e.data === 'object' && !Array.isArray(e.data) ? e.data : {};\nconst trigger = { kind: e.kind, at: typeof data.scheduledAt === 'string' ? data.scheduledAt : e.occurredAt, source: e.id };\n// A schedule carries no item. An app's event holds its own payload under data ({type, data}); a\n// webhook or another project finishing is the item itself.\nif (e.kind === 'schedule.tick') return { trigger };\nconst own = data.data && typeof data.data === 'object' && !Array.isArray(data.data) && typeof data.type === 'string';\nreturn { trigger, item: own ? data.data : data };\n",
|
|
100
|
+
"tests": [
|
|
101
|
+
{
|
|
102
|
+
"name": "Manual run",
|
|
103
|
+
"input": {},
|
|
104
|
+
"expected": {
|
|
105
|
+
"trigger": {
|
|
106
|
+
"kind": "manual",
|
|
107
|
+
"at": "",
|
|
108
|
+
"source": "manual"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "Schedule tick has no item",
|
|
114
|
+
"input": {
|
|
115
|
+
"event": {
|
|
116
|
+
"id": "schedule:2026-10-12T13:00:00.000Z",
|
|
117
|
+
"kind": "schedule.tick",
|
|
118
|
+
"occurredAt": "2026-10-12T13:00:04.000Z",
|
|
119
|
+
"preview": false,
|
|
120
|
+
"data": {
|
|
121
|
+
"scheduledAt": "2026-10-12T13:00:00.000Z"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"expected": {
|
|
126
|
+
"trigger": {
|
|
127
|
+
"kind": "schedule.tick",
|
|
128
|
+
"at": "2026-10-12T13:00:00.000Z",
|
|
129
|
+
"source": "schedule:2026-10-12T13:00:00.000Z"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"name": "An app event hands on its own payload",
|
|
135
|
+
"input": {
|
|
136
|
+
"event": {
|
|
137
|
+
"id": "msg-1",
|
|
138
|
+
"kind": "external.updated",
|
|
139
|
+
"occurredAt": "2026-10-22T09:00:00.000Z",
|
|
140
|
+
"preview": false,
|
|
141
|
+
"data": {
|
|
142
|
+
"type": "gmail_new_gmail_message",
|
|
143
|
+
"data": {
|
|
144
|
+
"sender": "ana@example.com",
|
|
145
|
+
"subject": "Pricing for 20 seats"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"expected": {
|
|
151
|
+
"trigger": {
|
|
152
|
+
"kind": "external.updated",
|
|
153
|
+
"at": "2026-10-22T09:00:00.000Z",
|
|
154
|
+
"source": "msg-1"
|
|
155
|
+
},
|
|
156
|
+
"item": {
|
|
157
|
+
"sender": "ana@example.com",
|
|
158
|
+
"subject": "Pricing for 20 seats"
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"name": "A webhook call is the item",
|
|
164
|
+
"input": {
|
|
165
|
+
"event": {
|
|
166
|
+
"id": "order-7",
|
|
167
|
+
"kind": "external.updated",
|
|
168
|
+
"occurredAt": "2026-10-22T09:05:00.000Z",
|
|
169
|
+
"preview": false,
|
|
170
|
+
"data": {
|
|
171
|
+
"order": "7",
|
|
172
|
+
"total": 42
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
"expected": {
|
|
177
|
+
"trigger": {
|
|
178
|
+
"kind": "external.updated",
|
|
179
|
+
"at": "2026-10-22T09:05:00.000Z",
|
|
180
|
+
"source": "order-7"
|
|
181
|
+
},
|
|
182
|
+
"item": {
|
|
183
|
+
"order": "7",
|
|
184
|
+
"total": 42
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
],
|
|
189
|
+
"portable": {
|
|
190
|
+
"format": "stillmade-block/1",
|
|
191
|
+
"dependencies": [],
|
|
192
|
+
"sources": [
|
|
193
|
+
{
|
|
194
|
+
"kind": "original",
|
|
195
|
+
"license": "MIT",
|
|
196
|
+
"paths": [
|
|
197
|
+
"LICENSE",
|
|
198
|
+
"src/run.js"
|
|
199
|
+
],
|
|
200
|
+
"evidence": [
|
|
201
|
+
"upstream/0/LICENSE",
|
|
202
|
+
"upstream/0/src/run.js"
|
|
203
|
+
],
|
|
204
|
+
"changes": "Original implementation declared by the packager; no reused repository or dependency code."
|
|
205
|
+
}
|
|
206
|
+
],
|
|
207
|
+
"files": {
|
|
208
|
+
"upstream/0/LICENSE": "MIT License\n\nCopyright (c) 2026 StillMade contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
|
209
|
+
"upstream/0/src/run.js": "const e = input.event;\nif (!e) return { trigger: { kind: 'manual', at: '', source: 'manual' } };\nconst data = e.data && typeof e.data === 'object' && !Array.isArray(e.data) ? e.data : {};\nconst trigger = { kind: e.kind, at: typeof data.scheduledAt === 'string' ? data.scheduledAt : e.occurredAt, source: e.id };\n// A schedule carries no item. An app's event holds its own payload under data ({type, data}); a\n// webhook or another project finishing is the item itself.\nif (e.kind === 'schedule.tick') return { trigger };\nconst own = data.data && typeof data.data === 'object' && !Array.isArray(data.data) && typeof data.type === 'string';\nreturn { trigger, item: own ? data.data : data };\n"
|
|
210
|
+
},
|
|
211
|
+
"lineage": {
|
|
212
|
+
"parents": [],
|
|
213
|
+
"changes": "Packaged original Block source for StillMade."
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
package/package.json
CHANGED
|
@@ -3,8 +3,9 @@ import {blockFeatures} from '../block-sdk/feature-map.js';
|
|
|
3
3
|
import {assertRemixReady} from '../block-sdk/remix-policy.js';
|
|
4
4
|
import {remixBlock,bundledRemixSource,assertRemixAttribution} from '../block-platform/block-remix.js';
|
|
5
5
|
import {verifyPortableSources} from '../block-sdk/portable-github.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import {packageFromFolder} from '../block-sdk/folder-package.js';
|
|
7
|
+
import {permissionUsage,fixPermissions} from '../block-sdk/permission-usage.js';
|
|
8
|
+
import {summarizePermissions} from '../block-sdk/permission-summary.js';
|
|
8
9
|
import {createRequire} from 'node:module';
|
|
9
10
|
import {randomBytes} from 'node:crypto';
|
|
10
11
|
import {readdir,lstat} from 'node:fs/promises';
|
|
@@ -35,7 +36,7 @@ import {textGenerate} from '../block-sdk/capability-example.js';
|
|
|
35
36
|
import {scanPackage} from '../block-sdk/admission.js';
|
|
36
37
|
import {admitLocalPackage} from '../block-sdk/local-admission.js';
|
|
37
38
|
import {requiresDesktopExecution} from '../block-sdk/desktop-execution.js';
|
|
38
|
-
import {digest} from '../block-sdk/contracts.js';
|
|
39
|
+
import {digest,validateManifest} from '../block-sdk/contracts.js';
|
|
39
40
|
import { testPackage, validatePackage } from '../block-sdk/testing.js';
|
|
40
41
|
import { runPackageAsync } from '../block-sdk/runtime.js';
|
|
41
42
|
import { validateProjectType } from '../block-platform/project-types.js';
|
|
@@ -46,7 +47,7 @@ import {defineBrowserApplicationSource} from '../block-sdk/browser-application-e
|
|
|
46
47
|
import { BUILTIN_BLOCKS } from '../block-platform/catalog.js';
|
|
47
48
|
import { RECIPE_TYPE_EXAMPLE, typeWorkspaceSupport } from '../block-platform/type-workspace.js';
|
|
48
49
|
import {PLATFORM_CHECK_POLICY,PLATFORM_CHECK_TARGETS,PLATFORM_CHECK_THEMES} from '../block-sdk/platform-checks.js';
|
|
49
|
-
import {
|
|
50
|
+
import {MODULE_LIMITS} from '../block-sdk/module-runtime.js';
|
|
50
51
|
import {createModuleBundle,readModuleBundle,hasDigestFiles,MODULE_BUNDLE_SUFFIX} from '../block-sdk/module-bundle.js';
|
|
51
52
|
import {createNodeModuleExecutor} from '../block-sdk/module-node.js';
|
|
52
53
|
import {createMockHost,standInFixtures,validateMocks,MOCKS_FILE} from '../block-sdk/mock-host.js';
|
|
@@ -58,10 +59,6 @@ import {HELP,formatResult,formatError} from './format.js';
|
|
|
58
59
|
// The bytes stay beside the package: tests and previews run them locally, and
|
|
59
60
|
// pack writes one .stillmade-module.json for StillMade to import and upload.
|
|
60
61
|
const moduleFiles=new WeakMap();
|
|
61
|
-
async function readModuleFolder(folder,entry){
|
|
62
|
-
const files=await readFolderBytes(folder);
|
|
63
|
-
return {module:await describeModuleFiles(entry,files),files};
|
|
64
|
-
}
|
|
65
62
|
async function readFolderBytes(folder){
|
|
66
63
|
const files={};let total=0;
|
|
67
64
|
const walk=async(dir,prefix='')=>{for(const item of await readdir(dir,{withFileTypes:true})){
|
|
@@ -169,28 +166,12 @@ export async function readPackage(path) {
|
|
|
169
166
|
moduleFiles.set(bundle.package,files);return bundle.package;
|
|
170
167
|
}
|
|
171
168
|
if (!info.isDirectory()) { if (info.size > 4000000) throw new Error('Package exceeds 4 MB'); return JSON.parse(await readFile(path, 'utf8')); }
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
} else if (manifest.runtime === 'javascript') {
|
|
179
|
-
const codePath = join(path, 'src/run.js');
|
|
180
|
-
if ((await stat(codePath)).size > 262144) throw new Error('JavaScript exceeds 256 KiB');
|
|
181
|
-
source = { code: await readFile(codePath, 'utf8') };
|
|
182
|
-
} else if(manifest.runtime === 'comfyui') source = { comfyui: await read('src/comfyui.json') };
|
|
183
|
-
else if(manifest.runtime === 'capability') source = { capability: await read('src/capability.json') };
|
|
184
|
-
else source = { recipe: await read('src/recipe.json') };
|
|
185
|
-
// A frame view's script may be a framework bundle (up to 4 MiB).
|
|
186
|
-
const sources={};for(const name of ['view.json',VIEW_CONFIG_FILE,...Object.keys(VIEW_SOURCE_FILES)]){try{const file=join(path,'src',name);if((await stat(file)).size>(name==='view.js'?4194304:262144))throw new Error(`Interface source ${name} is too large`);sources[name]=await readFile(file,'utf8');}catch(error){if(error.code!=='ENOENT')throw error;}}
|
|
187
|
-
const view=viewFromSource(sources);
|
|
188
|
-
// A frame view's assets live in src/view-files/ and are named by SHA-256.
|
|
189
|
-
let viewFiles=null;
|
|
190
|
-
if(view?.runtime==='frame'){try{const bytes=await readFolderBytes(join(path,'src','view-files'));view.files=await describeViewFiles(bytes);viewFiles=Object.fromEntries(Object.entries(bytes).map(([name,value])=>[`view:${name}`,value]));}catch(error){if(error.code!=='ENOENT')throw error;}}
|
|
191
|
-
let desktop;try{desktop=await read('src/desktop.json');}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
192
|
-
const pkg={ manifest, ...source, ...(desktop!==undefined?{desktop}:{}), ...(view!==undefined?{view}:{}), tests: await read('tests/fixtures.json') };
|
|
193
|
-
if(localModule||viewFiles)moduleFiles.set(pkg,{...(localModule?.files||{}),...(viewFiles||{})});
|
|
169
|
+
// The folder layout is read by the SDK's shared reader, the same one the app's Import folder uses.
|
|
170
|
+
const {pkg,files}=await packageFromFolder({
|
|
171
|
+
read:async file=>{const full=join(path,file);let info;try{info=await lstat(full);}catch(error){if(error.code==='ENOENT')return undefined;throw error;}if(info.isSymbolicLink())throw new Error('Package symlinks are unavailable');if(!info.isFile())return undefined;return new Uint8Array(await readFile(full));},
|
|
172
|
+
list:async prefix=>{try{return await readFolderBytes(join(path,prefix));}catch(error){if(error.code==='ENOENT')return {};throw error;}},
|
|
173
|
+
});
|
|
174
|
+
if(files)moduleFiles.set(pkg,files);
|
|
194
175
|
return pkg;
|
|
195
176
|
}
|
|
196
177
|
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.';
|
|
@@ -236,7 +217,7 @@ export async function main(rawArgs) {
|
|
|
236
217
|
const file=join(path,'stillmade-env.d.ts');await writeFile(file,blockTypeDeclarations(manifest));
|
|
237
218
|
return {types:file,inputs:Object.keys(manifest.inputs||{}),outputs:Object.keys(manifest.outputs||{})};
|
|
238
219
|
}
|
|
239
|
-
if (!['validate', 'test', 'preview', 'pack', 'inspect', 'remix', 'ready'].includes(command)) throw Object.assign(new Error(`Unknown command ${command}. Run stillmade-block --help for the commands.`),{code:'USAGE'});
|
|
220
|
+
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'});
|
|
240
221
|
let pkg = await readPackage(path);
|
|
241
222
|
if(args.includes('--original')){
|
|
242
223
|
if(command!=='pack'||!extra||extra.startsWith('--'))throw new Error('Use pack <source> <output.stillmade-block> --original only for your own original implementation.');
|
|
@@ -279,6 +260,7 @@ export async function main(rawArgs) {
|
|
|
279
260
|
const review=hosted ? {reviewRequired:true,liveVerified:false,workflowComplete:false,workflowStatus:'deferred'} : {};
|
|
280
261
|
if (command === 'validate') return { valid: true, id: pkg.id,...(args.includes('--mobile')?{mobileProfile:mobileProfile()}:{}),...(hosted ? {tests:0,...review} : {}) };
|
|
281
262
|
if (command === 'inspect') return { id: pkg.id, version: pkg.version, stages: pkg.stages, workspace: typeWorkspaceSupport(pkg) };
|
|
263
|
+
if(command==='permissions')return {id:pkg.id,blocks:(pkg.packages||[]).map(block=>({id:block.manifest.id,summary:summarizePermissions(block.manifest)}))};
|
|
282
264
|
if (command === 'preview') return previewProjectType(pkg,{inputs:extra ? JSON.parse(await readFile(resolve(extra),'utf8')) : {}});
|
|
283
265
|
// Export source without executing local or hosted code. This is not an
|
|
284
266
|
// admission report; every embedded package still needs host review later.
|
|
@@ -298,6 +280,18 @@ export async function main(rawArgs) {
|
|
|
298
280
|
await writeFile(output, JSON.stringify(pkg, null, 2), { flag: 'wx' });
|
|
299
281
|
return { file: output, digest: report.digest, tests: (pkg.packages || []).reduce((count,dependency)=>count+dependency.tests.length,0), workflowComplete:report.workflowComplete, workflowStatus:report.workflowStatus };
|
|
300
282
|
}
|
|
283
|
+
// permissions: what the Block can do in plain words, and what its source uses that
|
|
284
|
+
// the manifest does not declare (or declares and never uses). --fix corrects the
|
|
285
|
+
// manifest; it runs before validation so it can repair one that does not validate.
|
|
286
|
+
if(command==='permissions'){
|
|
287
|
+
const usage=permissionUsage(pkg,moduleFiles.get(pkg)||{});
|
|
288
|
+
if(!args.includes('--fix'))return {id:pkg.manifest.id,summary:summarizePermissions(pkg.manifest),usage};
|
|
289
|
+
if(!(await lstat(path)).isDirectory())throw Object.assign(new Error('permissions --fix changes stillmade.block.json in a Block folder.'),{code:'USAGE'});
|
|
290
|
+
const file=join(path,'stillmade.block.json'),manifest=fixPermissions(JSON.parse(await readFile(file,'utf8')),usage);
|
|
291
|
+
const checked=validateManifest(manifest);if(!checked.valid)throw Object.assign(new Error(checked.errors.map(error=>error.message).join('. ')),{code:'PERMISSIONS'});
|
|
292
|
+
await writeFile(file,JSON.stringify(manifest,null,2)+'\n');
|
|
293
|
+
return {id:pkg.manifest.id,fixed:true,file,added:usage.undeclared,removed:usage.unused,summary:summarizePermissions(manifest)};
|
|
294
|
+
}
|
|
301
295
|
validatePackage(pkg);
|
|
302
296
|
if (['validate','test','pack'].includes(command)) assertRemixReady(pkg);
|
|
303
297
|
if(pkg.manifest.provenance?.remixedFrom&&['validate','test','pack','preview'].includes(command)){const original=await bundledRemixSource(pkg),expected=await remixBlock(original,{checkPreference:false});assertRemixAttribution(pkg,expected.content);}
|
|
@@ -315,7 +309,8 @@ export async function main(rawArgs) {
|
|
|
315
309
|
}
|
|
316
310
|
}
|
|
317
311
|
const advice=command !== 'preview'?appearanceAdvice(pkg.view):null;
|
|
318
|
-
|
|
312
|
+
const usage=command==='validate'?permissionUsage(pkg,moduleFiles.get(pkg)||{}):null,permissionAdvice=usage&&(usage.undeclared.length||usage.unused.length)?usage:null;
|
|
313
|
+
if (command === 'validate') return { valid: true, id: pkg.manifest.id,...(advice?{appearanceAdvice:advice}:{}),...(permissionAdvice?{permissionAdvice}:{}),...(args.includes('--mobile')?{mobileProfile:mobileProfile()}:{}),...(pkg.manifest.runtime==='capability'||requiresDesktopExecution(pkg)?{tests:0,fixtureContracts:pkg.tests.length,execution:'not-run',reviewRequired:true,liveVerified:false}:{}) };
|
|
319
314
|
if (command === 'inspect') return { manifest: pkg.manifest, operations: pkg.recipe?.steps.map(s => s.op) || [], platforms: {phone:'not_verified',browser:'not_verified',desktop:'not_verified'}, runtime: pkg.manifest.runtime };
|
|
320
315
|
if (command === 'preview') {
|
|
321
316
|
let inputPath,statePath,savePath;
|