From 9477f32b1092c977b3b854d2aa9dc8102f709e4f Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Tue, 22 Sep 2026 11:16:26 +0530 Subject: [PATCH 1/2] feat: update docs - add Advanced APIs pages for Executor and System - document when to use the foreground vs background executor (short- vs long-running commands) - add Plugin Context (ctx) page covering encrypted secrets and permission checks, warning that plugin.json permissions are not enforced by Acode yet - document the ctx and fileIcons init options in core-file, understanding-plugin and acode - fold the terminal page's Executor.execute section into the new Executor page - drop the unused permissions entry from the manifest example - add sidebar entries for the new pages --- .vitepress/config.mts | 12 + docs/advanced-apis/executor.md | 165 +++++++++++++ docs/advanced-apis/system.md | 237 +++++++++++++++++++ docs/advanced-apis/terminal.md | 22 +- docs/getting-started/understanding-plugin.md | 62 +++-- docs/global-apis/acode.md | 1 + docs/plugin-essentials/core-file.md | 66 ++++-- docs/plugin-essentials/plugin-context.md | 136 +++++++++++ 8 files changed, 639 insertions(+), 62 deletions(-) create mode 100644 docs/advanced-apis/executor.md create mode 100644 docs/advanced-apis/system.md create mode 100644 docs/plugin-essentials/plugin-context.md diff --git a/.vitepress/config.mts b/.vitepress/config.mts index ec0cd4a..8a40a56 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -96,6 +96,10 @@ export default defineConfig({ text: "Core File", link: "/docs/plugin-essentials/core-file", }, + { + text: "Plugin Context (ctx)", + link: "/docs/plugin-essentials/plugin-context", + }, ], }, { @@ -328,6 +332,14 @@ export default defineConfig({ text: "Terminal", link: "/docs/advanced-apis/terminal", }, + { + text: "Executor", + link: "/docs/advanced-apis/executor", + }, + { + text: "System", + link: "/docs/advanced-apis/system", + }, { text: "LSP", link: "/docs/advanced-apis/lsp", diff --git a/docs/advanced-apis/executor.md b/docs/advanced-apis/executor.md new file mode 100644 index 0000000..c980ca2 --- /dev/null +++ b/docs/advanced-apis/executor.md @@ -0,0 +1,165 @@ +# Executor + +The `Executor` API lets you run shell commands on the device without opening a visual terminal session. It supports one-off commands, long-running processes with real-time streaming, stdin writes, and background execution via a foreground service. + +> [!Warning] +> Prefer visible terminals for transparency. Avoid hiding work in the background and do not start long‑running processes without good reason. For interactive or long‑lived tasks, use a [terminal session](./terminal.md) instead. + +## Access + +The global `Executor` is an `Executor` instance (clobbered to `window.Executor` by the terminal plugin). It has a built-in `BackgroundExecutor` instance for background-mode processes. + +```js +const Executor = globalThis.Executor; // Executor instance +const background = Executor.BackgroundExecutor; // BackgroundExecutor instance +``` + +Both instances share the same methods. + +> [!NOTE] +> **Which executor should I use?** +> +> - Use the **background executor** (`Executor.BackgroundExecutor`) for **short-running commands**. It starts processes directly with no foreground service and no notification, so it is lighter, but Android can kill the process once the app leaves the foreground. Use it for quick, self-contained commands that finish in seconds. +> - Use the **foreground executor** (`Executor`) for **long-running commands**. Its processes run under a foreground service with a persistent notification, which keeps them alive while the app is in the background. This is the default mode; `moveToForeground()` / `moveToBackground()` switch it at runtime. + +## One-off execution + +### `execute(command, alpine?)` + +- Purpose: Runs a single shell command and waits for it to finish. Output is returned after the process exits (no live streaming of output). +- Parameters: + - `command` (string): The command to run. + - `alpine` (boolean, optional): Run inside the Alpine sandbox when `true`; run in the Android environment when `false`. +- Returns: `Promise` that resolves with stdout on success, or rejects with an error/stderr on failure. + +```js +// Outputting hello on stdout +Executor.execute('echo hello') + .then(console.log) + .catch(console.error); +``` + +or with `async/await`: + +```js +const output = await Executor.execute('echo hello'); +console.log(output); +``` + +> [!Warning] +> Do not run things like an infinite loop or a shell because `execute()` waits for the process to exit and a shell never exits on its own, avoid running those commands with this function. + +## Long-running processes + +### `start(command, onData, alpine?)` + +- Starts a shell process and enables real-time streaming of `stdout`, `stderr`, and `exit`. +- Parameters: + - `command` (string): The command to run (e.g. `"sh"`, `"ls -al"`). + - `onData` (function): `(type, data) => void`. `type` is `"stdout"`, `"stderr"`, or `"exit"` (the process exit code); `data` is the output line or exit code. + - `alpine` (boolean, optional): Run inside the Alpine sandbox when `true`. +- Returns: `Promise` resolving to a unique process UUID used by `write()`, `stop()`, and `isRunning()`. + +```js +const uuid = await Executor.start("sh", (type, data) => { + console.log(`[${type}] ${data}`); +}); +Executor.write(uuid, "echo Hello World\r"); +Executor.stop(uuid); +``` + +### `write(uuid, input)` + +Sends input to a running process's stdin. + +- Returns: `Promise`. + +```js +await Executor.write(uuid, "ls /sdcard\r"); +``` + +### `stop(uuid)` + +Terminates a running process. + +- Returns: `Promise`. + +### `isRunning(uuid)` + +Checks whether a process is still running. + +- Returns: `Promise`. + +```js +if (await Executor.isRunning(uuid)) { + await Executor.stop(uuid); +} +``` + +### `spawnStream(cmd, callback, onError?)` + +Spawns a process and exposes it as a raw WebSocket stream. Once the process is ready the callback is invoked with the connected `WebSocket`; use `ws.send()` to write to stdin and `ws.onmessage` to read stdout. + +- Parameters: + - `cmd` (string[]): Command and arguments (e.g. `["sh", "-c", "echo hi"]`). + - `callback` (function): `(ws) => void`. + - `onError` (function, optional): error handler. + +## Managing processes + +### `listProcesses()` + +Lists the processes currently managed by this Executor. + +- Returns: `Promise>`. `background` is `true` for a `BackgroundExecutor`. + +### `listAllProcesses()` + +Lists all running OS processes under the app's user id. + +- Returns: `Promise>`. + +### `killProcess(pid)` + +Forcefully kills a process by its native PID. + +- Returns: `Promise`. + +## Service control + +### `moveToForeground()` / `moveToBackground()` + +Moves the Executor service between foreground (shows the notification) and background. + +- Returns: `Promise`. + +### `stopService()` + +Stops the Executor service completely. This does **not** guarantee that all running processes are killed - the service just stops being active. The processes will keep running until stopped. + +- Returns: `Promise`. + +## Advanced + +### `loadLibrary(path)` + +Loads a native library from the given path. + +- Returns: `Promise`. + +```js +await Executor.loadLibrary('/path/to/library.so'); +``` + +> [!Warning] +> `loadLibrary()` has been deprecated and is no longer supported on newer Acode versions. + +### `setProotDebug(enabled)` + +Toggles proot debug output (used for the Alpine sandbox). + +- Returns: `Promise`. + +## Related APIs + +- Visual terminal sessions: [Terminal](./terminal.md) diff --git a/docs/advanced-apis/system.md b/docs/advanced-apis/system.md new file mode 100644 index 0000000..923542c --- /dev/null +++ b/docs/advanced-apis/system.md @@ -0,0 +1,237 @@ +# System + +The `system` module wraps Acode's native Android bridge (`cordova-plugin-system`). It is clobbered to `window.system` and provides low-level device, file, storage, permission, intent, and shortcut utilities that Acode itself uses. + +```js +const system = window.system; +``` + +Most methods are callback-based (`(success, error) => void`). Wrap them with `helpers.promisify` when you prefer promises: + +```js +const helpers = acode.require("helpers"); +const filesDir = await helpers.promisify(system.getFilesDir); +``` + +## Files + +### `getFilesDir(success, error)` + +Resolves the app's internal files directory path. + +```js +const filesDir = await helpers.promisify(system.getFilesDir); +``` + +### `getParentPath(path, success, error)` + +Resolves the parent directory of `path`. + +### `listChildren(path, success, error)` + +Lists the children of a directory path. + +### `mkdirs(path, success, error)` + +Recursively creates directories. + +### `fileExists(path, countSymlinks, success, error)` + +Checks whether a file exists. `countSymlinks` is a boolean passed as a string. + +### `copyToUri(srcUri, destUri, fileName, success, error)` + +Copies a file to a destination uri under `fileName`. + +### `writeText(path, content, success, error)` + +Writes text content to a file path. + +### `deleteFile(path, success, error)` + +Deletes a file path. + +### `createSymlink(target, linkPath, success, error)` + +Creates a symlink at `linkPath` pointing to `target`. + +### `setExec(path, executable, success, error)` + +Marks a file path as executable (`executable` is a boolean passed as a string). + +### `extractAsset(assetName, destinationPath, success, error)` + +Extracts an app asset to a destination path. + +### `getNativeLibraryPath(success, error)` + +Resolves the directory where native libraries are stored. + +## Storage management + +### `isManageExternalStorageDeclared(success, error)` + +Checks whether the app declares all-files access in its manifest. + +### `hasGrantedStorageManager(success, error)` + +Checks whether the app has been granted "All files access". + +### `requestStorageManager(success, error)` + +Requests the "All files access" permission. + +### `manageAllFiles(success, error)` + +Opens the system screen to grant all-files access. + +### `isExternalStorageManager(success, error)` + +Checks whether the app is currently an external storage manager. + +## Permissions + +### `hasPermission(permission, success, error)` + +Checks whether a runtime permission is granted. + +### `requestPermission(permission, success, error)` + +Requests a single runtime permission. + +### `requestPermissions(permissions, success, error)` + +Requests multiple runtime permissions at once. + +## App & device info + +### `getAppInfo(success, error)` + +Resolves information about the Acode app. + +### `getInstaller(success, error)` + +Resolves the package that installed the app (used for `window.appInstallSource`). + +### `getAndroidVersion(success, error)` + +Resolves the Android OS version. + +### `getArch(success, error)` + +Resolves the device architecture (e.g. `arm64-v8a`). + +### `getWebviewInfo(success, error)` + +Resolves WebView information (used by the terminal's engine detection). + +### `isPowerSaveMode(success, error)` + +Checks whether the device is in power-save mode. + +### `getGlobalSetting(key, success, error)` + +Reads a global Android setting by key. + +### `clearCache(success, error)` + +Clears the app's cache. + +## File actions & sharing + +### `fileAction(fileUri, filename, action, mimeType, error?)` + +Launches an Android intent for a file. `action` is one of `VIEW`, `EDIT`, `SEND`, or `RUN` (the app prepends `android.intent.action.`). Arguments are flexible: `system.fileAction(uri, filename, action, mimeType, onFail)`. + +```js +system.fileAction(fileUri, filename, "VIEW", "text/plain"); +``` + +### `shareText(text, success, error)` + +Shares a text string through the system share sheet. + +### `openInBrowser(src)` + +Opens a url in the system browser. + +### `inAppBrowser(url, title, showButtons, disableCache)` + +Opens a url in Acode's in-app browser. Returns an object with `onOpenExternalBrowser` and `onError` callbacks that can be assigned: + +```js +const browser = system.inAppBrowser(url, title, true, false); +browser.onOpenExternalBrowser = (url) => console.log("opened externally", url); +``` + +### `launchApp(app, className, extras?, success?, error?)` + +Launches an Android activity by package and class name, optionally passing intent extras (string/number/boolean values). + +```js +system.launchApp( + "com.example.app", + "com.example.app.MainActivity", + { user: "example", premium: true }, + (msg) => console.log(msg), + (err) => console.error(err), +); +``` + +## Shortcuts + +### `addShortcut(shortcut, success, error)` + +Adds a home-screen shortcut. `shortcut` is `{ id, label, description, icon, action, data }`. + +### `removeShortcut(id, success, error)` + +Removes a shortcut by id. + +### `pinShortcut(id, success, error)` + +Pins a shortcut. + +### `pinFileShortcut(shortcut, success, error)` + +Pins a file shortcut. + +## Intents + +### `getCordovaIntent(success, error)` + +Resolves the intent that launched the app (for handling external open requests). + +### `setIntentHandler(handler, onerror)` + +Registers a handler for intents received while the app is running. `handler` receives the intent data. + +## Text comparison + +Used by the editor's dirty-tracking and file-change detection. Both methods compare in a background thread. + +### `compareFileText(fileUri, encoding, currentText): Promise` + +Reads the file at `fileUri` and compares it to `currentText`. Resolves `true` when the content **differs**, `false` when it matches. + +### `compareTexts(text1, text2): Promise` + +Compares two strings. Resolves `true` when they **differ**, `false` when equal. + +```js +const changed = await system.compareFileText(file.uri, file.encoding, text); +``` + +## UI + +### `setUiTheme(systemBarColor, theme, success?, error?)` + +Sets the Android system bar colors to match a theme. `systemBarColor` is a hex color; `theme` is the theme id. A pure white color is mapped to `#fffffe` so status bar icons stay visible. + +### `setInputType(type, success, error)` + +Changes the soft-keyboard input type. + +### `setNativeContextMenuDisabled(disabled, success, error)` + +Enables or disables the native context menu on the WebView. diff --git a/docs/advanced-apis/terminal.md b/docs/advanced-apis/terminal.md index 462f511..f99a8c9 100644 --- a/docs/advanced-apis/terminal.md +++ b/docs/advanced-apis/terminal.md @@ -155,27 +155,9 @@ This is useful when a plugin needs to decide whether it can use terminal-backed ## Background Execution (No Terminal) -Use the globally available `Executor` when you need to run a one‑off shell command without opening a visual terminal session. +Use the globally available `Executor` to run shell commands without opening a visual terminal session - one-off commands, long-running processes with streaming output, and background-mode execution. -> [!Warning] -> Prefer visible terminals for transparency. Avoid hiding work in the background and do not start long‑running processes via `Executor.execute`. For interactive or long‑lived tasks, use a terminal session instead. - -### `Executor.execute(command, alpine?)` - -- Purpose: Runs a single shell command and waits for it to finish. Output is returned after the process exits (no live streaming of output). -- Parameters: - - `command` (string): The command to run. - - `alpine` (boolean, optional): Run inside the Alpine sandbox when `true`; run in the Android environment when `false`. -- Returns: `Promise` that resolves with stdout on success, or rejects with an error/stderr on failure. - -#### Example - -```js -// Quick directory listing without opening a terminal UI -Executor.execute('ls -l') - .then(console.log) - .catch(console.error); -``` +See [Executor](./executor.md). ## Example: Themed Output Terminal diff --git a/docs/getting-started/understanding-plugin.md b/docs/getting-started/understanding-plugin.md index f2615d9..15e3396 100644 --- a/docs/getting-started/understanding-plugin.md +++ b/docs/getting-started/understanding-plugin.md @@ -27,11 +27,11 @@ If you skip `setPluginInit`, your script may load, but your plugin logic will no ## What You Get In `init` -Your init function receives: +The `init` callback registered with `setPluginInit` receives three arguments: -- `baseUrl`: internal base URL to your plugin files +- `baseUrl`: internal base URL to your plugin files (normalize it with a trailing slash, see below) - `$page`: a plugin page object for UI screens -- `cache`: object with: +- `options`: object with: - `cacheFileUrl` - `cacheFile` - `firstInit` @@ -40,37 +40,53 @@ Your init function receives: Use `firstInit` for one-time setup or migration. +`ctx` is your plugin's native-backed context: encrypted secret storage and permission checks. See [Plugin Context (`ctx`)](../plugin-essentials/plugin-context.md). + ## Recommended `main.js` Shape +The official templates structure your plugin as an `AcodePlugin` class with `init()` and `destroy()`: + ```js import plugin from "../plugin.json"; -function init(baseUrl, $page, cache) { - const commands = acode.require("commands"); - - commands.addCommand({ - name: "example.open", - description: "Open Example Panel", - exec: () => { - $page.innerHTML = "

