ekwo-os 0.11.0 → 0.12.0

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.
Files changed (67) hide show
  1. package/README.md +223 -794
  2. package/dist/assets/expected-objects.json +728 -2
  3. package/dist/assets/migrations/20261007190000_an_export_fits_a_key_request.sql +574 -0
  4. package/dist/assets/migrations/20261007192418_a_filing_is_proved_by_its_hash.sql +505 -0
  5. package/dist/assets/migrations/20261007200000_schema_version_0_11_1.sql +38 -0
  6. package/dist/assets/migrations/20261010181201_schema_version_0_12_0.sql +40 -0
  7. package/dist/assets/modules/einvoicing/module.json +12 -0
  8. package/dist/assets/modules/einvoicing/supabase/migrations/20261009213006_einvoicing.sql +844 -0
  9. package/dist/assets/seed/00_currencies.sql +20 -1
  10. package/dist/assets/seed/00_territories.sql +471 -0
  11. package/dist/assets/seed/10_pack_be.sql +89 -28
  12. package/dist/assets/seed/11_pack_fr.sql +4 -2
  13. package/dist/assets/seed/125_pack_pk.sql +717 -0
  14. package/dist/assets/seed/126_pack_bd.sql +733 -0
  15. package/dist/assets/seed/127_pack_lk.sql +639 -0
  16. package/dist/assets/seed/128_pack_am.sql +603 -0
  17. package/dist/assets/seed/129_pack_az.sql +604 -0
  18. package/dist/assets/seed/130_pack_hn.sql +622 -0
  19. package/dist/assets/seed/131_pack_sv.sql +601 -0
  20. package/dist/assets/seed/132_pack_ni.sql +599 -0
  21. package/dist/assets/seed/133_pack_et.sql +583 -0
  22. package/dist/assets/seed/134_pack_mu.sql +641 -0
  23. package/dist/assets/seed/135_pack_mz.sql +887 -0
  24. package/dist/assets/seed/136_pack_ao.sql +789 -0
  25. package/dist/assets/seed/137_pack_zm.sql +600 -0
  26. package/dist/assets/seed/138_pack_bm.sql +432 -0
  27. package/dist/assets/seed/139_pack_ky.sql +429 -0
  28. package/dist/assets/seed/140_pack_vg.sql +440 -0
  29. package/dist/assets/seed/141_pack_gg.sql +531 -0
  30. package/dist/assets/seed/142_pack_qa.sql +444 -0
  31. package/dist/assets/seed/143_pack_kw.sql +434 -0
  32. package/dist/assets/seed/144_pack_bn.sql +448 -0
  33. package/dist/assets/seed/145_pack_mo.sql +474 -0
  34. package/dist/assets/seed/146_pack_je.sql +669 -0
  35. package/dist/assets/seed/147_pack_zw.sql +669 -0
  36. package/dist/assets/seed/45_pack_ar.sql +16 -2
  37. package/dist/assets/seed/46_pack_cl.sql +6 -6
  38. package/dist/assets/seed/68_pack_cz.sql +8 -8
  39. package/dist/assets/seed/72_pack_ro.sql +14 -14
  40. package/dist/assets/seed/modules/corporate_tax/10_pack_be.sql +1 -1
  41. package/dist/assets/seed/modules/corporate_tax/11_pack_fr.sql +1 -1
  42. package/dist/assets/seed/modules/fixed_assets/10_pack_be.sql +1 -1
  43. package/dist/assets/seed/modules/fixed_assets/11_pack_fr.sql +1 -1
  44. package/dist/cli.d.ts +3 -2
  45. package/dist/cli.d.ts.map +1 -1
  46. package/dist/cli.js +37 -2
  47. package/dist/cli.js.map +1 -1
  48. package/dist/commands/document.d.ts.map +1 -1
  49. package/dist/commands/document.js +59 -4
  50. package/dist/commands/document.js.map +1 -1
  51. package/dist/commands/einvoice.d.ts +23 -0
  52. package/dist/commands/einvoice.d.ts.map +1 -0
  53. package/dist/commands/einvoice.js +185 -0
  54. package/dist/commands/einvoice.js.map +1 -0
  55. package/dist/commands/proof.d.ts +31 -0
  56. package/dist/commands/proof.d.ts.map +1 -0
  57. package/dist/commands/proof.js +175 -0
  58. package/dist/commands/proof.js.map +1 -0
  59. package/dist/output.js +2 -2
  60. package/dist/output.js.map +1 -1
  61. package/dist/pack/read.d.ts.map +1 -1
  62. package/dist/pack/read.js +39 -0
  63. package/dist/pack/read.js.map +1 -1
  64. package/dist/schema.d.ts +1 -1
  65. package/dist/schema.js +1 -1
  66. package/package.json +3 -3
  67. package/schema/output.1.json +124 -0
package/README.md CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  The installer and the operator's tool for [Ekwo OS](https://github.com/Ekwo-ai/ekwo-os).
4
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.
5
+ books: the schema, every country pack, the first administrator, the first
6
+ company and its first financial year. The same command line then keeps books,
7
+ as a signed-in person.
7
8
 
8
9
  ```sh
9
10
  npx -y ekwo-os@latest init
@@ -12,15 +13,11 @@ npx -y ekwo-os@latest init
12
13
  The package is `ekwo-os` and the command it installs is `ekwo`:
13
14
  `npx -y ekwo-os@latest <command>` runs it without installing anything, and
14
15
  after `npm install -g ekwo-os` every example below that starts with `ekwo `
15
- works as written. Keep the version in the command: run from inside a clone of
16
- the Ekwo repository, a bare `npx ekwo-os` finds the workspace package of the
17
- same name, which links no command, and answers `ekwo: command not found`. With
18
- `@latest`, `npx` fetches the published CLI wherever it is started; the clone's
19
- own is `node packages/cli/dist/bin.js`, after `npm run build`.
16
+ works as written. Keep `@latest` in the command: from inside a clone of the
17
+ repository, a bare `npx ekwo-os` finds the workspace package and answers
18
+ `ekwo: command not found`.
20
19
 
21
- You need Node 20 or later. That is the whole list. The Supabase CLI is not
22
- required — this talks to Postgres directly — and Docker is not required
23
- either.
20
+ You need Node 20 or later. No Supabase CLI, no Docker.
24
21
 
25
22
  ## From a free Supabase account to a first invoice
26
23
 
@@ -28,29 +25,14 @@ either.
28
25
  is enough to start. Ekwo does not create it, does not pay for it and has no
29
26
  access to it: it is yours from the first row.
30
27
  2. **Copy two things** from the dashboard:
31
- - Project Settings → Database → **Connection string** (URI). It contains
32
- your database password.
33
- - Project Settings → API Keys → the **secret key** (`sb_secret_…`), and the
34
- **Project URL**. A project created before the new keys has the legacy
35
- **`service_role` key** instead, and it works the same. These are used
36
- once, to create the first administrator in your own Supabase Auth, and
37
- are never written to disk.
38
-
39
- **Take the pooler string, not the direct one, unless you know you have
40
- IPv6.** The direct host `db.<ref>.supabase.co` resolves to an IPv6 address
41
- only on any recent project, so from an IPv4-only network it simply never
42
- connects. The session pooler answers on IPv4 and supports everything a
43
- migration needs:
44
-
45
- ```
46
- postgresql://postgres.<ref>:<password>@aws-1-<region>.pooler.supabase.com:5432/postgres
47
- ```
48
-
49
- The region is in the hostname the dashboard gives you, and so is the
50
- generation prefix, which the region does not determine: verified on a real
51
- project on 11 September 2026 in `eu-west-3`, where `aws-1` worked and
52
- `aws-0` answered "Tenant or user not found". Copy the line from the
53
- dashboard — Connect → Session pooler — rather than building it by hand.
28
+ - **Connect → Session pooler**: the connection string. It contains your
29
+ database password. Take the session pooler line rather than the direct
30
+ one: the direct host answers on IPv6 only, which many networks cannot
31
+ reach.
32
+ - **Project Settings → API Keys**: the **secret key** (`sb_secret_…`, or
33
+ the legacy `service_role` key on an older project) and the **Project
34
+ URL**. They are used once, to create the first administrator in your own
35
+ Supabase Auth, and are never written to disk.
54
36
  3. **Run the installer.**
55
37
 
56
38
  ```sh
@@ -60,72 +42,41 @@ either.
60
42
  It asks for the connection string, the country, the chart of accounts and
61
43
  the language where the pack offers a choice, your organisation, the
62
44
  currency, the first company, the address of the first administrator and —
