scratch-pseudocode 1.0.0
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/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +299 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adonis Canada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# scratch-pseudocode
|
|
2
|
+
|
|
3
|
+
A command-line utility for generating pseudocode from public [MIT Scratch](https://scratch.mit.edu/) projects.
|
|
4
|
+
I created this tool to experiment with AI feedback generation for student Scratch projects, tuned for Anthropic Claude.
|
|
5
|
+
|
|
6
|
+
Given a project URL, it fetches the project and prints each sprite's variables, lists, costumes, and scripts in Python-style pseudocode.
|
|
7
|
+
This is especially useful for sending Scratch projects to AI agents for analysis, reading through project logic without opening the editor, or for diffing and reviewing projects in plain text.
|
|
8
|
+
This package is based on the library I made for [MixGit](https://github.com/HunterCogan/mixgit), where I used pseudocode to reduce input tokens by [__87%__](https://github.com/HunterCogan/mixgit/pull/55).
|
|
9
|
+
|
|
10
|
+
Rewritten with simplicity in mind and to work with zero runtime dependencies.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Run in the command-line:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
npx scratch-pseudocode https://scratch.mit.edu/projects/1327031806
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
It can be installed globally:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
npm install -g scratch-pseudocode
|
|
24
|
+
scratch-pseudocode https://scratch.mit.edu/projects/847915138
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires Node.js 20 or later.
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
scratch-pseudocode [-t <name>]... <url>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The Scratch project URL must contain a project ID, and the project must be public and shared. Path segments are accepted:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
scratch-pseudocode https://scratch.mit.edu/projects/847915138/editor/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Options
|
|
42
|
+
|
|
43
|
+
Include `-t, --target <name>` to only output the named target. Repeat the flag to include multiple:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
scratch-pseudocode -t Stage https://scratch.mit.edu/projects/1218216947
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
When excluded, all targets are printed.
|
|
50
|
+
|
|
51
|
+
## Demo
|
|
52
|
+
|
|
53
|
+
Redirect output to a text file and read a snippet:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
scratch-pseudocode -t Stage -t plane https://scratch.mit.edu/projects/847915138 > pseudocode.txt
|
|
57
|
+
head -n 20 pseudocode.txt
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Output
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Target: `Stage`
|
|
64
|
+
Variables: [SCORE=`156`, SPEED=`1`, ☁ HIGHSCORE=`1953`]
|
|
65
|
+
Costumes: [`backdrop1`]
|
|
66
|
+
event_whenflagclicked():
|
|
67
|
+
event_broadcast(BROADCAST_INPUT=`home screen`)
|
|
68
|
+
control_forever():
|
|
69
|
+
sound_playuntildone(`What What?! Wowow!`)
|
|
70
|
+
|
|
71
|
+
Target: `plane`
|
|
72
|
+
Variables: [gravity=`-21.260000000000005`]
|
|
73
|
+
Costumes: [`costume1`]
|
|
74
|
+
event_whenbroadcastreceived(BROADCAST_OPTION=`play game`):
|
|
75
|
+
data_setvariableto(VARIABLE=`SCORE`, VALUE=`0`)
|
|
76
|
+
data_setvariableto(VARIABLE=`SPEED`, VALUE=`0`)
|
|
77
|
+
data_setvariableto(VARIABLE=`gravity`, VALUE=`0`)
|
|
78
|
+
motion_gotoxy(X=`-70`, Y=`0`)
|
|
79
|
+
looks_show()
|
|
80
|
+
control_forever():
|
|
81
|
+
motion_changeyby(DY=(`0.06` * (sensing_mousey() - motion_yposition())))
|
|
82
|
+
motion_pointindirection(DIRECTION=(((motion_yposition() - sensing_mousey()) / `9`) + `90`))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Each target begins with a header containing local variables, lists, and costumes.
|
|
86
|
+
Then, scripts are shown block-by-block.
|
|
87
|
+
|
|
88
|
+
Most of the blocks are rendered with the code, inputs, and fields from the fetched `project.json`.
|
|
89
|
+
Procedures, operators, and shadow blocks are an exception, restructured for readability.
|
|
90
|
+
In the above example, you can see that `operator_subtract(NUM1=motion_yposition(), NUM2=sensing_mousey())` was simplified to `(motion_yposition() - sensing_mousey())`.
|
|
91
|
+
These choices were made to improve Anthropic Claude's comprehension of student projects; I plan to add more output options soon!
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { parseArgs } from "util";
|
|
5
|
+
var VALUE_TRUNCATE = 50;
|
|
6
|
+
var LIST_TRUNCATE = 30;
|
|
7
|
+
var USAGE = "Usage: scratch-pseudocode [-t <name>]... <url>";
|
|
8
|
+
var HELP = `Generate pseudocode from public Scratch projects.
|
|
9
|
+
|
|
10
|
+
${USAGE}
|
|
11
|
+
|
|
12
|
+
Arguments:
|
|
13
|
+
url A Scratch project URL containing a numeric project ID,
|
|
14
|
+
e.g. https://scratch.mit.edu/projects/60917032
|
|
15
|
+
The project must be public and shared.
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
-t, --target <name> Only output the named target.
|
|
19
|
+
Repeat the flag to include several.
|
|
20
|
+
-h, --help Show this help.`;
|
|
21
|
+
var operatorCodes = {
|
|
22
|
+
operator_add: "%s + %s",
|
|
23
|
+
operator_subtract: "%s - %s",
|
|
24
|
+
operator_multiply: "%s * %s",
|
|
25
|
+
operator_divide: "%s / %s",
|
|
26
|
+
operator_mod: "%s mod %s",
|
|
27
|
+
operator_round: "round %s",
|
|
28
|
+
operator_mathop: "%s of %s",
|
|
29
|
+
operator_random: "pick random %s to %s",
|
|
30
|
+
operator_lt: "%s < %s",
|
|
31
|
+
operator_equals: "%s == %s",
|
|
32
|
+
operator_gt: "%s > %s",
|
|
33
|
+
operator_and: "%s and %s",
|
|
34
|
+
operator_or: "%s or %s",
|
|
35
|
+
operator_not: "not %s",
|
|
36
|
+
operator_join: "join %s %s",
|
|
37
|
+
operator_letter_of: "letter %s of %s",
|
|
38
|
+
operator_length: "length of %s",
|
|
39
|
+
operator_contains: "%s contains %s"
|
|
40
|
+
};
|
|
41
|
+
var values;
|
|
42
|
+
var positionals;
|
|
43
|
+
try {
|
|
44
|
+
({ values, positionals } = parseArgs({
|
|
45
|
+
options: {
|
|
46
|
+
target: {
|
|
47
|
+
type: "string",
|
|
48
|
+
short: "t",
|
|
49
|
+
multiple: true
|
|
50
|
+
},
|
|
51
|
+
help: {
|
|
52
|
+
type: "boolean",
|
|
53
|
+
short: "h"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
allowPositionals: true
|
|
57
|
+
}));
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error(USAGE);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
if (values.help) {
|
|
63
|
+
console.log(HELP);
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
var input = positionals[0];
|
|
67
|
+
if (!input) {
|
|
68
|
+
console.error(USAGE);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
var match = input.trim().match(/scratch\.mit\.edu\/projects\/(\d+)/);
|
|
72
|
+
if (!match) {
|
|
73
|
+
console.error(USAGE);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
var projectId = match[1];
|
|
77
|
+
if (!projectId) {
|
|
78
|
+
console.error(USAGE);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
async function fetchProject(projectId2) {
|
|
82
|
+
const metaRes = await fetch(
|
|
83
|
+
`https://api.scratch.mit.edu/projects/${projectId2}`
|
|
84
|
+
);
|
|
85
|
+
const projectMeta = await metaRes.json();
|
|
86
|
+
const token = projectMeta?.project_token;
|
|
87
|
+
const projectRes = await fetch(
|
|
88
|
+
`https://projects.scratch.mit.edu/${projectId2}?token=${encodeURIComponent(
|
|
89
|
+
token
|
|
90
|
+
)}`
|
|
91
|
+
);
|
|
92
|
+
const project = await projectRes.json();
|
|
93
|
+
return { project, projectMeta };
|
|
94
|
+
}
|
|
95
|
+
function formatValue(value, len) {
|
|
96
|
+
const fmt = `\`${value.toString()}\``;
|
|
97
|
+
return `${fmt.slice(0, len)}${fmt.length > len ? `... (${fmt.length} chars)` : ""}`;
|
|
98
|
+
}
|
|
99
|
+
function formatList(items, cap, len) {
|
|
100
|
+
const fmt = items.map((i) => {
|
|
101
|
+
return formatValue(i, len);
|
|
102
|
+
});
|
|
103
|
+
return `[${fmt.slice(0, cap).join(", ")}${fmt.length > cap ? `... (${fmt.length} items)` : ""}]`;
|
|
104
|
+
}
|
|
105
|
+
function parseProcedure(proccode, args) {
|
|
106
|
+
let i = 0;
|
|
107
|
+
return proccode.replace(/%[snb]/g, () => `${args[i++]}`);
|
|
108
|
+
}
|
|
109
|
+
function parseOperator(operator, args) {
|
|
110
|
+
return `(${parseProcedure(operatorCodes[operator] ?? operator, args)})`;
|
|
111
|
+
}
|
|
112
|
+
function collectBlocks(startId, blockMap, collected) {
|
|
113
|
+
let currentId = startId;
|
|
114
|
+
while (currentId !== null) {
|
|
115
|
+
const block = blockMap[currentId];
|
|
116
|
+
if (!block) break;
|
|
117
|
+
const stamped = { ...block, id: currentId };
|
|
118
|
+
collected.push(stamped);
|
|
119
|
+
for (const key of ["SUBSTACK", "SUBSTACK2"]) {
|
|
120
|
+
const bodyId = stamped.inputs[key]?.[1];
|
|
121
|
+
if (typeof bodyId === "string") {
|
|
122
|
+
collectBlocks(bodyId, blockMap, collected);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
currentId = block.next;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function getScripts(project) {
|
|
129
|
+
const result = {};
|
|
130
|
+
for (const target of project.targets) {
|
|
131
|
+
const scripts = [];
|
|
132
|
+
for (const [id, block] of Object.entries(target.blocks)) {
|
|
133
|
+
if (block.topLevel && !block.shadow) {
|
|
134
|
+
const script = [];
|
|
135
|
+
collectBlocks(id, target.blocks, script);
|
|
136
|
+
scripts.push(script);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
result[target.name] = scripts;
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
function formatInput(input2, blocks, variables, lists) {
|
|
144
|
+
if (typeof input2[1] === "string") {
|
|
145
|
+
const id = input2[1];
|
|
146
|
+
if (blocks[id] && (blocks[id].shadow || blocks[id]?.opcode.startsWith("argument"))) {
|
|
147
|
+
const values2 = Object.values(blocks[id].fields).map(
|
|
148
|
+
(field) => `${formatValue(field[0], VALUE_TRUNCATE)}`
|
|
149
|
+
);
|
|
150
|
+
return values2.join(" ");
|
|
151
|
+
}
|
|
152
|
+
return toLine(blocks, input2[1], variables, lists);
|
|
153
|
+
}
|
|
154
|
+
return input2[1] ? formatValue(input2[1][1], VALUE_TRUNCATE) : "";
|
|
155
|
+
}
|
|
156
|
+
function getIndents(script) {
|
|
157
|
+
const blocks = new Map(script.map((block) => [block.id, block]));
|
|
158
|
+
const memo = /* @__PURE__ */ new Map();
|
|
159
|
+
function depth(id) {
|
|
160
|
+
const m = memo.get(id);
|
|
161
|
+
if (m !== void 0) return m;
|
|
162
|
+
const block = blocks.get(id);
|
|
163
|
+
let result = 0;
|
|
164
|
+
if (block && block.parent) {
|
|
165
|
+
const parent = blocks.get(block.parent);
|
|
166
|
+
if (parent) {
|
|
167
|
+
if (parent.next === id) {
|
|
168
|
+
result = Math.max(depth(block.parent), 1);
|
|
169
|
+
} else {
|
|
170
|
+
result = depth(block.parent) + 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
memo.set(id, result);
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
return new Map([...blocks.keys()].map((id) => [id, depth(id)]));
|
|
178
|
+
}
|
|
179
|
+
function toLine(blocks, id, variables, lists) {
|
|
180
|
+
if (!blocks[id]) return "";
|
|
181
|
+
const fields = Object.entries(blocks[id].fields);
|
|
182
|
+
const inputs = Object.entries(blocks[id].inputs);
|
|
183
|
+
const fieldParts = fields.map(([key, [name, ref]]) => {
|
|
184
|
+
if (blocks[id]?.opcode.startsWith("operator")) {
|
|
185
|
+
return formatValue(name, VALUE_TRUNCATE);
|
|
186
|
+
}
|
|
187
|
+
if (key === "VARIABLE" && ref) {
|
|
188
|
+
return `${key}=${variables[ref] ? formatValue(variables[ref][0], VALUE_TRUNCATE) : "undefined"}`;
|
|
189
|
+
}
|
|
190
|
+
if (key === "LIST" && ref) {
|
|
191
|
+
return `${key}=${lists[ref] ? formatList(lists[ref][1], 10, VALUE_TRUNCATE) : "undefined"}`;
|
|
192
|
+
}
|
|
193
|
+
return `${key}=${formatValue(name, VALUE_TRUNCATE)}`;
|
|
194
|
+
});
|
|
195
|
+
const inputParts = inputs.filter(([key, _]) => {
|
|
196
|
+
return !key.startsWith("SUBSTACK");
|
|
197
|
+
}).map(([key, input2]) => {
|
|
198
|
+
if (blocks[id]?.opcode.startsWith("operator") || blocks[id]?.opcode.startsWith("argument") || typeof input2[1] === "string" && blocks[input2[1]]?.shadow) {
|
|
199
|
+
return formatInput(input2, blocks, variables, lists);
|
|
200
|
+
}
|
|
201
|
+
return `${key}=${formatInput(input2, blocks, variables, lists)}`;
|
|
202
|
+
});
|
|
203
|
+
const start = blocks[id].topLevel || inputs.some(([key, _]) => key.startsWith("SUBSTACK"));
|
|
204
|
+
if (blocks[id].opcode.startsWith("procedures")) {
|
|
205
|
+
let args;
|
|
206
|
+
if (blocks[id].mutation) {
|
|
207
|
+
const arr = JSON.parse(blocks[id].mutation.argumentids);
|
|
208
|
+
args = arr.map((arg) => {
|
|
209
|
+
if (blocks[id]?.inputs[arg]) {
|
|
210
|
+
return formatInput(
|
|
211
|
+
blocks[id].inputs[arg],
|
|
212
|
+
blocks,
|
|
213
|
+
variables,
|
|
214
|
+
lists
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}).filter((arg) => arg !== void 0);
|
|
218
|
+
return `${blocks[id].opcode}(${parseProcedure(
|
|
219
|
+
blocks[id].mutation.proccode,
|
|
220
|
+
args
|
|
221
|
+
)})`;
|
|
222
|
+
}
|
|
223
|
+
const ref = blocks[id].inputs["custom_block"];
|
|
224
|
+
if (ref && typeof ref[1] === "string") {
|
|
225
|
+
const proc = blocks[ref[1]];
|
|
226
|
+
if (proc && proc.mutation && proc.mutation.argumentnames) {
|
|
227
|
+
const args2 = JSON.parse(proc.mutation.argumentnames);
|
|
228
|
+
return `${blocks[id].opcode}(${parseProcedure(
|
|
229
|
+
proc.mutation.proccode,
|
|
230
|
+
args2
|
|
231
|
+
)})`;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (blocks[id].opcode.startsWith("operator")) {
|
|
236
|
+
const args = [...fieldParts, ...inputParts];
|
|
237
|
+
if (args[0]) {
|
|
238
|
+
return parseOperator(blocks[id].opcode, args);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return `${blocks[id].opcode}(${[...fieldParts, ...inputParts].join(", ")})${start ? ":" : ""}`;
|
|
242
|
+
}
|
|
243
|
+
function toPseudocode(project, targets) {
|
|
244
|
+
const scripts = getScripts(project);
|
|
245
|
+
let pseudocode = "";
|
|
246
|
+
for (const [targetName, targetScripts] of Object.entries(scripts)) {
|
|
247
|
+
if (!targets || targets.includes(targetName)) {
|
|
248
|
+
const target = project.targets.find((t) => t.name === targetName);
|
|
249
|
+
if (target) {
|
|
250
|
+
const variables = target.isStage ? target.variables : {
|
|
251
|
+
...target.variables,
|
|
252
|
+
...project.targets.find((t) => t.isStage)?.variables
|
|
253
|
+
};
|
|
254
|
+
const lists = target.isStage ? target.lists : {
|
|
255
|
+
...target.lists,
|
|
256
|
+
...project.targets.find((t) => t.isStage)?.lists
|
|
257
|
+
};
|
|
258
|
+
pseudocode += `
|
|
259
|
+
Target: ${formatValue(target.name, VALUE_TRUNCATE)}
|
|
260
|
+
`;
|
|
261
|
+
if (Object.values(target.variables).length > 0) {
|
|
262
|
+
pseudocode += `Variables: [${Object.values(target.variables).map((v) => `${v[0]}=${formatValue(v[1], VALUE_TRUNCATE)}`).join(", ")}]
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
265
|
+
if (Object.values(target.lists).length > 0) {
|
|
266
|
+
pseudocode += `Lists: [${Object.values(target.lists).map(
|
|
267
|
+
([name, items]) => `${name}=${formatList(items, LIST_TRUNCATE, VALUE_TRUNCATE)}`
|
|
268
|
+
)}]
|
|
269
|
+
`;
|
|
270
|
+
}
|
|
271
|
+
if (Object.values(target.costumes).length > 0) {
|
|
272
|
+
pseudocode += `Costumes: ${formatList(
|
|
273
|
+
Object.values(target.costumes).map((costume) => costume.name),
|
|
274
|
+
LIST_TRUNCATE,
|
|
275
|
+
VALUE_TRUNCATE
|
|
276
|
+
)}
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
if (targetScripts.length > 0) {
|
|
280
|
+
for (const script of targetScripts) {
|
|
281
|
+
const indents = getIndents(script);
|
|
282
|
+
for (const [i, block] of script.entries()) {
|
|
283
|
+
const line = toLine(target.blocks, block.id, variables, lists);
|
|
284
|
+
const indent = indents.get(block.id) ?? 0;
|
|
285
|
+
pseudocode += `${` `.repeat(indent ?? 0)}${line}
|
|
286
|
+
`;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return pseudocode.trim();
|
|
294
|
+
}
|
|
295
|
+
async function main(projectId2) {
|
|
296
|
+
const { project, projectMeta } = await fetchProject(projectId2);
|
|
297
|
+
console.log(toPseudocode(project, values.target));
|
|
298
|
+
}
|
|
299
|
+
main(projectId);
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scratch-pseudocode",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A utility for generating pseudocode from public Scratch projects.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"scratch",
|
|
7
|
+
"scratch-mit",
|
|
8
|
+
"pseudocode",
|
|
9
|
+
"parser",
|
|
10
|
+
"sb3"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/adoniscanada/scratch-pseudocode#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/adoniscanada/scratch-pseudocode/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/adoniscanada/scratch-pseudocode.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Adonis Canada",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"scratch-pseudocode": "./dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup src/cli.ts --format esm --dts --clean",
|
|
34
|
+
"prepublishOnly": "npm run build",
|
|
35
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^26.0.1",
|
|
39
|
+
"tsup": "^8.5.1",
|
|
40
|
+
"typescript": "^6.0.3"
|
|
41
|
+
}
|
|
42
|
+
}
|