Example Plugin

"; - $page.show(); - }, - }); -} +class AcodePlugin { + baseUrl = ""; -function unmount() { - const commands = acode.require("commands"); - commands.removeCommand("example.open"); + async init(_page, _cacheFile, _cacheFileUrl, _firstInit, _ctx, _fileIcons) { + // plugin code + } + + async destroy() { + // plugin clean up + } } -acode.setPluginInit(plugin.id, init); -acode.setPluginUnmount(plugin.id, unmount); +if (window.acode) { + const acodePlugin = new AcodePlugin(); + + acode.setPluginInit(plugin.id, async (baseUrl, $page, { cacheFileUrl, cacheFile, firstInit, ctx, fileIcons }) => { + acodePlugin.baseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; + await acodePlugin.init($page, cacheFile, cacheFileUrl, firstInit, ctx, fileIcons); + }); + + acode.setPluginUnmount(plugin.id, () => { + acodePlugin.destroy(); + }); +} ``` +Breaking that down: + +- `window.acode` is only present once Acode's API is ready, so registration is wrapped in a guard. +- `plugin.id` comes from your `plugin.json`, so the registration always matches the installed id. +- The `init` callback receives `(baseUrl, $page, options)`, where `options` is `{ cacheFileUrl, cacheFile, firstInit, ctx, fileIcons }`. Those are forwarded to your class's `init`. `fileIcons` is also available as `acode.require("fileIcons")` if you prefer to capture it in the main script — see [File Icons](../utilities/file-icons.md). +- `baseUrl` is stored with a guaranteed trailing slash so you can build file paths with `Url.join` or string concatenation. +- `destroy()` is wired to `setPluginUnmount` so it runs on disable/reload/uninstall. `init` is awaited, so heavy setup can be done inside it. + ## What Happens On Disable / Enable / Uninstall - Disable: - - Acode calls `acode.unmountPlugin(id)` which triggers your unmount. + - Acode calls `acode.unmountPlugin(id)` which triggers your registered unmount (your class's `destroy()`). - Plugin runtime state is cleared (including plugin cache file). - Enable: - Acode loads the plugin again and runs init again. @@ -78,7 +94,7 @@ acode.setPluginUnmount(plugin.id, unmount); - Plugin files are removed. - Acode runs unmount cleanup for loaded resources. -Treat `init` as repeatable and `unmount` as mandatory cleanup. +Treat `init` as repeatable and `destroy` as mandatory cleanup. ## Failure Behavior You Should Know @@ -97,5 +113,5 @@ acode.clearBrokenPluginMark("com.example.plugin"); - Keep `init` fast; do heavy work lazily. - Register commands through `acode.require("commands")`. -- Always remove listeners, commands, intervals, and UI hooks in `unmount`. +- Always remove listeners, commands, intervals, and UI hooks in `destroy`. - Avoid storing important state only in memory; use cache/settings when needed. diff --git a/docs/global-apis/acode.md b/docs/global-apis/acode.md index 0711bce..7f789f6 100644 --- a/docs/global-apis/acode.md +++ b/docs/global-apis/acode.md @@ -43,6 +43,7 @@ When the init function is called, it will receive 3 parameters: * `cacheFile File: object` File object of the cached file. Using this object, you can write/read the file. * `firstInit: boolean` If this is the first time the plugin is loaded, this value will be true. Otherwise, it will be `false`. + * `ctx: PluginContext` Your plugin's native context: encrypted secret storage and permission checks. See [Plugin Context (`ctx`)](../plugin-essentials/plugin-context.md). * `fileIcons` Plugin-bound [File Icons](../utilities/file-icons.md) API. Same instance as `acode.require("fileIcons")` captured in the main script. Available from **versionCode `1012`**. ### `Settings Object` diff --git a/docs/plugin-essentials/core-file.md b/docs/plugin-essentials/core-file.md index dd25062..fb7a6d3 100644 --- a/docs/plugin-essentials/core-file.md +++ b/docs/plugin-essentials/core-file.md @@ -24,7 +24,7 @@ To register your plugin, utilize the `acode.setPluginInit(pluginId: string, init 2. **init function:** - The function to be executed when the plugin is loaded. -Upon execution, the `init` function will receive three parameters: +Upon execution, the `init` function will receive three arguments: - **baseUrl (string):** - The base URL of the plugin, allowing access to files within the plugin directory. @@ -38,38 +38,66 @@ Upon execution, the `init` function will receive three parameters: - URL of the cached file. - **cacheFile (File):** - File object of the cached file, enabling file read/write operations. + - **firstInit (boolean):** + - `true` when the plugin is being installed/loaded for the first time. + - **ctx (PluginContext):** + - Your plugin's native context. Provides encrypted secret storage (`getSecret`, `setSecret`, `deleteSecret`, `clearAllSecrets`) and permission checks (`grantedPermission`, `listAllPermissions`). See [Plugin Context (`ctx`)](./plugin-context.md). - **fileIcons:** - Plugin-bound [File Icons](../utilities/file-icons.md) API (`register`, `icon`, `onChange`). Same instance as `acode.require("fileIcons")` captured in the main script. Available from **versionCode `1012`**. ### Example main.js File -Here's an illustrative example of a `main.js` file: +The official templates structure the plugin as an `AcodePlugin` class. Here is an illustrative example of a `main.js` file: ```javascript -acode.setPluginInit('com.example.plugin', (baseUrl, $page, cache) => { - const commands = acode.require("commands"); - commands.addCommand({ - name: 'example-plugin', - bindKey: { win: 'Ctrl-Alt-E', mac: 'Command-Alt-E' }, - exec: () => { - $page.innerHTML = ` -

