Skip to content

OS Development Guide ​

Prerequisites ​

  • Node.js 18+
  • Motanamy CLI (npm install -g motanamy-cli)
  • A running Motanamy OS instance for testing

File Structure ​

Every OS plugin follows this structure:

my-plugin/
├── 1.0.0/                  ← version folder
│   ├── setting.json        ← plugin manifest
│   ├── script.js           ← registration script (runs on load)
│   ├── index.vue           ← main app view (root route)
│   ├── setting.vue         ← settings page (optional)
│   ├── README-AR.md        ← Arabic documentation
│   ├── README-EN.md        ← English documentation
│   └── images/             ← plugin images/icons
└── database/
    └── my-plugin/
        └── data.json       ← initial data

OS uses index.vue as the main view, not widget.vue. There is no widget.vue in OS plugins.

When you release a new version, add a new version folder alongside 1.0.0/:

my-plugin/
├── 1.0.0/
├── 1.1.0/          ← new version
└── database/

setting.json ​

json
{
    "versionNumber": "0",
    "version": "1.0.0",
    "name": "my-plugin",
    "icon": "images/logo.svg",
    "type": "OS",
    "script": "script.js",
    "id": "_uniqueId",
    "packages": []
}
FieldDescription
versionNumberInternal version index (start at "0")
versionSemver version string shown in store
namePlugin identifier — must match the folder name
iconPlugin icon path (relative to version folder)
typeAlways "OS"
scriptEntry script filename
idUnique store ID (assigned on upload)
packagesOptional npm package dependencies

script.js ​

script.js runs when the OS plugin window opens. Register your plugin by assigning a function to plugins["name"]. Inside the function, this exposes all registration methods.

js
plugins["my-plugin"] = function () {
    // 1. Set window frame
    this.setFrame({
        title: "Motanamy OS | My Plugin",
        icon: {
            type: "icon",
            name: "mdi mdi-puzzle-outline"
        }
    });

    // 2. Register translations
    this.addLanguage("ar", "my-plugin", { name: "البرنامج" });
    this.addLanguage("en", "my-plugin", { name: "My Plugin" });

    // 3. Set toolbar buttons
    this.setToolbar([
        { icon: "pi pi-cog", to: "/setting" }
    ]);

    // 4. Register page routes
    this.addPage({
        name: "/[my-plugin]index.vue",
        path: "",
        file: ["index.vue"]
    });

    this.addPage({
        name: "/[my-plugin]setting.vue",
        path: "/setting",
        file: ["setting.vue"]
    });
}

See the script.js API Reference for all available methods.

index.vue ​

The root page of your plugin — loaded when the OS window opens. This is the primary UI of your plugin:

vue
<template>
    <div class="h-full flex align-items-center justify-content-center">
        <div class="card w-full">
            <h5>{{ $t("my-plugin.name") }}</h5>
            <!-- main content -->
        </div>
    </div>
</template>

<script>
import api from 'api';

export default {
    data() {
        return { items: [] }
    },
    created() {
        this.items = api.database("my-plugin").readJson(["data.json"]);
    }
}
</script>

setting.vue ​

The settings page. Navigate back to root with $router.push('/'):

vue
<template>
    <div class="m-2">
        <div class="card flex align-items-center justify-content-between">
            <Button icon="pi pi-arrow-left" class="p-button-text" @click="$router.push('/')" />
            <h5 class="m-0">{{ $t("my-plugin.name") }}</h5>
            <div>
                <Button icon="pi pi-save" class="p-button-text" @click="save()" />
                <Button icon="pi pi-refresh" class="p-button-text" @click="init()" />
            </div>
        </div>
        <div class="card">
            <!-- settings content -->
        </div>
    </div>
</template>

Database ​

OS plugins use the api module to read and write local JSON files — same as SA:

js
import api from 'api';

// Read
const data = api.database("my-plugin").readJson(["data.json"]);

// Write
api.database("my-plugin").writeJson(["data.json"], data);

The initial state comes from database/my-plugin/data.json.

Native File Dialogs ​

OS plugins run in Electron and can open native file/folder dialogs via ipcRenderer:

js
import { ipcRenderer } from "electron";
import { basename } from "node:path";

// Open a file picker
ipcRenderer.invoke("openDialog", {
    title: "Select File",
    filters: [
        { name: "Audio", extensions: ["mp3", "ogg"] }
    ],
    properties: ["openFile", "multiSelections"]
}).then(files => {
    if (files) {
        files.forEach(filePath => {
            this.items.push({
                name: basename(filePath),
                path: filePath
            });
        });
    }
});

ipcRenderer is only available in OS plugins (Electron renderer process). Do not use it in EA or SA plugins.

Routing ​

OS plugins use root-relative paths — not /app/... like EA/SA:

js
// Root page
this.addPage({ path: "", ... });         // → /

// Sub-page
this.addPage({ path: "/setting", ... }); // → /setting
this.addPage({ path: "/detail/:id", ... }); // → /detail/:id

// Navigate in component
this.$router.push('/setting');
this.$router.push('/');
this.$router.push(`/detail/${id}`);

// Read param
const id = this.$route.params.id;

i18n Usage ​

vue
<template>
    <div>
        <h5>{{ $t("my-plugin.name") }}</h5>
        <Button :label="$t('button.save')" />
    </div>
</template>

Development Workflow ​

bash
mot create my-plugin --type OS
mot link add      # once: point the CLI at your installed OS app
cd my-plugin
mot dev           # run the plugin inside OS
mot publish       # publish to the store (see Publish to Store)

Resources ​