Structurally they are the same: a manifest, a class with register() and boot(), routes, migrations, services and templates. The difference is where it lives and how it arrives.
The console has the whole lifecycle:
php tools/convoro ext:scaffold my-thing
php tools/convoro ext:list
php tools/convoro ext:install path/to/thing.zip
php tools/convoro ext:enable my-thing
php tools/convoro ext:disable my-thing
php tools/convoro ext:package my-thing
php tools/convoro ext:uninstall my-thing🚨 extensions/ is site state, not source. It is excluded from deployment for the same reason content/ is — a deploy that syncs the source tree over it uninstalls every extension the operator added.
🚨 Disabling and uninstalling are different. Disabling hides; uninstalling removes. Data is never touched by the first, and that distinction should hold in your own code too.