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.
- package/README.md +223 -794
- package/dist/assets/expected-objects.json +728 -2
- package/dist/assets/migrations/20261007190000_an_export_fits_a_key_request.sql +574 -0
- package/dist/assets/migrations/20261007192418_a_filing_is_proved_by_its_hash.sql +505 -0
- package/dist/assets/migrations/20261007200000_schema_version_0_11_1.sql +38 -0
- package/dist/assets/migrations/20261010181201_schema_version_0_12_0.sql +40 -0
- package/dist/assets/modules/einvoicing/module.json +12 -0
- package/dist/assets/modules/einvoicing/supabase/migrations/20261009213006_einvoicing.sql +844 -0
- package/dist/assets/seed/00_currencies.sql +20 -1
- package/dist/assets/seed/00_territories.sql +471 -0
- package/dist/assets/seed/10_pack_be.sql +89 -28
- package/dist/assets/seed/11_pack_fr.sql +4 -2
- package/dist/assets/seed/125_pack_pk.sql +717 -0
- package/dist/assets/seed/126_pack_bd.sql +733 -0
- package/dist/assets/seed/127_pack_lk.sql +639 -0
- package/dist/assets/seed/128_pack_am.sql +603 -0
- package/dist/assets/seed/129_pack_az.sql +604 -0
- package/dist/assets/seed/130_pack_hn.sql +622 -0
- package/dist/assets/seed/131_pack_sv.sql +601 -0
- package/dist/assets/seed/132_pack_ni.sql +599 -0
- package/dist/assets/seed/133_pack_et.sql +583 -0
- package/dist/assets/seed/134_pack_mu.sql +641 -0
- package/dist/assets/seed/135_pack_mz.sql +887 -0
- package/dist/assets/seed/136_pack_ao.sql +789 -0
- package/dist/assets/seed/137_pack_zm.sql +600 -0
- package/dist/assets/seed/138_pack_bm.sql +432 -0
- package/dist/assets/seed/139_pack_ky.sql +429 -0
- package/dist/assets/seed/140_pack_vg.sql +440 -0
- package/dist/assets/seed/141_pack_gg.sql +531 -0
- package/dist/assets/seed/142_pack_qa.sql +444 -0
- package/dist/assets/seed/143_pack_kw.sql +434 -0
- package/dist/assets/seed/144_pack_bn.sql +448 -0
- package/dist/assets/seed/145_pack_mo.sql +474 -0
- package/dist/assets/seed/146_pack_je.sql +669 -0
- package/dist/assets/seed/147_pack_zw.sql +669 -0
- package/dist/assets/seed/45_pack_ar.sql +16 -2
- package/dist/assets/seed/46_pack_cl.sql +6 -6
- package/dist/assets/seed/68_pack_cz.sql +8 -8
- package/dist/assets/seed/72_pack_ro.sql +14 -14
- package/dist/assets/seed/modules/corporate_tax/10_pack_be.sql +1 -1
- package/dist/assets/seed/modules/corporate_tax/11_pack_fr.sql +1 -1
- package/dist/assets/seed/modules/fixed_assets/10_pack_be.sql +1 -1
- package/dist/assets/seed/modules/fixed_assets/11_pack_fr.sql +1 -1
- package/dist/cli.d.ts +3 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +37 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/document.d.ts.map +1 -1
- package/dist/commands/document.js +59 -4
- package/dist/commands/document.js.map +1 -1
- package/dist/commands/einvoice.d.ts +23 -0
- package/dist/commands/einvoice.d.ts.map +1 -0
- package/dist/commands/einvoice.js +185 -0
- package/dist/commands/einvoice.js.map +1 -0
- package/dist/commands/proof.d.ts +31 -0
- package/dist/commands/proof.d.ts.map +1 -0
- package/dist/commands/proof.js +175 -0
- package/dist/commands/proof.js.map +1 -0
- package/dist/output.js +2 -2
- package/dist/output.js.map +1 -1
- package/dist/pack/read.d.ts.map +1 -1
- package/dist/pack/read.js +39 -0
- package/dist/pack/read.js.map +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +1 -1
- package/package.json +3 -3
- 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,
|
|
6
|
-
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
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
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
are
|
|
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 —
|
|
64
|
-
|
|
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**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
74
|
-
installer prints them at the end of a successful run
|
|
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-
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
145
|
-
ekwo company new "My Company
|
|
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`
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
221
|
-
|
|
222
|
-
|
|
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"**,
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
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 |
|
|
273
|
-
|
|
274
|
-
| 1 | Applies `supabase/migrations/*.sql` in order
|
|
275
|
-
| 2 | Applies every reference seed
|
|
276
|
-
| 3 | Creates the first administrator through the Supabase Auth admin API
|
|
277
|
-
| 4 |
|
|
278
|
-
| 5 | Writes `ekwo.json
|
|
279
|
-
| 6 | Asks whether to register with Ekwo
|
|
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
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
382
|
-
| `2` | The command was called wrong
|
|
383
|
-
| `3` | **The database refused
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
is
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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.
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
|
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
|
-
|
|
500
|
-
and
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
|
566
|
-
|
|
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
|
|
570
|
-
#
|
|
571
|
-
|
|
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
|
|
584
|
-
| `fec` | A *fichier des écritures comptables
|
|
585
|
-
| `journal-items` | The lines of every entry exported as CSV from
|
|
586
|
-
| `journal-report` | A journal report or
|
|
587
|
-
| `transaction-journal` | A transaction journal
|
|
588
|
-
| `xaf` | An XML Audit File Financial, version 3.2 or 4.0
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
the
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
|
761
|
-
|
|
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
|
|
367
|
+
ekwo doctor --db-url "$URL" --json # the whole report
|
|
768
368
|
```
|
|
769
369
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
|
827
|
-
|
|
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
|
|
838
|
-
ekwo pack describe
|
|
839
|
-
ekwo pack build
|
|
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
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
910
|
-
|
|
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
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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`,
|
|
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
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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": "
|
|
518
|
+
"country": "<cc>",
|
|
1025
519
|
"edition": "community",
|
|
1026
|
-
"schema_version": "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
|
-
|
|
1033
|
-
|
|
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
|
|
1043
|
-
|
|
1044
|
-
|
|
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,
|
|
1055
|
-
|
|
1056
|
-
|
|
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
|
|