63
- optionally — the identifier of your main bank account (an IBAN, a routing number and an account number, a sort code… whatever your country's banks use), then does the rest. Five
64
- to ten seconds on a free project. Nothing is preselected for you on the
45
+ optionally — your main bank account, written the way your country's banks
46
+ write it. A few seconds on a free project. Nothing is preselected on the
65
47
  three questions whose wrong answer is expensive: the country, the chart and
66
48
  the language.
67
49
 
68
- 4. **Sign in** to your project as that administrator and start booking — in
69
- the web application, [Ekwo Cloud](https://cloud.ekwo.ai), which opens your
70
- own instance, or through the REST API Supabase generates from the schema,
71
- `psql` or `@ekwo-ai/core`.
50
+ 4. **Sign in** as that administrator and start booking — in the web
51
+ application, [Ekwo Cloud](https://cloud.ekwo.ai), which opens your own
52
+ instance; through an AI agent and the [MCP server](../mcp/README.md); from
53
+ this command line; or through the REST API Supabase generates.
72
54
 
73
- 5. **Do the four things below**, while the dashboard is still open. The
74
- installer prints them at the end of a successful run, because three of them
75
- are settings of your project rather than rows in your database, and nothing
76
- holding a connection string can reach them.
55
+ 5. **Do the four things below** while the dashboard is still open. The
56
+ installer prints them at the end of a successful run.
77
57
 
78
58
  Everything above in one non-interactive line:
79
59
 
80
60
  ```sh
81
61
  npx -y ekwo-os@latest init \
82
- --db-url "postgresql://postgres.YOURREF:PASSWORD@aws-1-eu-west-3.pooler.supabase.com:5432/postgres" \
62
+ --db-url "postgresql://postgres.YOURREF:PASSWORD@aws-1-REGION.pooler.supabase.com:5432/postgres" \
83
63
  --supabase-url "https://YOURREF.supabase.co" \
84
64
  --service-role-key "$SUPABASE_SERVICE_ROLE_KEY" \
85
- --country BE \
86
- --chart default \
87
- --language fr \
65
+ --country <cc> \
88
66
  --org "My Organisation" \
89
67
  --company "My Company" \
90
68
  --admin-email "you@example.com" \
91
69
  --admin-password "a-long-password" \
92
70
  --fiscal-year 2026 \
93
- --bank-identifier "BE71096123456769" \
94
71
  --yes
95
72
  ```
96
73
 
97
- `--chart` and `--language` are in that line because the Belgian pack offers a
98
- choice on both, and `--yes` means there is nobody to ask. See "Installing
99
- without a terminal" below.
100
-
101
- ## Installing without a terminal
102
-
103
- `--yes` turns off every question, and then every answer has to arrive as a flag
104
- or an environment variable. Two of them are worth knowing about before you
105
- write the script, because `ekwo init` **refuses rather than picking one for
106
- you**:
107
-
108
- - **the chart of accounts**, where the country publishes more than one. Belgium
109
- publishes two, a company chart and an association chart. Pass `--chart`; the
110
- refusal lists the codes the pack carries.
111
- - **the language of the books**, where the pack publishes more than one. Pass
112
- `--language`; the refusal lists them. The choice decides which label of the
113
- pack lands in `accounts.name`, and the others stay beside it in `name_i18n`,
114
- so it is not irreversible — but it is not a question a script should answer
115
- by accident either.
116
-
117
- `--country` behaves the same way and has no default at all: the refusal names
118
- the packs the database holds. A preselected country is a chart of accounts
119
- nobody chose.
120
-
121
- The same is true of the financial year: a pack that declares no usual opening
122
- month makes `--fiscal-year-start` required. Most packs name the calendar year
123
- in `defaults.fiscal_year_default`; one that names none — the United Kingdom's,
124
- where a company's year ends on the accounting reference date it chose — asks
125
- for the day.
126
-
127
- `ekwo company new` asks the same questions about every other company, and
128
- refuses the same way.
74
+ With `--yes` nobody is asked anything, so **`ekwo init` refuses rather than
75
+ choosing for you**: no country without `--country`, and `--chart`,
76
+ `--language` or `--fiscal-year-start` wherever the country's pack offers more
77
+ than one answer. Each refusal lists the values the pack accepts.
78
+ `ekwo pack describe <cc>` (in a clone) or the country's page on
79
+ [ekwo.ai/countries](https://ekwo.ai/countries/) says which a country needs.
129
80
 
130
81
  ## Several countries in one installation: `init --no-company`
131
82
 
@@ -141,164 +92,63 @@ npx -y ekwo-os@latest init --no-company \
141
92
  --service-role-key "$SUPABASE_SERVICE_ROLE_KEY" \
142
93
  --org "My Group" --admin-email "you@example.com" --yes
143
94
 
144
- ekwo company new "My Company Belgium" --country BE --chart default --language nl --yes
145
- ekwo company new "My Company France" --country FR --language fr --yes
95
+ ekwo company new "My Company One" --country <cc> --yes
96
+ ekwo company new "My Company Two" --country <other cc> --yes
146
97
  ekwo company list
147
98
  ```
148
99
 
149
- `--no-company` runs the migrations, the seeds of every pack, the modules, the
150
- first administrator, `init_instance()` and `claim_instance_admin()`, and
151
- nothing else. The instance row records no country. A flag that only describes
152
- a company — `--country`, `--company`, `--chart`, `--language`, `--currency`,
153
- `--fiscal-year`, `--fiscal-year-start`, `--vat-period`, `--filing-period`,
154
- `--bank-identifier` (and its old spelling `--iban`), `--bic`, `--bank-name` — is refused with it, exit code 2, before the
155
- database is touched: it would describe nothing.
156
-
157
- **`ekwo company new` is `create_company()`**, the function the `create_company`
158
- tool of the MCP server calls: the country pack copied in, the first financial
159
- year opened, the administrator its first owner. It is called as an
160
- administrator of the installation — `--as-user`, or the only one when there is
161
- exactly one — so the database judges that person exactly as it judges the tool:
162
- anybody else is refused, `not_instance_admin`, exit code 3. Before the call,
163
- the CLI asks what `ekwo init` asks about its first company, from the packs the
164
- installation holds, and refuses the same way off a terminal: no country without
165
- `--country`, and `--chart`, `--language` and `--fiscal-year-start` wherever the
166
- pack offers a choice. `--currency` and `--fiscal-year` override the pack and
167
- this year.
168
-
169
- Without `--no-company`, `ekwo init` does what it always did, and the installation
170
- takes more companies later the same way. Neither writes a country into
171
- `ekwo.json`: see below.
172
-
173
- ## Where table access comes from
174
-
175
- **The schema grants its own rights.** Every table, view and function of Ekwo
176
- names the roles that may reach it — `anon`, `authenticated`, `service_role` —
177
- in the migration that creates it. `ekwo doctor` reads the privileges of a live
178
- database and reports a grant that is missing, a grant wider than the release
179
- declares, and a table `anon` can reach at all.
180
-
181
- Two rules follow, and both are worth knowing before you change anything by
182
- hand.
183
-
184
- **`anon` holds no privilege on any table.** The anonymous role — the one behind
185
- the publishable key your front end ships — may execute the ten helper functions
186
- row level security calls on its behalf, and nothing else. An anonymous request
187
- to a table is refused at the privilege, before any policy is read. If part of
188
- your application reads a table without signing a user in, it will stop working,
189
- and that is the intended answer: sign the user in, or grant a function
190
- deliberately.
191
-
192
- **`authenticated` may attempt exactly the verbs a policy of that table is
193
- prepared to judge.** A grant and a policy are two halves of one sentence: a
194
- grant says which verbs may be attempted, a policy says on which rows they
195
- succeed. The reference tables a country pack fills, the tables written only by
196
- a `security definer` function, and the audit trail are readable and not
197
- writable — by privilege as well as by policy.
198
-
199
- **It was not always so, and the history explains a symptom you may still meet
200
- on an installation nobody has migrated.** Until the migration of 14 September
201
- 2026, nothing in `supabase/migrations` granted table access at all. Row level
202
- security was written in the migrations in full and the underlying `GRANT` was
203
- not: on a Supabase project it came from that project's own default privileges
204
- on the `public` schema, which are there before Ekwo is. Those privileges live
205
- in `pg_default_acl`, keyed by the schema, so dropping and recreating `public`
206
- took them away — and then the reinstall succeeded, `ekwo doctor` reported a
207
- healthy installation, and the first read through PostgREST answered
208
- `permission denied for table companies`. Nothing was wrong with the schema; the
209
- grant that had never been in it was missing.
210
-
211
- On an installation that has run `ekwo migrate` since, that cannot happen: the
212
- migrations put the privileges back themselves, and they take away the blanket
213
- table access the project's defaults had handed `anon`. Dropping `public` is
214
- still not something to do on a project you intend to keep — it takes your books
215
- with it. The decision and what it changed are in
216
- [decision 0003](../../docs/decisions/0003-the-schema-grants-its-own-rights.md).
100
+ `--no-company` refuses every flag that only describes a company — `--country`,
101
+ `--chart`, `--language`, `--currency` and the rest — since there is no company
102
+ for it to describe. `ekwo company new` is the same `create_company()` the MCP
103
+ tool calls: it runs as an administrator of the installation (`--as-user`, or
104
+ the only one), asks the same questions as `init` and refuses the same way off a
105
+ terminal.
217
106
 
218
107
  ## Before you go live: four things on your project
219
108
 
220
- An installation leaves four things undone, and they are undone on purpose:
221
- they are yours to decide, on a project Ekwo does not have access to. `ekwo
222
- init` prints this list at the end of a successful run. `ekwo doctor` does not
223
- check it and does not mention it — a database connection cannot see the
224
- settings of the project it is connected to.
109
+ An installation leaves four things undone on purpose: they are yours to
110
+ decide, on a project Ekwo has no access to. `ekwo init` prints this list at the
111
+ end of a successful run.
225
112
 
226
113
  **1. Turn off self sign-up on your project.**
227
114
  Supabase dashboard → **Authentication → Sign In / Providers → "Allow new users
228
- to sign up"**, and switch it off. A fresh Supabase project accepts anyone who
229
- posts an e-mail address and a password to its authentication endpoint, which is
230
- the right default for a public application and the wrong one for a set of
231
- books. An Ekwo installation is closed: the people who keep the books are
232
- invited to it. Row level security means a stranger who signs up sees nothing —
233
- they are a member of no company — but they are a row in `auth.users` that
234
- nobody asked for, on a project whose sign-up endpoint is open to the internet.
115
+ to sign up"**, switched off. An Ekwo installation is closed: the people who
116
+ keep the books are invited. A stranger who signs up sees nothing, but there is
117
+ no reason to let them in.
235
118
 
236
119
  **2. Keep two administrators.**
237
- An instance administrator is what claims the instance and invites everybody
238
- else. With one, a lost password, a closed mailbox or a person on holiday is a
239
- set of books that nobody can let anyone into. Create the second account in your
240
- Supabase Auth and add it with `claim_instance_admin()`, or invite it from the
241
- application once it is signed in.
120
+ An instance administrator invites everybody else. With one, a lost password or
121
+ a closed mailbox is a set of books nobody can let anyone into. Create a second
122
+ account and add it with `claim_instance_admin()`, or invite it from the
123
+ application.
242
124
 
243
125
  **3. Keep the service_role key off every machine that does not need it.**
244
- It is not a powerful user: it is the absence of a door. A request carrying it
245
- bypasses row level security entirely and reads every company in the instance.
246
- This CLI reads it from a flag, an environment variable or a masked prompt, uses
247
- it once to create the first account, and writes it nowhere — see
248
- [Secrets](#secrets). Anywhere else it sits, it sits as a copy of your whole
249
- ledger. `--admin-user-id` installs against an account that already exists and
250
- needs no key at all.
126
+ A request carrying it bypasses row level security and reads every company in
127
+ the instance. This CLI uses it once, to create the first account, and writes it
128
+ nowhere. `--admin-user-id` installs against an account that already exists and
129
+ needs no key at all. A script that needs to work on its own gets an API key,
130
+ scoped to one company.
251
131
 
252
132
  **4. Read DISCLAIMER.md before you file anything.**
253
133
  [`DISCLAIMER.md`](../../DISCLAIMER.md), at the root of the repository. A
254
- country pack is a reading of a country's rules at the date of its version, and
255
- its golden test proves that the pack agrees with itself — not that it agrees
256
- with the law. `ekwo init` prints the certification status of the pack it
257
- installs for the same reason. The books are yours, in every country where you
258
- file.
259
-
260
- None of these is an action Ekwo performs on your project, now or later. The
261
- project is yours from the first row: the settings are yours to change, the key
262
- is yours to hold, and what you file is yours to answer for.
263
-
264
- Automatic verification of the first three is a phase 1 question, and it is not
265
- free: they are answered by the Supabase management API, so checking them means
266
- handing `ekwo doctor` a management token, and a token that can read a project's
267
- settings can change them. Until that trade is worth making, the list is printed
268
- and read by a person.
134
+ country pack is a reading of a country's rules at the date of its version.
135
+ `ekwo init` prints the review status of the pack it installs for the same
136
+ reason. The books are yours, in every country where you file.
269
137
 
270
138
  ## What `init` does, step by step
271
139
 
272
- | Step | What happens | Why it is done this way |
273
- |---|---|---|
274
- | 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 |
275
- | 2 | Applies every reference seed of `supabase/seed/`, in file-name order: `00_currencies.sql`, `00_territories.sql`, `05_framework_generic.sql`, then one `<n>_pack_<cc>.sql` per country pack, in the order of the number each pack declares | The currencies, the territories the tax rules name, the country-less financial statements every chart falls back on, and every country pack of the release — so a company in any of them can be created later without installing anything. They are exactly the seeds `supabase/config.toml` lists (a list `ekwo pack build` writes from `packs/`), 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 |
276
- | 3 | Creates the first administrator through the Supabase Auth admin API | See below: a database connection cannot be a signed-in user |
277
- | 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 identifier was given | The six steps of the root README, in the same order, plus the one thing nobody can derive |
278
- | 5 | Writes `ekwo.json` | The installation: project URL and schema version. Nothing else, ever — no country, since each company carries its own. A file written by 0.6 or earlier also names a country; it is still read, and the key is taken for nothing |
279
- | 6 | Asks whether to register with Ekwo | The default answer is no, and no is a supported answer forever |
140
+ | Step | What happens |
141
+ |---|---|
142
+ | 1 | Applies `supabase/migrations/*.sql` in order, recorded in the Supabase CLI's own history table — so `supabase db push` and `ekwo migrate` stay interchangeable |
143
+ | 2 | Applies every reference seed: currencies, territories, the generic financial statements, and every country pack of the release |
144
+ | 3 | Creates the first administrator through the Supabase Auth admin API — a database connection cannot be a signed-in user, which is the only reason the secret key is asked for |
145
+ | 4 | Records the instance, takes the administrator seat, creates the company with you as owner, copies its country pack, opens the first financial year, and adds the bank account when one was given |
146
+ | 5 | Writes `ekwo.json`: the project URL and the schema version, nothing else |
147
+ | 6 | Asks whether to register with Ekwo; the default answer is no |
280
148
 
281
149
  Every step checks before it acts. Running `ekwo init` twice on the same
282
150
  project reports what was already there and creates nothing a second time.
283
151
 
284
- ### Why the first user goes through Supabase Auth
285
-
286
- Every row level security policy in the schema compares `auth.uid()` against a
287
- row, and `auth.uid()` reads the JWT of the request. The CLI holds a Postgres
288
- connection, not a session: it runs as the database owner, `auth.uid()` is
289
- NULL, and row level security is *bypassed* rather than satisfied. So the
290
- installer cannot be the first user. It can only create one and then write the
291
- rows that user will be recognised by.
292
-
293
- Creating that user in SQL is not an option either. `auth.users` belongs to
294
- GoTrue — the password hash, the confirmation state, the identity row — and
295
- writing it by hand produces an account that looks right and cannot sign in.
296
- Hence the order: the admin API first, its user id second, `instance_admins`
297
- and `company_members` third.
298
-
299
- This is the only reason `--service-role-key` exists. Pass `--admin-user-id`
300
- instead if the account already exists, and no key is needed.
301
-
302
152
  ## Commands
303
153
 
304
154
  | Command | What it does |
@@ -326,6 +176,9 @@ instead if the account already exists, and no key is needed.
326
176
  | `ekwo match <transaction> <document>` | A bank statement line pays a document, through `settle_from_statement()`. |
327
177
  | `ekwo import <source> <file>…` | Books kept elsewhere — `trial-balance`, `fec`, `journal-items`, `journal-report`, `transaction-journal`, `xaf` — whole or not at all, through `import_books()`, with a correspondence of accounts and journals you save and give back; or a bank statement — `camt.053`, `coda`, `cfonb120` — as pending lines. `--dry-run` rehearses. See [taking over books](#taking-over-books-ekwo-import). |
328
178
  | `ekwo doc list` / `show` | What exists, and with `--unpaid` what is posted and still owed. See [keeping books](#keeping-books). |
179
+ | `ekwo doc pdf <document>` | The PDF of a sale invoice or credit note, rendered from the books by [`@ekwo-ai/invoice-pdf`](../formats/invoice-pdf/README.md), with the logo the company names by URL (fetched by this command, from a public http(s) address only). Written to `--out`, or under its own name in the current directory. `--factur-x` embeds the CII of a posted sale and makes it a Factur-X PDF/A-3 (the rules it breaks end on exit 1); `--page-size Letter`; `--labels <file.json>` gives the words of the layout in the document's language. `ekwo document` is the whole word of `ekwo doc`. Records nothing. |
180
+ | `ekwo proof stamp` / `upgrade` / `verify` | The sha256 of a filed file — a declaration, the annual accounts of a financial year, a document — committed to Bitcoin through the public OpenTimestamps calendars; only the hash leaves. `upgrade` completes the pending proofs of the company and is made to run on a schedule; `verify` checks a file against its proof and the block it names, and `--out` writes the `.ots` file any verifier reads. See [`docs/filing-proofs.md`](../../docs/filing-proofs.md). |
181
+ | `ekwo einvoice validate` / `issue` / `status` / `list` | The electronic invoice of a posted sale, in the format its country pack declares, through the `einvoicing` module: `validate` writes it and names the rules it breaks (exit 1 if any), `issue` keeps it and with `--send --to <directory>` writes it to that folder, `status --refresh` follows it. See [`modules/einvoicing`](../../modules/einvoicing/README.md). |
329
182
 
330
183
  There is no `eject`, because there is nothing to eject from. The schema is in
331
184
  your database, the migrations are in the repository under AGPL-3.0, and
@@ -333,18 +186,9 @@ your database, the migrations are in the repository under AGPL-3.0, and
333
186
 
334
187
  ## What a command answers: `--json` and the exit codes
335
188
 
336
- Every command prints for a person by default — aligned columns, colour only on
337
- a terminal and never when `NO_COLOR` is set, no spinner and no line redrawn in
338
- place, so the output reads the same in a file or a CI log — and takes `--json`
339
- for a program.
340
-
341
- `ekwo help --json` answers the list of commands and, in `usage`, the whole
342
- text of `--help` without colour: every flag and variable, for an agent
343
- that reads one document rather than a terminal.
344
-
345
- Under `--json` the standard output is **one JSON document and nothing else**;
346
- the prose still goes by, on the standard error. The document has the same
347
- shape whatever happened:
189
+ Every command prints for a person by default, and takes `--json` for a
190
+ program. Under `--json` the standard output is **one JSON document and nothing
191
+ else**, with the same shape whatever happened:
348
192
 
349
193
  ```json
350
194
  {
@@ -361,125 +205,50 @@ shape whatever happened:
361
205
  }
362
206
  ```
363
207
 
364
- | Field | |
365
- |---|---|
366
- | `ok` | `exitCode` is 0. |
367
- | `command` | The words that named it: `status`, `pack upgrade`. |
368
- | `exitCode` | The code the process ends on. |
369
- | `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. |
370
- | `warnings` | Every warning the command printed, without the colours. |
371
- | `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. |
372
-
373
- The shape is published as
374
- [`schema/output.1.json`](schema/output.1.json), ships in the package, and is
375
- what `tests/cli/output-contract.test.ts` validates every command against. An
376
- amount is a decimal string and never a JSON number; a date is ISO 8601.
208
+ `data` carries what the command has to say; its shape per command is
209
+ published in [`schema/output.1.json`](schema/output.1.json). Amounts are
210
+ decimal strings, dates are ISO 8601. `ekwo help --json` answers the whole
211
+ reference of commands and flags in one document, for an agent.
377
212
 
378
213
  | Exit code | Means |
379
214
  |---|---|
380
215
  | `0` | Done. |
381
- | `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`. |
382
- | `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. |
383
- | `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. |
384
-
385
- A refusal is printed as the database wrote it — `period_locked: …`,
386
- `tax_territory_mismatch: …` — and never rephrased; the CLI does not move a
387
- date or retry differently to get past one. Its name is the part to match on,
388
- in a field of its own under `--json`.
216
+ | `1` | It failed for a reason that is not the books — the network, a bug — **or a check found something** (`doctor`, `status`, `pack check`). |
217
+ | `2` | The command was called wrong, or needed an answer with no terminal to ask it on. |
218
+ | `3` | **The database refused**: a locked period, a capability you do not hold, a constraint. The call was well formed; the accounting said no. |
389
219
 
390
- **No command waits on a question when there is nobody to answer.** Off a
391
- terminal, or under `--json`, or with `--yes`, a missing answer is exit code 2
392
- with the flag to pass. That holds underneath the commands too: a prompt that
393
- is reached with no terminal stops instead of waiting.
220
+ A refusal is printed as the database wrote it — `period_locked: …` — and never
221
+ rephrased or worked around. **No command waits on a question when there is
222
+ nobody to answer**: off a terminal, under `--json` or with `--yes`, a missing
223
+ answer is exit code 2 with the flag to pass.
394
224
 
395
225
  ## Acting as a person: `login`, `use`, `whoami`
396
226
 
397
- The commands above install and operate, and connect as the owner of the
398
- database — they say so when they connect. Anything that keeps books acts as
399
- **a person**, through the instance's API, under row level security: the same
400
- route, and the same functions of the schema, as the MCP server.
227
+ `init`, `migrate`, `status` and `doctor` connect as the owner of the database.
228
+ Anything that keeps books acts as **a person**, through the instance's API,
229
+ under row level security — the same route and the same functions as the MCP
230
+ server.
401
231
 
402
232
  ```bash
403
- ekwo login --supabase-url https://<ref>.supabase.co --anon-key <publishable key> --email you@example.test
233
+ ekwo login --supabase-url https://<ref>.supabase.co --anon-key <publishable key> --email you@example.com
404
234
  ekwo whoami
405
235
  ekwo use "Example One"
406
- ekwo whoami --json
407
- ```
408
-
409
- `login` asks the instance for a session and keeps it. In a directory that has
410
- an `ekwo.json`, the URL is read from it. The password is prompted, masked, when
411
- `--password` and `EKWO_PASSWORD` are absent; it is sent to the instance once
412
- and written nowhere. Signing in again after a session ended is `ekwo login`
413
- and a password: the profile remembers the rest.
414
-
415
- **Where the session is kept.** In `$EKWO_CONFIG_DIR`, else
416
- `$XDG_CONFIG_HOME/ekwo`, else `~/.config/ekwo`, in two files written `0600` in
417
- a `0700` directory: `profiles.json` says where each profile points and holds
418
- no token; `credentials.json` holds the access token and the refresh token.
419
- The CLI refuses — `config_dir_in_repository` — to write either inside a
420
- repository, where one `git add .` would publish them. The access token lasts
421
- about an hour and is renewed on its own, ahead of time and again if the
422
- instance answers 401 anyway; the rotated refresh token replaces the old one
423
- on disk before the call is retried. A session that cannot be renewed is
424
- `session_expired`, exit code 2, and the fix is `ekwo login`.
425
-
426
- **Profiles.** `--profile <name>`, or `EKWO_PROFILE`: a demo instance,
427
- production, one client of a firm. Each holds one instance, one person and one
428
- company in use. The profile last signed in to is the one used when none is
429
- named.
430
-
431
- **The environment comes first, and touches no file.** With `SUPABASE_URL`,
432
- `SUPABASE_ANON_KEY` and either `EKWO_ACCESS_TOKEN` or `EKWO_EMAIL` with
433
- `EKWO_PASSWORD` — the variables the MCP server reads — a command signs in for
434
- its own duration, reads no profile and writes nothing: a CI job. The
435
- environment is taken whole: a user there with no instance beside it is a wrong
436
- call, never a fallback on a profile's instance. It has nowhere to keep a
437
- company, so pass `--company`.
438
-
439
- **The company in use.** `ekwo use <name or id>` checks the company against the
440
- instance, as you, and records it; `--company` names another for one command.
441
- A company you cannot see is `unknown_company`, and is not named in the
442
- refusal. Under `--json` every answer of a command that acts as a person
443
- carries a `context` — the profile, the instance and **the company the answer
444
- was rendered for**, `null` when none is in use — on success and on a refusal
445
- alike. A caller that keeps two sets of books reads it before it believes the
446
- rest.
447
-
448
- ```json
449
- { "ok": true, "command": "whoami", "exitCode": 0,
450
- "context": { "profile": "default", "instance": "https://<ref>.supabase.co",
451
- "company": { "id": "…", "name": "Example One" } },
452
- "data": { "user": { "id": "…", "email": "you@example.test" },
453
- "instance": { "url": "…", "schemaVersion": "0.3.0" },
454
- "capabilities": ["…"], "companies": [ … ] },
455
- "warnings": [] }
456
236
  ```
457
237
 
458
- `whoami` works nothing out. The companies are the rows the policies let you
459
- read, the capabilities are what `member_capabilities()` answers for the
460
- company in use — the function behind `your_capabilities` in the MCP server —
461
- and the schema version is `ekwo_schema_version()`.
462
-
463
- **Never a `service_role` key.** It is refused by name, `service_role_refused`,
464
- with exit code 2 and before anything is sent, at every door it can arrive by:
465
- `--anon-key` or `SUPABASE_ANON_KEY`, `EKWO_ACCESS_TOKEN`, a session file
466
- somebody edited, and `--service-role-key` typed out of habit. The test for it
467
- and the sentence are in `@ekwo-ai/core`, where the MCP server reads them too.
468
- `ekwo init` remains the one command that takes that key, to create the first
469
- user, and never keeps it.
470
-
471
- | Refusal of the CLI's own | Exit code | Means |
472
- |---|---|---|
473
- | `not_signed_in`, `unknown_profile`, `session_expired` | 2 | There is nobody to act as. `ekwo login`. |
474
- | `service_role_refused`, `config_dir_in_repository`, `missing_configuration` | 2 | The call has to change, not be retried. |
475
- | `unknown_company`, `ambiguous_company` | 2 | Name it differently, or by its id. |
476
- | `no_company` | 2 | A verb that keeps books ran with no company in use. `ekwo use`, or `--company`. |
477
- | `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. |
478
- | `sign_in_failed`, `instance_unreachable` | 1 | The instance declined the address and the password, or did not answer. |
479
-
480
- A refusal of the database that arrives over this route is still exit code 3:
481
- PostgREST passes on the SQLSTATE, the detail and the hint, and the CLI reads
482
- them as it reads a driver's.
238
+ - **The session** is kept in `$EKWO_CONFIG_DIR`, else `$XDG_CONFIG_HOME/ekwo`,
239
+ else `~/.config/ekwo`, readable by you alone, renewed on its own, and refused
240
+ inside a repository. The password is sent to the instance once and written
241
+ nowhere. `ekwo logout` ends it.
242
+ - **Profiles** (`--profile`, `EKWO_PROFILE`) keep one instance, one person and
243
+ one company in use each: a demo, production, one client of a firm.
244
+ - **For a CI job**, `SUPABASE_URL`, `SUPABASE_ANON_KEY` and either
245
+ `EKWO_ACCESS_TOKEN` or `EKWO_EMAIL` with `EKWO_PASSWORD` sign in for one
246
+ command and write nothing; pass `--company`.
247
+ - **The company in use** is set with `ekwo use`, or `--company` for one
248
+ command. Under `--json`, every answer carries a `context` naming the profile,
249
+ the instance and the company it was rendered for.
250
+ - **Never a `service_role` key**: it is refused by name, at every door it could
251
+ arrive by. `ekwo init` remains the one command that takes it.
483
252
 
484
253
  ## Keeping books
485
254
 
@@ -489,408 +258,153 @@ ekwo doc new --contact client --date 2026-06-15 --ref job-7 \
489
258
  --line "name=Audit,price=1500.00,account=<account code>,tax=<tax code>"
490
259
  ekwo doc line add job-7 --name Travel --price 250.00 --account <account code>
491
260
  ekwo post job-7 --dry-run # the entry the database would write; nothing is written
492
- ekwo post job-7 # post_document()
261
+ ekwo post job-7
493
262
  ekwo cancel job-7 # back to draft where the country allows it, else the credit note
494
263
  ekwo payment record --doc job-7 --amount 1750.00 --date 2026-06-30 --bank-account <id> --ref bank-1
495
264
  ekwo match <bank transaction id> job-9
496
265
  ekwo doc list --unpaid --since 2026-06-01 --json
497
266
  ```
498
267
 
499
- They run as the person signed in, on the company in use ([above](#acting-as-a-person-login-use-whoami)),
500
- and none is ever picked for you: with no company in use a verb ends on
501
- `no_company`, exit code 2, and `context.company` is `null`.
502
-
503
- **Each verb is one function, and it is not ours.** The functions live in
504
- `@ekwo-ai/core` and the MCP server calls the same ones: `contact add` is
505
- `create_contact`, `doc new` is `create_document`, `post` is
506
- `post_document`, `cancel` is `unpost_document` or `cancel_document` — `unpost_refusal` chooses —, `reverse` is `reverse_entry`,
507
- `payment record` is `record_payment`, `doc list` and
508
- `doc show` are `list_documents` and `get_document`. Underneath them the rules
509
- are the schema's — the balance, the numbering, the locks, the taxes, the
510
- territory, the tax point. **This CLI computes no amount**: what you type goes
511
- in as text, what is printed is what came back, and
512
- `tests/cli/no-rules.test.ts` reads the commands to keep it that way. An amount
513
- is a decimal string in both directions, `1500.00`.
514
-
515
- **A refusal is the answer.** A locked period, a policy, a constraint: exit
516
- code 3, the database's sentence word for word, its name in `error.name`. The
517
- CLI does not move a date or try something else. What is refused *before* the
518
- database is asked — an account code that does not exist, a document that is
519
- not a draft, a document with nothing open — is exit code 2: the call has to
520
- change.
521
-
522
- **`--ref`, so that nothing is created twice.** On what creates (`contact add`,
523
- `doc new`, `payment record`), `--ref <your reference>` is kept on the row,
524
- unique per company. The same reference a second time returns what the first
525
- call created, with `"replayed": true`, and writes nothing — and finishes what
526
- a dropped connection left half done: a draft whose lines never arrived, a
527
- payment inserted and never booked. Two callers racing each other are settled
528
- by the unique index, which refuses the slower one with exit code 3. A
529
- `<document>` is its id, its number, or the `--ref` it was created under, which
530
- is how a draft — it has no number yet — is named.
531
-
532
- **`--dry-run`, where the database can answer without writing.** Today that is
533
- `post`. `rehearse_post_document()` calls `post_document()` for real inside a
534
- block it then rolls back, so the entry shown is the one that would be written,
535
- under the number it would take, and a rehearsal is refused exactly as posting
536
- would be. No other verb has one, because for no other verb does the database
537
- know how.
538
-
539
- **`--stdin`, the form that is authoritative.** One JSON object on the standard
540
- input, with the fields of the MCP tool of the same meaning (`contact_type`,
541
- `document_date`, `lines: [{ name, unit_price, account_code, tax_code, … }]`,
542
- `client_ref`). A field nobody defined is refused rather than dropped. Flags
543
- given beside it win.
544
-
545
- ```bash
546
- echo '{"contact":"client","document_date":"2026-06-15","client_ref":"job-8",
547
- "lines":[{"name":"Review, \"urgent\"","unit_price":"200.00","account_code":"<code>"}]}' \
548
- | ekwo doc new --stdin --json
549
- ```
550
-
551
- **`--line`, for a person.** Named fields, never positions: `name`, `price`,
552
- `qty`, `account`, `tax`, `product`, `unit`, `discount`, `description`; a comma
553
- inside a value is `\,`. A tax and an account are named by their **code** —
554
- never a rate, since several taxes share one. The free-text form
555
- (`"Audit 1 500 EUR@21"`) is not accepted: `1 500` is one number or two, `@21`
556
- is a rate where the books need a tax, and a currency belongs to the document
557
- (`--currency`), not to a line.
558
-
559
- Two values are supplied when nobody gives them, and said when they are:
560
- `--type` is `sale_invoice`, and `--date` is today on the machine running the
561
- command. Whether that date may be booked on is the database's decision.
268
+ - **Each verb is one function of `@ekwo-ai/core`**, the same the MCP server
269
+ calls, and the rules underneath are the schema's. This CLI computes no
270
+ amount: what you type goes in as text, what is printed is what came back.
271
+ - **A tax and an account are named by their code**, never by a rate: several
272
+ taxes share one. The codes come from the company's country pack.
273
+ - **`--ref`, so that nothing is created twice.** The same reference a second
274
+ time returns what the first call created and writes nothing. A `<document>`
275
+ is its id, its number, or the `--ref` it was created under.
276
+ - **`--dry-run` on `post`** runs the posting for real and rolls it back, so the
277
+ entry shown is the one that would be written, and a refusal is the real one.
278
+ - **`--stdin`** takes one JSON object with the fields of the matching MCP tool,
279
+ for a program.
562
280
 
563
281
  ## Taking over books: `ekwo import`
564
282
 
565
- To try Ekwo on your own books, bring them. One command, one reader per
566
- source, and nothing is posted while an account has no answer:
283
+ To try Ekwo on your own books, bring them. One reader per source, and nothing
284
+ is posted while an account has no answer:
567
285
 
568
286
  ```bash
569
- ekwo import fec 123456789FEC20251231.txt --dry-run --open-years --save-mapping map.json
570
- # read, propose a correspondence, rehearse the import in the database, take it back
571
- $EDITOR map.json # answer what is null — read `suggested` — correct what is wrong
572
- ekwo import fec 123456789FEC20251231.txt --mapping map.json --open-years
573
- ekwo import trial-balance balance.csv --opening-date 2026-01-01 --dry-run --save-mapping map.json
574
- ekwo import journal-items items.csv accounts.csv partners.csv --dry-run --save-mapping map.json
575
- ekwo import journal-report report.csv chart.csv --date-order dmy --dry-run --save-mapping map.json
576
- ekwo import transaction-journal journal.csv account-list.csv --date-order mdy --dry-run --save-mapping map.json
577
- ekwo import xaf books.xaf --open-years --dry-run --save-mapping map.json
287
+ ekwo import fec FEC20251231.txt --dry-run --open-years --save-mapping map.json
288
+ $EDITOR map.json # answer what is null, check what is suggested
289
+ ekwo import fec FEC20251231.txt --mapping map.json --open-years
578
290
  ekwo import camt.053 statement.xml # or coda, cfonb120: pending lines for `ekwo match`
579
291
  ```
580
292
 
581
293
  | Source | What it reads |
582
294
  |---|---|
583
- | `trial-balance` | A trial balance as CSV — `account`, `debit`, `credit`, or one signed `balance` — which becomes the opening entry of the year `--opening-date` starts |
584
- | `fec` | A *fichier des écritures comptables*: the eighteen columns of the arrêté of 29 July 2013, tab or bar separated |
585
- | `journal-items` | The lines of every entry exported as CSV from the list view of an ERP whose ledger is a table of lines, with the chart of accounts and the partners exported beside it |
586
- | `journal-report` | A journal report or a general ledger detail, saved as CSV from a cloud service's spreadsheet export, with its Journal ID and Account Code columns, and the chart and the contacts beside it |
587
- | `transaction-journal` | A transaction journal, saved as CSV from a spreadsheet export: each transaction a run of rows under one date, type and number, closed by its total; the list of accounts beside it |
588
- | `xaf` | An XML Audit File Financial, version 3.2 or 4.0: the accounts, the parties, the opening balance — on the day the file gives it — and every transaction of a year, in one file |
295
+ | `trial-balance` | A trial balance as CSV, which becomes the opening entry of the year `--opening-date` starts |
296
+ | `fec` | A French *fichier des écritures comptables* |
297
+ | `journal-items` | The lines of every entry exported as CSV from a ledger kept as a table of lines, with its chart of accounts and partners |
298
+ | `journal-report` | A journal report or general ledger detail saved as CSV, with its chart and contacts |
299
+ | `transaction-journal` | A transaction journal saved as CSV, with its list of accounts |
300
+ | `xaf` | An XML Audit File Financial, version 3.2 or 4.0 |
589
301
  | `camt.053`, `coda`, `cfonb120` | A bank statement: its lines, pending, ready for `ekwo match` |
590
302
 
591
- Each reader is a brick of [`packages/formats/`](../formats/README.md), named
592
- after the file; its README says which columns it reads and which official
593
- pages the format was read from. The exports of other ledgers can also be named
594
- by the software they come from: `ekwo import --help` lists those names, and
595
- [`docs/compatibility.md`](../../docs/compatibility.md) gives each one's export,
596
- official page and state.
597
-
598
- **The correspondence is yours.** Every account of the old chart has to become
599
- an account of the company's chart, and every old journal a journal of the
600
- company. The codes give a candidate — the same code, the same digits without
601
- the zeros a chart pads with (`411` and `411000`), or the account whose digits
602
- are the longest beginning of the old code, three at least (`401ACME` and
603
- `401000`); a tie is no answer — and what the files say of the old account —
604
- the type its export gives it, its name, the side of its balance — is held
605
- against the type of the candidate in the chart, because two charts give the
606
- same digits to different things. Only the same code, not contradicted, is
607
- `exact`. Anything else is `suggested`, with its reason, and waits for you; a
608
- candidate the files contradict is dropped for the one account of the chart of
609
- the kind they say. `--dry-run` prints the lines to read first, each with its
610
- reason, and `--save-mapping` writes the correspondence as JSON:
611
-
612
- ```json
613
- {
614
- "version": 1,
615
- "source": "fec",
616
- "accounts": { "411000": "411000", "401ACME": null, "471200": null },
617
- "journals": { "VE": "SAL", "AN": "@opening", "BQ1": "MISC" },
618
- "suggested": { "401ACME": { "target": "401000", "reason": "the longest beginning of the code the chart has; …" } }
619
- }
620
- ```
621
-
622
- and `--mapping` gives it back: what it answers wins over the proposal, so the
623
- second run posts what the first one showed. A suggestion is confirmed by
624
- writing its code under `accounts`; `--accept-suggestions` takes all of them
625
- once you have read them. Nothing is posted while one the books use is only
626
- suggested. A journal mapped to `@opening`
627
- becomes the opening entry of its year instead of ordinary entries — the
628
- *à-nouveaux* of a FEC, typically. A journal whose code is the one the pack
629
- opens years on is proposed as `@opening` on its own; `*` stands for entries
630
- the source gives no journal.
631
-
632
- **Whole or not at all.** `import_books()` is one call and one transaction: the
633
- fiscal years it needs (with `--open-years`, as years of the same length and
634
- first day as the company's own; without, an entry outside every year is
635
- refused by name), the parties the lines name — found by their code or their
636
- name, or created as customers or suppliers according to where their lines are
637
- booked — every entry as a draft then posted by `post_entry()`, and the opening
638
- through `opening_balance()`. One refusal — a locked period, an account the
639
- chart does not have, a capability you do not hold — and nothing stays, not
640
- even the years or the parties.
641
-
642
- **`--dry-run` is the real thing, taken back.** The database runs the whole
643
- import and rolls it back, so the numbers shown are the ones the entries would
644
- take now and a refusal is the one the import would give. For a bank statement a
645
- dry run reads the file and asks nothing: a statement books nothing anyway.
646
-
647
- What else to know:
648
-
649
- - **Refused before the database is asked**, and listed under `refusals` by a
650
- dry run: an account or a journal with no answer or only a suggestion, books whose reader found
651
- something that does not add up (an entry that does not balance, a line of a
652
- draft entry), a currency the files name that is not the company's.
653
- - **The numbers** are drawn by the journal each entry goes to; the old number
654
- is kept as the entry's reference. `--keep-numbers` posts each under its old
655
- number instead, where the country allows a number chosen by hand or you hold
656
- `entries.import`.
657
- - **No tax.** An imported line carries an account and an amount, not the tax
658
- that produced it: the history feeds the ledger, the trial balance and the
659
- statements, and no box of a VAT return. A period kept elsewhere was declared
660
- from where it was kept.
661
- - **The same files twice are refused** (`import_already_done`): `book_imports`
662
- keeps the checksum of what each import read, and the refusal says which
663
- files, when, and what that import wrote.
664
- - **Dates and encodings are said, never guessed**: `--encoding` for a file
665
- that is not UTF-8, `--date-order` for a report that writes dates in digits.
666
- - **Reconciliation marks are read and not re-applied yet**: the matching of an
667
- imported receivable against its payment is done in Ekwo, with `reconcile`.
303
+ `ekwo import --help` also lists the exports of other ledgers by the software
304
+ they come from, and [`docs/compatibility.md`](../../docs/compatibility.md)
305
+ gives each one's export and official page.
306
+
307
+ - **The correspondence is yours.** Every account and journal of the old books
308
+ needs an answer in the company's chart. Ekwo proposes one for each — `exact`
309
+ only for the same code, otherwise `suggested` with its reason — and
310
+ `--save-mapping` writes them as JSON for you to check. `--mapping` gives it
311
+ back; `--accept-suggestions` takes every suggestion once you have read them.
312
+ - **`--dry-run` is the real import, taken back**, so the numbers shown are the
313
+ ones the entries would take and a refusal is the real one.
314
+ - **Whole or not at all.** The import is one transaction: one refusal and
315
+ nothing stays. The same files twice are refused, saying when they were
316
+ imported and what that import wrote.
317
+ - **No tax on history.** An imported line feeds the ledger, the balances and
318
+ the statements, not the boxes of a declaration: a period kept elsewhere was
319
+ declared from where it was kept.
668
320
 
669
321
  The MCP server offers the same as `import_books`, and `import_bank_statement`
670
322
  for a statement. [`docs/import.md`](../../docs/import.md) is the long form.
671
323
 
672
324
  ## `ekwo module`
673
325
 
674
- A module is a Postgres schema beside the socle — `fixed_assets` for fixed assets,
675
- `budgets` for a plan against the ledger, `tax` for the corporate income tax
676
- estimated from the books. Its migrations travel with this package, and
677
- `ekwo migrate` applies them by default.
326
+ A module is a Postgres schema beside the core — fixed assets, budgets,
327
+ corporate income tax, electronic invoicing. Its migrations travel with this
328
+ package, and `ekwo migrate` applies them by default.
678
329
 
679
330
  ```sh
680
331
  ekwo module list # what this release carries, and what the database holds
681
332
  ekwo module migrate [<code>] # the migrations, and the country seeds they need
682
- # (the code, `assets`, or the folder, `fixed-assets`)
683
333
  ekwo module enable assets --company "…" # turn it on for one company
684
334
  ekwo module disable assets --company "…" # turn it off; nothing it wrote is deleted
685
335
  ```
686
336
 
687
- `enable` and `disable` go through `enable_module()` and `disable_module()`
688
- rather than writing the table: the guard is in the function, so it applies to
689
- psql and PostgREST alike. The CLI sets the request claim for an owner of the
690
- company, the way `ekwo register` does, and `--as-user <uuid>` names another.
691
-
692
- **One thing this CLI cannot do**, and says so every time: PostgREST serves a
693
- schema other than `public` only once the project lists it under its exposed
694
- schemas. That is a setting of the API, not of the database, so `ekwo module
695
- enable` prints the line to add — Supabase dashboard → Project Settings → API,
696
- or `[api] schemas` in `supabase/config.toml`.
697
-
698
- **When a release renames a module's schema**, `ekwo migrate` applies the
699
- rename and says so: the exposed schemas of the project still name the old one,
700
- and only the operator can change that. The fixed assets module moved from
701
- `assets` to `fixed_assets` in its version 2.0.0 — replace one with the other in
702
- Project Settings → API. Its code, `assets`, is what `ekwo module` takes, and
703
- never changes; [`modules/fixed-assets/README.md`](../../modules/fixed-assets/README.md#upgrading-from-version-1-of-the-module)
704
- is the whole upgrade.
705
-
706
- **Before `supabase db push`**, run `ekwo migrate --no-modules`. The Supabase
707
- CLI knows the socle's migration files and not a module's, so it would report
708
- them as history it has no file for.
337
+ PostgREST serves a schema other than `public` only once the project lists it
338
+ under its exposed schemas — a setting of the API that no database connection
339
+ can change — so `ekwo module enable` prints the line to add (Supabase
340
+ dashboard → Project Settings → API). Before `supabase db push`, run
341
+ `ekwo migrate --no-modules`.
709
342
 
710
343
  ## `ekwo company`
711
344
 
712
- A firm keeps several companies in one installation, each in its own country,
713
- and each of them belongs to somebody. `new` creates one and `list` shows them —
714
- see [several countries](#several-countries-in-one-installation-init---no-company):
715
-
716
345
  ```sh
717
- ekwo company new "My Company Belgium" --country BE --chart default --language fr
718
- ekwo company new "My Company France" --country FR --language fr
346
+ ekwo company new "My Company" --country <cc>
719
347
  ekwo company list # name, country, currency, language, chart, pack, members
720
- ```
721
-
722
- `company new --json` answers the company (`id`, `country`, `currency`,
723
- `language`, `chart`, `packVersion`), its first financial year and its owner;
724
- `company list --json` answers `companies`, one per company. Both shapes are
725
- under `$defs/data` of the output schema, and the exit codes are the ones every
726
- command has: 2 for a missing answer, 3 when the database refuses.
727
-
728
- The two commands below are how a company leaves, and arrives somewhere else.
729
-
730
- ```sh
731
- ekwo company export "My Company" --out ./my-company # manifest.json + data/<schema>.<table>.jsonl
348
+ ekwo company show --json # the company in use: years, lock dates, journals, accounts by role
349
+ ekwo company export "My Company" --out ./my-company # manifest.json + one JSON Lines file per table
732
350
  ekwo company import ./my-company --owner <user id> # whole, or not at all
733
351
  ```
734
352
 
735
- **`export` runs as a member, under row level security**, although the
736
- connection belongs to the owner of the database: inside one transaction the CLI
737
- steps down to `authenticated` with the claim of the member it acts for —
738
- `--as-user`, an owner of the company by default. That member needs
739
- `company.export`, which the `owner` and `client` presets hold. An archive is
740
- whole or it is not written: a member who may not read one of the tables is
741
- refused, by table, with exit code 3. The act is written on the audit trail of
742
- the company.
743
-
744
- **`import` is for the installer or an administrator of the installation**
745
- (`--as-user`), the two who may create a company. The files are checked against
746
- the manifest before the database is asked anything; then `import_company()`
747
- takes all of it or none of it. A company already there is refused — exit code
748
- 3, `company_already_here` — which is also what running the command twice gets.
749
- Members do not travel: `--owner` names the first one.
750
-
751
- **The files the attachments point at are not carried.** They are in the storage
752
- bucket, not in the database; `manifest.json` lists them and both commands say
753
- how many are left to copy.
754
-
755
- The format, what travels and what does not, and every refusal are in
353
+ **A company leaves with its books.** `export` runs as a member holding
354
+ `company.export`, under row level security, and writes an archive anybody can
355
+ read. `import` takes it into another installation in one transaction, or
356
+ refuses; members do not travel, `--owner` names the first one. Attached files
357
+ live in storage and are listed in the manifest to copy. The format is in
756
358
  [`docs/company-archive.md`](../../docs/company-archive.md).
757
359
 
758
360
  ## `ekwo doctor`
759
361
 
760
- What a healthy installation is true of, and nothing in the schema can enforce
761
- on its own. It reads and reports; it never repairs, because the fix for a
762
- missing policy is a migration and the fix for an orphaned membership is a
763
- decision about who should have access.
362
+ What a healthy installation is true of. It reads and reports; it never
363
+ repairs.
764
364
 
765
365
  ```sh
766
366
  ekwo doctor --db-url "$URL" # readable
767
- ekwo doctor --db-url "$URL" --json # the whole report, findings included
367
+ ekwo doctor --db-url "$URL" --json # the whole report
768
368
  ```
769
369
 
770
- **The `catalogue` check compares your database to an inventory of everything
771
- this release defines** — tables and their columns, views, functions with their
772
- identity arguments, policies, triggers and types. That inventory is
773
- [`assets/expected-objects.json`](assets/expected-objects.json), generated from
774
- the migrations themselves and shipped inside this package, so it cannot be a
775
- list somebody forgot to update. In `--json` output it is the check named
776
- `catalogue`. Four outcomes, and they are not the same thing:
777
-
778
- | Finding | What it means | Severity |
779
- |---|---|---|
780
- | Missing | The installation is behind or has been damaged. | Problem |
781
- | Extra | Your own table, function or trigger. Reported so you know it is there. | Information |
782
- | 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 |
783
- | 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 |
784
-
785
- A module's objects are required only of a database that carries the module.
786
- One you never installed is named and skipped.
787
-
788
- **A database older than this CLI is still compared.** The report says which
789
- schema version the inventory describes and which one the database reports, and
790
- goes on to list what differs — refusing to look would be refusing the case the
791
- check exists for.
792
-
793
- **Exit codes.** `0` when there is no problem, warnings and information
794
- included; `1` when there is at least one problem, or when the schema is not
795
- installed at all. Nothing else. So `ekwo doctor` is usable as a deployment
796
- gate, and an operator's own extra table never turns a pipeline red.
797
-
798
- **What the catalogue does not cover.** Constraints, indexes and the bodies of
799
- functions. A dropped unique index is real damage and this check will not see
800
- it: the question it answers is "is the object there, and is it still that
801
- shape". `docs/schema.md` lists the constraints for a human reader, and the
802
- argument against putting them in the inventory is that each is an order of
803
- magnitude more text for a diff that would move on every Postgres upgrade — and
804
- an inventory whose diff nobody reads is worth nothing.
805
-
806
- **The `grants` check compares the privileges**, from the same inventory: the
807
- `grants` section of each schema says which of `anon`, `authenticated` and
808
- `service_role` may reach each table, view and function, and with which verbs.
809
- Its own check rather than a category of `catalogue`, because the rule is not
810
- the same.
811
-
812
- | Finding | What it means | Severity |
813
- |---|---|---|
814
- | 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 |
815
- | 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 |
816
- | 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 |
817
- | 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 |
818
-
819
- In a checkout, `npm run inventory` regenerates the inventory from the
820
- migrations; the CI regenerates it and fails on any difference, the way it does
821
- for `docs/schema.md`, and a second job checks that the copy shipped in `dist`
822
- is the one in the repository.
370
+ It compares the database with an inventory of everything this release defines
371
+ — tables, columns, views, functions, policies, triggers, types, and the
372
+ privileges each role holds — shipped inside this package and generated from
373
+ the migrations. A missing object, a missing or extra policy, a changed column
374
+ type and a privilege the anonymous role should not hold are problems; your own
375
+ extra tables are reported as information. It also checks row level security on
376
+ every table, pending migrations, memberships of deleted users, companies
377
+ without a bank account, and posted entries that do not balance.
378
+
379
+ Exit code `0` when there is no problem, `1` otherwise — usable as a deployment
380
+ gate.
823
381
 
824
382
  ## `ekwo pack`, in a checkout
825
383
 
826
- A country is data: `packs/<cc>/` holds a manifest, the chart of accounts as
827
- CSV, the taxes and where they post, the boxes of the declaration, the financial
828
- statements, the sentences the country requires on an invoice, the translations,
829
- and a year of books with the figures it produces. The compiler turns one into
830
- `supabase/seed/<n>_pack_<cc>.sql`, which is committed — and, where a pack
831
- carries a section for a module, into
832
- `supabase/seed/modules/<section>/<n>_pack_<cc>.sql` — `fixed_assets` for the
833
- fixed assets module — applied by the module
834
- migration runner and by nothing else.
384
+ A country is data under `packs/<cc>/`; the compiler turns it into the seed
385
+ `ekwo init` applies. These commands run in a clone of the repository:
835
386
 
836
387
  ```sh
837
- ekwo pack list # the packs this checkout carries, and their certification
838
- ekwo pack describe # everything each of them says; one country with `describe <cc>`
839
- ekwo pack build be # write supabase/seed/10_pack_be.sql from packs/be, and the lists of packs
840
- ekwo pack build --all
841
- ekwo pack check be # validate one pack, compare its seed and the lists of packs
388
+ ekwo pack list # the packs this checkout carries, and their review status
389
+ ekwo pack describe <cc> # everything one pack says; --json for the whole object
390
+ ekwo pack build <cc> # write its seed, and the lists of packs in the docs
842
391
  ekwo pack check --all # exit 1 if a committed seed or list is not the output of the packs
843
392
  ```
844
393
 
845
- The lists are the blocks of other files that name every pack — the seeds of
846
- `supabase/config.toml` and of the root README, the `/packs/<cc>/` lines of
847
- `.github/CODEOWNERS`, the table of `docs/packs.md` — each between a
848
- `generated:<name>` marker and `/generated`. They are written from `packs/` the
849
- way a seed is, so a country is added in `packs/<cc>/` and nowhere else.
850
-
851
- `list` is a line per country. `describe` is the whole of one: the charts and
852
- who each is published for, the taxes and their distinct rates, the periodic
853
- declaration with its cadences and its boxes, whether the country states a rule
854
- for when the return is due, the brick that writes the file it is deposited as
855
- or that it is filed by hand on a portal, the e-invoicing profile and the day it
856
- starts, the accounts the tax balance lands on, every bank statement format the
857
- country names and whether anything here reads it, the financial statements, and
858
- the texts the pack was built from with the day each was last opened. Every
859
- answer is read from the pack, and a "not yet" is printed rather than left out.
394
+ `pack describe --json` is the same object the site builds each country's page
395
+ from. The format and every rule `pack check` applies are in
396
+ [`docs/packs.md`](../../docs/packs.md).
860
397
 
861
- ```sh
862
- ekwo pack describe --json | jq '.data.packs[] | {country, version}'
863
- ```
864
-
865
- Under `--json` the whole description of every pack is the result. It is the
866
- same object the site at [ekwo.ai](https://ekwo.ai/countries/) builds each
867
- country's page from, so the site and the command line cannot come to say
868
- different things about a country.
869
-
870
- `check` validates every file of the pack against
871
- [`packs/schema/pack.1.json`](../../packs/schema/pack.1.json) and against the
872
- rest of the pack, then compares the committed seed with what the compiler makes
873
- of it now. It is what the CI runs, so the SQL cannot drift from the pack. Every
874
- rule it applies is listed in [`docs/packs.md`](../../docs/packs.md), under
875
- "What `ekwo pack check` refuses".
876
-
877
- Neither command touches a database: the seed is applied by `ekwo init`,
878
- `supabase db push` or `psql -f`, like every other seed. A published
879
- installation has the compiled seeds and no `packs/` folder, and the command
880
- says so rather than guessing.
881
-
882
- Two commands under `ekwo pack` do the opposite and read an installation rather
883
- than a checkout, so they take a connection and work without `packs/`:
398
+ Two commands read an installation instead, and need no clone:
884
399
 
885
400
  ```sh
886
401
  ekwo pack status --db-url "$EKWO_DB_URL" # which pack version each company copied
887
402
  ekwo pack upgrade "My Company" --db-url "…" # move it to the version this installation holds
888
403
  ```
889
404
 
890
- `status` changes nothing and exits 1 while a company is behind, so a scheduled
891
- job can ask. `upgrade` applies an addition and a closed validity by itself,
892
- lists everything else for a person to read, and never removes anything from a
893
- company's books; `--apply` is what accepts the differences it listed.
405
+ `upgrade` applies additions on its own, lists everything else for a person to
406
+ read, never removes anything from a company's books, and takes `--apply` to
407
+ accept what it listed.
894
408
 
895
409
  ## Flags
896
410
 
@@ -906,15 +420,8 @@ Every command takes the connection flags:
906
420
  | `--service-role-key <key>` | Needed only to create a user. Either form Supabase issues: a secret key, `sb_secret_…`, sent on the `apikey` header alone because it is not a JWT, or the legacy `service_role` JWT, sent on `apikey` and as a Bearer as before. |
907
421
  | `--yes`, `-y` | Never ask a question. Everything must come from flags or the environment. |
908
422
 
909
- `--project-ref` with `--db-password` and `--db-region` builds the session
910
- pooler host. It no longer guesses which one: the pooler hostname carries a
911
- generation prefix as well as a region, and the region does not determine it,
912
- so `aws-0-<region>` and `aws-1-<region>` are both opened on port 5432 and the
913
- one that answers is kept and printed. Without `--db-region` nothing is
914
- derived — the CLI asks for the connection string, because the direct host
915
- `db.<ref>.supabase.co` is IPv6-only on recent projects and deriving it
916
- silently produces a hang rather than an error. `--db-url`, copied from the
917
- dashboard under Connect → Session pooler, is the form that is never derived.
423
+ Without `--db-url` or `--db-region`, nothing is derived and the CLI asks for
424
+ the connection string: copy it from the dashboard, Connect → Session pooler.
918
425
 
919
426
  `ekwo init` adds:
920
427
 
@@ -960,6 +467,7 @@ Every command takes `--json`; see [what a command answers](#what-a-command-answe
960
467
  | `EKWO_EMAIL`, `EKWO_PASSWORD` | Sign in for one command, writing nothing. `ekwo login` reads them too. |
961
468
  | `EKWO_ACCESS_TOKEN` | The same, with a session token already in hand. It is not renewed. |
962
469
  | `EKWO_PROFILE` | `--profile` |
470
+ | `EKWO_EINVOICE_DIRECTORY` | `--to`, for `ekwo einvoice`. The MCP server reads it too. |
963
471
  | `EKWO_CONFIG_DIR` | Where profiles and sessions are kept. |
964
472
  | `EKWO_REGISTRY_URL` | `--registry-url`. Default `https://api.ekwo.ai/v1/registrations`. |
965
473
  | `EKWO_NO_REGISTER_INVITE` | Set to `1` to hide the invitation to register that `ekwo init` and `ekwo status` show on an installation that is not registered. The MCP server reads it too. |
@@ -970,147 +478,68 @@ See [`.env.example`](../../.env.example) at the root of the repository.
970
478
  ## Secrets
971
479
 
972
480
  The CLI never writes a password or a key to disk. The database password and
973
- the `service_role` key are read from a flag, an environment variable or a
974
- masked prompt, used, and forgotten; nothing is in `ekwo.json` but the project
975
- URL, the country and the schema version.
976
-
977
- One thing is kept, since `ekwo login`: the session of the person who signed
978
- in — an access token and the refresh token that renews it — in their own
979
- configuration directory, readable by them alone, and refused anywhere inside a
980
- repository. It is in a file and not in the keychain of the operating system;
981
- `ekwo logout` removes it and ends it on the instance, and a job that should
982
- keep nothing sets the environment variables instead.
983
-
984
- It has one runtime dependency from outside this repository, the Postgres
985
- driver. Argument parsing, prompts and the masked input are a few dozen lines
986
- each in this package rather than packages from the registry, because
987
- everything this CLI is handed is a secret and every dependency added is one
988
- more thing that could read it. The other dependency is `@ekwo-ai/core`, this
989
- repository's own, where the CLI and the MCP server read a refusal of the
990
- database the same way.
481
+ the secret key are read from a flag, an environment variable or a masked
482
+ prompt, used, and forgotten; `ekwo.json` holds the project URL and the schema
483
+ version.
484
+
485
+ The one thing kept is the session of the person who signed in with
486
+ `ekwo login`, in their own configuration directory, readable by them alone,
487
+ and refused anywhere inside a repository. `ekwo logout` removes it.
488
+
489
+ Outside this repository the CLI has one runtime dependency, the Postgres
490
+ driver: everything it is handed is a secret, and every dependency is one more
491
+ thing that could read it.
991
492
 
992
493
  ## Registering with Ekwo
993
494
 
994
- At the end of `ekwo init`, in a terminal, you are asked:
495
+ At the end of an interactive `ekwo init`, you are asked:
995
496
 
996
497
  > Register this installation with Ekwo, for security advisories that concern
997
498
  > your version and release notes? Everything works the same without it.
998
499
 
999
- The default answer is no, and no is supported forever.
1000
-
1001
- Where nobody is asked — `--yes`, `--json`, a script — the end of `ekwo init`
1002
- shows a short invitation instead, after everything else, and `ekwo status`
1003
- shows the same one on an installation that is not registered: what registering
1004
- gives, the six fields it sends and the one command,
1005
- `npx -y ekwo-os@latest register --email <your address>`. Under `--json` it is
1006
- the field `registration` of the document, `{ "registered": false,
1007
- "invitation": { … } }`, and the standard output stays one document. It never
1008
- waits for an answer, it disappears once the installation is registered, and
1009
- `EKWO_NO_REGISTER_INVITE=1` hides it. The MCP server gives its agent one line
1010
- to relay to the person, once, on the same terms; see
1011
- [`docs/agents.md`](../../docs/agents.md#registering-the-installation). Community works
1012
- unregistered: nothing in the schema and nothing in this CLI reads
1013
- `contact_email` or `registered_at` to decide what you may do, and `edition`
1014
- gates no feature.
1015
-
1016
- If you say yes, two things happen, independently. `register_instance(email)`
1017
- writes the address and a date onto your instance row, and a POST goes to
1018
- `EKWO_REGISTRY_URL` carrying exactly six fields:
500
+ The default answer is no, and no is supported forever. Where nobody is asked
501
+ (`--yes`, `--json`), the end of `ekwo init` and `ekwo status` show a short
502
+ invitation instead, with the one command:
503
+
504
+ ```sh
505
+ npx -y ekwo-os@latest register --email <your address>
506
+ ```
507
+
508
+ `EKWO_NO_REGISTER_INVITE=1` hides the invitation. Community works
509
+ unregistered: nothing reads the registration to decide what you may do.
510
+
511
+ Registering writes the address and a date on your instance row, and sends
512
+ exactly six fields:
1019
513
 
1020
514
  ```json
1021
515
  {
1022
516
  "instance_id": "…",
1023
517
  "organization": "My Organisation",
1024
- "country": "BE",
518
+ "country": "<cc>",
1025
519
  "edition": "community",
1026
- "schema_version": "0.2.0",
520
+ "schema_version": "0.12.0",
1027
521
  "contact_email": "you@example.com"
1028
522
  }
1029
523
  ```
1030
524
 
1031
525
  No ledger data, no user list, no connection string. `instance_id` is generated
1032
- locally by your own database and is not a licence key: no code path anywhere
1033
- checks it.
1034
-
1035
- **The endpoint does not exist yet.** A failed POST is a soft message, not a
1036
- failed install: the local record stands and `ekwo register` retries it later.
1037
- `ekwo unregister` clears the local fields; it sends nothing, because the CLI
1038
- only holds the local row.
526
+ by your own database and is not a licence key. A failed announcement is a soft
527
+ message, not a failed install: `ekwo register` retries it. `ekwo unregister`
528
+ clears the local fields.
1039
529
 
1040
530
  ## Testing it against a real project
1041
531
 
1042
- The test suite runs against Postgres compiled to WebAssembly, so it proves the
1043
- migration runner, the installation sequence and the checks without a Supabase
1044
- project. Four things it cannot prove: the network driver, PostgREST, GoTrue,
1045
- and the extensions a hosted project has.
1046
-
1047
- **The automated way.** From a checkout of the repository, against an empty
1048
- project you can throw away:
532
+ The test suite runs against Postgres compiled to WebAssembly. To exercise the
533
+ network, PostgREST and Supabase Auth as well, run the end-to-end script from a
534
+ clone, against an empty project you can throw away:
1049
535
 
1050
536
  ```sh
1051
537
  npm run e2e:supabase
1052
538
  ```
1053
539
 
1054
- It installs, migrates, upgrades the pack, signs in, books, files the
1055
- declaration and closes the year, and prints a pass/fail table with **how long
1056
- each step took** — which is the number worth reading, because what matters
1057
- about a release is which step holds it rather than the total. Everything comes
1058
- from the environment and no secret reaches the output; it refuses a database
1059
- that already holds an `instance` row, and `--reset` empties a throwaway project
1060
- so a failed run can be replayed.
1061
-
1062
- Point `EKWO_E2E_PREVIOUS` at the last tag to make the run upgrade an
1063
- installation instead of creating one. The packages are not on npm yet, so it
1064
- takes **a path to a built binary of an older checkout** rather than a version:
1065
-
1066
- ```sh
1067
- git worktree add /tmp/prev v0.2.0
1068
- (cd /tmp/prev && npm ci && npm run build)
1069
- EKWO_E2E_PREVIOUS=/tmp/prev/packages/cli/dist/bin.js npm run e2e:supabase
1070
- ```
1071
-
1072
- [`docs/releasing.md`](../../docs/releasing.md) lists every variable it reads and
1073
- every refusal it makes. It is run by hand before a release is tagged, never by
1074
- the CI.
1075
-
1076
- **By hand**, if you want to watch each step:
1077
-
1078
- ```sh
1079
- npm install && npm run build
1080
-
1081
- # 1. A project you can throw away. Note its ref, password, URL and key.
1082
- # --chart and --language are required here and not optional: the Belgian
1083
- # pack publishes two charts of accounts and four languages, and `ekwo init`
1084
- # refuses to pick either for you when there is nobody to ask.
1085
- node packages/cli/dist/bin.js init \
1086
- --db-url "postgresql://postgres.SCRATCHREF:PASSWORD@aws-1-REGION.pooler.supabase.com:5432/postgres" \
1087
- --supabase-url "https://SCRATCHREF.supabase.co" \
1088
- --service-role-key "$KEY" \
1089
- --country BE --chart default --language fr \
1090
- --org "Scratch" --company "Scratch BV" \
1091
- --admin-email "you@example.com" --admin-password "a-long-password" \
1092
- --fiscal-year 2026 --bank-identifier "BE71096123456769" --yes
1093
-
1094
- # 2. Everything should be green, and nothing pending.
1095
- node packages/cli/dist/bin.js status --db-url "$URL"
1096
- node packages/cli/dist/bin.js doctor --db-url "$URL"
1097
-
1098
- # 3. The history must be the Supabase one: this should report no difference.
1099
- supabase link --project-ref SCRATCHREF
1100
- supabase migration list
1101
-
1102
- # 4. And the other direction: db push finds nothing left to do.
1103
- supabase db push
1104
-
1105
- # 5. Run init again. Every step should say it was already there.
1106
- node packages/cli/dist/bin.js init --db-url "$URL" --country BE \
1107
- --chart default --language fr \
1108
- --org "Scratch" --company "Scratch BV" --admin-email "you@example.com" \
1109
- --admin-user-id "<the uuid from step 1>" --fiscal-year 2026 --yes
1110
-
1111
- # 6. Sign in as the administrator and confirm row level security really binds:
1112
- # a company you were not invited to must be invisible.
1113
- ```
540
+ It installs, migrates, signs in, books, files a declaration and closes the
541
+ year, and prints a pass/fail table with how long each step took.
542
+ [`docs/releasing.md`](../../docs/releasing.md) lists what it reads.
1114
543
 
1115
544
  ## Licence
1116
545