@mikrojs/native 0.0.7
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/CMakeLists.txt +198 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/cmake/mikrojs_bytecode.cmake +146 -0
- package/cmake.js +22 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +132 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/include/byteorder_apple.h +11 -0
- package/include/byteorder_windows.h +12 -0
- package/include/mikrojs/cbor_helpers.h +24 -0
- package/include/mikrojs/cutils_wrap.h +59 -0
- package/include/mikrojs/errors.h +144 -0
- package/include/mikrojs/mem.h +11 -0
- package/include/mikrojs/mik_color.h +32 -0
- package/include/mikrojs/mikrojs.h +331 -0
- package/include/mikrojs/platform.h +82 -0
- package/include/mikrojs/private.h +281 -0
- package/include/mikrojs/utils.h +125 -0
- package/package.json +100 -0
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/ble/ble.ts +231 -0
- package/runtime/ble/types.ts +194 -0
- package/runtime/ble/uuid.ts +89 -0
- package/runtime/ble/validators.ts +61 -0
- package/runtime/cbor/cbor.ts +1 -0
- package/runtime/cbor/types.ts +8 -0
- package/runtime/console/types.ts +50 -0
- package/runtime/env/env.ts +17 -0
- package/runtime/env/types.ts +12 -0
- package/runtime/format/types.ts +4 -0
- package/runtime/fs/fs.ts +93 -0
- package/runtime/fs/types.ts +92 -0
- package/runtime/globals.d.ts +87 -0
- package/runtime/http/helpers.ts +222 -0
- package/runtime/http/native.ts +151 -0
- package/runtime/http/request.ts +25 -0
- package/runtime/i2c/i2c.ts +35 -0
- package/runtime/i2c/types.ts +55 -0
- package/runtime/inspect/types.ts +10 -0
- package/runtime/internal.d.ts +456 -0
- package/runtime/kv/nvs.ts +17 -0
- package/runtime/kv/rtc.ts +17 -0
- package/runtime/kv/shared.ts +107 -0
- package/runtime/kv/types.ts +150 -0
- package/runtime/neopixel/neopixel.ts +38 -0
- package/runtime/neopixel/types.ts +27 -0
- package/runtime/pin/pin.ts +51 -0
- package/runtime/pin/types.ts +49 -0
- package/runtime/pwm/pwm.ts +32 -0
- package/runtime/pwm/types.ts +29 -0
- package/runtime/reader/reader.ts +167 -0
- package/runtime/reader/types.ts +34 -0
- package/runtime/result/native-result.node-shim.ts +44 -0
- package/runtime/result/result.ts +26 -0
- package/runtime/result/types.ts +60 -0
- package/runtime/schema/schema.ts +321 -0
- package/runtime/schema/types.ts +152 -0
- package/runtime/sleep/sleep.ts +14 -0
- package/runtime/sleep/types.ts +44 -0
- package/runtime/sntp/sntp.ts +54 -0
- package/runtime/sntp/types.ts +38 -0
- package/runtime/spi/spi.ts +31 -0
- package/runtime/spi/types.ts +42 -0
- package/runtime/stdio/stdio.ts +44 -0
- package/runtime/stdio/types.ts +22 -0
- package/runtime/stream/stream.ts +150 -0
- package/runtime/stream/types.ts +47 -0
- package/runtime/sys/sys.ts +90 -0
- package/runtime/sys/types.ts +131 -0
- package/runtime/test/test.ts +595 -0
- package/runtime/test/types.ts +97 -0
- package/runtime/uart/types.ts +75 -0
- package/runtime/uart/uart.ts +51 -0
- package/runtime/wifi/types.ts +156 -0
- package/runtime/wifi/wifi.ts +208 -0
- package/scripts/bundle-runtime.js +149 -0
- package/scripts/compare-minifiers.js +189 -0
- package/scripts/compile-bytecode.sh +38 -0
- package/scripts/copy-prebuild.js +20 -0
- package/scripts/generate-symbol-map.js +146 -0
- package/src/builtins.cpp +82 -0
- package/src/cutils_compat.c +38 -0
- package/src/eval_bytecode.cpp +42 -0
- package/src/fs.cpp +878 -0
- package/src/mem.cpp +63 -0
- package/src/mik_abort.cpp +160 -0
- package/src/mik_app_config.cpp +358 -0
- package/src/mik_cbor.cpp +334 -0
- package/src/mik_color.cpp +46 -0
- package/src/mik_console.cpp +422 -0
- package/src/mik_inspect.cpp +850 -0
- package/src/mik_repl.cpp +1122 -0
- package/src/mik_result.cpp +344 -0
- package/src/mik_stdio.cpp +147 -0
- package/src/mik_sys.cpp +239 -0
- package/src/mik_text_encoding.cpp +443 -0
- package/src/mikrojs.cpp +942 -0
- package/src/modules.cpp +944 -0
- package/src/platform_posix.cpp +134 -0
- package/src/timers.cpp +208 -0
- package/src/utils.cpp +173 -0
package/src/mik_repl.cpp
ADDED
|
@@ -0,0 +1,1122 @@
|
|
|
1
|
+
#include <dirent.h>
|
|
2
|
+
#include <stdarg.h>
|
|
3
|
+
#include <stdio.h>
|
|
4
|
+
#include <string.h>
|
|
5
|
+
#include <sys/stat.h>
|
|
6
|
+
#include <unistd.h>
|
|
7
|
+
|
|
8
|
+
#include <string>
|
|
9
|
+
#include <vector>
|
|
10
|
+
|
|
11
|
+
#include <nanocbor/nanocbor.h>
|
|
12
|
+
|
|
13
|
+
#include "mikrojs/cutils_wrap.h"
|
|
14
|
+
#include "mikrojs/platform.h"
|
|
15
|
+
#include "mikrojs/private.h"
|
|
16
|
+
#include "mikrojs/utils.h"
|
|
17
|
+
|
|
18
|
+
static const char* TAG = "mik_repl";
|
|
19
|
+
|
|
20
|
+
/* ── REPL state ──────────────────────────────────────────────────── */
|
|
21
|
+
|
|
22
|
+
static bool repl_active = false;
|
|
23
|
+
static bool repl_evaluating = false; /* true during eval */
|
|
24
|
+
static JSContext* repl_ctx = nullptr;
|
|
25
|
+
static MIKRuntime* repl_mik_rt = nullptr;
|
|
26
|
+
static int show_depth = 2;
|
|
27
|
+
static bool show_hidden = false;
|
|
28
|
+
static bool show_time = false;
|
|
29
|
+
|
|
30
|
+
/* ── Protocol mode state ─────────────────────────────────────────── */
|
|
31
|
+
|
|
32
|
+
static bool repl_protocol_mode = false;
|
|
33
|
+
static bool repl_paused = false;
|
|
34
|
+
static bool repl_async_skipped = false; /* set when eval bails on paused async */
|
|
35
|
+
static MIKReplTransport* repl_transport = nullptr;
|
|
36
|
+
static uint8_t ready_buf[96];
|
|
37
|
+
static size_t ready_len = 0;
|
|
38
|
+
|
|
39
|
+
/* Transient flag: set by MIK_ProtocolExit to break the current ServeLoop
|
|
40
|
+
* without closing the session. Distinct from repl_active (which signals
|
|
41
|
+
* "session open"). Cleared at the top of each ServeLoop call. */
|
|
42
|
+
static bool s_exit_serve_loop = false;
|
|
43
|
+
|
|
44
|
+
bool mik__repl_is_protocol_mode(void) {
|
|
45
|
+
return repl_protocol_mode && repl_active;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ── TLV frame helpers ───────────────────────────────────────────── */
|
|
49
|
+
|
|
50
|
+
/* Send a TLV-framed message: [ type: uint8 ] [ length: uint32_le ] [ payload ] */
|
|
51
|
+
void mik__proto_send(MIKReplTransport* transport, uint8_t type, const void* data,
|
|
52
|
+
size_t len) {
|
|
53
|
+
uint8_t header[MIK_PROTO_HEADER_SIZE];
|
|
54
|
+
header[0] = type;
|
|
55
|
+
header[1] = (uint8_t)(len & 0xFF);
|
|
56
|
+
header[2] = (uint8_t)((len >> 8) & 0xFF);
|
|
57
|
+
header[3] = (uint8_t)((len >> 16) & 0xFF);
|
|
58
|
+
header[4] = (uint8_t)((len >> 24) & 0xFF);
|
|
59
|
+
transport->write(header, MIK_PROTO_HEADER_SIZE, transport->ctx);
|
|
60
|
+
if (len > 0 && data) {
|
|
61
|
+
transport->write(data, len, transport->ctx);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
void mik__proto_send_ok(MIKReplTransport* transport) {
|
|
66
|
+
mik__proto_send(transport, MIK_MSG_OK, nullptr, 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
void mik__proto_send_err(MIKReplTransport* transport, const char* msg) {
|
|
70
|
+
mik__proto_send(transport, MIK_MSG_ERR, msg, strlen(msg));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Public wrapper used by mik_console.cpp and mik_stdio.cpp */
|
|
74
|
+
void mik__repl_proto_send_output(uint8_t msg_type, const void* data, size_t len) {
|
|
75
|
+
if (repl_transport) {
|
|
76
|
+
mik__proto_send(repl_transport, msg_type, data, len);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Read exactly n bytes from transport, blocking with event loop pumping.
|
|
81
|
+
* Returns false on transport EOF/error, when MIK_ProtocolExit() is
|
|
82
|
+
* signalled from inside the pump (the supervisor's mechanism for ending
|
|
83
|
+
* a test-file's serve loop after __testFileDone fires), or when MIK_Loop
|
|
84
|
+
* reports the attached runtime has halted (e.g. an unhandled rejection
|
|
85
|
+
* set stop_requested — a test that crashed mid-execution). */
|
|
86
|
+
bool mik__proto_read_exact(MIKReplTransport* transport, void* buf, size_t n) {
|
|
87
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
88
|
+
uint8_t* p = static_cast<uint8_t*>(buf);
|
|
89
|
+
size_t total = 0;
|
|
90
|
+
while (total < n) {
|
|
91
|
+
int r = transport->read(p + total, n - total, transport->ctx);
|
|
92
|
+
if (r > 0) {
|
|
93
|
+
total += r;
|
|
94
|
+
} else if (r == 0 || (r < 0 && errno != EAGAIN)) {
|
|
95
|
+
return false;
|
|
96
|
+
} else {
|
|
97
|
+
if (repl_mik_rt && !repl_paused) {
|
|
98
|
+
if (MIK_Loop(repl_mik_rt) != 0) {
|
|
99
|
+
/* Runtime halted (typically unhandled rejection sets
|
|
100
|
+
* stop_requested). Further pumping is a no-op, so
|
|
101
|
+
* exit the serve loop and let the caller swap in the
|
|
102
|
+
* next runtime — or restart, in non-supervisor mode. */
|
|
103
|
+
s_exit_serve_loop = true;
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (repl_ctx) {
|
|
108
|
+
mik__execute_jobs(repl_ctx);
|
|
109
|
+
}
|
|
110
|
+
/* A pumped job (e.g. the test runtime's __testFileDone) may
|
|
111
|
+
* have signalled exit. Bail before yielding another time slice. */
|
|
112
|
+
if (s_exit_serve_loop) return false;
|
|
113
|
+
platform->yield();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Discard n bytes from transport. */
|
|
120
|
+
bool mik__proto_drain(MIKReplTransport* transport, size_t n) {
|
|
121
|
+
uint8_t buf[256];
|
|
122
|
+
while (n > 0) {
|
|
123
|
+
size_t chunk = n > sizeof(buf) ? sizeof(buf) : n;
|
|
124
|
+
if (!mik__proto_read_exact(transport, buf, chunk)) return false;
|
|
125
|
+
n -= chunk;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Read only the 5-byte TLV header. Returns false on EOF/error. */
|
|
131
|
+
static bool proto_read_header(MIKReplTransport* transport, uint8_t* out_type,
|
|
132
|
+
uint32_t* out_payload_len) {
|
|
133
|
+
uint8_t header[MIK_PROTO_HEADER_SIZE];
|
|
134
|
+
if (!mik__proto_read_exact(transport, header, MIK_PROTO_HEADER_SIZE)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
*out_type = header[0];
|
|
138
|
+
*out_payload_len = (uint32_t)header[1] | ((uint32_t)header[2] << 8) |
|
|
139
|
+
((uint32_t)header[3] << 16) | ((uint32_t)header[4] << 24);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Read payload into a std::string (for small payloads like REPL commands). */
|
|
144
|
+
static bool proto_read_payload(MIKReplTransport* transport, uint32_t len,
|
|
145
|
+
std::string& out) {
|
|
146
|
+
out.resize(len);
|
|
147
|
+
if (len == 0) return true;
|
|
148
|
+
return mik__proto_read_exact(transport, out.data(), len);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
/* ── Const/let rewriting ─────────────────────────────────────────── */
|
|
153
|
+
|
|
154
|
+
/* Rewrite top-level const/let to var so re-declarations don't error */
|
|
155
|
+
static std::string repl_rewrite_const_let(const char* code) {
|
|
156
|
+
std::string result;
|
|
157
|
+
const char* p = code;
|
|
158
|
+
bool at_line_start = true;
|
|
159
|
+
|
|
160
|
+
while (*p) {
|
|
161
|
+
if (at_line_start) {
|
|
162
|
+
const char* start = p;
|
|
163
|
+
while (*p == ' ' || *p == '\t') p++;
|
|
164
|
+
|
|
165
|
+
if (strncmp(p, "const ", 6) == 0) {
|
|
166
|
+
result.append(start, p - start);
|
|
167
|
+
result.append("var ");
|
|
168
|
+
p += 6;
|
|
169
|
+
at_line_start = false;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (strncmp(p, "let ", 4) == 0) {
|
|
173
|
+
result.append(start, p - start);
|
|
174
|
+
result.append("var ");
|
|
175
|
+
p += 4;
|
|
176
|
+
at_line_start = false;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
p = start;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (*p == '\n') {
|
|
183
|
+
at_line_start = true;
|
|
184
|
+
} else {
|
|
185
|
+
at_line_start = false;
|
|
186
|
+
}
|
|
187
|
+
result.push_back(*p++);
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* ── Wrap object literals ────────────────────────────────────────── */
|
|
193
|
+
|
|
194
|
+
/* If the expression starts with '{' (ignoring whitespace), wrap in parens
|
|
195
|
+
* to disambiguate from a block statement. */
|
|
196
|
+
static std::string repl_maybe_wrap_object(const std::string& code) {
|
|
197
|
+
const char* p = code.c_str();
|
|
198
|
+
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
|
|
199
|
+
if (*p == '{') {
|
|
200
|
+
return "(" + code + ")";
|
|
201
|
+
}
|
|
202
|
+
return code;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* ── Eval and pump loop ──────────────────────────────────────────── */
|
|
206
|
+
|
|
207
|
+
/* Evaluate an expression, pump the event loop if it returns a promise,
|
|
208
|
+
* and return the result. Caller must JS_FreeValue the return value. */
|
|
209
|
+
static JSValue repl_eval_and_pump(JSContext* ctx, const char* code, size_t len) {
|
|
210
|
+
JSValue result = JS_Eval(ctx, code, len, "<repl>",
|
|
211
|
+
JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_ASYNC |
|
|
212
|
+
JS_EVAL_FLAG_BACKTRACE_BARRIER);
|
|
213
|
+
|
|
214
|
+
if (JS_IsException(result)) {
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!JS_IsPromise(result)) {
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
223
|
+
|
|
224
|
+
for (;;) {
|
|
225
|
+
JSPromiseStateEnum state = JS_PromiseState(ctx, result);
|
|
226
|
+
if (state == JS_PROMISE_FULFILLED) {
|
|
227
|
+
JSValue pr = JS_PromiseResult(ctx, result);
|
|
228
|
+
JS_FreeValue(ctx, result);
|
|
229
|
+
if (JS_IsObject(pr)) {
|
|
230
|
+
JSValue value = JS_GetPropertyStr(ctx, pr, "value");
|
|
231
|
+
JS_FreeValue(ctx, pr);
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
return pr;
|
|
235
|
+
}
|
|
236
|
+
if (state == JS_PROMISE_REJECTED) {
|
|
237
|
+
JSValue reason = JS_PromiseResult(ctx, result);
|
|
238
|
+
JS_FreeValue(ctx, result);
|
|
239
|
+
JS_Throw(ctx, reason);
|
|
240
|
+
return JS_EXCEPTION;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (repl_paused) {
|
|
244
|
+
repl_async_skipped = true;
|
|
245
|
+
JS_FreeValue(ctx, result);
|
|
246
|
+
return JS_UNDEFINED;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
MIK_Loop(repl_mik_rt);
|
|
250
|
+
mik__execute_jobs(ctx);
|
|
251
|
+
platform->yield();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* ── Tab completion ──────────────────────────────────────────────── */
|
|
256
|
+
|
|
257
|
+
static constexpr int MAX_COMPLETIONS = 32;
|
|
258
|
+
static constexpr int MAX_COMPLETION_LEN = 128;
|
|
259
|
+
|
|
260
|
+
/* Scratch space for a single completion call. Lives on the caller's stack
|
|
261
|
+
* (4 KB) so we don't pay for it in .bss — tab completion is synchronous and
|
|
262
|
+
* rare, and the main task has 24 KB of stack to spare. */
|
|
263
|
+
struct CompletionBuf {
|
|
264
|
+
char bufs[MAX_COMPLETIONS][MAX_COMPLETION_LEN];
|
|
265
|
+
int count;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
static void add_completion(CompletionBuf& cb, const char* str) {
|
|
269
|
+
if (cb.count >= MAX_COMPLETIONS) return;
|
|
270
|
+
strncpy(cb.bufs[cb.count], str, MAX_COMPLETION_LEN - 1);
|
|
271
|
+
cb.bufs[cb.count][MAX_COMPLETION_LEN - 1] = '\0';
|
|
272
|
+
cb.count++;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* Walk prototype chains to collect property names for tab completion. */
|
|
276
|
+
static void completion_callback(CompletionBuf& cb, int argc, const char* const* argv) {
|
|
277
|
+
cb.count = 0;
|
|
278
|
+
|
|
279
|
+
if (!repl_ctx || argc == 0) return;
|
|
280
|
+
|
|
281
|
+
const char* last_token = argv[argc - 1];
|
|
282
|
+
|
|
283
|
+
/* Check for directive completion */
|
|
284
|
+
if (argc == 1 && last_token[0] == '/') {
|
|
285
|
+
static const char* directives[] = {"/help", "/mem", "/gc", "/exit",
|
|
286
|
+
"/ls", "/cat", "/rm", "/time",
|
|
287
|
+
"/depth", "/hidden", "/df", "/du",
|
|
288
|
+
"/pause", "/resume", "/info"};
|
|
289
|
+
size_t tok_len = strlen(last_token);
|
|
290
|
+
for (size_t i = 0; i < countof(directives); i++) {
|
|
291
|
+
if (strncmp(directives[i], last_token, tok_len) == 0) {
|
|
292
|
+
add_completion(cb, directives[i]);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* Find the last "word" boundary within the token (after operators) */
|
|
299
|
+
const char* word_start = last_token + strlen(last_token);
|
|
300
|
+
while (word_start > last_token) {
|
|
301
|
+
char c = *(word_start - 1);
|
|
302
|
+
if (c == '(' || c == '[' || c == '{' || c == ',' || c == ';' || c == '!' || c == '=' ||
|
|
303
|
+
c == '+' || c == '-' || c == '*' || c == '/' || c == '%' || c == '|' || c == '&' ||
|
|
304
|
+
c == '<' || c == '>' || c == '~' || c == '^' || c == '?') {
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
word_start--;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* Split "obj.prop.sub" into object part and partial property */
|
|
311
|
+
std::string word(word_start, last_token + strlen(last_token));
|
|
312
|
+
if (word.empty()) return;
|
|
313
|
+
|
|
314
|
+
std::string obj_part;
|
|
315
|
+
std::string prop_prefix;
|
|
316
|
+
size_t last_dot = word.rfind('.');
|
|
317
|
+
if (last_dot != std::string::npos) {
|
|
318
|
+
obj_part = word.substr(0, last_dot);
|
|
319
|
+
prop_prefix = word.substr(last_dot + 1);
|
|
320
|
+
} else {
|
|
321
|
+
/* Complete on globalThis properties */
|
|
322
|
+
obj_part = "globalThis";
|
|
323
|
+
prop_prefix = word;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* Evaluate the object part to get a JSValue */
|
|
327
|
+
JSValue obj = JS_Eval(repl_ctx, obj_part.c_str(), obj_part.size(), "<completion>",
|
|
328
|
+
JS_EVAL_TYPE_GLOBAL);
|
|
329
|
+
if (JS_IsException(obj)) {
|
|
330
|
+
JS_FreeValue(repl_ctx, JS_GetException(repl_ctx));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* Build prefix: everything in the token before the word we're completing */
|
|
335
|
+
std::string token_prefix(last_token, word_start);
|
|
336
|
+
|
|
337
|
+
/* Walk the prototype chain and collect matching property names */
|
|
338
|
+
JSValue cur = JS_DupValue(repl_ctx, obj);
|
|
339
|
+
|
|
340
|
+
for (int depth = 0; depth < 10 && JS_IsObject(cur); depth++) {
|
|
341
|
+
JSPropertyEnum* ptab = nullptr;
|
|
342
|
+
uint32_t plen = 0;
|
|
343
|
+
|
|
344
|
+
if (JS_GetOwnPropertyNames(repl_ctx, &ptab, &plen, cur,
|
|
345
|
+
JS_GPN_STRING_MASK | JS_GPN_SYMBOL_MASK) == 0) {
|
|
346
|
+
for (uint32_t i = 0; i < plen; i++) {
|
|
347
|
+
const char* name = JS_AtomToCString(repl_ctx, ptab[i].atom);
|
|
348
|
+
if (name) {
|
|
349
|
+
if (prop_prefix.empty() ||
|
|
350
|
+
strncmp(name, prop_prefix.c_str(), prop_prefix.size()) == 0) {
|
|
351
|
+
std::string completion;
|
|
352
|
+
if (last_dot != std::string::npos) {
|
|
353
|
+
completion = token_prefix + obj_part + "." + name;
|
|
354
|
+
} else {
|
|
355
|
+
completion = token_prefix + name;
|
|
356
|
+
}
|
|
357
|
+
add_completion(cb, completion.c_str());
|
|
358
|
+
}
|
|
359
|
+
JS_FreeCString(repl_ctx, name);
|
|
360
|
+
}
|
|
361
|
+
JS_FreeAtom(repl_ctx, ptab[i].atom);
|
|
362
|
+
}
|
|
363
|
+
js_free(repl_ctx, ptab);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
JSValue proto = JS_GetPrototype(repl_ctx, cur);
|
|
367
|
+
JS_FreeValue(repl_ctx, cur);
|
|
368
|
+
cur = proto;
|
|
369
|
+
}
|
|
370
|
+
JS_FreeValue(repl_ctx, cur);
|
|
371
|
+
JS_FreeValue(repl_ctx, obj);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* ── Directives ──────────────────────────────────────────────────── */
|
|
375
|
+
|
|
376
|
+
/* snprintf-append helper for directive output buffer */
|
|
377
|
+
__attribute__((format(printf, 2, 3)))
|
|
378
|
+
static void dir_printf(std::string& out, const char* fmt, ...) {
|
|
379
|
+
char buf[512];
|
|
380
|
+
va_list args;
|
|
381
|
+
va_start(args, fmt);
|
|
382
|
+
int n = vsnprintf(buf, sizeof(buf), fmt, args);
|
|
383
|
+
va_end(args);
|
|
384
|
+
if (n > 0) {
|
|
385
|
+
out.append(buf, n < (int)sizeof(buf) ? n : (int)sizeof(buf) - 1);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* Returns true if the line was handled as a directive.
|
|
390
|
+
* Output is written to `out`. */
|
|
391
|
+
static bool handle_directive_impl(JSContext* ctx, const char* line, std::string& out) {
|
|
392
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
393
|
+
|
|
394
|
+
if (line[0] != '/') return false;
|
|
395
|
+
|
|
396
|
+
if (strcmp(line, "/help") == 0) {
|
|
397
|
+
out.append(
|
|
398
|
+
"/help Show this help\n"
|
|
399
|
+
"/mem Show heap memory usage\n"
|
|
400
|
+
"/gc Run garbage collector\n"
|
|
401
|
+
"/time Toggle timing display\n"
|
|
402
|
+
"/depth N Set object inspection depth (default 2)\n"
|
|
403
|
+
"/hidden Toggle hidden property display\n"
|
|
404
|
+
"/pause Pause app (suspend timers/callbacks)\n"
|
|
405
|
+
"/resume Resume app\n"
|
|
406
|
+
"/ls [DIR] List directory contents\n"
|
|
407
|
+
"/cat FILE Print file contents\n"
|
|
408
|
+
"/rm FILE Remove a file\n"
|
|
409
|
+
"/info Show device and runtime info\n"
|
|
410
|
+
"/df Show filesystem free/total space\n"
|
|
411
|
+
"/du [PATH] Show disk usage for path\n"
|
|
412
|
+
"/exit Exit REPL\n");
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (strcmp(line, "/info") == 0) {
|
|
417
|
+
/* Device info from transport (set by platform) */
|
|
418
|
+
if (repl_transport && repl_transport->chip_name) {
|
|
419
|
+
dir_printf(out, "Chip: %s\n", repl_transport->chip_name);
|
|
420
|
+
}
|
|
421
|
+
const char* dev_id = MIK_GetPlatform()->get_device_id();
|
|
422
|
+
if (dev_id) {
|
|
423
|
+
dir_printf(out, "Device ID: %s\n", dev_id);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/* Runtime uptime */
|
|
427
|
+
int64_t uptime_us = platform->get_boot_us();
|
|
428
|
+
double uptime_s = (double)uptime_us / 1000000.0;
|
|
429
|
+
if (uptime_s < 60) {
|
|
430
|
+
dir_printf(out, "Uptime: %.1fs\n", uptime_s);
|
|
431
|
+
} else if (uptime_s < 3600) {
|
|
432
|
+
dir_printf(out, "Uptime: %.1fm\n", uptime_s / 60.0);
|
|
433
|
+
} else {
|
|
434
|
+
dir_printf(out, "Uptime: %.1fh\n", uptime_s / 3600.0);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* JS heap */
|
|
438
|
+
JSMemoryUsage mem;
|
|
439
|
+
JS_ComputeMemoryUsage(JS_GetRuntime(ctx), &mem);
|
|
440
|
+
long limit = mem.malloc_limit > 0 ? (long)mem.malloc_limit : (long)mem.malloc_size;
|
|
441
|
+
dir_printf(out, "JS heap: %ld / %ld bytes (%.1f%% used)\n", (long)mem.malloc_size, limit,
|
|
442
|
+
limit > 0 ? (double)mem.malloc_size / (double)limit * 100.0 : 0.0);
|
|
443
|
+
|
|
444
|
+
/* System memory */
|
|
445
|
+
size_t free_mem = platform->get_free_system_mem();
|
|
446
|
+
size_t total_mem =
|
|
447
|
+
platform->get_total_system_mem ? platform->get_total_system_mem() : 0;
|
|
448
|
+
if (total_mem > 0) {
|
|
449
|
+
size_t used_mem = total_mem > free_mem ? total_mem - free_mem : 0;
|
|
450
|
+
dir_printf(out, "System heap: %lu / %lu bytes (%.1f%% used)\n",
|
|
451
|
+
(unsigned long)used_mem, (unsigned long)total_mem,
|
|
452
|
+
(double)used_mem / (double)total_mem * 100.0);
|
|
453
|
+
} else if (free_mem > 0) {
|
|
454
|
+
dir_printf(out, "System heap: %lu free\n", (unsigned long)free_mem);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/* Filesystem */
|
|
458
|
+
size_t fs_total = 0, fs_used = 0;
|
|
459
|
+
if (platform->get_fs_info("user", &fs_total, &fs_used)) {
|
|
460
|
+
dir_printf(out, "Filesystem: %u / %u bytes (%.1f%% used)\n", (unsigned)fs_used,
|
|
461
|
+
(unsigned)fs_total,
|
|
462
|
+
fs_total > 0 ? (double)fs_used / (double)fs_total * 100.0 : 0.0);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (strcmp(line, "/mem") == 0) {
|
|
469
|
+
JSMemoryUsage mem;
|
|
470
|
+
JS_ComputeMemoryUsage(JS_GetRuntime(ctx), &mem);
|
|
471
|
+
size_t free_mem = platform->get_free_system_mem();
|
|
472
|
+
size_t min_free = platform->get_min_free_system_mem();
|
|
473
|
+
size_t total_mem = platform->get_total_system_mem();
|
|
474
|
+
// JS allocations fail on whichever ceiling we hit first: QuickJS's
|
|
475
|
+
// self-imposed malloc_limit (configured via mikro.config.ts
|
|
476
|
+
// `memReserved`) or the actual system heap. Report the tighter of
|
|
477
|
+
// the two so the number matches what you can actually spend.
|
|
478
|
+
long qjs_free = mem.malloc_limit > 0
|
|
479
|
+
? (long)mem.malloc_limit - (long)mem.malloc_size
|
|
480
|
+
: -1;
|
|
481
|
+
long available = qjs_free;
|
|
482
|
+
if (free_mem > 0 && (available < 0 || (long)free_mem < available)) {
|
|
483
|
+
available = (long)free_mem;
|
|
484
|
+
}
|
|
485
|
+
if (available >= 0) {
|
|
486
|
+
dir_printf(out, "Available: %.1f KB\n\n", available / 1024.0);
|
|
487
|
+
}
|
|
488
|
+
dir_printf(out, "Usage:\n");
|
|
489
|
+
if (mem.malloc_limit > 0) {
|
|
490
|
+
double pct = (double)mem.malloc_size / (double)mem.malloc_limit * 100.0;
|
|
491
|
+
dir_printf(out, " QuickJS: %5.1f / %5.1f KB used (%.0f%%)\n",
|
|
492
|
+
mem.malloc_size / 1024.0, mem.malloc_limit / 1024.0, pct);
|
|
493
|
+
} else {
|
|
494
|
+
dir_printf(out, " QuickJS: %.1f KB used (no limit)\n", mem.malloc_size / 1024.0);
|
|
495
|
+
}
|
|
496
|
+
if (free_mem > 0 && total_mem > 0) {
|
|
497
|
+
size_t used_mem = total_mem - free_mem;
|
|
498
|
+
size_t peak_used = total_mem - min_free;
|
|
499
|
+
double pct = (double)used_mem / (double)total_mem * 100.0;
|
|
500
|
+
double peak_pct = (double)peak_used / (double)total_mem * 100.0;
|
|
501
|
+
dir_printf(out, " System: %5.1f / %5.1f KB used (%.0f%%, peak %.0f%%)\n",
|
|
502
|
+
used_mem / 1024.0, total_mem / 1024.0, pct, peak_pct);
|
|
503
|
+
} else if (free_mem > 0) {
|
|
504
|
+
dir_printf(out, " System: %.1f KB free (min seen: %.1f KB)\n", free_mem / 1024.0,
|
|
505
|
+
min_free / 1024.0);
|
|
506
|
+
}
|
|
507
|
+
// Reserved is the chunk of system heap intentionally kept out of
|
|
508
|
+
// QuickJS's reach via `memReserved` in mikro.config.ts, so native
|
|
509
|
+
// subsystems (WiFi/lwIP/TLS/HTTP) have room to breathe. Showing it
|
|
510
|
+
// makes the QuickJS limit explainable: limit ≈ free_heap_at_init −
|
|
511
|
+
// reserved.
|
|
512
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
513
|
+
if (mik_rt != NULL && mik_rt->config.mem_reserved > 0) {
|
|
514
|
+
dir_printf(out, " Reserved: %.1f KB (memReserved for native subsystems)\n",
|
|
515
|
+
mik_rt->config.mem_reserved / 1024.0);
|
|
516
|
+
}
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (strcmp(line, "/gc") == 0) {
|
|
521
|
+
JSMemoryUsage before;
|
|
522
|
+
JS_ComputeMemoryUsage(JS_GetRuntime(ctx), &before);
|
|
523
|
+
JS_RunGC(JS_GetRuntime(ctx));
|
|
524
|
+
JSMemoryUsage after;
|
|
525
|
+
JS_ComputeMemoryUsage(JS_GetRuntime(ctx), &after);
|
|
526
|
+
long freed = (long)before.malloc_size - (long)after.malloc_size;
|
|
527
|
+
dir_printf(out, "GC collected %ld bytes (%ld -> %ld)\n", freed,
|
|
528
|
+
(long)before.malloc_size, (long)after.malloc_size);
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (strcmp(line, "/time") == 0) {
|
|
533
|
+
show_time = !show_time;
|
|
534
|
+
dir_printf(out, "Timing %s\n", show_time ? "on" : "off");
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (strncmp(line, "/depth", 6) == 0) {
|
|
539
|
+
const char* arg = line + 6;
|
|
540
|
+
while (*arg == ' ') arg++;
|
|
541
|
+
if (*arg) {
|
|
542
|
+
int d = atoi(arg);
|
|
543
|
+
if (d >= 0) {
|
|
544
|
+
show_depth = d;
|
|
545
|
+
dir_printf(out, "Depth set to %d\n", show_depth);
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
dir_printf(out, "Current depth: %d\n", show_depth);
|
|
549
|
+
}
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (strcmp(line, "/hidden") == 0) {
|
|
554
|
+
show_hidden = !show_hidden;
|
|
555
|
+
dir_printf(out, "Show hidden properties: %s\n", show_hidden ? "on" : "off");
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (strcmp(line, "/pause") == 0) {
|
|
560
|
+
if (repl_paused) {
|
|
561
|
+
out.append("App is already paused\n");
|
|
562
|
+
} else {
|
|
563
|
+
repl_paused = true;
|
|
564
|
+
out.append("App paused (timers, callbacks suspended). Use /resume to continue.\n");
|
|
565
|
+
}
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (strcmp(line, "/resume") == 0) {
|
|
570
|
+
if (!repl_paused) {
|
|
571
|
+
out.append("App is not paused\n");
|
|
572
|
+
} else {
|
|
573
|
+
repl_paused = false;
|
|
574
|
+
out.append("App resumed\n");
|
|
575
|
+
}
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (strcmp(line, "/df") == 0) {
|
|
580
|
+
size_t total = 0, used = 0;
|
|
581
|
+
if (platform->get_fs_info("user", &total, &used)) {
|
|
582
|
+
size_t free_bytes = total - used;
|
|
583
|
+
dir_printf(out, "Filesystem: %u / %u bytes used (%.1f%%), %u bytes free\n",
|
|
584
|
+
(unsigned)used, (unsigned)total,
|
|
585
|
+
total > 0 ? (double)used / (double)total * 100.0 : 0.0,
|
|
586
|
+
(unsigned)free_bytes);
|
|
587
|
+
} else {
|
|
588
|
+
const char* root = repl_mik_rt->fs_root ? repl_mik_rt->fs_root
|
|
589
|
+
: repl_mik_rt->fs_base_path;
|
|
590
|
+
if (root) {
|
|
591
|
+
size_t fs_used = mik__fs_dir_usage(root);
|
|
592
|
+
size_t fs_total = repl_mik_rt->fs_limit > 0 ? repl_mik_rt->fs_limit : fs_used;
|
|
593
|
+
size_t fs_free = fs_total > fs_used ? fs_total - fs_used : 0;
|
|
594
|
+
dir_printf(out, "Filesystem: %lu / %lu bytes used (%.1f%%), %lu bytes free\n",
|
|
595
|
+
(unsigned long)fs_used, (unsigned long)fs_total,
|
|
596
|
+
fs_total > 0 ? (double)fs_used / (double)fs_total * 100.0 : 0.0,
|
|
597
|
+
(unsigned long)fs_free);
|
|
598
|
+
} else {
|
|
599
|
+
dir_printf(out, "Cannot get filesystem info\n");
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (strncmp(line, "/du", 3) == 0 && (line[3] == '\0' || line[3] == ' ')) {
|
|
606
|
+
const char* arg = line + 3;
|
|
607
|
+
while (*arg == ' ') arg++;
|
|
608
|
+
|
|
609
|
+
/* Default to root "/" if no arg */
|
|
610
|
+
char dirname[PATH_MAX];
|
|
611
|
+
if (arg[0] == '\0') {
|
|
612
|
+
snprintf(dirname, sizeof(dirname), "/");
|
|
613
|
+
} else if (arg[0] != '/') {
|
|
614
|
+
snprintf(dirname, sizeof(dirname), "/%s", arg);
|
|
615
|
+
} else {
|
|
616
|
+
snprintf(dirname, sizeof(dirname), "%s", arg);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
char path[PATH_MAX];
|
|
620
|
+
if (mik__resolve_fs_root(repl_ctx, dirname, path, sizeof(path)) < 0) {
|
|
621
|
+
dir_printf(out, "Permission denied: '%s'\n", arg[0] ? arg : "/");
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/* Stat single file or recurse directory */
|
|
626
|
+
struct stat st;
|
|
627
|
+
if (stat(path, &st) != 0) {
|
|
628
|
+
dir_printf(out, "Cannot stat '%s': %s\n", arg[0] ? arg : "/", strerror(errno));
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (!S_ISDIR(st.st_mode)) {
|
|
633
|
+
dir_printf(out, " %ld\t%s\n", (long)st.st_size, dirname);
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/* Recursive directory walk */
|
|
638
|
+
long dir_total = 0;
|
|
639
|
+
DIR* dir = opendir(path);
|
|
640
|
+
if (!dir) {
|
|
641
|
+
dir_printf(out, "Cannot open '%s': %s\n", arg[0] ? arg : "/", strerror(errno));
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
const char* trailing = (dirname[strlen(dirname) - 1] == '/') ? "" : "/";
|
|
645
|
+
struct dirent* entry;
|
|
646
|
+
while ((entry = readdir(dir)) != NULL) {
|
|
647
|
+
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
|
|
648
|
+
char child_path[PATH_MAX];
|
|
649
|
+
snprintf(child_path, sizeof(child_path), "%s/%s", path, entry->d_name);
|
|
650
|
+
struct stat child_st;
|
|
651
|
+
if (stat(child_path, &child_st) == 0 && S_ISREG(child_st.st_mode)) {
|
|
652
|
+
dir_printf(out, " %ld\t%s%s%s\n", (long)child_st.st_size, dirname, trailing,
|
|
653
|
+
entry->d_name);
|
|
654
|
+
dir_total += child_st.st_size;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
closedir(dir);
|
|
658
|
+
dir_printf(out, " %ld\ttotal\n", dir_total);
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (strncmp(line, "/cat ", 5) == 0) {
|
|
663
|
+
const char* arg = line + 5;
|
|
664
|
+
while (*arg == ' ') arg++;
|
|
665
|
+
|
|
666
|
+
/* Ensure path starts with / for resolve */
|
|
667
|
+
char filename[PATH_MAX];
|
|
668
|
+
if (arg[0] != '/') {
|
|
669
|
+
snprintf(filename, sizeof(filename), "/%s", arg);
|
|
670
|
+
} else {
|
|
671
|
+
snprintf(filename, sizeof(filename), "%s", arg);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
char path[PATH_MAX];
|
|
675
|
+
if (mik__resolve_fs_root(repl_ctx, filename, path, sizeof(path)) < 0) {
|
|
676
|
+
dir_printf(out, "Permission denied: '%s'\n", arg);
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
struct stat cat_st;
|
|
681
|
+
if (stat(path, &cat_st) != 0) {
|
|
682
|
+
dir_printf(out, "Cannot open '%s': %s\n", arg, strerror(errno));
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
FILE* f = fopen(path, "r");
|
|
687
|
+
if (!f) {
|
|
688
|
+
dir_printf(out, "Cannot open '%s': %s\n", arg, strerror(errno));
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
char buf[256];
|
|
692
|
+
size_t text_len = 0;
|
|
693
|
+
while (fgets(buf, sizeof(buf), f)) {
|
|
694
|
+
out.append(buf);
|
|
695
|
+
text_len += strlen(buf);
|
|
696
|
+
}
|
|
697
|
+
fclose(f);
|
|
698
|
+
if (text_len == 0 && cat_st.st_size > 0) {
|
|
699
|
+
dir_printf(out, "(binary, size: %ld)\n", (long)cat_st.st_size);
|
|
700
|
+
} else {
|
|
701
|
+
out.append("\n");
|
|
702
|
+
}
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (strncmp(line, "/rm ", 4) == 0) {
|
|
707
|
+
const char* arg = line + 4;
|
|
708
|
+
while (*arg == ' ') arg++;
|
|
709
|
+
|
|
710
|
+
if (*arg == '\0') {
|
|
711
|
+
out.append("Usage: /rm FILE\n");
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/* Ensure path starts with / for resolve */
|
|
716
|
+
char filename[PATH_MAX];
|
|
717
|
+
if (arg[0] != '/') {
|
|
718
|
+
snprintf(filename, sizeof(filename), "/%s", arg);
|
|
719
|
+
} else {
|
|
720
|
+
snprintf(filename, sizeof(filename), "%s", arg);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
char path[PATH_MAX];
|
|
724
|
+
if (mik__resolve_fs_root(repl_ctx, filename, path, sizeof(path)) < 0) {
|
|
725
|
+
dir_printf(out, "Permission denied: '%s'\n", arg);
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (remove(path) != 0) {
|
|
730
|
+
dir_printf(out, "Cannot remove '%s': %s\n", arg, strerror(errno));
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
dir_printf(out, "Removed '%s'\n", arg);
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (strncmp(line, "/ls", 3) == 0 && (line[3] == '\0' || line[3] == ' ')) {
|
|
739
|
+
const char* arg = line + 3;
|
|
740
|
+
while (*arg == ' ') arg++;
|
|
741
|
+
|
|
742
|
+
/* Default to root "/" if no arg */
|
|
743
|
+
char dirname[PATH_MAX];
|
|
744
|
+
if (arg[0] == '\0') {
|
|
745
|
+
snprintf(dirname, sizeof(dirname), "/");
|
|
746
|
+
} else if (arg[0] != '/') {
|
|
747
|
+
snprintf(dirname, sizeof(dirname), "/%s", arg);
|
|
748
|
+
} else {
|
|
749
|
+
snprintf(dirname, sizeof(dirname), "%s", arg);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
char path[PATH_MAX];
|
|
753
|
+
if (mik__resolve_fs_root(repl_ctx, dirname, path, sizeof(path)) < 0) {
|
|
754
|
+
dir_printf(out, "Permission denied: '%s'\n", arg[0] ? arg : "/");
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
DIR* dir = opendir(path);
|
|
759
|
+
if (!dir) {
|
|
760
|
+
dir_printf(out, "Cannot open '%s': %s\n", arg[0] ? arg : "/", strerror(errno));
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
const char* trailing = (dirname[strlen(dirname) - 1] == '/') ? "" : "/";
|
|
764
|
+
struct dirent* entry;
|
|
765
|
+
while ((entry = readdir(dir)) != NULL) {
|
|
766
|
+
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
|
|
767
|
+
char child_path[PATH_MAX];
|
|
768
|
+
snprintf(child_path, sizeof(child_path), "%s/%s", path, entry->d_name);
|
|
769
|
+
DIR* child = opendir(child_path);
|
|
770
|
+
if (child) {
|
|
771
|
+
closedir(child);
|
|
772
|
+
dir_printf(out, " %s%s%s/\n", dirname, trailing, entry->d_name);
|
|
773
|
+
} else {
|
|
774
|
+
dir_printf(out, " %s%s%s\n", dirname, trailing, entry->d_name);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
closedir(dir);
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (strcmp(line, "/exit") == 0) {
|
|
782
|
+
repl_active = false;
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
dir_printf(out, "Unknown command: %s (try /help)\n", line);
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/* ── Public API ──────────────────────────────────────────────────── */
|
|
791
|
+
|
|
792
|
+
bool MIK_IsReplActive(void) {
|
|
793
|
+
return repl_active;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
bool mik__repl_is_evaluating(void) {
|
|
797
|
+
return repl_evaluating;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
bool mik__repl_is_paused(void) {
|
|
801
|
+
return repl_paused;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
void mik__repl_set_paused(bool paused) {
|
|
805
|
+
repl_paused = paused;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/* ── Protocol completions helper ─────────────────────────────────── */
|
|
809
|
+
|
|
810
|
+
/* Build CBOR completion response from a partial expression.
|
|
811
|
+
* Uses the same property-walking logic as completion_callback. */
|
|
812
|
+
static std::vector<uint8_t> proto_complete(JSContext* ctx, const char* partial, size_t len) {
|
|
813
|
+
CompletionBuf cb{};
|
|
814
|
+
const char* argv[1] = {partial};
|
|
815
|
+
completion_callback(cb, 1, argv);
|
|
816
|
+
|
|
817
|
+
/* Two-pass CBOR encode: {"prefix": tstr, "items": [tstr, ...]} */
|
|
818
|
+
nanocbor_encoder_t enc;
|
|
819
|
+
nanocbor_encoder_init(&enc, nullptr, 0);
|
|
820
|
+
nanocbor_fmt_map(&enc, 2);
|
|
821
|
+
nanocbor_put_tstr(&enc, "prefix");
|
|
822
|
+
nanocbor_put_tstrn(&enc, partial, len);
|
|
823
|
+
nanocbor_put_tstr(&enc, "items");
|
|
824
|
+
nanocbor_fmt_array(&enc, cb.count);
|
|
825
|
+
for (int i = 0; i < cb.count; i++) {
|
|
826
|
+
nanocbor_put_tstr(&enc, cb.bufs[i]);
|
|
827
|
+
}
|
|
828
|
+
size_t needed = nanocbor_encoded_len(&enc);
|
|
829
|
+
|
|
830
|
+
std::vector<uint8_t> buf(needed);
|
|
831
|
+
nanocbor_encoder_init(&enc, buf.data(), needed);
|
|
832
|
+
nanocbor_fmt_map(&enc, 2);
|
|
833
|
+
nanocbor_put_tstr(&enc, "prefix");
|
|
834
|
+
nanocbor_put_tstrn(&enc, partial, len);
|
|
835
|
+
nanocbor_put_tstr(&enc, "items");
|
|
836
|
+
nanocbor_fmt_array(&enc, cb.count);
|
|
837
|
+
for (int i = 0; i < cb.count; i++) {
|
|
838
|
+
nanocbor_put_tstr(&enc, cb.bufs[i]);
|
|
839
|
+
}
|
|
840
|
+
return buf;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/* ── Protocol-mode REPL ──────────────────────────────────────────── */
|
|
844
|
+
|
|
845
|
+
void MIK_ProtocolOpen(MIKReplTransport* transport) {
|
|
846
|
+
if (repl_active) return;
|
|
847
|
+
|
|
848
|
+
repl_transport = transport;
|
|
849
|
+
repl_protocol_mode = true;
|
|
850
|
+
repl_paused = false;
|
|
851
|
+
repl_active = true;
|
|
852
|
+
show_depth = 2;
|
|
853
|
+
show_hidden = false;
|
|
854
|
+
show_time = false;
|
|
855
|
+
|
|
856
|
+
/* Build MSG_READY with CBOR device info: {"chip": tstr, "id": tstr, "v": tstr}.
|
|
857
|
+
* Cached here and only emitted in response to a client CMD_HELLO so a
|
|
858
|
+
* passive serial viewer (e.g. idf.py monitor) doesn't see the device
|
|
859
|
+
* spamming its identity unprompted. */
|
|
860
|
+
{
|
|
861
|
+
const char* chip = transport->chip_name ? transport->chip_name : "unknown";
|
|
862
|
+
const char* id = MIK_GetPlatform()->get_device_id();
|
|
863
|
+
if (!id) id = "";
|
|
864
|
+
const char* version =
|
|
865
|
+
#ifdef MIK_FW_VERSION
|
|
866
|
+
MIK_FW_VERSION;
|
|
867
|
+
#else
|
|
868
|
+
"0.0.0-dev";
|
|
869
|
+
#endif
|
|
870
|
+
|
|
871
|
+
nanocbor_encoder_t enc;
|
|
872
|
+
nanocbor_encoder_init(&enc, nullptr, 0);
|
|
873
|
+
nanocbor_fmt_map(&enc, 3);
|
|
874
|
+
nanocbor_put_tstr(&enc, "chip");
|
|
875
|
+
nanocbor_put_tstr(&enc, chip);
|
|
876
|
+
nanocbor_put_tstr(&enc, "id");
|
|
877
|
+
nanocbor_put_tstr(&enc, id);
|
|
878
|
+
nanocbor_put_tstr(&enc, "v");
|
|
879
|
+
nanocbor_put_tstr(&enc, version);
|
|
880
|
+
ready_len = nanocbor_encoded_len(&enc);
|
|
881
|
+
|
|
882
|
+
nanocbor_encoder_init(&enc, ready_buf, sizeof(ready_buf));
|
|
883
|
+
nanocbor_fmt_map(&enc, 3);
|
|
884
|
+
nanocbor_put_tstr(&enc, "chip");
|
|
885
|
+
nanocbor_put_tstr(&enc, chip);
|
|
886
|
+
nanocbor_put_tstr(&enc, "id");
|
|
887
|
+
nanocbor_put_tstr(&enc, id);
|
|
888
|
+
nanocbor_put_tstr(&enc, "v");
|
|
889
|
+
nanocbor_put_tstr(&enc, version);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
void MIK_ProtocolAttach(MIKRuntime* mik_rt) {
|
|
894
|
+
if (!mik_rt) return;
|
|
895
|
+
repl_ctx = MIK_GetJSContext(mik_rt);
|
|
896
|
+
repl_mik_rt = mik_rt;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
void MIK_ProtocolDetach(void) {
|
|
900
|
+
repl_ctx = nullptr;
|
|
901
|
+
repl_mik_rt = nullptr;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
void MIK_ProtocolExit(void) { s_exit_serve_loop = true; }
|
|
905
|
+
|
|
906
|
+
void MIK_ProtocolClose(void) {
|
|
907
|
+
if (repl_transport && repl_transport->session_end) {
|
|
908
|
+
repl_transport->session_end(repl_transport->session_end_ctx);
|
|
909
|
+
}
|
|
910
|
+
repl_active = false;
|
|
911
|
+
repl_protocol_mode = false;
|
|
912
|
+
repl_paused = false;
|
|
913
|
+
repl_transport = nullptr;
|
|
914
|
+
repl_ctx = nullptr;
|
|
915
|
+
repl_mik_rt = nullptr;
|
|
916
|
+
s_exit_serve_loop = false;
|
|
917
|
+
MIK_GetPlatform()->log(MIK_LOG_INFO, TAG, "Protocol REPL exited");
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
void MIK_ProtocolServeLoop(void) {
|
|
921
|
+
if (!repl_active || !repl_transport) return;
|
|
922
|
+
|
|
923
|
+
MIKReplTransport* transport = repl_transport;
|
|
924
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
925
|
+
s_exit_serve_loop = false;
|
|
926
|
+
|
|
927
|
+
while (repl_active && !s_exit_serve_loop) {
|
|
928
|
+
uint8_t cmd_type;
|
|
929
|
+
uint32_t payload_len;
|
|
930
|
+
|
|
931
|
+
/* Read the next command header. mik__proto_read_exact pumps the
|
|
932
|
+
* runtime event loop while waiting, and bails on MIK_ProtocolExit
|
|
933
|
+
* so a supervisor ending the per-test serve loop (via
|
|
934
|
+
* __testFileDone) returns promptly instead of waiting for a CLI
|
|
935
|
+
* command the CLI isn't going to send. */
|
|
936
|
+
if (!proto_read_header(transport, &cmd_type, &payload_len)) {
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
JSContext* ctx = repl_ctx;
|
|
941
|
+
|
|
942
|
+
/* REPL commands: read full payload (small), handle internally */
|
|
943
|
+
if (cmd_type >= MIK_CMD_EVAL && cmd_type <= MIK_CMD_HELLO) {
|
|
944
|
+
std::string payload;
|
|
945
|
+
if (!proto_read_payload(transport, payload_len, payload)) {
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
switch (cmd_type) {
|
|
950
|
+
case MIK_CMD_EVAL: {
|
|
951
|
+
/* Rewrite const/let -> var, wrap object literals */
|
|
952
|
+
std::string code = repl_rewrite_const_let(payload.c_str());
|
|
953
|
+
code = repl_maybe_wrap_object(code);
|
|
954
|
+
|
|
955
|
+
int64_t t0 = 0;
|
|
956
|
+
if (show_time) {
|
|
957
|
+
t0 = platform->get_boot_us();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
repl_evaluating = true;
|
|
961
|
+
repl_async_skipped = false;
|
|
962
|
+
JSValue result = repl_eval_and_pump(ctx, code.c_str(), code.size());
|
|
963
|
+
repl_evaluating = false;
|
|
964
|
+
|
|
965
|
+
if (repl_async_skipped) {
|
|
966
|
+
static const char info[] = "Promise detached (app is paused)";
|
|
967
|
+
mik__proto_send(transport, MIK_MSG_INFO, info, sizeof(info) - 1);
|
|
968
|
+
repl_async_skipped = false;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (JS_IsException(result)) {
|
|
972
|
+
JSValue exc = JS_GetException(ctx);
|
|
973
|
+
|
|
974
|
+
/* Format the error */
|
|
975
|
+
std::string msg = "Uncaught ";
|
|
976
|
+
if (JS_IsObject(exc)) {
|
|
977
|
+
JSValue name_val = JS_GetPropertyStr(ctx, exc, "name");
|
|
978
|
+
JSValue msg_val = JS_GetPropertyStr(ctx, exc, "message");
|
|
979
|
+
const char* name = JS_ToCString(ctx, name_val);
|
|
980
|
+
const char* emsg = JS_ToCString(ctx, msg_val);
|
|
981
|
+
|
|
982
|
+
if (name && name[0]) {
|
|
983
|
+
msg += name;
|
|
984
|
+
if (emsg && emsg[0]) {
|
|
985
|
+
msg += ": ";
|
|
986
|
+
msg += emsg;
|
|
987
|
+
}
|
|
988
|
+
} else if (emsg && emsg[0]) {
|
|
989
|
+
msg += emsg;
|
|
990
|
+
} else {
|
|
991
|
+
msg += mik_inspect(ctx, exc);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (name) JS_FreeCString(ctx, name);
|
|
995
|
+
if (emsg) JS_FreeCString(ctx, emsg);
|
|
996
|
+
JS_FreeValue(ctx, name_val);
|
|
997
|
+
JS_FreeValue(ctx, msg_val);
|
|
998
|
+
|
|
999
|
+
JSValue stack_val = JS_GetPropertyStr(ctx, exc, "stack");
|
|
1000
|
+
if (JS_IsString(stack_val)) {
|
|
1001
|
+
const char* stack = JS_ToCString(ctx, stack_val);
|
|
1002
|
+
if (stack && stack[0]) {
|
|
1003
|
+
msg += "\n";
|
|
1004
|
+
msg += stack;
|
|
1005
|
+
}
|
|
1006
|
+
if (stack) JS_FreeCString(ctx, stack);
|
|
1007
|
+
}
|
|
1008
|
+
JS_FreeValue(ctx, stack_val);
|
|
1009
|
+
} else {
|
|
1010
|
+
msg += mik_inspect(ctx, exc);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
JS_FreeValue(ctx, exc);
|
|
1014
|
+
|
|
1015
|
+
/* Send timing before error so CLI can attach it to the input echo */
|
|
1016
|
+
if (show_time) {
|
|
1017
|
+
int64_t elapsed = platform->get_boot_us() - t0;
|
|
1018
|
+
char timing[64];
|
|
1019
|
+
int tlen =
|
|
1020
|
+
snprintf(timing, sizeof(timing), "%.1fms", (double)elapsed / 1000.0);
|
|
1021
|
+
mik__proto_send(transport, MIK_MSG_PROMPT, timing, tlen);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
mik__proto_send(transport, MIK_MSG_EVAL_ERROR, msg.c_str(), msg.size());
|
|
1025
|
+
} else if (!JS_IsUndefined(result)) {
|
|
1026
|
+
std::string inspected = mik_inspect(ctx, result, show_depth, false, show_hidden);
|
|
1027
|
+
|
|
1028
|
+
/* Set globalThis._ = result */
|
|
1029
|
+
JSValue global = JS_GetGlobalObject(ctx);
|
|
1030
|
+
JS_SetPropertyStr(ctx, global, "_", JS_DupValue(ctx, result));
|
|
1031
|
+
JS_FreeValue(ctx, global);
|
|
1032
|
+
|
|
1033
|
+
/* Send timing before result so CLI can attach it to the input echo */
|
|
1034
|
+
if (show_time) {
|
|
1035
|
+
int64_t elapsed = platform->get_boot_us() - t0;
|
|
1036
|
+
char timing[64];
|
|
1037
|
+
int tlen =
|
|
1038
|
+
snprintf(timing, sizeof(timing), "%.1fms", (double)elapsed / 1000.0);
|
|
1039
|
+
mik__proto_send(transport, MIK_MSG_PROMPT, timing, tlen);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
mik__proto_send(transport, MIK_MSG_RESULT, inspected.c_str(),
|
|
1043
|
+
inspected.size());
|
|
1044
|
+
} else {
|
|
1045
|
+
/* Send timing before empty result */
|
|
1046
|
+
if (show_time) {
|
|
1047
|
+
int64_t elapsed = platform->get_boot_us() - t0;
|
|
1048
|
+
char timing[64];
|
|
1049
|
+
int tlen =
|
|
1050
|
+
snprintf(timing, sizeof(timing), "%.1fms", (double)elapsed / 1000.0);
|
|
1051
|
+
mik__proto_send(transport, MIK_MSG_PROMPT, timing, tlen);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/* undefined result — send empty result so CLI knows eval finished */
|
|
1055
|
+
mik__proto_send(transport, MIK_MSG_RESULT, nullptr, 0);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
JS_FreeValue(ctx, result);
|
|
1059
|
+
|
|
1060
|
+
/* Run GC and report reclaimed bytes */
|
|
1061
|
+
JSMemoryUsage gc_before;
|
|
1062
|
+
JS_ComputeMemoryUsage(JS_GetRuntime(ctx), &gc_before);
|
|
1063
|
+
JS_RunGC(JS_GetRuntime(ctx));
|
|
1064
|
+
JSMemoryUsage gc_after;
|
|
1065
|
+
JS_ComputeMemoryUsage(JS_GetRuntime(ctx), &gc_after);
|
|
1066
|
+
long gc_freed = (long)gc_before.malloc_size - (long)gc_after.malloc_size;
|
|
1067
|
+
if (gc_freed > 0) {
|
|
1068
|
+
size_t free_mem = platform->get_free_system_mem();
|
|
1069
|
+
char gc_info[128];
|
|
1070
|
+
int gc_len = snprintf(gc_info, sizeof(gc_info),
|
|
1071
|
+
"GC reclaimed %ld bytes (%lu bytes free)", gc_freed,
|
|
1072
|
+
(unsigned long)free_mem);
|
|
1073
|
+
mik__proto_send(transport, MIK_MSG_INFO, gc_info, gc_len);
|
|
1074
|
+
}
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
case MIK_CMD_COMPLETE: {
|
|
1079
|
+
auto cbor = proto_complete(ctx, payload.c_str(), payload.size());
|
|
1080
|
+
mik__proto_send(transport, MIK_MSG_COMPLETIONS, cbor.data(), cbor.size());
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
case MIK_CMD_DIRECTIVE: {
|
|
1085
|
+
std::string out;
|
|
1086
|
+
handle_directive_impl(ctx, payload.c_str(), out);
|
|
1087
|
+
mik__proto_send(transport, MIK_MSG_INFO, out.c_str(), out.size());
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
case MIK_CMD_EXIT:
|
|
1092
|
+
repl_active = false;
|
|
1093
|
+
break;
|
|
1094
|
+
|
|
1095
|
+
case MIK_CMD_HELLO:
|
|
1096
|
+
/* Client-initiated handshake: reply with cached MSG_READY. */
|
|
1097
|
+
mik__proto_send(transport, MIK_MSG_READY, ready_buf, ready_len);
|
|
1098
|
+
break;
|
|
1099
|
+
}
|
|
1100
|
+
} else {
|
|
1101
|
+
/* Non-REPL command: forward to platform handler with unread payload.
|
|
1102
|
+
* The handler reads payload bytes directly from the transport. */
|
|
1103
|
+
if (transport->command_handler) {
|
|
1104
|
+
transport->command_handler(transport, cmd_type, payload_len,
|
|
1105
|
+
transport->command_handler_ctx);
|
|
1106
|
+
} else {
|
|
1107
|
+
/* No handler: drain the payload to stay in sync */
|
|
1108
|
+
mik__proto_drain(transport, payload_len);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/* Pump event loop between commands (skip timers/callbacks when paused,
|
|
1113
|
+
* but always run microtasks so promises can settle) */
|
|
1114
|
+
if (repl_mik_rt && !repl_paused) {
|
|
1115
|
+
MIK_Loop(repl_mik_rt);
|
|
1116
|
+
}
|
|
1117
|
+
if (ctx) {
|
|
1118
|
+
mik__execute_jobs(ctx);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|