ekwo-os 0.4.1
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/README.md +873 -0
- package/dist/args.d.ts +57 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +134 -0
- package/dist/args.js.map +1 -0
- package/dist/assets/expected-objects.json +9432 -0
- package/dist/assets/migrations/20260911120000_core_companies.sql +260 -0
- package/dist/assets/migrations/20260911120100_accounts_journals.sql +211 -0
- package/dist/assets/migrations/20260911120200_contacts.sql +90 -0
- package/dist/assets/migrations/20260911120300_taxes.sql +146 -0
- package/dist/assets/migrations/20260911120400_entries.sql +338 -0
- package/dist/assets/migrations/20260911120500_documents.sql +279 -0
- package/dist/assets/migrations/20260911120600_payments_reconciliation.sql +251 -0
- package/dist/assets/migrations/20260911120700_bank.sql +178 -0
- package/dist/assets/migrations/20260911120800_analytics_attachments.sql +128 -0
- package/dist/assets/migrations/20260911120900_post_document.sql +279 -0
- package/dist/assets/migrations/20260911121000_reporting.sql +235 -0
- package/dist/assets/migrations/20260911121100_country_templates.sql +226 -0
- package/dist/assets/migrations/20260911121200_fec.sql +71 -0
- package/dist/assets/migrations/20260911130000_instance.sql +77 -0
- package/dist/assets/migrations/20260911130100_instance_members.sql +224 -0
- package/dist/assets/migrations/20260911140000_instance_admins.sql +112 -0
- package/dist/assets/migrations/20260911160000_tax_posting_templates_unique.sql +17 -0
- package/dist/assets/migrations/20260911170000_document_amount_paid.sql +82 -0
- package/dist/assets/migrations/20260911173000_post_payment.sql +122 -0
- package/dist/assets/migrations/20260911173100_sequence_counters_under_rls.sql +84 -0
- package/dist/assets/migrations/20260911183000_country_journal_defaults.sql +143 -0
- package/dist/assets/migrations/20260911193853_line_account_defaults.sql +274 -0
- package/dist/assets/migrations/20260911195054_products.sql +221 -0
- package/dist/assets/migrations/20260911210131_anon_surface.sql +85 -0
- package/dist/assets/migrations/20260912074712_country_packs.sql +318 -0
- package/dist/assets/migrations/20260912080311_report_code_and_region.sql +198 -0
- package/dist/assets/migrations/20260912081014_pack_certification_maintained.sql +31 -0
- package/dist/assets/migrations/20260912081015_pack_certification_backfill.sql +16 -0
- package/dist/assets/migrations/20260912090407_tax_report_boxes.sql +329 -0
- package/dist/assets/migrations/20260912091917_tax_on_base_value.sql +25 -0
- package/dist/assets/migrations/20260912091918_tax_engine_columns.sql +776 -0
- package/dist/assets/migrations/20260912094412_opening_and_closing.sql +759 -0
- package/dist/assets/migrations/20260912095825_charts_of_accounts.sql +326 -0
- package/dist/assets/migrations/20260912100412_financial_statements.sql +632 -0
- package/dist/assets/migrations/20260912104719_one_formula_evaluator.sql +209 -0
- package/dist/assets/migrations/20260912105720_entry_kind_appropriation.sql +26 -0
- package/dist/assets/migrations/20260912105721_appropriation_entry_kind.sql +444 -0
- package/dist/assets/migrations/20260912111751_document_rules.sql +274 -0
- package/dist/assets/migrations/20260912112132_cash_basis_vat_and_fx.sql +1189 -0
- package/dist/assets/migrations/20260913074512_modules.sql +493 -0
- package/dist/assets/migrations/20260913075903_asset_disposal_roles.sql +47 -0
- package/dist/assets/migrations/20260913083216_capabilities.sql +637 -0
- package/dist/assets/migrations/20260913083901_company_invitations.sql +262 -0
- package/dist/assets/migrations/20260913084402_user_preferences.sql +376 -0
- package/dist/assets/migrations/20260913084847_company_profile.sql +261 -0
- package/dist/assets/migrations/20260913085436_numbering_reads_the_pack.sql +235 -0
- package/dist/assets/migrations/20260913085932_api_keys.sql +303 -0
- package/dist/assets/migrations/20260913090216_fiscal_year_bounds.sql +159 -0
- package/dist/assets/migrations/20260913092527_entries_import.sql +219 -0
- package/dist/assets/migrations/20260913101536_null_safe_guards.sql +188 -0
- package/dist/assets/migrations/20260913102115_installer_is_named.sql +462 -0
- package/dist/assets/migrations/20260913102758_no_currency_default.sql +138 -0
- package/dist/assets/migrations/20260913103355_cash_basis_needs_a_box.sql +453 -0
- package/dist/assets/migrations/20260913104014_aged_balance_named_group.sql +79 -0
- package/dist/assets/migrations/20260913104232_foreign_key_indexes.sql +144 -0
- package/dist/assets/migrations/20260913105120_declared_no_reader.sql +41 -0
- package/dist/assets/migrations/20260913111407_pack_languages.sql +223 -0
- package/dist/assets/migrations/20260913112233_schema_comments_for_a_reader.sql +27 -0
- package/dist/assets/migrations/20260913114535_fec_opening_balances.sql +307 -0
- package/dist/assets/migrations/20260914103412_audit_log.sql +509 -0
- package/dist/assets/migrations/20260914111907_pack_upgrade.sql +428 -0
- package/dist/assets/migrations/20260914120500_rounding_reads_the_currency.sql +206 -0
- package/dist/assets/migrations/20260914121200_amounts_round_at_the_currency.sql +2403 -0
- package/dist/assets/migrations/20260914134325_schema_version_0_2_0.sql +36 -0
- package/dist/assets/migrations/20260914143915_accounts_in_use.sql +428 -0
- package/dist/assets/migrations/20260914144731_account_code_frozen.sql +99 -0
- package/dist/assets/migrations/20260914151207_schema_grants_its_own_rights.sql +284 -0
- package/dist/assets/migrations/20260914152840_pack_upgrade_records_its_own_line.sql +237 -0
- package/dist/assets/migrations/20260914163943_declaration_periodicity.sql +349 -0
- package/dist/assets/migrations/20260915094000_foreign_services_received.sql +37 -0
- package/dist/assets/migrations/20260915094500_mentions_read_a_foreign_service.sql +72 -0
- package/dist/assets/migrations/20260915153000_document_shares.sql +568 -0
- package/dist/assets/migrations/20260915160000_ec_sales_list.sql +181 -0
- package/dist/assets/migrations/20260915161842_pack_sources.sql +56 -0
- package/dist/assets/migrations/20260915170500_schema_version_0_3_0.sql +45 -0
- package/dist/assets/migrations/20260915174500_a_vat_category_is_a_code.sql +177 -0
- package/dist/assets/migrations/20260915180000_document_rule_references.sql +85 -0
- package/dist/assets/migrations/20260915181000_territories.sql +342 -0
- package/dist/assets/migrations/20260915181500_ec_sales_list_reads_the_territories.sql +222 -0
- package/dist/assets/migrations/20260915182000_intracom_triangular.sql +53 -0
- package/dist/assets/migrations/20260915182500_a_triangular_supply_says_reverse_charge.sql +99 -0
- package/dist/assets/migrations/20260915191200_a_document_knows_its_language.sql +633 -0
- package/dist/assets/migrations/20260915195000_a_price_that_holds_its_tax.sql +649 -0
- package/dist/assets/migrations/20260915200000_a_posting_names_its_boxes.sql +1004 -0
- package/dist/assets/migrations/20260916094500_a_company_files_more_than_one_declaration.sql +795 -0
- package/dist/assets/migrations/20260916103000_a_ledger_line_names_its_posting.sql +902 -0
- package/dist/assets/migrations/20260916123000_a_vocabulary_for_a_tax_that_is_not_a_vat.sql +144 -0
- package/dist/assets/migrations/20260916124000_a_box_can_be_a_rate_of_a_box.sql +525 -0
- package/dist/assets/migrations/20260916125000_a_tax_follows_the_territory.sql +1318 -0
- package/dist/assets/migrations/20260916126000_a_tax_point_and_its_exception.sql +1211 -0
- package/dist/assets/migrations/20260917090000_a_counterparty_that_learns.sql +561 -0
- package/dist/assets/migrations/20260917120000_what_the_money_pays.sql +545 -0
- package/dist/assets/migrations/20260917150000_a_filing_is_a_row.sql +488 -0
- package/dist/assets/migrations/20260917170000_when_it_is_due.sql +194 -0
- package/dist/assets/migrations/20260917180000_a_box_is_a_number_and_a_kind.sql +213 -0
- package/dist/assets/migrations/20260917190000_what_a_declaration_owes.sql +486 -0
- package/dist/assets/migrations/20260917200000_what_moved_after_it_went.sql +227 -0
- package/dist/assets/migrations/20260918090000_a_form_names_its_file.sql +27 -0
- package/dist/assets/migrations/20260918110000_a_deposit_and_what_came_back.sql +286 -0
- package/dist/assets/migrations/20260918113741_a_fourth_preset_is_named.sql +9 -0
- package/dist/assets/migrations/20260918113807_a_client_reads_and_hands_over.sql +96 -0
- package/dist/assets/migrations/20260918114322_two_definer_functions_check_their_caller.sql +75 -0
- package/dist/assets/migrations/20260918130000_every_company_somebody_keeps.sql +177 -0
- package/dist/assets/migrations/20260918140000_a_session_nobody_prepared.sql +40 -0
- package/dist/assets/migrations/20260918141107_a_line_keeps_the_tax_it_was_posted_with.sql +256 -0
- package/dist/assets/migrations/20260918141342_a_company_has_an_electronic_address.sql +66 -0
- package/dist/assets/migrations/20260918141605_an_invoice_reads_whole_from_the_views.sql +313 -0
- package/dist/assets/migrations/20260918141627_a_policy_asks_once.sql +132 -0
- package/dist/assets/migrations/20260918143352_a_suggestion_reads_the_words_once.sql +204 -0
- package/dist/assets/migrations/20260918143417_a_reference_of_the_caller_and_a_rehearsal.sql +115 -0
- package/dist/assets/migrations/20260918150712_a_company_leaves_with_its_books.sql +1119 -0
- package/dist/assets/migrations/20260918150931_a_statement_is_imported_once.sql +654 -0
- package/dist/assets/migrations/20260918161204_a_posted_document_does_not_move.sql +437 -0
- package/dist/assets/migrations/20260918161538_a_posted_entry_does_not_move.sql +252 -0
- package/dist/assets/migrations/20260918171946_an_entry_is_posted_by_post_entry.sql +289 -0
- package/dist/assets/migrations/20260918174312_schema_version_0_4_0.sql +53 -0
- package/dist/assets/modules/assets/module.json +13 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913081447_assets.sql +1190 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913104234_foreign_key_indexes.sql +47 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913104500_assets_capabilities.sql +145 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914122100_assets_round_at_the_currency.sql +564 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914145012_assets_accounts_in_use.sql +37 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914151530_assets_grants_its_own_rights.sql +63 -0
- package/dist/assets/modules/assets/supabase/migrations/20260918113912_assets_client_reads.sql +20 -0
- package/dist/assets/modules/assets/supabase/migrations/20260918150801_assets_leave_with_the_company.sql +41 -0
- package/dist/assets/modules/budgets/module.json +12 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913083012_budgets.sql +210 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913104233_foreign_key_indexes.sql +37 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913104501_budgets_capabilities.sql +72 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914122400_budgets_round_at_the_currency.sql +68 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914145048_budgets_accounts_in_use.sql +30 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914151812_budgets_grants_its_own_rights.sql +31 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260918113954_budgets_client_reads.sql +20 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260918150823_budgets_leave_with_the_company.sql +37 -0
- package/dist/assets/modules/schema/module.1.json +69 -0
- package/dist/assets/seed/00_currencies.sql +15 -0
- package/dist/assets/seed/00_territories.sql +280 -0
- package/dist/assets/seed/05_framework_generic.sql +110 -0
- package/dist/assets/seed/10_pack_be.sql +1284 -0
- package/dist/assets/seed/11_pack_fr.sql +1112 -0
- package/dist/assets/seed/12_pack_lu.sql +2484 -0
- package/dist/assets/seed/13_pack_ee.sql +733 -0
- package/dist/assets/seed/14_pack_gb.sql +796 -0
- package/dist/assets/seed/15_pack_us.sql +751 -0
- package/dist/assets/seed/90_demo_company.sql +368 -0
- package/dist/assets/seed/modules/assets/10_pack_be.sql +58 -0
- package/dist/assets/seed/modules/assets/11_pack_fr.sql +57 -0
- package/dist/assets/seed/modules/assets/14_pack_gb.sql +58 -0
- package/dist/assets/seed/modules/assets/15_pack_us.sql +60 -0
- package/dist/auth.d.ts +59 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +134 -0
- package/dist/auth.js.map +1 -0
- package/dist/bin.d.ts +6 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +8 -0
- package/dist/bin.js.map +1 -0
- package/dist/books.d.ts +60 -0
- package/dist/books.d.ts.map +1 -0
- package/dist/books.js +168 -0
- package/dist/books.js.map +1 -0
- package/dist/bootstrap.d.ts +236 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +459 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bundle.d.ts +27 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +47 -0
- package/dist/bundle.js.map +1 -0
- package/dist/checklist.d.ts +35 -0
- package/dist/checklist.d.ts.map +1 -0
- package/dist/checklist.js +64 -0
- package/dist/checklist.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +318 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/company.d.ts +59 -0
- package/dist/commands/company.d.ts.map +1 -0
- package/dist/commands/company.js +287 -0
- package/dist/commands/company.js.map +1 -0
- package/dist/commands/contact.d.ts +9 -0
- package/dist/commands/contact.d.ts.map +1 -0
- package/dist/commands/contact.js +82 -0
- package/dist/commands/contact.js.map +1 -0
- package/dist/commands/demo.d.ts +19 -0
- package/dist/commands/demo.d.ts.map +1 -0
- package/dist/commands/demo.js +72 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +68 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/document.d.ts +17 -0
- package/dist/commands/document.d.ts.map +1 -0
- package/dist/commands/document.js +217 -0
- package/dist/commands/document.js.map +1 -0
- package/dist/commands/init.d.ts +31 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +535 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +24 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +128 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/migrate.d.ts +30 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +115 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/module.d.ts +32 -0
- package/dist/commands/module.d.ts.map +1 -0
- package/dist/commands/module.js +285 -0
- package/dist/commands/module.js.map +1 -0
- package/dist/commands/pack.d.ts +22 -0
- package/dist/commands/pack.d.ts.map +1 -0
- package/dist/commands/pack.js +481 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/payment.d.ts +15 -0
- package/dist/commands/payment.d.ts.map +1 -0
- package/dist/commands/payment.js +97 -0
- package/dist/commands/payment.js.map +1 -0
- package/dist/commands/register.d.ts +20 -0
- package/dist/commands/register.d.ts.map +1 -0
- package/dist/commands/register.js +134 -0
- package/dist/commands/register.js.map +1 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +134 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/whoami.d.ts +21 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +133 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/company.d.ts +16 -0
- package/dist/company.d.ts.map +1 -0
- package/dist/company.js +26 -0
- package/dist/company.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +50 -0
- package/dist/config.js.map +1 -0
- package/dist/connection.d.ts +79 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +116 -0
- package/dist/connection.js.map +1 -0
- package/dist/context.d.ts +47 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +93 -0
- package/dist/context.js.map +1 -0
- package/dist/doctor.d.ts +45 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +448 -0
- package/dist/doctor.js.map +1 -0
- package/dist/grants.d.ts +88 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +174 -0
- package/dist/grants.js.map +1 -0
- package/dist/identity.d.ts +54 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +132 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory.d.ts +155 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +303 -0
- package/dist/inventory.js.map +1 -0
- package/dist/migrations.d.ts +76 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +226 -0
- package/dist/migrations.js.map +1 -0
- package/dist/module/read.d.ts +91 -0
- package/dist/module/read.d.ts.map +1 -0
- package/dist/module/read.js +172 -0
- package/dist/module/read.js.map +1 -0
- package/dist/output.d.ts +110 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +173 -0
- package/dist/output.js.map +1 -0
- package/dist/pack/certification.d.ts +27 -0
- package/dist/pack/certification.d.ts.map +1 -0
- package/dist/pack/certification.js +38 -0
- package/dist/pack/certification.js.map +1 -0
- package/dist/pack/compile.d.ts +75 -0
- package/dist/pack/compile.d.ts.map +1 -0
- package/dist/pack/compile.js +801 -0
- package/dist/pack/compile.js.map +1 -0
- package/dist/pack/filing.d.ts +48 -0
- package/dist/pack/filing.d.ts.map +1 -0
- package/dist/pack/filing.js +56 -0
- package/dist/pack/filing.js.map +1 -0
- package/dist/pack/read.d.ts +766 -0
- package/dist/pack/read.d.ts.map +1 -0
- package/dist/pack/read.js +2336 -0
- package/dist/pack/read.js.map +1 -0
- package/dist/pack/schema.d.ts +26 -0
- package/dist/pack/schema.d.ts.map +1 -0
- package/dist/pack/schema.js +181 -0
- package/dist/pack/schema.js.map +1 -0
- package/dist/pack/territories.d.ts +98 -0
- package/dist/pack/territories.d.ts.map +1 -0
- package/dist/pack/territories.js +0 -0
- package/dist/pack/territories.js.map +1 -0
- package/dist/pack/upgrade.d.ts +86 -0
- package/dist/pack/upgrade.d.ts.map +1 -0
- package/dist/pack/upgrade.js +76 -0
- package/dist/pack/upgrade.js.map +1 -0
- package/dist/pack/vat-codes.d.ts +273 -0
- package/dist/pack/vat-codes.d.ts.map +1 -0
- package/dist/pack/vat-codes.js +513 -0
- package/dist/pack/vat-codes.js.map +1 -0
- package/dist/profiles.d.ts +67 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +187 -0
- package/dist/profiles.js.map +1 -0
- package/dist/prompt.d.ts +40 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +120 -0
- package/dist/prompt.js.map +1 -0
- package/dist/registry.d.ts +84 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +104 -0
- package/dist/registry.js.map +1 -0
- package/dist/rest.d.ts +81 -0
- package/dist/rest.d.ts.map +1 -0
- package/dist/rest.js +245 -0
- package/dist/rest.js.map +1 -0
- package/dist/schema.d.ts +15 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +15 -0
- package/dist/schema.js.map +1 -0
- package/dist/seeds.d.ts +27 -0
- package/dist/seeds.d.ts.map +1 -0
- package/dist/seeds.js +47 -0
- package/dist/seeds.js.map +1 -0
- package/dist/session.d.ts +39 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +92 -0
- package/dist/session.js.map +1 -0
- package/dist/sql.d.ts +48 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +112 -0
- package/dist/sql.js.map +1 -0
- package/dist/status.d.ts +94 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +120 -0
- package/dist/status.js.map +1 -0
- package/dist/ui.d.ts +55 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +114 -0
- package/dist/ui.js.map +1 -0
- package/package.json +29 -0
- package/schema/output.1.json +613 -0
package/README.md
ADDED
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
# ekwo
|
|
2
|
+
|
|
3
|
+
The installer and the operator's tool for [Ekwo OS](https://github.com/Ekwo-ai/ekwo-os).
|
|
4
|
+
One command turns a Supabase project you already own into a set of double-entry
|
|
5
|
+
books: the schema, the chart of accounts, the VAT codes, the first
|
|
6
|
+
administrator, the first company and its first financial year.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npx ekwo-os init
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The package is `ekwo-os` and the command it installs is `ekwo`: `npx ekwo-os
|
|
13
|
+
<command>` runs it without installing anything, and after
|
|
14
|
+
`npm install -g ekwo-os` every example below that starts with `ekwo ` works as
|
|
15
|
+
written.
|
|
16
|
+
|
|
17
|
+
You need Node 20 or later. That is the whole list. The Supabase CLI is not
|
|
18
|
+
required — this talks to Postgres directly — and Docker is not required
|
|
19
|
+
either.
|
|
20
|
+
|
|
21
|
+
## From a free Supabase account to a first invoice
|
|
22
|
+
|
|
23
|
+
1. **Create a project** at [supabase.com](https://supabase.com). The free plan
|
|
24
|
+
is enough to start. Ekwo does not create it, does not pay for it and has no
|
|
25
|
+
access to it: it is yours from the first row.
|
|
26
|
+
2. **Copy two things** from the dashboard:
|
|
27
|
+
- Project Settings → Database → **Connection string** (URI). It contains
|
|
28
|
+
your database password.
|
|
29
|
+
- Project Settings → API → **`service_role` key**, and the **Project URL**.
|
|
30
|
+
These are used once, to create the first administrator in your own
|
|
31
|
+
Supabase Auth, and are never written to disk.
|
|
32
|
+
|
|
33
|
+
**Take the pooler string, not the direct one, unless you know you have
|
|
34
|
+
IPv6.** The direct host `db.<ref>.supabase.co` resolves to an IPv6 address
|
|
35
|
+
only on any recent project, so from an IPv4-only network it simply never
|
|
36
|
+
connects. The session pooler answers on IPv4 and supports everything a
|
|
37
|
+
migration needs:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
postgresql://postgres.<ref>:<password>@aws-1-<region>.pooler.supabase.com:5432/postgres
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The region is in the hostname the dashboard gives you, and so is the
|
|
44
|
+
generation prefix, which the region does not determine: verified on a real
|
|
45
|
+
project on 11 September 2026 in `eu-west-3`, where `aws-1` worked and
|
|
46
|
+
`aws-0` answered "Tenant or user not found". Copy the line from the
|
|
47
|
+
dashboard — Connect → Session pooler — rather than building it by hand.
|
|
48
|
+
3. **Run the installer.**
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npx ekwo-os init
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
It asks for the connection string, the country, the chart of accounts and
|
|
55
|
+
the language where the pack offers a choice, your organisation, the
|
|
56
|
+
currency, the first company, the address of the first administrator and —
|
|
57
|
+
optionally — the IBAN of your main bank account, then does the rest. Five
|
|
58
|
+
to ten seconds on a free project. Nothing is preselected for you on the
|
|
59
|
+
three questions whose wrong answer is expensive: the country, the chart and
|
|
60
|
+
the language.
|
|
61
|
+
|
|
62
|
+
4. **Sign in** to your project as that administrator and start booking. Until
|
|
63
|
+
the Community web application lands, the interface is the REST API Supabase
|
|
64
|
+
generates from the schema, or `psql`, or `@ekwo-ai/core`.
|
|
65
|
+
|
|
66
|
+
5. **Do the four things below**, while the dashboard is still open. The
|
|
67
|
+
installer prints them at the end of a successful run, because three of them
|
|
68
|
+
are settings of your project rather than rows in your database, and nothing
|
|
69
|
+
holding a connection string can reach them.
|
|
70
|
+
|
|
71
|
+
Everything above in one non-interactive line:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
npx ekwo-os init \
|
|
75
|
+
--db-url "postgresql://postgres.YOURREF:PASSWORD@aws-1-eu-west-3.pooler.supabase.com:5432/postgres" \
|
|
76
|
+
--supabase-url "https://YOURREF.supabase.co" \
|
|
77
|
+
--service-role-key "$SUPABASE_SERVICE_ROLE_KEY" \
|
|
78
|
+
--country BE \
|
|
79
|
+
--chart default \
|
|
80
|
+
--language fr \
|
|
81
|
+
--org "My Organisation" \
|
|
82
|
+
--company "My Company" \
|
|
83
|
+
--admin-email "you@example.com" \
|
|
84
|
+
--admin-password "a-long-password" \
|
|
85
|
+
--fiscal-year 2026 \
|
|
86
|
+
--iban "BE71096123456769" \
|
|
87
|
+
--yes
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`--chart` and `--language` are in that line because the Belgian pack offers a
|
|
91
|
+
choice on both, and `--yes` means there is nobody to ask. See "Installing
|
|
92
|
+
without a terminal" below.
|
|
93
|
+
|
|
94
|
+
## Installing without a terminal
|
|
95
|
+
|
|
96
|
+
`--yes` turns off every question, and then every answer has to arrive as a flag
|
|
97
|
+
or an environment variable. Two of them are worth knowing about before you
|
|
98
|
+
write the script, because `ekwo init` **refuses rather than picking one for
|
|
99
|
+
you**:
|
|
100
|
+
|
|
101
|
+
- **the chart of accounts**, where the country publishes more than one. Belgium
|
|
102
|
+
publishes two, a company chart and an association chart. Pass `--chart`; the
|
|
103
|
+
refusal lists the codes the pack carries.
|
|
104
|
+
- **the language of the books**, where the pack publishes more than one. Pass
|
|
105
|
+
`--language`; the refusal lists them. The choice decides which label of the
|
|
106
|
+
pack lands in `accounts.name`, and the others stay beside it in `name_i18n`,
|
|
107
|
+
so it is not irreversible — but it is not a question a script should answer
|
|
108
|
+
by accident either.
|
|
109
|
+
|
|
110
|
+
`--country` behaves the same way and has no default at all: the refusal names
|
|
111
|
+
the packs the database holds. A preselected country is a chart of accounts
|
|
112
|
+
nobody chose.
|
|
113
|
+
|
|
114
|
+
The same is true of the financial year: a pack that declares no usual opening
|
|
115
|
+
month makes `--fiscal-year-start` required. Both packs shipped here open on the
|
|
116
|
+
calendar year, so it rarely comes up.
|
|
117
|
+
|
|
118
|
+
## Where table access comes from
|
|
119
|
+
|
|
120
|
+
**The schema grants its own rights.** Every table, view and function of Ekwo
|
|
121
|
+
names the roles that may reach it — `anon`, `authenticated`, `service_role` —
|
|
122
|
+
in the migration that creates it. `ekwo doctor` reads the privileges of a live
|
|
123
|
+
database and reports a grant that is missing, a grant wider than the release
|
|
124
|
+
declares, and a table `anon` can reach at all.
|
|
125
|
+
|
|
126
|
+
Two rules follow, and both are worth knowing before you change anything by
|
|
127
|
+
hand.
|
|
128
|
+
|
|
129
|
+
**`anon` holds no privilege on any table.** The anonymous role — the one behind
|
|
130
|
+
the publishable key your front end ships — may execute the ten helper functions
|
|
131
|
+
row level security calls on its behalf, and nothing else. An anonymous request
|
|
132
|
+
to a table is refused at the privilege, before any policy is read. If part of
|
|
133
|
+
your application reads a table without signing a user in, it will stop working,
|
|
134
|
+
and that is the intended answer: sign the user in, or grant a function
|
|
135
|
+
deliberately.
|
|
136
|
+
|
|
137
|
+
**`authenticated` may attempt exactly the verbs a policy of that table is
|
|
138
|
+
prepared to judge.** A grant and a policy are two halves of one sentence: a
|
|
139
|
+
grant says which verbs may be attempted, a policy says on which rows they
|
|
140
|
+
succeed. The reference tables a country pack fills, the tables written only by
|
|
141
|
+
a `security definer` function, and the audit trail are readable and not
|
|
142
|
+
writable — by privilege as well as by policy.
|
|
143
|
+
|
|
144
|
+
**It was not always so, and the history explains a symptom you may still meet
|
|
145
|
+
on an installation nobody has migrated.** Until the migration of 14 September
|
|
146
|
+
2026, nothing in `supabase/migrations` granted table access at all. Row level
|
|
147
|
+
security was written in the migrations in full and the underlying `GRANT` was
|
|
148
|
+
not: on a Supabase project it came from that project's own default privileges
|
|
149
|
+
on the `public` schema, which are there before Ekwo is. Those privileges live
|
|
150
|
+
in `pg_default_acl`, keyed by the schema, so dropping and recreating `public`
|
|
151
|
+
took them away — and then the reinstall succeeded, `ekwo doctor` reported a
|
|
152
|
+
healthy installation, and the first read through PostgREST answered
|
|
153
|
+
`permission denied for table companies`. Nothing was wrong with the schema; the
|
|
154
|
+
grant that had never been in it was missing.
|
|
155
|
+
|
|
156
|
+
On an installation that has run `ekwo migrate` since, that cannot happen: the
|
|
157
|
+
migrations put the privileges back themselves, and they take away the blanket
|
|
158
|
+
table access the project's defaults had handed `anon`. Dropping `public` is
|
|
159
|
+
still not something to do on a project you intend to keep — it takes your books
|
|
160
|
+
with it. The decision and what it changed are in
|
|
161
|
+
[`docs/decisions.md`](../../docs/decisions.md).
|
|
162
|
+
|
|
163
|
+
## Before you go live: four things on your project
|
|
164
|
+
|
|
165
|
+
An installation leaves four things undone, and they are undone on purpose:
|
|
166
|
+
they are yours to decide, on a project Ekwo does not have access to. `ekwo
|
|
167
|
+
init` prints this list at the end of a successful run. `ekwo doctor` does not
|
|
168
|
+
check it and does not mention it — a database connection cannot see the
|
|
169
|
+
settings of the project it is connected to.
|
|
170
|
+
|
|
171
|
+
**1. Turn off self sign-up on your project.**
|
|
172
|
+
Supabase dashboard → **Authentication → Sign In / Providers → "Allow new users
|
|
173
|
+
to sign up"**, and switch it off. A fresh Supabase project accepts anyone who
|
|
174
|
+
posts an e-mail address and a password to its authentication endpoint, which is
|
|
175
|
+
the right default for a public application and the wrong one for a set of
|
|
176
|
+
books. An Ekwo installation is closed: the people who keep the books are
|
|
177
|
+
invited to it. Row level security means a stranger who signs up sees nothing —
|
|
178
|
+
they are a member of no company — but they are a row in `auth.users` that
|
|
179
|
+
nobody asked for, on a project whose sign-up endpoint is open to the internet.
|
|
180
|
+
|
|
181
|
+
**2. Keep two administrators.**
|
|
182
|
+
An instance administrator is what claims the instance and invites everybody
|
|
183
|
+
else. With one, a lost password, a closed mailbox or a person on holiday is a
|
|
184
|
+
set of books that nobody can let anyone into. Create the second account in your
|
|
185
|
+
Supabase Auth and add it with `claim_instance_admin()`, or invite it from the
|
|
186
|
+
application once it is signed in.
|
|
187
|
+
|
|
188
|
+
**3. Keep the service_role key off every machine that does not need it.**
|
|
189
|
+
It is not a powerful user: it is the absence of a door. A request carrying it
|
|
190
|
+
bypasses row level security entirely and reads every company in the instance.
|
|
191
|
+
This CLI reads it from a flag, an environment variable or a masked prompt, uses
|
|
192
|
+
it once to create the first account, and writes it nowhere — see
|
|
193
|
+
[Secrets](#secrets). Anywhere else it sits, it sits as a copy of your whole
|
|
194
|
+
ledger. `--admin-user-id` installs against an account that already exists and
|
|
195
|
+
needs no key at all.
|
|
196
|
+
|
|
197
|
+
**4. Read DISCLAIMER.md before you file anything.**
|
|
198
|
+
[`DISCLAIMER.md`](../../DISCLAIMER.md), at the root of the repository. A
|
|
199
|
+
country pack is a reading of a country's rules at the date of its version, and
|
|
200
|
+
its golden test proves that the pack agrees with itself — not that it agrees
|
|
201
|
+
with the law. `ekwo init` prints the certification status of the pack it
|
|
202
|
+
installs for the same reason. The books are yours, in every country where you
|
|
203
|
+
file.
|
|
204
|
+
|
|
205
|
+
None of these is an action Ekwo performs on your project, now or later. The
|
|
206
|
+
project is yours from the first row: the settings are yours to change, the key
|
|
207
|
+
is yours to hold, and what you file is yours to answer for.
|
|
208
|
+
|
|
209
|
+
Automatic verification of the first three is a phase 1 question, and it is not
|
|
210
|
+
free: they are answered by the Supabase management API, so checking them means
|
|
211
|
+
handing `ekwo doctor` a management token, and a token that can read a project's
|
|
212
|
+
settings can change them. Until that trade is worth making, the list is printed
|
|
213
|
+
and read by a person.
|
|
214
|
+
|
|
215
|
+
## What `init` does, step by step
|
|
216
|
+
|
|
217
|
+
| Step | What happens | Why it is done this way |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| 1 | Applies `supabase/migrations/*.sql` in order | Recorded in `supabase_migrations.schema_migrations`, the Supabase CLI's own history table, so `supabase db push` and `ekwo migrate` stay interchangeable |
|
|
220
|
+
| 2 | Applies the six reference seeds, in file-name order: `00_currencies.sql`, `05_framework_generic.sql`, `10_pack_be.sql`, `11_pack_fr.sql`, `12_pack_lu.sql`, `13_pack_ee.sql` | The currencies, the country-less financial statements every chart falls back on, and the four country packs. They are exactly the six `supabase/config.toml` lists, so `supabase db push` installs the same set; a test compares both paths row by row. `90_demo_company.sql` is sample data and is never applied here |
|
|
221
|
+
| 3 | Creates the first administrator through the Supabase Auth admin API | See below: a database connection cannot be a signed-in user |
|
|
222
|
+
| 4 | `init_instance()`, `claim_instance_admin()`, the company, `company_members` as owner, `install_country_template()`, the first financial year, and the bank account when an IBAN was given | The six steps of the root README, in the same order, plus the one thing nobody can derive |
|
|
223
|
+
| 5 | Writes `ekwo.json` | Project URL, country, schema version. Nothing else, ever |
|
|
224
|
+
| 6 | Asks whether to register with Ekwo | The default answer is no, and no is a supported answer forever |
|
|
225
|
+
|
|
226
|
+
Every step checks before it acts. Running `ekwo init` twice on the same
|
|
227
|
+
project reports what was already there and creates nothing a second time.
|
|
228
|
+
|
|
229
|
+
### Why the first user goes through Supabase Auth
|
|
230
|
+
|
|
231
|
+
Every row level security policy in the schema compares `auth.uid()` against a
|
|
232
|
+
row, and `auth.uid()` reads the JWT of the request. The CLI holds a Postgres
|
|
233
|
+
connection, not a session: it runs as the database owner, `auth.uid()` is
|
|
234
|
+
NULL, and row level security is *bypassed* rather than satisfied. So the
|
|
235
|
+
installer cannot be the first user. It can only create one and then write the
|
|
236
|
+
rows that user will be recognised by.
|
|
237
|
+
|
|
238
|
+
Creating that user in SQL is not an option either. `auth.users` belongs to
|
|
239
|
+
GoTrue — the password hash, the confirmation state, the identity row — and
|
|
240
|
+
writing it by hand produces an account that looks right and cannot sign in.
|
|
241
|
+
Hence the order: the admin API first, its user id second, `instance_admins`
|
|
242
|
+
and `company_members` third.
|
|
243
|
+
|
|
244
|
+
This is the only reason `--service-role-key` exists. Pass `--admin-user-id`
|
|
245
|
+
instead if the account already exists, and no key is needed.
|
|
246
|
+
|
|
247
|
+
## Commands
|
|
248
|
+
|
|
249
|
+
| Command | What it does |
|
|
250
|
+
|---|---|
|
|
251
|
+
| `ekwo init` | The whole installation, interactive or not. |
|
|
252
|
+
| `ekwo migrate` | Applies the migrations this release adds, after showing the gap — the socle's, then the modules'. Re-applies the reference seeds, which are idempotent. `--no-modules` leaves the modules alone. |
|
|
253
|
+
| `ekwo status` | Schema version installed against available, pending migrations, the instance, its administrators, the country packs it holds and, per company, the pack version it copied. Exits 1 when something is pending. |
|
|
254
|
+
| `ekwo doctor` | Every object this release defines and every privilege it grants, against what the database holds; row level security on every table, a policy on every protected table, no pending migration, no membership pointing at a deleted user, every company with a bank account, statements that tie to their lines, posted entries that balance. Exits 1 on a problem, 0 on warnings. |
|
|
255
|
+
| `ekwo register` | Opt in to security advisories and release notes. Also the retry when the announcement did not go through. |
|
|
256
|
+
| `ekwo unregister` | Opt back out. Clears the address and the date on the instance row. |
|
|
257
|
+
| `ekwo demo` | Loads the sample company. Fictional data, explicit request only. |
|
|
258
|
+
| `ekwo module` | What is installed beside the socle, applies a module's migrations and its country seeds, and turns one on or off for a company. |
|
|
259
|
+
| `ekwo company` | One company leaves an installation with its books — `export` writes an archive anybody can read, as a member under row level security — and arrives in another one alive: `import` takes it in whole or not at all. |
|
|
260
|
+
| `ekwo pack` | Compiles a country pack into its seed, and refuses a seed that is no longer the output of its pack. Runs in a checkout of the repository only. |
|
|
261
|
+
| `ekwo login` | Signs in to an instance as yourself and keeps the session, in your own configuration directory. See [acting as a person](#acting-as-a-person-login-use-whoami). |
|
|
262
|
+
| `ekwo logout` | Ends that session, here and on the instance. |
|
|
263
|
+
| `ekwo use <company>` | Picks the company the next commands run on. |
|
|
264
|
+
| `ekwo whoami` | Who you are on which instance, the companies you can see, and what you may do on the one in use. |
|
|
265
|
+
| `ekwo contact add` / `list` | A customer or a supplier, and finding one again. |
|
|
266
|
+
| `ekwo invoice new` / `invoice line add` | A draft document — any kind, with `--type` — and one more line on it. A draft books nothing. |
|
|
267
|
+
| `ekwo post <document>` | Books it, through `post_document()`. `--dry-run` shows the entry the database would write and writes nothing. |
|
|
268
|
+
| `ekwo payment record` | Money in or out, booked and matched. With `--doc`, against that document. |
|
|
269
|
+
| `ekwo match <transaction> <document>` | A bank statement line pays a document, through `settle_from_statement()`. |
|
|
270
|
+
| `ekwo doc list` / `show` | What exists, and with `--unpaid` what is posted and still owed. See [keeping books](#keeping-books). |
|
|
271
|
+
|
|
272
|
+
There is no `eject`, because there is nothing to eject from. The schema is in
|
|
273
|
+
your database, the migrations are in the repository under AGPL-3.0, and
|
|
274
|
+
`supabase db push` applies them without this CLI ever running again.
|
|
275
|
+
|
|
276
|
+
## What a command answers: `--json` and the exit codes
|
|
277
|
+
|
|
278
|
+
Every command prints for a person by default — aligned columns, colour only on
|
|
279
|
+
a terminal and never when `NO_COLOR` is set, no spinner and no line redrawn in
|
|
280
|
+
place, so the output reads the same in a file or a CI log — and takes `--json`
|
|
281
|
+
for a program.
|
|
282
|
+
|
|
283
|
+
Under `--json` the standard output is **one JSON document and nothing else**;
|
|
284
|
+
the prose still goes by, on the standard error. The document has the same
|
|
285
|
+
shape whatever happened:
|
|
286
|
+
|
|
287
|
+
```json
|
|
288
|
+
{
|
|
289
|
+
"ok": false,
|
|
290
|
+
"command": "module enable",
|
|
291
|
+
"exitCode": 3,
|
|
292
|
+
"warnings": [],
|
|
293
|
+
"error": {
|
|
294
|
+
"kind": "refusal",
|
|
295
|
+
"name": "not_allowed",
|
|
296
|
+
"message": "not_allowed: enabling a module on this company needs company.write",
|
|
297
|
+
"sqlstate": "42501"
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
| Field | |
|
|
303
|
+
|---|---|
|
|
304
|
+
| `ok` | `exitCode` is 0. |
|
|
305
|
+
| `command` | The words that named it: `status`, `pack upgrade`. |
|
|
306
|
+
| `exitCode` | The code the process ends on. |
|
|
307
|
+
| `data` | What the command has to say. Its shape is per command, under `$defs/data/<command>` of the schema. Absent when it failed before having anything to say. |
|
|
308
|
+
| `warnings` | Every warning the command printed, without the colours. |
|
|
309
|
+
| `error` | Only when something went wrong: `kind` (`refusal`, `usage` or `technical`), the `message` word for word, the `name` it starts with when it has one, and the `sqlstate`, `detail` and `hint` when the database gave them. |
|
|
310
|
+
|
|
311
|
+
The shape is published as
|
|
312
|
+
[`schema/output.1.json`](schema/output.1.json), ships in the package, and is
|
|
313
|
+
what `tests/cli/output-contract.test.ts` validates every command against. An
|
|
314
|
+
amount is a decimal string and never a JSON number; a date is ISO 8601.
|
|
315
|
+
|
|
316
|
+
| Exit code | Means |
|
|
317
|
+
|---|---|
|
|
318
|
+
| `0` | Done. |
|
|
319
|
+
| `1` | It failed for a reason that is not the books — the network, a database that does not answer, a bug — **or a check found something**: a `doctor` problem, a pending migration in `status`, a stale seed in `pack check`, a company behind its pack. In the second case `data` says what and there is no `error`. |
|
|
320
|
+
| `2` | The command was called wrong: an unknown option, a missing argument, or a question that needed an answer with no terminal to ask it on. |
|
|
321
|
+
| `3` | **The database refused.** A locked period, a capability you do not hold, a row level security policy, a constraint. The call was well formed and everything worked; the accounting said no. |
|
|
322
|
+
|
|
323
|
+
A refusal is printed as the database wrote it — `period_locked: …`,
|
|
324
|
+
`tax_territory_mismatch: …` — and never rephrased; the CLI does not move a
|
|
325
|
+
date or retry differently to get past one. Its name is the part to match on,
|
|
326
|
+
in a field of its own under `--json`.
|
|
327
|
+
|
|
328
|
+
**No command waits on a question when there is nobody to answer.** Off a
|
|
329
|
+
terminal, or under `--json`, or with `--yes`, a missing answer is exit code 2
|
|
330
|
+
with the flag to pass. That holds underneath the commands too: a prompt that
|
|
331
|
+
is reached with no terminal stops instead of waiting.
|
|
332
|
+
|
|
333
|
+
## Acting as a person: `login`, `use`, `whoami`
|
|
334
|
+
|
|
335
|
+
The commands above install and operate, and connect as the owner of the
|
|
336
|
+
database — they say so when they connect. Anything that keeps books acts as
|
|
337
|
+
**a person**, through the instance's API, under row level security: the same
|
|
338
|
+
route, and the same functions of the schema, as the MCP server.
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
ekwo login --supabase-url https://<ref>.supabase.co --anon-key <publishable key> --email you@example.test
|
|
342
|
+
ekwo whoami
|
|
343
|
+
ekwo use "Example One"
|
|
344
|
+
ekwo whoami --json
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
`login` asks the instance for a session and keeps it. In a directory that has
|
|
348
|
+
an `ekwo.json`, the URL is read from it. The password is prompted, masked, when
|
|
349
|
+
`--password` and `EKWO_PASSWORD` are absent; it is sent to the instance once
|
|
350
|
+
and written nowhere. Signing in again after a session ended is `ekwo login`
|
|
351
|
+
and a password: the profile remembers the rest.
|
|
352
|
+
|
|
353
|
+
**Where the session is kept.** In `$EKWO_CONFIG_DIR`, else
|
|
354
|
+
`$XDG_CONFIG_HOME/ekwo`, else `~/.config/ekwo`, in two files written `0600` in
|
|
355
|
+
a `0700` directory: `profiles.json` says where each profile points and holds
|
|
356
|
+
no token; `credentials.json` holds the access token and the refresh token.
|
|
357
|
+
The CLI refuses — `config_dir_in_repository` — to write either inside a
|
|
358
|
+
repository, where one `git add .` would publish them. The access token lasts
|
|
359
|
+
about an hour and is renewed on its own, ahead of time and again if the
|
|
360
|
+
instance answers 401 anyway; the rotated refresh token replaces the old one
|
|
361
|
+
on disk before the call is retried. A session that cannot be renewed is
|
|
362
|
+
`session_expired`, exit code 2, and the fix is `ekwo login`.
|
|
363
|
+
|
|
364
|
+
**Profiles.** `--profile <name>`, or `EKWO_PROFILE`: a demo instance,
|
|
365
|
+
production, one client of a firm. Each holds one instance, one person and one
|
|
366
|
+
company in use. The profile last signed in to is the one used when none is
|
|
367
|
+
named.
|
|
368
|
+
|
|
369
|
+
**The environment comes first, and touches no file.** With `SUPABASE_URL`,
|
|
370
|
+
`SUPABASE_ANON_KEY` and either `EKWO_ACCESS_TOKEN` or `EKWO_EMAIL` with
|
|
371
|
+
`EKWO_PASSWORD` — the variables the MCP server reads — a command signs in for
|
|
372
|
+
its own duration, reads no profile and writes nothing: a CI job. The
|
|
373
|
+
environment is taken whole: a user there with no instance beside it is a wrong
|
|
374
|
+
call, never a fallback on a profile's instance. It has nowhere to keep a
|
|
375
|
+
company, so pass `--company`.
|
|
376
|
+
|
|
377
|
+
**The company in use.** `ekwo use <name or id>` checks the company against the
|
|
378
|
+
instance, as you, and records it; `--company` names another for one command.
|
|
379
|
+
A company you cannot see is `unknown_company`, and is not named in the
|
|
380
|
+
refusal. Under `--json` every answer of a command that acts as a person
|
|
381
|
+
carries a `context` — the profile, the instance and **the company the answer
|
|
382
|
+
was rendered for**, `null` when none is in use — on success and on a refusal
|
|
383
|
+
alike. A caller that keeps two sets of books reads it before it believes the
|
|
384
|
+
rest.
|
|
385
|
+
|
|
386
|
+
```json
|
|
387
|
+
{ "ok": true, "command": "whoami", "exitCode": 0,
|
|
388
|
+
"context": { "profile": "default", "instance": "https://<ref>.supabase.co",
|
|
389
|
+
"company": { "id": "…", "name": "Example One" } },
|
|
390
|
+
"data": { "user": { "id": "…", "email": "you@example.test" },
|
|
391
|
+
"instance": { "url": "…", "schemaVersion": "0.3.0" },
|
|
392
|
+
"capabilities": ["…"], "companies": [ … ] },
|
|
393
|
+
"warnings": [] }
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
`whoami` works nothing out. The companies are the rows the policies let you
|
|
397
|
+
read, the capabilities are what `member_capabilities()` answers for the
|
|
398
|
+
company in use — the function behind `your_capabilities` in the MCP server —
|
|
399
|
+
and the schema version is `ekwo_schema_version()`.
|
|
400
|
+
|
|
401
|
+
**Never a `service_role` key.** It is refused by name, `service_role_refused`,
|
|
402
|
+
with exit code 2 and before anything is sent, at every door it can arrive by:
|
|
403
|
+
`--anon-key` or `SUPABASE_ANON_KEY`, `EKWO_ACCESS_TOKEN`, a session file
|
|
404
|
+
somebody edited, and `--service-role-key` typed out of habit. The test for it
|
|
405
|
+
and the sentence are in `@ekwo-ai/core`, where the MCP server reads them too.
|
|
406
|
+
`ekwo init` remains the one command that takes that key, to create the first
|
|
407
|
+
user, and never keeps it.
|
|
408
|
+
|
|
409
|
+
| Refusal of the CLI's own | Exit code | Means |
|
|
410
|
+
|---|---|---|
|
|
411
|
+
| `not_signed_in`, `unknown_profile`, `session_expired` | 2 | There is nobody to act as. `ekwo login`. |
|
|
412
|
+
| `service_role_refused`, `config_dir_in_repository`, `missing_configuration` | 2 | The call has to change, not be retried. |
|
|
413
|
+
| `unknown_company`, `ambiguous_company` | 2 | Name it differently, or by its id. |
|
|
414
|
+
| `no_company` | 2 | A verb that keeps books ran with no company in use. `ekwo use`, or `--company`. |
|
|
415
|
+
| `unknown_contact`, `ambiguous_contact`, `unknown_document`, `unknown_account_code`, `unknown_tax_code`, `document_not_draft`, `nothing_open`, `bad_line`, `unknown_field`, `bad_json` | 2 | Decided before the database was asked, by the CLI or by the functions it shares with the MCP server. The call has to change. |
|
|
416
|
+
| `sign_in_failed`, `instance_unreachable` | 1 | The instance declined the address and the password, or did not answer. |
|
|
417
|
+
|
|
418
|
+
A refusal of the database that arrives over this route is still exit code 3:
|
|
419
|
+
PostgREST passes on the SQLSTATE, the detail and the hint, and the CLI reads
|
|
420
|
+
them as it reads a driver's.
|
|
421
|
+
|
|
422
|
+
## Keeping books
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
ekwo contact add "Client Example" --country <cc> --ref crm-42
|
|
426
|
+
ekwo invoice new --contact client --date 2026-06-15 --ref job-7 \
|
|
427
|
+
--line "name=Audit,price=1500.00,account=<account code>,tax=<tax code>"
|
|
428
|
+
ekwo invoice line add job-7 --name Travel --price 250.00 --account <account code>
|
|
429
|
+
ekwo post job-7 --dry-run # the entry the database would write; nothing is written
|
|
430
|
+
ekwo post job-7 # post_document()
|
|
431
|
+
ekwo payment record --doc job-7 --amount 1750.00 --date 2026-06-30 --bank-account <id> --ref bank-1
|
|
432
|
+
ekwo match <bank transaction id> job-9
|
|
433
|
+
ekwo doc list --unpaid --since 2026-06-01 --json
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
They run as the person signed in, on the company in use ([above](#acting-as-a-person-login-use-whoami)),
|
|
437
|
+
and none is ever picked for you: with no company in use a verb ends on
|
|
438
|
+
`no_company`, exit code 2, and `context.company` is `null`.
|
|
439
|
+
|
|
440
|
+
**Each verb is one function, and it is not ours.** The functions live in
|
|
441
|
+
`@ekwo-ai/core` and the MCP server calls the same ones: `contact add` is
|
|
442
|
+
`create_contact`, `invoice new` is `create_document`, `post` is
|
|
443
|
+
`post_document`, `payment record` is `record_payment`, `doc list` and
|
|
444
|
+
`doc show` are `list_documents` and `get_document`. Underneath them the rules
|
|
445
|
+
are the schema's — the balance, the numbering, the locks, the taxes, the
|
|
446
|
+
territory, the tax point. **This CLI computes no amount**: what you type goes
|
|
447
|
+
in as text, what is printed is what came back, and
|
|
448
|
+
`tests/cli/no-rules.test.ts` reads the commands to keep it that way. An amount
|
|
449
|
+
is a decimal string in both directions, `1500.00`.
|
|
450
|
+
|
|
451
|
+
**A refusal is the answer.** A locked period, a policy, a constraint: exit
|
|
452
|
+
code 3, the database's sentence word for word, its name in `error.name`. The
|
|
453
|
+
CLI does not move a date or try something else. What is refused *before* the
|
|
454
|
+
database is asked — an account code that does not exist, a document that is
|
|
455
|
+
not a draft, a document with nothing open — is exit code 2: the call has to
|
|
456
|
+
change.
|
|
457
|
+
|
|
458
|
+
**`--ref`, so that nothing is created twice.** On what creates (`contact add`,
|
|
459
|
+
`invoice new`, `payment record`), `--ref <your reference>` is kept on the row,
|
|
460
|
+
unique per company. The same reference a second time returns what the first
|
|
461
|
+
call created, with `"replayed": true`, and writes nothing — and finishes what
|
|
462
|
+
a dropped connection left half done: a draft whose lines never arrived, a
|
|
463
|
+
payment inserted and never booked. Two callers racing each other are settled
|
|
464
|
+
by the unique index, which refuses the slower one with exit code 3. A
|
|
465
|
+
`<document>` is its id, its number, or the `--ref` it was created under, which
|
|
466
|
+
is how a draft — it has no number yet — is named.
|
|
467
|
+
|
|
468
|
+
**`--dry-run`, where the database can answer without writing.** Today that is
|
|
469
|
+
`post`. `rehearse_post_document()` calls `post_document()` for real inside a
|
|
470
|
+
block it then rolls back, so the entry shown is the one that would be written,
|
|
471
|
+
under the number it would take, and a rehearsal is refused exactly as posting
|
|
472
|
+
would be. No other verb has one, because for no other verb does the database
|
|
473
|
+
know how.
|
|
474
|
+
|
|
475
|
+
**`--stdin`, the form that is authoritative.** One JSON object on the standard
|
|
476
|
+
input, with the fields of the MCP tool of the same meaning (`contact_type`,
|
|
477
|
+
`document_date`, `lines: [{ name, unit_price, account_code, tax_code, … }]`,
|
|
478
|
+
`client_ref`). A field nobody defined is refused rather than dropped. Flags
|
|
479
|
+
given beside it win.
|
|
480
|
+
|
|
481
|
+
```bash
|
|
482
|
+
echo '{"contact":"client","document_date":"2026-06-15","client_ref":"job-8",
|
|
483
|
+
"lines":[{"name":"Review, \"urgent\"","unit_price":"200.00","account_code":"<code>"}]}' \
|
|
484
|
+
| ekwo invoice new --stdin --json
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**`--line`, for a person.** Named fields, never positions: `name`, `price`,
|
|
488
|
+
`qty`, `account`, `tax`, `product`, `unit`, `discount`, `description`; a comma
|
|
489
|
+
inside a value is `\,`. A tax and an account are named by their **code** —
|
|
490
|
+
never a rate, since several taxes share one. The free-text form
|
|
491
|
+
(`"Audit 1 500 EUR@21"`) is not accepted: `1 500` is one number or two, `@21`
|
|
492
|
+
is a rate where the books need a tax, and a currency belongs to the document
|
|
493
|
+
(`--currency`), not to a line.
|
|
494
|
+
|
|
495
|
+
Two values are supplied when nobody gives them, and said when they are:
|
|
496
|
+
`--type` is `sale_invoice`, and `--date` is today on the machine running the
|
|
497
|
+
command. Whether that date may be booked on is the database's decision.
|
|
498
|
+
|
|
499
|
+
## `ekwo module`
|
|
500
|
+
|
|
501
|
+
A module is a Postgres schema beside the socle — `assets` for fixed assets,
|
|
502
|
+
`budgets` for a plan against the ledger. Its migrations travel with this
|
|
503
|
+
package, and `ekwo migrate` applies them by default.
|
|
504
|
+
|
|
505
|
+
```sh
|
|
506
|
+
ekwo module list # what this release carries, and what the database holds
|
|
507
|
+
ekwo module migrate [<code>] # the migrations, and the country seeds they need
|
|
508
|
+
ekwo module enable assets --company "…" # turn it on for one company
|
|
509
|
+
ekwo module disable assets --company "…" # turn it off; nothing it wrote is deleted
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
`enable` and `disable` go through `enable_module()` and `disable_module()`
|
|
513
|
+
rather than writing the table: the guard is in the function, so it applies to
|
|
514
|
+
psql and PostgREST alike. The CLI sets the request claim for an owner of the
|
|
515
|
+
company, the way `ekwo register` does, and `--as-user <uuid>` names another.
|
|
516
|
+
|
|
517
|
+
**One thing this CLI cannot do**, and says so every time: PostgREST serves a
|
|
518
|
+
schema other than `public` only once the project lists it under its exposed
|
|
519
|
+
schemas. That is a setting of the API, not of the database, so `ekwo module
|
|
520
|
+
enable` prints the line to add — Supabase dashboard → Project Settings → API,
|
|
521
|
+
or `[api] schemas` in `supabase/config.toml`.
|
|
522
|
+
|
|
523
|
+
**Before `supabase db push`**, run `ekwo migrate --no-modules`. The Supabase
|
|
524
|
+
CLI knows the socle's migration files and not a module's, so it would report
|
|
525
|
+
them as history it has no file for.
|
|
526
|
+
|
|
527
|
+
## `ekwo company`
|
|
528
|
+
|
|
529
|
+
A firm keeps several companies in one installation, and each of them belongs to
|
|
530
|
+
somebody. These two commands are how one of them leaves, and arrives somewhere
|
|
531
|
+
else.
|
|
532
|
+
|
|
533
|
+
```sh
|
|
534
|
+
ekwo company export "My Company" --out ./my-company # manifest.json + data/<schema>.<table>.jsonl
|
|
535
|
+
ekwo company import ./my-company --owner <user id> # whole, or not at all
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
**`export` runs as a member, under row level security**, although the
|
|
539
|
+
connection belongs to the owner of the database: inside one transaction the CLI
|
|
540
|
+
steps down to `authenticated` with the claim of the member it acts for —
|
|
541
|
+
`--as-user`, an owner of the company by default. That member needs
|
|
542
|
+
`company.export`, which the `owner` and `client` presets hold. An archive is
|
|
543
|
+
whole or it is not written: a member who may not read one of the tables is
|
|
544
|
+
refused, by table, with exit code 3. The act is written on the audit trail of
|
|
545
|
+
the company.
|
|
546
|
+
|
|
547
|
+
**`import` is for the installer or an administrator of the installation**
|
|
548
|
+
(`--as-user`), the two who may create a company. The files are checked against
|
|
549
|
+
the manifest before the database is asked anything; then `import_company()`
|
|
550
|
+
takes all of it or none of it. A company already there is refused — exit code
|
|
551
|
+
3, `company_already_here` — which is also what running the command twice gets.
|
|
552
|
+
Members do not travel: `--owner` names the first one.
|
|
553
|
+
|
|
554
|
+
**The files the attachments point at are not carried.** They are in the storage
|
|
555
|
+
bucket, not in the database; `manifest.json` lists them and both commands say
|
|
556
|
+
how many are left to copy.
|
|
557
|
+
|
|
558
|
+
The format, what travels and what does not, and every refusal are in
|
|
559
|
+
[`docs/company-archive.md`](../../docs/company-archive.md).
|
|
560
|
+
|
|
561
|
+
## `ekwo doctor`
|
|
562
|
+
|
|
563
|
+
What a healthy installation is true of, and nothing in the schema can enforce
|
|
564
|
+
on its own. It reads and reports; it never repairs, because the fix for a
|
|
565
|
+
missing policy is a migration and the fix for an orphaned membership is a
|
|
566
|
+
decision about who should have access.
|
|
567
|
+
|
|
568
|
+
```sh
|
|
569
|
+
ekwo doctor --db-url "$URL" # readable
|
|
570
|
+
ekwo doctor --db-url "$URL" --json # the whole report, findings included
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**The `catalogue` check compares your database to an inventory of everything
|
|
574
|
+
this release defines** — tables and their columns, views, functions with their
|
|
575
|
+
identity arguments, policies, triggers and types. That inventory is
|
|
576
|
+
[`assets/expected-objects.json`](assets/expected-objects.json), generated from
|
|
577
|
+
the migrations themselves and shipped inside this package, so it cannot be a
|
|
578
|
+
list somebody forgot to update. In `--json` output it is the check named
|
|
579
|
+
`catalogue`. Four outcomes, and they are not the same thing:
|
|
580
|
+
|
|
581
|
+
| Finding | What it means | Severity |
|
|
582
|
+
|---|---|---|
|
|
583
|
+
| Missing | The installation is behind or has been damaged. | Problem |
|
|
584
|
+
| Extra | Your own table, function or trigger. Reported so you know it is there. | Information |
|
|
585
|
+
| Extra or missing **policy** on a table of this schema | Row level security is the security model. A policy that is gone closes everything; one that was added is a grant nobody reviewed. | Problem |
|
|
586
|
+
| A column whose type has moved | The schema was patched by hand. Reported as **changed**, not as missing: "missing" would send you looking for a migration that did land. | Problem |
|
|
587
|
+
|
|
588
|
+
A module's objects are required only of a database that carries the module.
|
|
589
|
+
One you never installed is named and skipped.
|
|
590
|
+
|
|
591
|
+
**A database older than this CLI is still compared.** The report says which
|
|
592
|
+
schema version the inventory describes and which one the database reports, and
|
|
593
|
+
goes on to list what differs — refusing to look would be refusing the case the
|
|
594
|
+
check exists for.
|
|
595
|
+
|
|
596
|
+
**Exit codes.** `0` when there is no problem, warnings and information
|
|
597
|
+
included; `1` when there is at least one problem, or when the schema is not
|
|
598
|
+
installed at all. Nothing else. So `ekwo doctor` is usable as a deployment
|
|
599
|
+
gate, and an operator's own extra table never turns a pipeline red.
|
|
600
|
+
|
|
601
|
+
**What the catalogue does not cover.** Constraints, indexes and the bodies of
|
|
602
|
+
functions. A dropped unique index is real damage and this check will not see
|
|
603
|
+
it: the question it answers is "is the object there, and is it still that
|
|
604
|
+
shape". `docs/schema.md` lists the constraints for a human reader, and the
|
|
605
|
+
argument against putting them in the inventory is that each is an order of
|
|
606
|
+
magnitude more text for a diff that would move on every Postgres upgrade — and
|
|
607
|
+
an inventory whose diff nobody reads is worth nothing.
|
|
608
|
+
|
|
609
|
+
**The `grants` check compares the privileges**, from the same inventory: the
|
|
610
|
+
`grants` section of each schema says which of `anon`, `authenticated` and
|
|
611
|
+
`service_role` may reach each table, view and function, and with which verbs.
|
|
612
|
+
Its own check rather than a category of `catalogue`, because the rule is not
|
|
613
|
+
the same.
|
|
614
|
+
|
|
615
|
+
| Finding | What it means | Severity |
|
|
616
|
+
|---|---|---|
|
|
617
|
+
| A privilege the release grants and the database does not hold | Nothing else notices it, and it reaches a client as `permission denied for table companies`. | Problem |
|
|
618
|
+
| Any privilege `anon` holds beyond what the release grants | The anonymous role reaches the ten policy helpers and no table. One more is a surface nobody reviewed. | Problem |
|
|
619
|
+
| A privilege `authenticated` or `service_role` holds and the release does not grant | Usually a local customisation. Row level security is then the only thing refusing a verb the schema meant to withhold. | Warning |
|
|
620
|
+
| A default privilege still standing on a schema | A privilege that comes from there comes from something no migration wrote, and a recreated schema takes it away. | Warning |
|
|
621
|
+
|
|
622
|
+
In a checkout, `npm run inventory` regenerates the inventory from the
|
|
623
|
+
migrations; the CI regenerates it and fails on any difference, the way it does
|
|
624
|
+
for `docs/schema.md`, and a second job checks that the copy shipped in `dist`
|
|
625
|
+
is the one in the repository.
|
|
626
|
+
|
|
627
|
+
## `ekwo pack`, in a checkout
|
|
628
|
+
|
|
629
|
+
A country is data: `packs/<cc>/` holds a manifest, the chart of accounts as
|
|
630
|
+
CSV, the taxes and where they post, the boxes of the declaration, the financial
|
|
631
|
+
statements, the sentences the country requires on an invoice, the translations,
|
|
632
|
+
and a year of books with the figures it produces. The compiler turns one into
|
|
633
|
+
`supabase/seed/<n>_pack_<cc>.sql`, which is committed — and, where a pack
|
|
634
|
+
carries a section for a module, into
|
|
635
|
+
`supabase/seed/modules/<code>/<n>_pack_<cc>.sql`, applied by the module
|
|
636
|
+
migration runner and by nothing else.
|
|
637
|
+
|
|
638
|
+
```sh
|
|
639
|
+
ekwo pack list # the packs this checkout carries, and their certification
|
|
640
|
+
ekwo pack build be # write supabase/seed/10_pack_be.sql from packs/be
|
|
641
|
+
ekwo pack build --all
|
|
642
|
+
ekwo pack check be # validate one pack and compare its seed
|
|
643
|
+
ekwo pack check --all # exit 1 if a committed seed is not the output of its pack
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
`check` validates every file of the pack against
|
|
647
|
+
[`packs/schema/pack.1.json`](../../packs/schema/pack.1.json) and against the
|
|
648
|
+
rest of the pack, then compares the committed seed with what the compiler makes
|
|
649
|
+
of it now. It is what the CI runs, so the SQL cannot drift from the pack. Every
|
|
650
|
+
rule it applies is listed in [`docs/packs.md`](../../docs/packs.md), under
|
|
651
|
+
"What `ekwo pack check` refuses".
|
|
652
|
+
|
|
653
|
+
Neither command touches a database: the seed is applied by `ekwo init`,
|
|
654
|
+
`supabase db push` or `psql -f`, like every other seed. A published
|
|
655
|
+
installation has the compiled seeds and no `packs/` folder, and the command
|
|
656
|
+
says so rather than guessing.
|
|
657
|
+
|
|
658
|
+
Two commands under `ekwo pack` do the opposite and read an installation rather
|
|
659
|
+
than a checkout, so they take a connection and work without `packs/`:
|
|
660
|
+
|
|
661
|
+
```sh
|
|
662
|
+
ekwo pack status --db-url "$EKWO_DB_URL" # which pack version each company copied
|
|
663
|
+
ekwo pack upgrade "My Company" --db-url "…" # move it to the version this installation holds
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
`status` changes nothing and exits 1 while a company is behind, so a scheduled
|
|
667
|
+
job can ask. `upgrade` applies an addition and a closed validity by itself,
|
|
668
|
+
lists everything else for a person to read, and never removes anything from a
|
|
669
|
+
company's books; `--apply` is what accepts the differences it listed.
|
|
670
|
+
|
|
671
|
+
## Flags
|
|
672
|
+
|
|
673
|
+
Every command takes the connection flags:
|
|
674
|
+
|
|
675
|
+
| Flag | Meaning |
|
|
676
|
+
|---|---|
|
|
677
|
+
| `--db-url <url>` | Postgres connection string. The reliable way. |
|
|
678
|
+
| `--project-ref <ref>` | With `--db-password` and `--db-region`, the session pooler host. |
|
|
679
|
+
| `--db-password <pw>` | Database password. Prompted, masked, when omitted. |
|
|
680
|
+
| `--db-region <region>` | With `--project-ref`, the session pooler in that region. Both generation prefixes are tried and the one that answers is kept. |
|
|
681
|
+
| `--supabase-url <url>` | `https://<ref>.supabase.co`. Needed only to create a user. |
|
|
682
|
+
| `--service-role-key <key>` | Needed only to create a user. |
|
|
683
|
+
| `--yes`, `-y` | Never ask a question. Everything must come from flags or the environment. |
|
|
684
|
+
|
|
685
|
+
`--project-ref` with `--db-password` and `--db-region` builds the session
|
|
686
|
+
pooler host. It no longer guesses which one: the pooler hostname carries a
|
|
687
|
+
generation prefix as well as a region, and the region does not determine it,
|
|
688
|
+
so `aws-0-<region>` and `aws-1-<region>` are both opened on port 5432 and the
|
|
689
|
+
one that answers is kept and printed. Without `--db-region` nothing is
|
|
690
|
+
derived — the CLI asks for the connection string, because the direct host
|
|
691
|
+
`db.<ref>.supabase.co` is IPv6-only on recent projects and deriving it
|
|
692
|
+
silently produces a hang rather than an error. `--db-url`, copied from the
|
|
693
|
+
dashboard under Connect → Session pooler, is the form that is never derived.
|
|
694
|
+
|
|
695
|
+
`ekwo init` adds:
|
|
696
|
+
|
|
697
|
+
| Flag | Meaning |
|
|
698
|
+
|---|---|
|
|
699
|
+
| `--country <cc>` | Which country pack: its chart of accounts, its journals, its taxes and its declaration. One of the packs the database holds — `ekwo pack list` names them, and there is no default. |
|
|
700
|
+
| `--chart <code>` | Which chart of accounts, where the country publishes several. Required outside a terminal when it does. |
|
|
701
|
+
| `--org <name>` | Your organisation, written on the instance row. |
|
|
702
|
+
| `--company <name>` | The first company. Defaults to `--org`. |
|
|
703
|
+
| `--admin-email <address>` | The first administrator, created in your Supabase Auth. |
|
|
704
|
+
| `--admin-password <pw>` | Their password. Omitted, an invite link is generated and printed. |
|
|
705
|
+
| `--admin-user-id <uuid>` | Use an account that already exists, instead of creating one. |
|
|
706
|
+
| `--fiscal-year <year>` | Calendar year of the first financial year. Defaults to this year. |
|
|
707
|
+
| `--fiscal-year-start <date>` | The day that year opens, as `YYYY-MM-DD`. Needed only where the pack names no usual opening month; the two packs shipped both open on the calendar year. |
|
|
708
|
+
| `--currency <code>` | Currency of the company. Defaults to what the country model says: `EUR` for both countries shipped. |
|
|
709
|
+
| `--language <xx>` | Language of the books, two letters. Defaults to `country_defaults.language_default`, which the pack fills. It decides which label of the pack lands on each account; the others are kept in `name_i18n`. |
|
|
710
|
+
| `--iban <iban>` | Creates the main bank account, wired to the bank journal and its ledger account. Omitted, no bank account is created and `ekwo doctor` says so. |
|
|
711
|
+
| `--bic <bic>` | Optional, on that account. |
|
|
712
|
+
| `--bank-name <name>` | Optional. It also names the account in the books. |
|
|
713
|
+
| `--demo` | Also load the sample company. |
|
|
714
|
+
| `--register` | Register without being asked. `--register-email` sets the address. |
|
|
715
|
+
| `--registry-url <url>` | Where the registration is announced. |
|
|
716
|
+
|
|
717
|
+
Every command takes `--json`; see [what a command answers](#what-a-command-answers---json-and-the-exit-codes).
|
|
718
|
+
`ekwo migrate` takes `--skip-seeds`.
|
|
719
|
+
|
|
720
|
+
## Environment variables
|
|
721
|
+
|
|
722
|
+
| Variable | Same as |
|
|
723
|
+
|---|---|
|
|
724
|
+
| `EKWO_DB_URL` | `--db-url`. `SUPABASE_DB_URL` also works. |
|
|
725
|
+
| `EKWO_DB_PASSWORD` | `--db-password` |
|
|
726
|
+
| `SUPABASE_URL` | `--supabase-url` |
|
|
727
|
+
| `SUPABASE_SERVICE_ROLE_KEY` | `--service-role-key`. `ekwo init` only. |
|
|
728
|
+
| `SUPABASE_ANON_KEY` | `--anon-key` |
|
|
729
|
+
| `EKWO_EMAIL`, `EKWO_PASSWORD` | Sign in for one command, writing nothing. `ekwo login` reads them too. |
|
|
730
|
+
| `EKWO_ACCESS_TOKEN` | The same, with a session token already in hand. It is not renewed. |
|
|
731
|
+
| `EKWO_PROFILE` | `--profile` |
|
|
732
|
+
| `EKWO_CONFIG_DIR` | Where profiles and sessions are kept. |
|
|
733
|
+
| `EKWO_REGISTRY_URL` | `--registry-url`. Default `https://api.ekwo.ai/v1/registrations`. |
|
|
734
|
+
| `NO_COLOR` | Plain output. |
|
|
735
|
+
|
|
736
|
+
See [`.env.example`](../../.env.example) at the root of the repository.
|
|
737
|
+
|
|
738
|
+
## Secrets
|
|
739
|
+
|
|
740
|
+
The CLI never writes a password or a key to disk. The database password and
|
|
741
|
+
the `service_role` key are read from a flag, an environment variable or a
|
|
742
|
+
masked prompt, used, and forgotten; nothing is in `ekwo.json` but the project
|
|
743
|
+
URL, the country and the schema version.
|
|
744
|
+
|
|
745
|
+
One thing is kept, since `ekwo login`: the session of the person who signed
|
|
746
|
+
in — an access token and the refresh token that renews it — in their own
|
|
747
|
+
configuration directory, readable by them alone, and refused anywhere inside a
|
|
748
|
+
repository. It is in a file and not in the keychain of the operating system;
|
|
749
|
+
`ekwo logout` removes it and ends it on the instance, and a job that should
|
|
750
|
+
keep nothing sets the environment variables instead.
|
|
751
|
+
|
|
752
|
+
It has one runtime dependency from outside this repository, the Postgres
|
|
753
|
+
driver. Argument parsing, prompts and the masked input are a few dozen lines
|
|
754
|
+
each in this package rather than packages from the registry, because
|
|
755
|
+
everything this CLI is handed is a secret and every dependency added is one
|
|
756
|
+
more thing that could read it. The other dependency is `@ekwo-ai/core`, this
|
|
757
|
+
repository's own, where the CLI and the MCP server read a refusal of the
|
|
758
|
+
database the same way.
|
|
759
|
+
|
|
760
|
+
## Registering with Ekwo
|
|
761
|
+
|
|
762
|
+
At the end of `ekwo init` you are asked:
|
|
763
|
+
|
|
764
|
+
> Register this installation with Ekwo to receive security advisories and
|
|
765
|
+
> release notes?
|
|
766
|
+
|
|
767
|
+
The default answer is no, and no is supported forever. Community works
|
|
768
|
+
unregistered: nothing in the schema and nothing in this CLI reads
|
|
769
|
+
`contact_email` or `registered_at` to decide what you may do, and `edition`
|
|
770
|
+
gates no feature.
|
|
771
|
+
|
|
772
|
+
If you say yes, two things happen, independently. `register_instance(email)`
|
|
773
|
+
writes the address and a date onto your instance row, and a POST goes to
|
|
774
|
+
`EKWO_REGISTRY_URL` carrying exactly six fields:
|
|
775
|
+
|
|
776
|
+
```json
|
|
777
|
+
{
|
|
778
|
+
"instance_id": "…",
|
|
779
|
+
"organization": "My Organisation",
|
|
780
|
+
"country": "BE",
|
|
781
|
+
"edition": "community",
|
|
782
|
+
"schema_version": "0.2.0",
|
|
783
|
+
"contact_email": "you@example.com"
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
No ledger data, no user list, no connection string. `instance_id` is generated
|
|
788
|
+
locally by your own database and is not a licence key: no code path anywhere
|
|
789
|
+
checks it.
|
|
790
|
+
|
|
791
|
+
**The endpoint does not exist yet.** A failed POST is a soft message, not a
|
|
792
|
+
failed install: the local record stands and `ekwo register` retries it later.
|
|
793
|
+
`ekwo unregister` clears the local fields; it sends nothing, because the CLI
|
|
794
|
+
only holds the local row.
|
|
795
|
+
|
|
796
|
+
## Testing it against a real project
|
|
797
|
+
|
|
798
|
+
The test suite runs against Postgres compiled to WebAssembly, so it proves the
|
|
799
|
+
migration runner, the installation sequence and the checks without a Supabase
|
|
800
|
+
project. Four things it cannot prove: the network driver, PostgREST, GoTrue,
|
|
801
|
+
and the extensions a hosted project has.
|
|
802
|
+
|
|
803
|
+
**The automated way.** From a checkout of the repository, against an empty
|
|
804
|
+
project you can throw away:
|
|
805
|
+
|
|
806
|
+
```sh
|
|
807
|
+
npm run e2e:supabase
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
It installs, migrates, upgrades the pack, signs in, books, files the
|
|
811
|
+
declaration and closes the year, and prints a pass/fail table with **how long
|
|
812
|
+
each step took** — which is the number worth reading, because what matters
|
|
813
|
+
about a release is which step holds it rather than the total. Everything comes
|
|
814
|
+
from the environment and no secret reaches the output; it refuses a database
|
|
815
|
+
that already holds an `instance` row, and `--reset` empties a throwaway project
|
|
816
|
+
so a failed run can be replayed.
|
|
817
|
+
|
|
818
|
+
Point `EKWO_E2E_PREVIOUS` at the last tag to make the run upgrade an
|
|
819
|
+
installation instead of creating one. The packages are not on npm yet, so it
|
|
820
|
+
takes **a path to a built binary of an older checkout** rather than a version:
|
|
821
|
+
|
|
822
|
+
```sh
|
|
823
|
+
git worktree add /tmp/prev v0.2.0
|
|
824
|
+
(cd /tmp/prev && npm ci && npm run build)
|
|
825
|
+
EKWO_E2E_PREVIOUS=/tmp/prev/packages/cli/dist/bin.js npm run e2e:supabase
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
[`docs/releasing.md`](../../docs/releasing.md) lists every variable it reads and
|
|
829
|
+
every refusal it makes. It is run by hand before a release is tagged, never by
|
|
830
|
+
the CI.
|
|
831
|
+
|
|
832
|
+
**By hand**, if you want to watch each step:
|
|
833
|
+
|
|
834
|
+
```sh
|
|
835
|
+
npm install && npm run build
|
|
836
|
+
|
|
837
|
+
# 1. A project you can throw away. Note its ref, password, URL and key.
|
|
838
|
+
# --chart and --language are required here and not optional: the Belgian
|
|
839
|
+
# pack publishes two charts of accounts and four languages, and `ekwo init`
|
|
840
|
+
# refuses to pick either for you when there is nobody to ask.
|
|
841
|
+
node packages/cli/dist/bin.js init \
|
|
842
|
+
--db-url "postgresql://postgres.SCRATCHREF:PASSWORD@aws-1-REGION.pooler.supabase.com:5432/postgres" \
|
|
843
|
+
--supabase-url "https://SCRATCHREF.supabase.co" \
|
|
844
|
+
--service-role-key "$KEY" \
|
|
845
|
+
--country BE --chart default --language fr \
|
|
846
|
+
--org "Scratch" --company "Scratch BV" \
|
|
847
|
+
--admin-email "you@example.com" --admin-password "a-long-password" \
|
|
848
|
+
--fiscal-year 2026 --iban "BE71096123456769" --yes
|
|
849
|
+
|
|
850
|
+
# 2. Everything should be green, and nothing pending.
|
|
851
|
+
node packages/cli/dist/bin.js status --db-url "$URL"
|
|
852
|
+
node packages/cli/dist/bin.js doctor --db-url "$URL"
|
|
853
|
+
|
|
854
|
+
# 3. The history must be the Supabase one: this should report no difference.
|
|
855
|
+
supabase link --project-ref SCRATCHREF
|
|
856
|
+
supabase migration list
|
|
857
|
+
|
|
858
|
+
# 4. And the other direction: db push finds nothing left to do.
|
|
859
|
+
supabase db push
|
|
860
|
+
|
|
861
|
+
# 5. Run init again. Every step should say it was already there.
|
|
862
|
+
node packages/cli/dist/bin.js init --db-url "$URL" --country BE \
|
|
863
|
+
--chart default --language fr \
|
|
864
|
+
--org "Scratch" --company "Scratch BV" --admin-email "you@example.com" \
|
|
865
|
+
--admin-user-id "<the uuid from step 1>" --fiscal-year 2026 --yes
|
|
866
|
+
|
|
867
|
+
# 6. Sign in as the administrator and confirm row level security really binds:
|
|
868
|
+
# a company you were not invited to must be invisible.
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
## Licence
|
|
872
|
+
|
|
873
|
+
[AGPL-3.0-only](../../LICENSE) © Ekwo AI.
|