holivia 0.11.0 → 0.13.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +74 -12
- data/SKILL.md +65 -7
- data/ai-skills/add-holivia-cli-endpoint.md +9 -4
- data/exe/holivia +4 -1
- data/lib/holivia/auth.rb +31 -19
- data/lib/holivia/browser_login.rb +125 -0
- data/lib/holivia/cli/help.rb +24 -1
- data/lib/holivia/cli.rb +4 -0
- data/lib/holivia/client.rb +14 -1
- data/lib/holivia/commands/auth.rb +5 -23
- data/lib/holivia/commands/tag.rb +101 -0
- data/lib/holivia/commands/topic.rb +41 -0
- data/lib/holivia/version.rb +1 -1
- data/lib/holivia.rb +1 -0
- metadata +9 -12
- data/.agents/skills/add-holivia-cli-endpoint/SKILL.md +0 -10
- data/.agents/skills/add-holivia-cli-endpoint/agents/openai.yaml +0 -4
- data/.rubocop.yml +0 -31
- data/CLAUDE.md +0 -17
- data/Rakefile +0 -15
- data/mise.toml +0 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5727339aa56fea9ffe71d4f5e0c49fdcd91a2d120aa3f88f7ff33f6a7b6eb49a
|
|
4
|
+
data.tar.gz: 6860e618178ca2b72dd346ec6f02b900cb1aaf48299db21bd6b2e98b6944ad9e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 573b090f0d92ee3676f920992e1c60f71b17b423627a000a4f4d118a1f394657670ce40c19d378f29bf4f56b2f54b682716d66ee70b80db1c7ccf1b088742d37
|
|
7
|
+
data.tar.gz: 26a6014d2c1351a3c2887c406ea572e7c5b73874077533c96c667ef8f11329e3a749954ae2ba141356066d51eff1ec970e758faa236d96687218db48f794d7da
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.13.0] - 2026-09-30
|
|
4
|
+
|
|
5
|
+
- Replace terminal password login with browser OAuth authorization using PKCE and existing password/company SSO sign-in.
|
|
6
|
+
- Require CLI upgrade and a new login; remove `--email`/`--password` and legacy `/api/v1/auth/*` endpoints.
|
|
7
|
+
- Use two-hour access tokens, rotating refresh tokens without a fixed expiry, and server revocation for logout.
|
|
8
|
+
- Store per-environment credentials atomically with owner-only permissions and omit credentials from debug logs.
|
|
9
|
+
- Keep browser and CLI sessions independent; existing API permissions continue to apply.
|
|
10
|
+
- Redirect browser authorization callbacks to the localized Holivia completion page while the CLI exchanges tokens separately.
|
|
11
|
+
- Requires the matching Holivia OAuth server deployment and provisioning of the public CLI application.
|
|
12
|
+
|
|
13
|
+
## [0.12.0] - 2026-09-16
|
|
14
|
+
|
|
15
|
+
- Add `holivia tag` commands for catalogue CRUD and incremental topic-link management through repeatable `--topic-tag-json` flags or stdin JSON
|
|
16
|
+
- Add read-only `holivia topic index` and `holivia topic show` commands with topic filters and pagination
|
|
17
|
+
- Document tag permissions, association IDs, stdin precedence, asynchronous translation, and deletion behavior
|
|
18
|
+
- Move gem development into the SANTEZ monorepo while preserving standalone installation and the `holivia` executable
|
|
19
|
+
- Tag commands require the corresponding backoffice tag API deployment
|
|
20
|
+
|
|
3
21
|
## [0.11.0] - 2026-07-26
|
|
4
22
|
|
|
5
23
|
- Add `holivia support-phone-numbers` commands for backoffice support phone number CRUD and filtering
|
data/README.md
CHANGED
|
@@ -73,21 +73,23 @@ holivia env debug --false
|
|
|
73
73
|
|
|
74
74
|
## Authentication
|
|
75
75
|
|
|
76
|
-
Log in
|
|
76
|
+
Log in through your browser using your existing password or company SSO:
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
79
|
holivia login
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
holivia login --email user@example.com --password secret
|
|
86
|
-
```
|
|
82
|
+
The CLI opens a browser and prints a URL you can open manually on the same computer. A temporary loopback listener receives the callback. Browserless login is not supported.
|
|
87
83
|
|
|
88
84
|
Credentials are stored per environment in `~/.holivia/credentials.<env>.json`. Access tokens are sent as bearer tokens, and the CLI automatically refreshes credentials after a `401` response when a refresh token is available.
|
|
89
85
|
|
|
90
|
-
|
|
86
|
+
Access tokens expire after two hours. Refresh tokens have no fixed expiry and rotate when used. Credential files are replaced atomically with owner-only permissions. Pundit permissions still determine which API operations the user may perform.
|
|
87
|
+
|
|
88
|
+
Existing users must upgrade the CLI and run `holivia login` again. The old `/api/v1/auth/login`, `/api/v1/auth/refresh`, and `/api/v1/auth/logout` endpoints and `--email`/`--password` flags have been removed.
|
|
89
|
+
|
|
90
|
+
The database migration drops the legacy `auth_tokens` table. Rolling it back restores the table structure only; deleted or revoked credentials are not restored. Users must log in again.
|
|
91
|
+
|
|
92
|
+
Logout revokes this CLI session and removes credentials for the current environment. Other CLI sessions and browser sessions remain signed in. If the server cannot be reached, credentials are retained so logout can be retried:
|
|
91
93
|
|
|
92
94
|
```bash
|
|
93
95
|
holivia logout
|
|
@@ -330,6 +332,63 @@ Publish it:
|
|
|
330
332
|
holivia highlight publish 123
|
|
331
333
|
```
|
|
332
334
|
|
|
335
|
+
## Tags And Topic Links
|
|
336
|
+
|
|
337
|
+
Tag reads require a content, therapist, or patient viewer/manager role, or `dev`. Creating, updating, and deleting tags and their topic links require `patient_manager` or `dev`. Topic discovery requires `content_viewer`, `content_manager`, or `dev`: patient-manager access alone is insufficient, but permits linking a known topic ID. A `403` means the account lacks the required role for that operation.
|
|
338
|
+
|
|
339
|
+
Discover topics and inspect existing tags:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
holivia topic index --page 1 --per-page 25
|
|
343
|
+
holivia topic index --theme-id 3 --slug work-pressure
|
|
344
|
+
holivia topic show 12
|
|
345
|
+
holivia tag index --page 1 --per-page 25
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
All IDs below are examples: replace `3` with a theme ID, `12`, `13`, and `14` with IDs from topic responses, and `42` with `tag.id` returned by creation or listing. Use `81` only after reading the actual association ID from `tag.topic_tags[].id` in `tag show`; it is not `topic_id`. Both hidden and visible links are returned with topic summaries.
|
|
349
|
+
|
|
350
|
+
Create and inspect a tag:
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
holivia tag create \
|
|
354
|
+
--code work_pressure \
|
|
355
|
+
--name-fr "Pression au travail" \
|
|
356
|
+
--name-en "Work pressure" \
|
|
357
|
+
--description "Support for work pressure" \
|
|
358
|
+
--topic-tag-json '{"topic_id":12,"hidden":false}'
|
|
359
|
+
holivia tag show 42
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Update only supplied fields; omitted scalar fields remain unchanged. Clear a description with `--description ""` without deleting the tag. Changes to French/English names schedule asynchronous translation after commit: the worker fills missing translations, prefers English as its source, and falls back to French. It does not overwrite existing translations; inspect the tag again later for newly translated names.
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
holivia tag update 42 --description ""
|
|
366
|
+
holivia tag update 42 --topic-tag-json '{"topic_id":13}' --topic-tag-json '{"topic_id":14}'
|
|
367
|
+
holivia tag update 42 --topic-tag-json '{"id":81,"topic_id":13}'
|
|
368
|
+
holivia tag update 42 --topic-tag-json '{"id":81,"hidden":true}'
|
|
369
|
+
holivia tag update 42 --topic-tag-json '{"id":81,"_destroy":true}'
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
These are separate operation examples: choose the needed change, and reassign only to a topic not already linked to that tag. Repeated flags add multiple changes to `topic_tags_attributes`. Removing an association keeps the tag and topic. `hidden` and `_destroy` must be JSON booleans. Scalar and link changes in one request save atomically; an invalid link prevents the entire write.
|
|
373
|
+
|
|
374
|
+
Both create and update accept root-level stdin JSON. Stdin wins over flags on duplicate top-level keys, including replacing the flag-built association array for that request:
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
printf '%s\n' '{"code":"rest","name_fr":"Repos","name_en":"Rest"}' | holivia tag create
|
|
378
|
+
printf '%s\n' '{"description":"From stdin","topic_tags_attributes":[{"id":81,"hidden":false}]}' | holivia tag update 42 --description "From flags"
|
|
379
|
+
printf '%s\n' '{"topic_tags_attributes":[]}' | holivia tag update 42 --topic-tag-json '{"topic_id":13}'
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Topic-link updates are incremental. Omitting `topic_tags_attributes` or sending `[]` preserves existing links; it never clears the persisted list. Use each association's `id` with `_destroy: true` to remove it.
|
|
383
|
+
|
|
384
|
+
Delete the tag when it is no longer needed:
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
holivia tag delete 42
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Deletion removes the tag's patient, therapist, and selfcare assignments plus topic/theme links. Patients, therapists, selfcare contents, topics, and themes remain. A successful API `204` produces `{"deleted":true,"id":42}`; failures do not print success.
|
|
391
|
+
|
|
333
392
|
## Files And Uploads
|
|
334
393
|
|
|
335
394
|
Selfcare create and update commands can upload images with `--image <path>`.
|
|
@@ -356,16 +415,19 @@ Crisis and collective intervention create and update commands accept nested cont
|
|
|
356
415
|
|
|
357
416
|
## Development
|
|
358
417
|
|
|
359
|
-
|
|
418
|
+
Use `bin/holivia` from the SANTEZ root to run the checked-out gem source.
|
|
419
|
+
|
|
420
|
+
After checking out SANTEZ, enter the gem directory and run:
|
|
360
421
|
|
|
361
422
|
```bash
|
|
423
|
+
cd gems/holivia
|
|
362
424
|
bin/setup
|
|
363
425
|
```
|
|
364
426
|
|
|
365
427
|
Run the test suite with:
|
|
366
428
|
|
|
367
429
|
```bash
|
|
368
|
-
rake
|
|
430
|
+
bundle exec rake test
|
|
369
431
|
```
|
|
370
432
|
|
|
371
433
|
Open an interactive console with:
|
|
@@ -386,11 +448,11 @@ To release a new version, update the version number in `lib/holivia/version.rb`,
|
|
|
386
448
|
bundle exec rake release
|
|
387
449
|
```
|
|
388
450
|
|
|
389
|
-
This creates a git tag, pushes commits and
|
|
451
|
+
This creates a `holivia-v<version>` git tag, pushes commits and the tag, and publishes the `.gem` file to [RubyGems.org](https://rubygems.org). Run releases from a clean, merged SANTEZ checkout.
|
|
390
452
|
|
|
391
453
|
## Contributing
|
|
392
454
|
|
|
393
|
-
Bug reports and pull requests are welcome
|
|
455
|
+
Bug reports and pull requests are welcome in the [SANTEZ repository](https://github.com/Holivia-Team/SANTEZ/tree/dockerized-staging/gems/holivia).
|
|
394
456
|
|
|
395
457
|
## License
|
|
396
458
|
|
|
@@ -398,4 +460,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
398
460
|
|
|
399
461
|
## Code Of Conduct
|
|
400
462
|
|
|
401
|
-
Everyone interacting in the Holivia project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [code of conduct](https://github.com/Holivia-Team/
|
|
463
|
+
Everyone interacting in the Holivia project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [code of conduct](https://github.com/Holivia-Team/SANTEZ/blob/dockerized-staging/gems/holivia/CODE_OF_CONDUCT.md).
|
data/SKILL.md
CHANGED
|
@@ -16,7 +16,7 @@ Use `holivia` in examples below. If working from a checkout before installation,
|
|
|
16
16
|
|
|
17
17
|
- Never invent API URLs, credentials, IDs, enum values, or content model fields.
|
|
18
18
|
- Never print, store, or commit passwords, access tokens, refresh tokens, or files under `~/.holivia/credentials.*.json`.
|
|
19
|
-
-
|
|
19
|
+
- Use `holivia login` and let the user authenticate in their browser. Terminal email/password flags are no longer supported.
|
|
20
20
|
- Before creating or updating content, run `holivia selfcare schema` and use its returned enum values and permitted item params as the source of truth.
|
|
21
21
|
- Before creating or updating intervention catalog items, run `holivia intervention schema` and use its returned enum values and locales as the source of truth.
|
|
22
22
|
- Before creating or updating HR home highlights, run `holivia highlight schema` and use its returned enum values and locales as the source of truth.
|
|
@@ -90,19 +90,19 @@ Debug logging may expose request details. Do not enable it in shared logs unless
|
|
|
90
90
|
|
|
91
91
|
Credentials are stored per environment at `~/.holivia/credentials.<env>.json`. The CLI sends the access token as a bearer token and refreshes credentials automatically after a `401` response when possible.
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
Browser login using your existing password or company SSO:
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
96
|
holivia login
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
A browser on the same computer is required. If it does not open automatically, open the printed URL manually. Do not ask for terminal credentials; browserless login is not supported.
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
holivia login --email user@example.com --password secret
|
|
103
|
-
```
|
|
101
|
+
Access tokens expire after two hours. Refresh tokens have no fixed expiry and rotate when used. Credential files are replaced atomically with owner-only permissions. Pundit permissions still determine which API operations the user may perform.
|
|
104
102
|
|
|
105
|
-
|
|
103
|
+
Existing users must upgrade the CLI and run `holivia login` again. The old `/api/v1/auth/login`, `/api/v1/auth/refresh`, and `/api/v1/auth/logout` endpoints and `--email`/`--password` flags have been removed.
|
|
104
|
+
|
|
105
|
+
Logout revokes this CLI session and removes credentials for the current environment. Other CLI sessions and browser sessions remain signed in. If the server cannot be reached, credentials are retained so logout can be retried:
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
108
|
holivia logout
|
|
@@ -295,6 +295,64 @@ holivia objective show 123
|
|
|
295
295
|
|
|
296
296
|
Objective translated fields are returned in the default locale, currently `fr`.
|
|
297
297
|
|
|
298
|
+
## Tags And Topic Links
|
|
299
|
+
|
|
300
|
+
Permissions: tag reads allow content, therapist, or patient viewer/manager roles and `dev`. Tag writes and deletion require `patient_manager` or `dev`. Topic discovery requires `content_viewer`, `content_manager`, or `dev`. A patient manager may link a known topic ID without topic-read access. Treat a `403` as a missing role for that operation; do not assume tag-write access grants topic discovery.
|
|
301
|
+
|
|
302
|
+
Discover topics and tags, then obtain IDs from their JSON responses:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
holivia topic index --page 1 --per-page 25
|
|
306
|
+
holivia topic index --theme-id 3 --slug work-pressure
|
|
307
|
+
holivia topic show 12
|
|
308
|
+
holivia tag index --page 1 --per-page 25
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The following IDs are examples, not real targets: `3` is a theme ID; `12`, `13`, and `14` are topic IDs from discovery; `42` is `tag.id` from creation/listing. `81` is a TopicTag association ID from `tag.topic_tags[].id` returned by `tag show`, distinct from `topic_id`. Hidden links are included in that response.
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
holivia tag create \
|
|
315
|
+
--code work_pressure \
|
|
316
|
+
--name-fr "Pression au travail" \
|
|
317
|
+
--name-en "Work pressure" \
|
|
318
|
+
--topic-tag-json '{"topic_id":12,"hidden":false}'
|
|
319
|
+
holivia tag show 42
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Use the appropriate operation below rather than blindly executing every example. Reassignment must target a topic not already linked to the tag:
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
# Add links; repeated flags form topic_tags_attributes
|
|
326
|
+
holivia tag update 42 --topic-tag-json '{"topic_id":13}' --topic-tag-json '{"topic_id":14}'
|
|
327
|
+
# Reassign one existing link
|
|
328
|
+
holivia tag update 42 --topic-tag-json '{"id":81,"topic_id":13}'
|
|
329
|
+
# Change visibility, then remove just that association
|
|
330
|
+
holivia tag update 42 --topic-tag-json '{"id":81,"hidden":true}'
|
|
331
|
+
holivia tag update 42 --topic-tag-json '{"id":81,"_destroy":true}'
|
|
332
|
+
# Clear description while keeping the tag
|
|
333
|
+
holivia tag update 42 --description ""
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Create/update scalar flags are `--code`, `--name-fr`, `--name-en`, and `--description`. Updates preserve omitted fields. French/English name changes enqueue translation after commit; missing translations are filled asynchronously using English first, French as fallback. Existing translations are not overwritten. Read again later when waiting for translated names.
|
|
337
|
+
|
|
338
|
+
Both write commands accept root-level stdin JSON. Stdin overrides duplicate top-level flag keys, including the complete flag-built `topic_tags_attributes` array for that request:
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
printf '%s\n' '{"code":"rest","name_fr":"Repos","name_en":"Rest"}' | holivia tag create
|
|
342
|
+
printf '%s\n' '{"description":"From stdin","topic_tags_attributes":[{"id":81,"hidden":false}]}' | holivia tag update 42 --description "From flags"
|
|
343
|
+
printf '%s\n' '{"topic_tags_attributes":[]}' | holivia tag update 42 --topic-tag-json '{"topic_id":13}'
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Omitted association input and `[]` are no-ops: existing links remain. Use association IDs and `_destroy: true` for removal. Use JSON booleans for `hidden` and `_destroy`. Scalar and association changes are atomic; correct a failed request rather than assuming some links were saved. Malformed flag JSON is rejected locally; unknown/foreign association IDs return `404`, and invalid fields or duplicate links return `422`.
|
|
347
|
+
|
|
348
|
+
Delete a tag with:
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
holivia tag delete 42
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
This removes patient, therapist, and selfcare taggings and topic/theme links while retaining all linked patients, therapists, contents, topics, and themes. Report deletion only after the CLI returns `{"deleted":true,"id":42}` following API success.
|
|
355
|
+
|
|
298
356
|
## Enterprises
|
|
299
357
|
|
|
300
358
|
Find an enterprise id:
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Keep endpoint commands thin and consistent with the nearest existing resource. Implement the API contract at the command layer; reuse the shared client for transport, authentication, retries, response parsing, and API errors.
|
|
4
4
|
|
|
5
|
+
The gem lives in the SANTEZ monorepo under `gems/holivia`. Paths in this guide are relative to that directory unless stated otherwise.
|
|
6
|
+
|
|
5
7
|
## Inspect Before Editing
|
|
6
8
|
|
|
7
|
-
1. Read `
|
|
9
|
+
1. Read the repository root `AGENTS.md` and check the worktree for user changes.
|
|
10
|
+
Read `ai-rules/cli-development.md` at the repository root for bundle selection, executable isolation, and verification commands.
|
|
8
11
|
2. Establish the endpoint contract from the request or authoritative API material: HTTP verb, exact path, path parameters, query parameters, body keys and types, file fields, response shape, and required CLI name. Do not invent missing contract details.
|
|
9
12
|
3. Read these architecture files:
|
|
10
13
|
- `lib/holivia/cli.rb` for top-level registration.
|
|
@@ -102,16 +105,18 @@ Keep these tests at the CLI-to-client boundary. Do not make network requests. Ad
|
|
|
102
105
|
|
|
103
106
|
## Verify and Review
|
|
104
107
|
|
|
108
|
+
Run these commands with `gems/holivia` as the working directory and its Gemfile explicitly selected. Follow `ai-rules/cli-development.md` at the repository root for execution-environment guidance, including changes spanning Rails and the CLI.
|
|
109
|
+
|
|
105
110
|
Run the narrow test first:
|
|
106
111
|
|
|
107
112
|
```bash
|
|
108
|
-
bundle exec ruby -Itest test/holivia/commands/test_<resource>.rb
|
|
113
|
+
BUNDLE_GEMFILE="$PWD/Gemfile" bundle exec ruby -Itest test/holivia/commands/test_<resource>.rb
|
|
109
114
|
```
|
|
110
115
|
|
|
111
|
-
Then run the
|
|
116
|
+
Then run the full CLI tests and lint, bypassing the Rails `bin/rake` wrapper:
|
|
112
117
|
|
|
113
118
|
```bash
|
|
114
|
-
bundle exec rake
|
|
119
|
+
BUNDLE_GEMFILE="$PWD/Gemfile" bundle exec ruby -e 'load Gem.bin_path("rake", "rake")'
|
|
115
120
|
```
|
|
116
121
|
|
|
117
122
|
Before finishing, inspect the diff and confirm:
|
data/exe/holivia
CHANGED
|
@@ -9,7 +9,10 @@ rescue Holivia::ApiError => e
|
|
|
9
9
|
warn "Error (#{e.status}): #{e.message}"
|
|
10
10
|
warn JSON.pretty_generate(e.details) if e.details.is_a?(Hash)
|
|
11
11
|
exit 1
|
|
12
|
-
rescue Holivia::ConfigError => e
|
|
12
|
+
rescue Holivia::ConfigError, Holivia::BrowserLogin::Error, OptionParser::ParseError, Faraday::Error => e
|
|
13
13
|
warn "Error: #{e.message}"
|
|
14
14
|
exit 1
|
|
15
|
+
rescue Interrupt
|
|
16
|
+
warn "Cancelled."
|
|
17
|
+
exit 130
|
|
15
18
|
end
|
data/lib/holivia/auth.rb
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require "tempfile"
|
|
5
6
|
|
|
6
7
|
module Holivia
|
|
7
8
|
class Auth
|
|
8
|
-
def initialize(client:
|
|
9
|
-
@client = client
|
|
9
|
+
def initialize(client: nil, configuration: Holivia.configuration)
|
|
10
|
+
@client = client || Client.new(base_url: configuration.base_url, auth: nil)
|
|
11
|
+
@credentials_path = configuration.credentials_path.dup.freeze
|
|
10
12
|
end
|
|
11
13
|
|
|
12
|
-
def login(
|
|
13
|
-
response = client.
|
|
14
|
+
def login(code:, code_verifier:, redirect_uri:)
|
|
15
|
+
response = client.post_form("/oauth/token", body: {
|
|
16
|
+
grant_type: "authorization_code", client_id: BrowserLogin::CLIENT_ID,
|
|
17
|
+
code: code, code_verifier: code_verifier, redirect_uri: redirect_uri
|
|
18
|
+
})
|
|
14
19
|
store_tokens(response)
|
|
15
20
|
response
|
|
16
21
|
end
|
|
@@ -18,15 +23,25 @@ module Holivia
|
|
|
18
23
|
def refresh
|
|
19
24
|
raise ApiError.new("No refresh token. Run `holivia login`.", status: 401) unless refresh_token
|
|
20
25
|
|
|
21
|
-
response = client.
|
|
26
|
+
response = client.post_form("/oauth/token", body: {
|
|
27
|
+
grant_type: "refresh_token", client_id: BrowserLogin::CLIENT_ID,
|
|
28
|
+
refresh_token: refresh_token
|
|
29
|
+
})
|
|
22
30
|
store_tokens(response)
|
|
23
31
|
response
|
|
32
|
+
rescue ApiError => e
|
|
33
|
+
raise unless e.details.is_a?(Hash) && e.details["error"] == "invalid_grant"
|
|
34
|
+
|
|
35
|
+
raise ApiError.new("Session expired or revoked. Run `holivia login`.", status: 401)
|
|
24
36
|
end
|
|
25
37
|
|
|
26
38
|
def logout
|
|
27
|
-
raise ApiError.new("Already logged out.", status: 401) unless
|
|
39
|
+
raise ApiError.new("Already logged out.", status: 401) unless refresh_token
|
|
28
40
|
|
|
29
|
-
response = client.
|
|
41
|
+
response = client.post_form("/oauth/revoke", body: {
|
|
42
|
+
client_id: BrowserLogin::CLIENT_ID, token: refresh_token,
|
|
43
|
+
token_type_hint: "refresh_token"
|
|
44
|
+
})
|
|
30
45
|
FileUtils.rm_f(credentials_path)
|
|
31
46
|
response
|
|
32
47
|
end
|
|
@@ -41,7 +56,7 @@ module Holivia
|
|
|
41
56
|
|
|
42
57
|
private
|
|
43
58
|
|
|
44
|
-
attr_reader :client
|
|
59
|
+
attr_reader :client, :credentials_path
|
|
45
60
|
|
|
46
61
|
def credentials
|
|
47
62
|
return nil unless File.exist?(credentials_path)
|
|
@@ -50,17 +65,14 @@ module Holivia
|
|
|
50
65
|
end
|
|
51
66
|
|
|
52
67
|
def store_tokens(data)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def credentials_path
|
|
63
|
-
Holivia.configuration.credentials_path
|
|
68
|
+
directory = File.dirname(credentials_path)
|
|
69
|
+
FileUtils.mkdir_p(directory, mode: 0o700)
|
|
70
|
+
Tempfile.create(["credentials", ".json"], directory) do |file|
|
|
71
|
+
file.write(JSON.pretty_generate(data.slice("access_token", "refresh_token")))
|
|
72
|
+
file.flush
|
|
73
|
+
file.fsync
|
|
74
|
+
File.rename(file.path, credentials_path)
|
|
75
|
+
end
|
|
64
76
|
end
|
|
65
77
|
end
|
|
66
78
|
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "socket"
|
|
6
|
+
require "timeout"
|
|
7
|
+
require "uri"
|
|
8
|
+
|
|
9
|
+
module Holivia
|
|
10
|
+
class BrowserLogin
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
CLIENT_ID = "holivia-cli"
|
|
14
|
+
|
|
15
|
+
def initialize(base_url: Holivia.configuration.base_url, output: $stderr, timeout: 180, browser_launcher: nil)
|
|
16
|
+
@base_url = base_url.dup.freeze
|
|
17
|
+
@output = output
|
|
18
|
+
@timeout = timeout
|
|
19
|
+
@browser_launcher = browser_launcher || method(:launch_browser)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
verifier = SecureRandom.urlsafe_base64(32)
|
|
24
|
+
state = SecureRandom.urlsafe_base64(32)
|
|
25
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
26
|
+
redirect_uri = "http://127.0.0.1:#{server.addr[1]}/callback"
|
|
27
|
+
|
|
28
|
+
Timeout.timeout(@timeout, Error, "Browser login timed out. Run holivia login to try again.") do
|
|
29
|
+
open_browser(authorization_url(verifier, state, redirect_uri))
|
|
30
|
+
code = receive_code(server, state)
|
|
31
|
+
{ code: code, code_verifier: verifier, redirect_uri: redirect_uri }
|
|
32
|
+
end
|
|
33
|
+
ensure
|
|
34
|
+
server&.close
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def authorization_url(verifier, state, redirect_uri)
|
|
40
|
+
url = URI.join(@base_url, "/oauth/authorize")
|
|
41
|
+
url.query = URI.encode_www_form(
|
|
42
|
+
client_id: CLIENT_ID, response_type: "code", redirect_uri: redirect_uri,
|
|
43
|
+
code_challenge: [Digest::SHA256.digest(verifier)].pack("m0").tr("+/", "-_").delete("="),
|
|
44
|
+
code_challenge_method: "S256", state: state
|
|
45
|
+
)
|
|
46
|
+
url.to_s
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def open_browser(url)
|
|
50
|
+
@output.puts "Open this URL in a browser on this computer to log in:\n#{url}"
|
|
51
|
+
@browser_launcher.call(url)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def launch_browser(url)
|
|
55
|
+
command = RUBY_PLATFORM.include?("darwin") ? "open" : "xdg-open"
|
|
56
|
+
system(command, url, out: File::NULL, err: File::NULL)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def receive_code(server, state)
|
|
60
|
+
loop do
|
|
61
|
+
code = handle_callback(server.accept, state)
|
|
62
|
+
return code if code
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def handle_callback(socket, state)
|
|
67
|
+
params = callback_params(socket.gets("\r\n\r\n", 8192))
|
|
68
|
+
unless valid_callback?(params, state)
|
|
69
|
+
respond(socket, "400 Bad Request", "Invalid login callback.")
|
|
70
|
+
return nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
complete_callback(socket, params)
|
|
74
|
+
rescue SystemCallError, IOError
|
|
75
|
+
nil # A disconnected browser must not prevent a later valid callback.
|
|
76
|
+
ensure
|
|
77
|
+
socket.close
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def callback_params(request)
|
|
81
|
+
return {} unless request&.end_with?("\r\n\r\n")
|
|
82
|
+
|
|
83
|
+
match = request.lines.first.match(%r{\AGET /callback\?([^\s]+) HTTP/1\.[01]\r\n\z})
|
|
84
|
+
return {} unless match
|
|
85
|
+
|
|
86
|
+
pairs = URI.decode_www_form(match[1])
|
|
87
|
+
return {} unless pairs.map(&:first).uniq.length == pairs.length
|
|
88
|
+
|
|
89
|
+
pairs.to_h
|
|
90
|
+
rescue ArgumentError
|
|
91
|
+
{}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def valid_callback?(params, state)
|
|
95
|
+
return false unless params["state"] == state
|
|
96
|
+
|
|
97
|
+
code = params["code"]
|
|
98
|
+
error = params["error"]
|
|
99
|
+
(code && !code.empty? && error.nil?) || (error && !error.empty? && code.nil?)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def complete_callback(socket, params)
|
|
103
|
+
if params["error"]
|
|
104
|
+
respond(socket, "200 OK", "Authorization was not granted. Return to the terminal.")
|
|
105
|
+
raise Error, "Authorization was not granted. Run holivia login to try again."
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
redirect_to_completion(socket)
|
|
109
|
+
params.fetch("code")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def redirect_to_completion(socket)
|
|
113
|
+
url = URI.join(@base_url, "/oauth/complete")
|
|
114
|
+
socket.write "HTTP/1.1 303 See Other\r\nLocation: #{url}\r\n" \
|
|
115
|
+
"Referrer-Policy: no-referrer\r\nCache-Control: no-store\r\n" \
|
|
116
|
+
"Content-Length: 0\r\nConnection: close\r\n\r\n"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def respond(socket, status, message)
|
|
120
|
+
socket.write "HTTP/1.1 #{status}\r\nContent-Type: text/plain; charset=utf-8\r\n" \
|
|
121
|
+
"Content-Length: #{message.bytesize}\r\nCache-Control: no-store\r\n" \
|
|
122
|
+
"Connection: close\r\n\r\n#{message}"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
data/lib/holivia/cli/help.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Holivia
|
|
|
17
17
|
Config stored at ~/.holivia/config.yml. Defaults to staging if no config exists.
|
|
18
18
|
|
|
19
19
|
Authentication:
|
|
20
|
-
holivia login
|
|
20
|
+
holivia login Log in through your browser (password or company SSO)
|
|
21
21
|
holivia logout Log out and clear stored credentials
|
|
22
22
|
Tokens are stored at ~/.holivia/credentials.<env>.json and refreshed automatically on 401.
|
|
23
23
|
|
|
@@ -50,6 +50,29 @@ module Holivia
|
|
|
50
50
|
holivia objective show <id> Show one objective
|
|
51
51
|
Translated fields are returned in the default locale (fr).
|
|
52
52
|
|
|
53
|
+
Topics:
|
|
54
|
+
holivia topic index [--theme-id ID --slug SLUG --page N --per-page N] List topics
|
|
55
|
+
holivia topic show <id> Show one topic by numeric ID
|
|
56
|
+
Requires content_viewer, content_manager, or dev. Patient-manager access alone is insufficient.
|
|
57
|
+
|
|
58
|
+
Tags:
|
|
59
|
+
holivia tag index [--page N --per-page N] List tags
|
|
60
|
+
holivia tag show <id> Show one tag with its topic links
|
|
61
|
+
holivia tag create [options] Create a tag
|
|
62
|
+
holivia tag update <id> [options] Update a tag
|
|
63
|
+
holivia tag delete <id> Delete a tag and its assignments and links; keep linked records
|
|
64
|
+
Write options: --code CODE --name-fr NAME --name-en NAME --description TEXT.
|
|
65
|
+
Piped JSON is merged after flags; stdin wins on duplicate keys.
|
|
66
|
+
Includes hidden links. Each topic_tags entry has an association ID distinct from its topic_id.
|
|
67
|
+
Create/update accept repeatable --topic-tag-json JSON flags, forming topic_tags_attributes:
|
|
68
|
+
Add: --topic-tag-json '{"topic_id":12,"hidden":false}'
|
|
69
|
+
Reassign: --topic-tag-json '{"id":81,"topic_id":13}'
|
|
70
|
+
Visibility: --topic-tag-json '{"id":81,"hidden":true}'
|
|
71
|
+
Remove link: --topic-tag-json '{"id":81,"_destroy":true}'
|
|
72
|
+
Example IDs: 12/13 are topic IDs; 81 is an association ID returned by tag show.
|
|
73
|
+
Changes are incremental: omitted links and [] preserve existing links; [] does not clear them.
|
|
74
|
+
Stdin topic_tags_attributes replaces the flag-built array for this request, not persisted links.
|
|
75
|
+
|
|
53
76
|
Enterprises:
|
|
54
77
|
holivia enterprise index [--query TEXT --headquarter-id ID --user-id ID --page N --per-page N]
|
|
55
78
|
Find enterprises and their enterprise_id values
|
data/lib/holivia/cli.rb
CHANGED
|
@@ -9,6 +9,8 @@ require_relative "commands/slide"
|
|
|
9
9
|
require_relative "commands/env"
|
|
10
10
|
require_relative "commands/item"
|
|
11
11
|
require_relative "commands/objective"
|
|
12
|
+
require_relative "commands/topic"
|
|
13
|
+
require_relative "commands/tag"
|
|
12
14
|
require_relative "commands/appointment"
|
|
13
15
|
require_relative "commands/communication_item"
|
|
14
16
|
require_relative "commands/intervention_item"
|
|
@@ -28,6 +30,8 @@ module Holivia
|
|
|
28
30
|
when "env" then Commands::Env.route(args)
|
|
29
31
|
when "selfcare" then Commands::Selfcare.route(args)
|
|
30
32
|
when "objective" then Commands::Objective.route(args)
|
|
33
|
+
when "topic" then Commands::Topic.route(args)
|
|
34
|
+
when "tag" then Commands::Tag.route(args)
|
|
31
35
|
when "appointment" then Commands::Appointment.route(args)
|
|
32
36
|
when "communication" then Commands::CommunicationItem.route(args)
|
|
33
37
|
when "intervention" then Commands::InterventionItem.route(args)
|
data/lib/holivia/client.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "faraday"
|
|
4
4
|
require "faraday/multipart"
|
|
5
|
+
require "uri"
|
|
5
6
|
|
|
6
7
|
module Holivia
|
|
7
8
|
class Client
|
|
@@ -15,6 +16,16 @@ module Holivia
|
|
|
15
16
|
request(:post, path, body:, headers:)
|
|
16
17
|
end
|
|
17
18
|
|
|
19
|
+
def post_form(path, body: {})
|
|
20
|
+
response = connection.post(path) do |req|
|
|
21
|
+
req.headers["Accept"] = "application/json"
|
|
22
|
+
req.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
23
|
+
req.body = URI.encode_www_form(body)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
handle_response(response)
|
|
27
|
+
end
|
|
28
|
+
|
|
18
29
|
def get(path, params: {}, headers: {})
|
|
19
30
|
request(:get, path, params:, headers:)
|
|
20
31
|
end
|
|
@@ -59,7 +70,7 @@ module Holivia
|
|
|
59
70
|
@connection ||= http.new(url: base_url) do |f|
|
|
60
71
|
f.request :multipart
|
|
61
72
|
f.request :json
|
|
62
|
-
f.response :logger if Holivia.configuration.debug?
|
|
73
|
+
f.response :logger, nil, headers: false, bodies: false if Holivia.configuration.debug?
|
|
63
74
|
f.response :json
|
|
64
75
|
f.adapter Faraday.default_adapter
|
|
65
76
|
end
|
|
@@ -77,6 +88,8 @@ module Holivia
|
|
|
77
88
|
return "Request failed" unless body.is_a?(Hash)
|
|
78
89
|
|
|
79
90
|
error = body["error"]
|
|
91
|
+
return [error, body["error_description"]].compact.join(": ") if error.is_a?(String)
|
|
92
|
+
|
|
80
93
|
error.is_a?(Hash) ? (error["message"] || "Request failed") : (error || "Request failed")
|
|
81
94
|
end
|
|
82
95
|
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "io/console"
|
|
4
3
|
require "optparse"
|
|
5
4
|
|
|
6
5
|
module Holivia
|
|
7
6
|
module Commands
|
|
8
7
|
class Auth
|
|
9
8
|
def login(args = [])
|
|
10
|
-
options = {}
|
|
11
9
|
OptionParser.new do |opts|
|
|
12
|
-
opts.banner = "Usage: holivia login
|
|
13
|
-
opts.on("--email EMAIL") { |v| options[:email] = v }
|
|
14
|
-
opts.on("--password PASSWORD") { |v| options[:password] = v }
|
|
10
|
+
opts.banner = "Usage: holivia login"
|
|
15
11
|
end.parse!(args)
|
|
12
|
+
raise OptionParser::InvalidArgument, args.join(" ") unless args.empty?
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Holivia::Auth.new.login(email: email, password: password)
|
|
14
|
+
auth = Holivia::Auth.new
|
|
15
|
+
exchange = BrowserLogin.new.call
|
|
16
|
+
auth.login(**exchange)
|
|
21
17
|
puts "Logged in successfully."
|
|
22
18
|
end
|
|
23
19
|
|
|
@@ -25,20 +21,6 @@ module Holivia
|
|
|
25
21
|
Holivia::Auth.new.logout
|
|
26
22
|
puts "Logged out successfully."
|
|
27
23
|
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def prompt(label)
|
|
32
|
-
print label
|
|
33
|
-
$stdin.gets.chomp
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def prompt_secret(label)
|
|
37
|
-
print label
|
|
38
|
-
result = $stdin.noecho(&:gets).chomp
|
|
39
|
-
puts
|
|
40
|
-
result
|
|
41
|
-
end
|
|
42
24
|
end
|
|
43
25
|
end
|
|
44
26
|
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Holivia
|
|
6
|
+
module Commands
|
|
7
|
+
class Tag < Base
|
|
8
|
+
BASE_PATH = "/api/v1/backoffice/tags"
|
|
9
|
+
|
|
10
|
+
def self.route(args)
|
|
11
|
+
subcommand = args.shift
|
|
12
|
+
case subcommand
|
|
13
|
+
when "index" then new.index(args)
|
|
14
|
+
when "show" then new.show(args)
|
|
15
|
+
when "create" then new.create(args)
|
|
16
|
+
when "update" then new.update(args)
|
|
17
|
+
when "delete" then new.delete(args)
|
|
18
|
+
else warn "Unknown tag command: #{subcommand}"
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def index(args = [])
|
|
24
|
+
params = {}
|
|
25
|
+
OptionParser.new do |opts|
|
|
26
|
+
opts.banner = "Usage: holivia tag index [options]"
|
|
27
|
+
opts.on("--page N", Integer) { |v| params[:page] = v }
|
|
28
|
+
opts.on("--per-page N", Integer) { |v| params[:per_page] = v }
|
|
29
|
+
end.parse!(args)
|
|
30
|
+
|
|
31
|
+
output(client.get(BASE_PATH, params: params))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def show(args = [])
|
|
35
|
+
id = args.shift
|
|
36
|
+
abort "Usage: holivia tag show <id>" unless id
|
|
37
|
+
|
|
38
|
+
output(client.get("#{BASE_PATH}/#{id}"))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def create(args = [])
|
|
42
|
+
options = parse_write_options(args, "Usage: holivia tag create [options]")
|
|
43
|
+
abort "No options provided. Use --help for usage." if options.empty?
|
|
44
|
+
|
|
45
|
+
output(client.post(BASE_PATH, body: options))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def update(args = [])
|
|
49
|
+
id = args.shift
|
|
50
|
+
abort "Usage: holivia tag update <id> [options]" unless id
|
|
51
|
+
|
|
52
|
+
options = parse_write_options(args, "Usage: holivia tag update <id> [options]")
|
|
53
|
+
abort "No options provided. Use --help for usage." if options.empty?
|
|
54
|
+
|
|
55
|
+
output(client.patch("#{BASE_PATH}/#{id}", body: options))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def delete(args = [])
|
|
59
|
+
id = args.shift
|
|
60
|
+
abort "Usage: holivia tag delete <id>" unless id
|
|
61
|
+
|
|
62
|
+
client.delete("#{BASE_PATH}/#{id}")
|
|
63
|
+
output(deleted: true, id: id.to_i)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def parse_write_options(args, banner)
|
|
69
|
+
options = {}
|
|
70
|
+
OptionParser.new do |opts|
|
|
71
|
+
opts.banner = banner
|
|
72
|
+
opts.on("--code CODE") { |v| options[:code] = v }
|
|
73
|
+
opts.on("--name-fr NAME") { |v| options[:name_fr] = v }
|
|
74
|
+
opts.on("--name-en NAME") { |v| options[:name_en] = v }
|
|
75
|
+
opts.on("--description TEXT") { |v| options[:description] = v }
|
|
76
|
+
opts.on("--topic-tag-json JSON", "Repeat for multiple changes") { |v| append_topic_tag(options, v) }
|
|
77
|
+
add_topic_link_help(opts)
|
|
78
|
+
end.parse!(args)
|
|
79
|
+
options.merge(piped_json)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def add_topic_link_help(opts)
|
|
83
|
+
opts.separator "Piped JSON is merged after flags; stdin wins on duplicate keys."
|
|
84
|
+
opts.separator "Topic links are incremental; omitted links and [] preserve existing links."
|
|
85
|
+
opts.separator "Use topic_id to add; id is the association ID from tag show, not the topic ID."
|
|
86
|
+
opts.separator 'Add: {"topic_id":12,"hidden":false}; reassign: {"id":81,"topic_id":13}.'
|
|
87
|
+
opts.separator 'Visibility: {"id":81,"hidden":true}; remove: {"id":81,"_destroy":true}.'
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def append_topic_tag(options, json)
|
|
91
|
+
link = JSON.parse(json, symbolize_names: true)
|
|
92
|
+
abort "--topic-tag-json must be a JSON object (for example: '{\"topic_id\":12}')." unless link.is_a?(Hash)
|
|
93
|
+
|
|
94
|
+
options[:topic_tags_attributes] ||= []
|
|
95
|
+
options[:topic_tags_attributes] << link
|
|
96
|
+
rescue JSON::ParserError
|
|
97
|
+
abort "Invalid JSON for --topic-tag-json. Pass one JSON object (for example: '{\"topic_id\":12}')."
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Holivia
|
|
6
|
+
module Commands
|
|
7
|
+
class Topic < Base
|
|
8
|
+
BASE_PATH = "/api/v1/backoffice/topics"
|
|
9
|
+
|
|
10
|
+
def self.route(args)
|
|
11
|
+
subcommand = args.shift
|
|
12
|
+
case subcommand
|
|
13
|
+
when "index" then new.index(args)
|
|
14
|
+
when "show" then new.show(args)
|
|
15
|
+
else warn "Unknown topic command: #{subcommand}"
|
|
16
|
+
exit 1
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def index(args = [])
|
|
21
|
+
params = {}
|
|
22
|
+
OptionParser.new do |opts|
|
|
23
|
+
opts.banner = "Usage: holivia topic index [options]"
|
|
24
|
+
opts.on("--theme-id ID", Integer) { |v| params[:theme_id] = v }
|
|
25
|
+
opts.on("--slug SLUG") { |v| params[:slug] = v }
|
|
26
|
+
opts.on("--page N", Integer) { |v| params[:page] = v }
|
|
27
|
+
opts.on("--per-page N", Integer) { |v| params[:per_page] = v }
|
|
28
|
+
end.parse!(args)
|
|
29
|
+
|
|
30
|
+
output(client.get(BASE_PATH, params: params))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def show(args = [])
|
|
34
|
+
id = args.shift
|
|
35
|
+
abort "Usage: holivia topic show <id>" unless id
|
|
36
|
+
|
|
37
|
+
output(client.get("#{BASE_PATH}/#{id}"))
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/holivia/version.rb
CHANGED
data/lib/holivia.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: holivia
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Holivia
|
|
@@ -45,15 +45,10 @@ executables:
|
|
|
45
45
|
extensions: []
|
|
46
46
|
extra_rdoc_files: []
|
|
47
47
|
files:
|
|
48
|
-
- ".agents/skills/add-holivia-cli-endpoint/SKILL.md"
|
|
49
|
-
- ".agents/skills/add-holivia-cli-endpoint/agents/openai.yaml"
|
|
50
|
-
- ".rubocop.yml"
|
|
51
48
|
- CHANGELOG.md
|
|
52
|
-
- CLAUDE.md
|
|
53
49
|
- CODE_OF_CONDUCT.md
|
|
54
50
|
- LICENSE.txt
|
|
55
51
|
- README.md
|
|
56
|
-
- Rakefile
|
|
57
52
|
- SKILL.md
|
|
58
53
|
- ai-skills/add-holivia-cli-endpoint.md
|
|
59
54
|
- example_content.xml
|
|
@@ -62,6 +57,7 @@ files:
|
|
|
62
57
|
- lib/holivia.rb
|
|
63
58
|
- lib/holivia/api_error.rb
|
|
64
59
|
- lib/holivia/auth.rb
|
|
60
|
+
- lib/holivia/browser_login.rb
|
|
65
61
|
- lib/holivia/cli.rb
|
|
66
62
|
- lib/holivia/cli/help.rb
|
|
67
63
|
- lib/holivia/client.rb
|
|
@@ -82,19 +78,20 @@ files:
|
|
|
82
78
|
- lib/holivia/commands/selfcare_objective.rb
|
|
83
79
|
- lib/holivia/commands/slide.rb
|
|
84
80
|
- lib/holivia/commands/support_phone_number.rb
|
|
81
|
+
- lib/holivia/commands/tag.rb
|
|
82
|
+
- lib/holivia/commands/topic.rb
|
|
85
83
|
- lib/holivia/config_error.rb
|
|
86
84
|
- lib/holivia/configuration.rb
|
|
87
85
|
- lib/holivia/env_manager.rb
|
|
88
86
|
- lib/holivia/version.rb
|
|
89
|
-
- mise.toml
|
|
90
87
|
- sig/holivia.rbs
|
|
91
|
-
homepage: https://github.com/Holivia-Team/holivia
|
|
88
|
+
homepage: https://github.com/Holivia-Team/SANTEZ/tree/dockerized-staging/gems/holivia
|
|
92
89
|
licenses:
|
|
93
90
|
- MIT
|
|
94
91
|
metadata:
|
|
95
|
-
homepage_uri: https://github.com/Holivia-Team/holivia
|
|
96
|
-
source_code_uri: https://github.com/Holivia-Team/holivia
|
|
97
|
-
changelog_uri: https://github.com/Holivia-Team/
|
|
92
|
+
homepage_uri: https://github.com/Holivia-Team/SANTEZ/tree/dockerized-staging/gems/holivia
|
|
93
|
+
source_code_uri: https://github.com/Holivia-Team/SANTEZ/tree/dockerized-staging/gems/holivia
|
|
94
|
+
changelog_uri: https://github.com/Holivia-Team/SANTEZ/blob/dockerized-staging/gems/holivia/CHANGELOG.md
|
|
98
95
|
rubygems_mfa_required: 'true'
|
|
99
96
|
rdoc_options: []
|
|
100
97
|
require_paths:
|
|
@@ -110,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
110
107
|
- !ruby/object:Gem::Version
|
|
111
108
|
version: '0'
|
|
112
109
|
requirements: []
|
|
113
|
-
rubygems_version:
|
|
110
|
+
rubygems_version: 4.0.10
|
|
114
111
|
specification_version: 4
|
|
115
112
|
summary: Ruby client and CLI for the Holivia API
|
|
116
113
|
test_files: []
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: add-holivia-cli-endpoint
|
|
3
|
-
description: Add, extend, or review Holivia Ruby CLI commands that expose API endpoints while preserving the repository's routing, option parsing, HTTP, JSON output, help, and Minitest conventions. Use when implementing a new top-level CLI resource, adding an action or CRUD endpoint to an existing resource, exposing filters or request fields, adding multipart uploads, or checking an endpoint implementation for architectural consistency.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Add a Holivia CLI Endpoint
|
|
7
|
-
|
|
8
|
-
Read `../../../ai-skills/add-holivia-cli-endpoint.md` completely before taking any task action. Treat that file as the canonical workflow and follow it for the rest of the task.
|
|
9
|
-
|
|
10
|
-
If the file cannot be read, stop and report that the canonical guide is unavailable instead of reconstructing its instructions from memory.
|
data/.rubocop.yml
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
AllCops:
|
|
2
|
-
TargetRubyVersion: 3.1
|
|
3
|
-
NewCops: enable
|
|
4
|
-
SuggestExtensions: false
|
|
5
|
-
|
|
6
|
-
Style/Documentation:
|
|
7
|
-
Enabled: false
|
|
8
|
-
|
|
9
|
-
Metrics/ClassLength:
|
|
10
|
-
Exclude:
|
|
11
|
-
- "test/**/*"
|
|
12
|
-
|
|
13
|
-
Metrics/AbcSize:
|
|
14
|
-
Max: 20
|
|
15
|
-
|
|
16
|
-
Metrics/MethodLength:
|
|
17
|
-
Max: 12
|
|
18
|
-
Exclude:
|
|
19
|
-
- "test/**/*"
|
|
20
|
-
|
|
21
|
-
Metrics/ParameterLists:
|
|
22
|
-
Max: 6
|
|
23
|
-
|
|
24
|
-
Naming/VariableNumber:
|
|
25
|
-
CheckMethodNames: false
|
|
26
|
-
|
|
27
|
-
Style/StringLiterals:
|
|
28
|
-
EnforcedStyle: double_quotes
|
|
29
|
-
|
|
30
|
-
Style/StringLiteralsInInterpolation:
|
|
31
|
-
EnforcedStyle: double_quotes
|
data/CLAUDE.md
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
## Project
|
|
2
|
-
|
|
3
|
-
This is the `holivia` Ruby gem — an API client library and CLI for the Holivia platform.
|
|
4
|
-
|
|
5
|
-
- **Gem name**: `holivia` (published on RubyGems.org)
|
|
6
|
-
- **Executable**: `holivia`
|
|
7
|
-
- **Repo**: `Holivia-Team/holivia-ruby`
|
|
8
|
-
|
|
9
|
-
## General Behavior
|
|
10
|
-
|
|
11
|
-
When asked for design options, architecture discussion, or planning — respond directly with ideas/options FIRST. Do not explore the codebase or use Task agents before answering the question. Only look at code if the user asks you to implement something.
|
|
12
|
-
|
|
13
|
-
When making changes, stay strictly within the scope of what was requested. Do not add, remove, or refactor code that wasn't asked for. If you think adjacent changes would be beneficial, mention them but wait for approval before making them.
|
|
14
|
-
|
|
15
|
-
When asked to commit changes, choose the appropriate workflow:
|
|
16
|
-
- **Full commit** (`/commit` skill): When Ruby files are changed — runs rubocop, rspec, and creates atomic conventional commits.
|
|
17
|
-
- **Quick commit**: When only non-Ruby files are changed (markdown, config, etc.) — just stage, write a conventional commit message, and commit directly.
|
data/Rakefile
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "bundler/gem_tasks"
|
|
4
|
-
require "rake/testtask"
|
|
5
|
-
|
|
6
|
-
Rake::TestTask.new(:test) do |t|
|
|
7
|
-
t.libs << "test"
|
|
8
|
-
t.test_files = FileList["test/**/test_*.rb"]
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
require "rubocop/rake_task"
|
|
12
|
-
|
|
13
|
-
RuboCop::RakeTask.new
|
|
14
|
-
|
|
15
|
-
task default: %i[test rubocop]
|
data/mise.toml
DELETED