Example Plugin

-

This is an example plugin.

- `; - $page.show(); - }, - }); -}); +import plugin from "../plugin.json"; + +class AcodePlugin { + baseUrl = ""; + + async init($page, cacheFile, cacheFileUrl, firstInit, ctx, fileIcons) { + const commands = acode.require("commands"); + commands.addCommand({ + name: "example-plugin", + bindKey: { win: "Ctrl-Alt-E", mac: "Command-Alt-E" }, + exec: () => { + $page.innerHTML = ` +

Example Plugin

+

This is an example plugin.

+ `; + $page.show(); + }, + }); + } + + async destroy() { + const commands = acode.require("commands"); + commands.removeCommand("example-plugin"); + } +} + +if (window.acode) { + const acodePlugin = new AcodePlugin(); + + acode.setPluginInit(plugin.id, async (baseUrl, $page, { cacheFileUrl, cacheFile, firstInit, ctx, fileIcons }) => { + acodePlugin.baseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; + await acodePlugin.init($page, cacheFile, cacheFileUrl, firstInit, ctx, fileIcons); + }); + + acode.setPluginUnmount(plugin.id, () => { + acodePlugin.destroy(); + }); +} ``` ## Plugin Unmount Function -The `main.js` file must also define an unmount function, which is called when the plugin is unloaded or uninstalled. This function allows you to perform cleanup operations associated with your plugin. +The `main.js` file must also define cleanup logic, which is called when the plugin is unloaded or uninstalled. This cleanup allows you to remove listeners, commands, intervals, and UI hooks associated with your plugin. In the class template this lives in the `destroy()` method, registered via `acode.setPluginUnmount`. ### Example Unmount Function ```javascript -acode.setPluginUnmount('com.example.plugin', () => { +acode.setPluginUnmount(plugin.id, () => { const commands = acode.require("commands"); commands.removeCommand('example-plugin'); }); @@ -82,5 +110,5 @@ For command registration APIs, see [Commands](../utilities/commands.md). ::: :::tip -You will not need to write this `unmount` or `initialize` functions for your plugin because templates comes with it , just you will need to write your plugin code inside the `AcodePlugin class` +You will not need to write these `init`/`destroy` registration functions for your plugin because the templates ship with them. You only need to write your plugin code inside the `AcodePlugin` class. ::: diff --git a/docs/plugin-essentials/plugin-context.md b/docs/plugin-essentials/plugin-context.md new file mode 100644 index 0000000..61c44d0 --- /dev/null +++ b/docs/plugin-essentials/plugin-context.md @@ -0,0 +1,136 @@ +# Plugin Context (`ctx`) + +The plugin context (`ctx`) is the third argument of the options object passed to your plugin's `init` function. It is a native-backed handle for your plugin that provides **encrypted secret storage** and **permission checks**. + +Your `init` function receives it as `options.ctx`: + +```js +function init(baseUrl, $page, options) { + const ctx = options.ctx; +} +``` + +## Overview + +`ctx` is a `PluginContext` instance (`src/lib/pluginContext.js`). It is created by Acode for **your plugin id only** and is backed by a cryptographically signed token issued by the native `Tee` plugin. Because of this: + +- Secrets are scoped to your plugin id - another plugin cannot read them. +- The token is bound to the permissions declared in your `plugin.json` at install/load time. +- The object is `Object.freeze`d, so its properties cannot be replaced or extended. + +### `created_at`, `uuid`, `toString()` + +- `created_at` - timestamp (ms) when the context was created. +- `uuid` - the opaque token string for this context. +- `ctx.toString()` - returns the `uuid` string. The object coerces to the uuid for string operations (numeric coercion returns `NaN`). + +```js +String(ctx) === ctx.uuid; // true +``` + +## Secrets + +Secrets are key/value strings stored in an **EncryptedPreferenceManager** on the native side (scoped to your plugin id). They survive app restarts. Use them for API tokens, oauth state, or other sensitive data - never store secrets in `localStorage`. + +### `getSecret(key, defaultValue = ""): Promise` + +Resolves the stored value for `key`, or `defaultValue` when the key has not been set. + +```js +const token = await ctx.getSecret("github_token", ""); +if (!token) { + await ctx.setSecret("github_token", "ghp_..."); +} +``` + +### `setSecret(key, value): Promise` + +Stores `value` for `key`. + +```js +await ctx.setSecret("access_token", "abc123"); +``` + +### `deleteSecret(key): Promise` + +Removes a single key. + +```js +await ctx.deleteSecret("access_token"); +``` + +### `clearAllSecrets(): Promise` + +Removes every secret stored for your plugin. + +```js +await ctx.clearAllSecrets(); +``` + +## Permissions + +::: warning Permissions are not enforced yet +Acode does **not** recognize or enforce the `permissions` array at this time. Declaring permission entries currently has **no effect** - no Acode capability is gated by them, and no consent dialog is shown to the user. + +The native context still records whatever you list, so `ctx.grantedPermission()` and `ctx.listAllPermissions()` return those values. They only echo the entries from your own `plugin.json`, not a grant that Acode has checked. Do not rely on `permissions` for security; treat it as reserved for future use. +::: + +Permissions are declared in your `plugin.json` as an array: + +```json +{ + "id": "com.example.plugin", + "main": "dist/main.js", + "permissions": ["read", "write"] +} +``` + +The list is bound to your context's token when the plugin loads. The native side records exactly the permissions listed; there is no runtime "request" dialog - a permission either is or is not present. + +### `grantedPermission(permission): Promise` + +Resolves `true` when your plugin was granted `permission`. + +```js +if (await ctx.grantedPermission("write")) { + // do something privileged +} +``` + +### `listAllPermissions(): Promise` + +Resolves the full list of permissions granted to your plugin. + +```js +const permissions = await ctx.listAllPermissions(); +``` + +## Full example + +```js +function init(baseUrl, $page, options) { + const ctx = options.ctx; + + (async () => { + console.log("permissions:", await ctx.listAllPermissions()); + + if (await ctx.grantedPermission("api-access")) { + const token = await ctx.getSecret("api_token"); + if (!token) { + await ctx.setSecret("api_token", prompt("Enter API token")); + } + } + })(); +} +``` + +## Notes + +- `ctx.invalidate()` exists but is used internally by Acode; plugins do not need to call it. +- If the trusted native session is not available (for example the token request fails), `ctx` may be `null` - guard against it if your plugin depends on it. +- Secrets are encrypted at rest and scoped per plugin id. + +## Related + +- [Manifest (`plugin.json`)](./manifest.md) +- [Core File](./core-file.md) - where `ctx` is passed to `init` From ea9e07d7cf78cc13c23438c3ac521d1487200add Mon Sep 17 00:00:00 2001 From: Rohit Kushvaha Date: Tue, 22 Sep 2026 11:23:15 +0530 Subject: [PATCH 2/2] Update docs/global-apis/acode.md Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- docs/global-apis/acode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/global-apis/acode.md b/docs/global-apis/acode.md index 7f789f6..04197c2 100644 --- a/docs/global-apis/acode.md +++ b/docs/global-apis/acode.md @@ -43,7 +43,7 @@ When the init function is called, it will receive 3 parameters: * `cacheFile File: object` File object of the cached file. Using this object, you can write/read the file. * `firstInit: boolean` If this is the first time the plugin is loaded, this value will be true. Otherwise, it will be `false`. - * `ctx: PluginContext` Your plugin's native context: encrypted secret storage and permission checks. See [Plugin Context (`ctx`)](../plugin-essentials/plugin-context.md). + * `ctx: PluginContext | null` Your plugin's native context: encrypted secret storage and permission checks. It may be `null` if the trusted native session is unavailable, so guard it before use. See [Plugin Context (`ctx`)](../plugin-essentials/plugin-context.md). * `fileIcons` Plugin-bound [File Icons](../utilities/file-icons.md) API. Same instance as `acode.require("fileIcons")` captured in the main script. Available from **versionCode `1012`**. ### `Settings Object`