Compare commits

..

36 Commits

Author SHA1 Message Date
b1def97ed7 Remove deps to openssl 2025-11-12 19:52:40 +01:00
5dd39d8019 Remove deps to openssl
All checks were successful
Release binary and debian package for cl-cli / webext-build (push) Successful in 31s
Release binary and debian package for cl-cli / build-and-release (push) Successful in 1m56s
2025-11-12 19:39:13 +01:00
a4c071b555 Remove deps to openssl 2025-11-12 19:38:47 +01:00
3fb120f8a4 add web extension to release
Some checks failed
Release binary and debian package for cl-cli / webext-build (push) Successful in 32s
Release binary and debian package for cl-cli / build-and-release (push) Failing after 1m49s
2025-11-12 19:11:09 +01:00
f524dbfea8 add web extension to release
Some checks failed
Release binary and debian package for cl-cli / webext-build (push) Successful in 30s
Release binary and debian package for cl-cli / build-and-release (push) Failing after 1m49s
2025-11-12 19:03:09 +01:00
42796684d7 add web extension to release
Some checks failed
Release binary and debian package for cl-cli / webext-build (push) Failing after 49s
Release binary and debian package for cl-cli / build-and-release (push) Has been skipped
2025-11-12 18:55:27 +01:00
2871530da2 script to sign web extension 2025-11-12 18:36:18 +01:00
f30d299071 fix build and release
Some checks failed
Release binary and debian package for cl-cli / build-and-release (push) Failing after 1m53s
2025-11-12 09:09:49 +01:00
028a803447 fix build and release
Some checks failed
Release binary and debian package for cl-cli / build-and-release (push) Failing after 1m47s
2025-11-12 08:56:40 +01:00
d3c611974f fix build and release
Some checks failed
Release binary and debian package for cl-cli / build-and-release (push) Failing after 1m49s
2025-11-11 23:04:31 +01:00
b8577b4bac fix build and release
Some checks failed
Release binary and debian package for cl-cli / build-and-release (push) Failing after 1m49s
2025-11-11 22:57:08 +01:00
b705e34086 fix build and release
Some checks failed
Release binary and debian package for cl-cli / build-and-release (push) Failing after 1m46s
2025-11-11 22:51:19 +01:00
ad4e58926e Update release workflow to include Debian package build and upload
Some checks failed
Check go code / build-and-release (push) Successful in 1m7s
Release binary and debian package for cl-cli / build-and-release (push) Failing after 1m49s
Check go code / build-and-release (pull_request) Successful in 1m8s
Check go code / build-and-release (pull_request_target) Successful in 1m5s
2025-11-11 22:44:45 +01:00
056c885e8b Append metadata to build debian package using cargo-deb 2025-11-11 22:12:02 +01:00
b0f2a1452c configure changie to update the version number automatically into Cargo.toml 2025-11-11 22:11:15 +01:00
af8983ecdd Add junie guidelines 2025-11-11 00:54:34 +01:00
5084f5b10d fix serialization of output message
All checks were successful
Check go code / build-and-release (push) Successful in 1m16s
2025-11-11 00:45:51 +01:00
55fb234db6 first web extension version 2025-11-11 00:44:49 +01:00
d8db96603b initialize webext 2025-11-07 22:48:18 +01:00
c45e12ed9a Setup a web extension and refactor code 2025-11-07 22:39:06 +01:00
d830f33eec Release v0.4.1
All checks were successful
Release binary for cl-cli / build-and-release (push) Successful in 1m23s
Check go code / build-and-release (push) Successful in 1m6s
2025-10-25 00:37:21 +02:00
825623c9f3 Fix release workflow (#4)
All checks were successful
Check go code / build-and-release (push) Successful in 1m6s
Reviewed-on: #4
Co-authored-by: Julien Fastré <julien.fastre@champs-libres.coop>
Co-committed-by: Julien Fastré <julien.fastre@champs-libres.coop>
2025-10-24 22:35:21 +00:00
81350f38e4 Update dependencies in Cargo.lock to the latest versions
All checks were successful
Check go code / build-and-release (push) Successful in 1m4s
2025-10-25 00:13:23 +02:00
1f2d42e1d5 Update release workflow to include pull_request_target event and specify branches
All checks were successful
Check go code / build-and-release (push) Successful in 1m3s
2025-10-24 23:43:11 +02:00
a0f67464b5 Simplify build-and-release workflow by switching to akkuman/gitea-release-action and enabling checksum generation.
Some checks failed
Release binary for cl-cli / build-and-release (push) Failing after 7s
2025-10-24 23:41:29 +02:00
ff5699e627 Update workflow to specify branches for push and pull_request events 2025-10-24 23:35:28 +02:00
dc0040c2d1 Add workflow to check Go code on push and pull requests 2025-10-24 23:34:15 +02:00
9950282d6e Update build-and-release workflow to use Gitea-hosted setup-go action
Some checks failed
Release binary for cl-cli / build-and-release (push) Failing after 7s
2025-10-24 23:33:31 +02:00
7c8e8eb236 Clean code by commenting out some variable, and allowing some dead code. 2025-10-24 23:24:47 +02:00
442b5d25b4 Release v0.4.0
Some checks failed
Release binary for cl-cli / build-and-release (push) Failing after 1m40s
2025-10-24 18:08:01 +02:00
4763d48290 Add PATCH support in Gitea client and update issue handling
Extend Gitea client with a `patch` method for executing PATCH requests. Introduce `IssueWriteSetBody` struct for serializing issue update payloads. Update Gitea action to append related issues and send PATCH requests to update issue descriptions.
2025-10-24 18:04:28 +02:00
1af6596b51 Enhance issue handling by adding utils module
Introduce `append_related_issues` function and `IssueRelated` enum to manage related issue linking. Update Gitea action to utilize the new functionality for appending related issues in work package content. Add corresponding tests.
2025-10-24 17:32:10 +02:00
c5f4f9fcf9 Release v0.3.0
Some checks failed
Release binary for cl-cli / build-and-release (push) Failing after 1m27s
2024-11-17 21:25:32 +01:00
9f544c66c2 Add "open" crate and automate browser opening for work packages
Added the "open" crate to Cargo.toml and corresponding dependencies to Cargo.lock. Updated the code to automatically open the newly created work package in the browser and handle potential failure cases gracefully.
2024-11-17 21:24:41 +01:00
7c1cdb64ec fix multiple gitlab token possible 2024-11-14 15:23:25 +01:00
4bb787488d example configuation file 2024-11-14 15:22:14 +01:00
45 changed files with 8471 additions and 1050 deletions

3
.changes/v0.3.0.md Normal file
View File

@@ -0,0 +1,3 @@
## v0.3.0 - 2024-11-17
### Added
* Open the newly create work package after the wp has been created

3
.changes/v0.4.0.md Normal file
View File

@@ -0,0 +1,3 @@
## v0.4.0 - 2025-10-24
### Added
* [planning - i2work] Met à jour le body du ticket gitea avec un lien vers le work package, dans une section "related issues"

3
.changes/v0.4.1.md Normal file
View File

@@ -0,0 +1,3 @@
## v0.4.1 - 2025-10-25
### Fixed
* Fix dependencies

View File

@@ -24,3 +24,9 @@ newlines:
beforeChangelogVersion: 1
endOfVersion: 1
envPrefix: CHANGIE_
# update the version in the cargo file, on release
replacements:
- path: Cargo.toml
find: '^version = ".*"'
replace: 'version = "{{.VersionNoPrefix}}"'

View File

@@ -1,4 +1,4 @@
name: Release binary for cl-cli
name: Release binary and debian package for cl-cli
on:
push:
@@ -6,31 +6,79 @@ on:
- v**
jobs:
webext-build:
runs-on: ubuntu-latest
container:
image: node:24
steps:
- name: Checkout repository
uses: https://github.com/actions/checkout@v4
- name: Install dependencies
run: cd web-extension/cl && npm install
- name: Build and sign web extension
env:
AMO_API_KEY: ${{ secrets.AMO_API_KEY }}
AMO_API_SECRET: ${{ secrets.AMO_API_SECRET }}
run: cd web-extension/cl && npm run sign
- name: Upload web extension artifact
uses: https://github.com/actions/upload-artifact@v3
with:
name: webext-xpi
path: web-extension/cl/web-ext-artifacts/*.xpi
retention-days: 7
if-no-files-found: error
build-and-release:
runs-on: ubuntu-latest
needs: [webext-build]
steps:
- name: Checkout repository
uses: https://github.com/actions/checkout@v4
- name: Install rust toolchain
uses: https://github.com/dtolnay/rust-toolchain@stable
- name: Build binaries
run: cargo build --release
- name: Read release content
uses: https://github.com/jaywcjlove/github-action-read-file@main
id: read_release
- name: Install cargo-deb
run: cargo install --quiet cargo-deb
- name: Build Debian package
run: cargo deb --quiet
- name: Upload Debian package to Gitea registry
env:
GITEA_OWNER: ${{ vars.PUBLISH_USERNAME }}
GITEA_SERVER_URL: ${{ github.server_url }}
GITEA_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
DEB_DISTRIBUTIONS: noble,plucky,questing
DEB_COMPONENT: main
DEB_ARCH: amd64
run: |
set -euo pipefail
DEB_FILE=$(ls target/debian/*.deb | head -n1)
if [ -z "${DEB_FILE}" ]; then
echo "No .deb file found in target/debian" >&2
exit 1
fi
# comma-separated in DEB_DISTRIBUTIONS, e.g. "noble,plucky,questing"
IFS=',' read -r -a DISTROS <<< "$DEB_DISTRIBUTIONS"
for DISTRO in "${DISTROS[@]}"; do
echo "Uploading ${DEB_FILE} to ${GITEA_SERVER_URL} for owner ${GITEA_OWNER}"
curl -sSf -X PUT \
--user "${GITEA_OWNER}:${GITEA_TOKEN}" \
--upload-file "${DEB_FILE}" \
"${GITEA_SERVER_URL}/api/packages/${GITEA_OWNER}/debian/pool/${DISTRO}/main/upload"
done
- name: Download web extension artifact
uses: https://github.com/actions/download-artifact@v3
with:
path: .changes/${{ github.ref_name }}.md
- name: Setup go for using go gitea actions
uses: https://github.com/actions/setup-go@v4
with:
go-version: '>=1.20.1'
- name: Use Go Action to release
id: use-go-action
uses: https://gitea.com/actions/release-action@main
name: webext-xpi
path: web-extension/cl/web-ext-artifacts
- name: Release
uses: https://gitea.com/akkuman/gitea-release-action@v1
env:
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
with:
files: |-
target/release/cl-cli
config.toml.dist
api_key: '${{secrets.RELEASE_TOKEN}}'
body: |
${{ steps.read_release.outputs.content }}
web-extension/cl/web-ext-artifacts/*.xpi
md5sum: true
sha256sum: true
body: Test release
prerelease: true
#body_path: .changes/${{ github.ref_name }}.md

View File

@@ -0,0 +1,25 @@
name: Check go code
on:
push:
branches:
- '**'
pull_request:
branches:
- '**'
pull_request_target:
branches:
- '**'
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: https://github.com/actions/checkout@v4
- name: Install rust toolchain
uses: https://github.com/dtolnay/rust-toolchain@stable
- name: Build binaries
run: cargo build
- name: Run tests
run: cargo test

130
.junie/guidelines.md Normal file
View File

@@ -0,0 +1,130 @@
# cl-cli — Project-specific development guidelines
This document captures project-specific knowledge to help advanced contributors work efficiently on this repository. It focuses on practical, non-obvious details (build, configuration, testing, and development tips) specific to this codebase.
## 1. Build and configuration
- Rust workspace/binaries
- Single crate with two binaries defined in `Cargo.toml`:
- `cl-cli``src/main.rs`: the CLI entrypoint.
- `webext``src/webext.rs`: a Native Messaging host used by the browser extension.
- Library module tree under `src/lib.rs` exposes common code to both binaries.
- Toolchain: stable Rust; CI uses dtolnay/rust-toolchain@stable.
- Dependencies and async runtime
- Async via `tokio` (features: `rt`, `rt-multi-thread`, `macros`). Both binaries use `#[tokio::main]`.
- HTTP via `reqwest`; serialization via `serde`/`serde_json`; configuration via `toml`.
- Build
- Standard build works without extra flags:
- `cargo build` (debug) or `cargo build --release` (optimized).
- Individual binaries:
- `cargo build --bin cl-cli`
- `cargo build --bin webext`
- The web extension host (`webext`) is a regular Rust binary; it is not bundled by NPM tooling.
- Configuration file
- Expected location by default: `$HOME/.config/cl-cli/config.toml`.
- The CLI allows overriding the path via `--config <PATH>`; the webext host always reads the default path.
- Template: `config.toml.dist`. Minimal fields:
- `[[gitlab]] { token, domain }`
- `[[gitea]] { token, domain }`
- `[openproject] { token, base_url }`
- Example of initial setup (copy the dist and edit):
```bash
mkdir -p "$HOME/.config/cl-cli"
cp config.toml.dist "$HOME/.config/cl-cli/config.toml"
$EDITOR "$HOME/.config/cl-cli/config.toml"
```
- Error surface: parsing and IO errors are surfaced as `GeneralError`; the CLI will print and exit nonzero, the `webext` host will return an error payload.
- Local OpenProject for development
- `docker-compose.yaml` provides a local OpenProject (13.x) at `http://localhost:8080` with persisted volumes under `.docker-data/`.
## 2. Testing
- Test types present
- Unit tests live alongside code under `#[cfg(test)]` modules (e.g., `src/gitea/issue.rs`, `src/webext.rs`, `src/planning/utils.rs`).
- Integration tests can be placed under `tests/`. The crate exposes internal modules via `src/lib.rs` for integration testing.
- Running tests
- `cargo test` runs unit and integration tests. CI also runs `cargo test` on every push/PR.
- Tests are pure and do not require network or external services; avoid adding networkbound tests.
- Adding new tests
- Prefer unit tests near the logic being tested. For integration coverage across modules, place files under `tests/` and use public items from `cl_cli`.
- Keep tests deterministic: no real HTTP calls; if needed, mock boundaries at the client layer.
- Example integration test (verified locally during preparation)
- Minimal example using a pure helper. Create `tests/example_gitea_url.rs`:
```rust
use url::Url;
#[test]
fn converts_gitea_issue_html_url_to_api_url() {
// Pure transformation; no network
let input = Url::parse("https://gitea.champs-libres.be/champs-libres/test/issues/1").unwrap();
let out = cl_cli::gitea::issue::issue_html_url_to_api(&input).unwrap();
assert_eq!(
out.as_str(),
"https://gitea.champs-libres.be/api/v1/repos/champs-libres/test/issues/1"
);
}
```
- Run it with `cargo test`. Remove the file if it was only created as a demo.
## 3. CLI usage tips (projectspecific)
- `cl-cli planning i2work <gitlab_issue_url> <openproject_project_identifier>` creates an OpenProject work package from a GitLab issue.
- The OpenProject project identifier is the short slug in URLs, e.g. `chill` in `.../projects/chill/...`.
- The command uses tokens from the configuration file to authenticate to GitLab and OpenProject.
- Example:
```bash
cl-cli planning i2work \
https://gitlab.com/Chill-Projet/chill-bundles/-/issues/240 \
chill
```
- `cl-cli test` invokes a debug routine (see `src/debug.rs`) primarily for local diagnostics.
## 4. Web extension native host (`webext`)
- Purpose: acts as a Native Messaging host for a browser extension under `web-extension/`.
- Protocol: communicates via stdin/stdout using 4byte lengthprefixed JSON messages.
- Input `serde` enum is tagged with `type`; currently supports `{"type":"Issue2Work", ...}`.
- Output is tagged with `result` (`Ok` or `Error`) and contains `data` when successful.
- Business flow: the host reads config from `$HOME/.config/cl-cli/config.toml`, translates the input into `Issue2Work` CLI arguments, delegates to `planning::issue2work::handle_issue2work`, and returns the created work package URL.
- Testing the transport: see unit tests in `src/webext.rs` that exercise message framing and deserialization.
## 5. Code style and architectural notes
- Module layout
- Public library facade (`src/lib.rs`) reexports internal modules for reuse by both binaries and tests.
- Planning features under `src/planning/` with a trait (`Issue2WorkActionTrait`) and concrete actions per platform.
- Git providers (`src/gitlab`, `src/gitea`) and OpenProject client live in dedicated modules.
- Error handling
- Unified lightweight `GeneralError` encapsulates userfacing messages; conversions from `reqwest::Error` and header errors are provided.
- Data parsing
- Input/Output types rely on `serde` with explicit tagging for clarity and forward compatibility.
- Concurrency
- All async entrypoints are `#[tokio::main]`; downstream async functions return `impl Future` to stay generic and testable.
- External calls
- Wrap external service interactions behind client modules; this eases mocking in tests and keeps pure helpers (like URL mappers) easily testable.
## 6. Continuous Integration
- `.gitea/workflows/release/check.yaml` performs:
- Checkout → install stable toolchain → `cargo build` → `cargo test`.
- Release job (see `.gitea/workflows/release/build-and-release.yaml`) builds release artifacts for distribution.
## 7. Troubleshooting / gotchas (projectspecific)
- Config path
- The CLI accepts `--config`, but the webext host reads the default path only. Ensure `$HOME/.config/cl-cli/config.toml` exists when using the browser integration.
- Tokens scope
- Ensure personal access tokens for GitLab and OpenProject have sufficient scopes for reading issues and creating work packages respectively.
- Locale/URLs
- `Issue2Work` requires the OpenProject project identifier (slug), not its display name.
- HTTP environment
- `reqwest` honors proxy vars (`HTTP_PROXY`, `HTTPS_PROXY`) if present; be aware for corporate environments.
## 8. Commands quick reference
- Build: `cargo build --release`
- Run CLI: `cargo run --bin cl-cli -- <args>`
- Run webext host directly for dev: `cargo run --bin webext`
- Tests: `cargo test`
- Linting: `cargo clippy` (not enforced in CI but recommended)

View File

@@ -6,6 +6,18 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v0.4.1 - 2025-10-25
### Fixed
* Fix dependencies
## v0.4.0 - 2025-10-24
### Added
* [planning - i2work] Met à jour le body du ticket gitea avec un lien vers le work package, dans une section "related issues"
## v0.3.0 - 2024-11-17
### Added
* Open the newly create work package after the wp has been created
## v0.2.0 - 2024-11-14
### Added
* Add an option `--assign-to-me` to automatically assign the openproject's user to the newly created work package

2054
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,41 @@
[package]
name = "cl-cli"
version = "0.1.0"
version = "0.5.0-alpha13"
edition = "2021"
license = "GPLv3"
description = "Some helpers scripts for Champs-Libres"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name="cl-cli"
path="src/main.rs"
[[bin]]
name="webext"
path="src/webext.rs"
[dependencies]
clap = { version = "4.1.13", features = ["derive"] }
gitlab = "0.1607.0"
reqwest = "0.11.23"
serde = { version = "1.0.158", features = ["derive"] }
toml = "0.7.3"
url = "2.3.1"
gitlab = "0.1804.0"
reqwest = { version = "0.12.24", default-features = false, features = ["rustls-tls", "http2", "charset"] }
serde = { version = "1.0.228", features = ["derive"] }
toml = "0.9.8"
url = "2.5.7"
tokio = { version = "1.0.0", features = ["rt", "rt-multi-thread", "macros"] }
simple-home-dir = "0.2.1"
log = "0.4.17"
simple-home-dir = "0.5.2"
log = "0.4.28"
open = "5.3.2"
serde_json = "1.0.145"
[package.metadata.deb]
maintainer = "Julien Fastré <julien.fastre@champs-libres.coop>"
copyright = "AGPLv3"
extended-description = """Helpers for Champs-Libres"""
depends = "$auto"
section = "utility"
priority = "optional"
assets = [
["target/release/cl-cli", "usr/bin/cl", "755"],
["target/release/webext", "usr/bin/", "755"],
]

View File

@@ -1,6 +1,11 @@
[gitlab]
[[gitlab]]
# generate from https://gitlab.com/-/user_settings/personal_access_tokens
token = "glpat-example"
domain = "gitlab.com"
[[gitea]]
token = "abcdexempletoken"
domain = "gitea.champs-libres.be"
[openproject]
# generate api token from https://champs-libres.openproject.com/my/access_token

View File

@@ -3,7 +3,7 @@ use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
#[derive(Parser)]
pub(crate) struct Cli {
pub struct Cli {
#[arg(short, long, value_name = "FILE")]
pub config: Option<PathBuf>,
@@ -12,19 +12,19 @@ pub(crate) struct Cli {
}
#[derive(Subcommand)]
pub(crate) enum Commands {
pub enum Commands {
#[command(subcommand)]
Planning(Planning),
Test,
}
#[derive(Subcommand)]
pub(crate) enum Planning {
pub enum Planning {
I2work(Issue2Work),
}
#[derive(Args, Debug)]
pub(crate) struct Issue2Work {
pub struct Issue2Work {
pub issue_url: String,
pub project_id: String,
#[arg(short, long)]

View File

@@ -1,26 +1,59 @@
use crate::error::GeneralError;
use serde::Deserialize;
use std::fs;
use std::path::Path;
#[derive(Deserialize, Debug)]
pub(crate) struct Config {
pub struct Config {
pub gitlab: Vec<GitlabConfig>,
pub gitea: Vec<GiteaConfig>,
pub openproject: OpenProjectConfig,
}
#[derive(Deserialize, Debug)]
pub(crate) struct GitlabConfig {
pub struct GitlabConfig {
pub token: String,
pub domain: String,
}
#[derive(Deserialize, Debug)]
pub(crate) struct GiteaConfig {
pub struct GiteaConfig {
pub token: String,
pub domain: String,
}
#[derive(Deserialize, Debug)]
pub(crate) struct OpenProjectConfig {
pub struct OpenProjectConfig {
pub token: String,
pub base_url: String,
}
pub struct BuildConfigError {
msg: String,
}
impl Into<GeneralError> for BuildConfigError {
fn into(self) -> GeneralError {
GeneralError {
description: format!("Could not build config: {}", self.msg),
}
}
}
pub fn build_config(config_path: &Path) -> Result<Config, BuildConfigError> {
let config_path_content = match fs::read_to_string(config_path) {
Ok(content) => content,
Err(e) => {
return Err(BuildConfigError {
msg: format!(
"Could not read a config file at {:?}, error: {}",
config_path, e
),
});
}
};
let config: Config = toml::from_str(&*config_path_content).expect("Could not parse config");
Ok(config)
}

View File

@@ -3,7 +3,7 @@ use crate::error::GeneralError;
use crate::gitea::issue::Issue;
use url::Url;
pub(crate) async fn debug(config: Config) -> Result<(), GeneralError> {
pub async fn debug(config: Config) -> Result<(), GeneralError> {
println!("test");
let gitea_client = crate::gitea::client::Client::from_config(config.gitea.first().unwrap());

View File

@@ -3,11 +3,11 @@ use reqwest::Error;
#[derive(Debug)]
pub struct GeneralError {
pub(crate) description: String,
pub description: String,
}
impl From<InvalidHeaderValue> for GeneralError {
fn from(value: InvalidHeaderValue) -> Self {
fn from(_value: InvalidHeaderValue) -> Self {
GeneralError {
description: "Unable to convert the token into header value".to_string(),
}

View File

@@ -2,16 +2,23 @@ use crate::cli::Issue2Work;
use crate::config::Config;
use crate::error::GeneralError;
use crate::gitea::client::has_client_for_url;
use crate::gitea::issue::{issue_html_url_to_api, Issue};
use crate::gitea::issue::{issue_html_url_to_api, Issue, IssueWriteSetBody};
use crate::openproject::user::{GetMe, User};
use crate::openproject::work::WorkPackageWriter;
use crate::planning::utils::{append_related_issues, IssueRelated};
use crate::planning::Issue2WorkActionTrait;
use crate::planning::Issue2WorkResult;
use url::Url;
pub(crate) struct GiteaAction {}
impl Issue2WorkActionTrait for GiteaAction {
async fn run(&self, url: &Url, config: &Config, args: &Issue2Work) -> Result<(), GeneralError> {
async fn run(
&self,
url: &Url,
config: &Config,
args: &Issue2Work,
) -> Result<Issue2WorkResult, GeneralError> {
let gitea_client = crate::gitea::client::Client::from_config(config.gitea.first().unwrap());
let issue: Issue = gitea_client.get(issue_html_url_to_api(url)?).await?;
let open_project_client =
@@ -32,12 +39,26 @@ impl Issue2WorkActionTrait for GiteaAction {
.create_work_package(&work_package, &args.project_id)
.await?;
println!(
"new work package created: {:?}, edit at {}/projects/{}/work_packages/{}",
work_package.subject, config.openproject.base_url, args.project_id, work_package.id
let url_wp = format!(
"{}/projects/{}/work_packages/{}",
config.openproject.base_url, args.project_id, work_package.id
);
Ok(())
let content = append_related_issues(
&IssueRelated::OpenProjectIssue(url_wp.to_string()),
&issue.body,
);
let _u: Issue = gitea_client
.patch(
issue_html_url_to_api(url)?,
&IssueWriteSetBody { body: content },
)
.await?;
Ok(Issue2WorkResult {
work_package_url: url_wp,
subject: work_package.subject,
})
}
fn supports(&self, url: &Url, config: &Config, _args: &Issue2Work) -> bool {
@@ -47,7 +68,10 @@ impl Issue2WorkActionTrait for GiteaAction {
fn create_work_package_from_issue(issue: &Issue, assignee: Option<User>) -> WorkPackageWriter {
WorkPackageWriter {
subject: format!("{} ({}/{})", issue.title, issue.repository.full_name, issue.number),
subject: format!(
"{} ({}/{})",
issue.title, issue.repository.full_name, issue.number
),
work_type: "TASK".into(),
description: crate::openproject::work::DescriptionWriter {
format: "markdown".into(),

View File

@@ -3,12 +3,13 @@ use crate::error::GeneralError;
use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION};
use reqwest::{ClientBuilder, StatusCode};
use serde::de::DeserializeOwned;
use serde::Serialize;
use url::Url;
#[derive(Debug)]
pub struct Client {
token: String,
base_uri: String,
// base_uri: String,
}
fn is_client_for_url(url: &Url, config: &GiteaConfig) -> bool {
@@ -28,6 +29,7 @@ pub(crate) fn has_client_for_url(url: &Url, config: &Config) -> bool {
false
}
#[allow(dead_code)]
fn client_for_url(url: &Url, config: &Config) -> Result<Client, GeneralError> {
for c in &config.gitea {
if is_client_for_url(url, c) {
@@ -45,10 +47,10 @@ impl Client {
Self::new(&config.token, &config.domain)
}
pub fn new(token: &String, domain: &String) -> Self {
pub fn new(token: &String, _domain: &String) -> Self {
Client {
token: token.clone(),
base_uri: format!("https://{}", domain.clone()),
// base_uri: format!("https://{}", domain.clone()),
}
}
@@ -79,6 +81,38 @@ impl Client {
}),
}
}
pub async fn patch<T: DeserializeOwned, B: Serialize>(
&self,
url: Url,
body: &B,
) -> Result<T, GeneralError> {
let mut headers = HeaderMap::new();
headers.append(AUTHORIZATION, format!("token {}", self.token).parse()?);
headers.append(ACCEPT, "application/json".parse()?);
let client = ClientBuilder::new()
.default_headers(headers)
.build()
.unwrap();
let response = client.patch(url.clone()).json(body).send().await?;
match response.status() {
StatusCode::OK | StatusCode::CREATED => {
let result: T = response.json().await?;
Ok(result)
}
_ => Err(GeneralError {
description: format!(
"Could not call PATCH on {:?}, error code {}",
url,
response.status()
),
}),
}
}
}
#[cfg(test)]

View File

@@ -1,12 +1,11 @@
use crate::error::GeneralError;
use crate::gitea::client::Client;
use crate::gitea::repository::Repository;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Debug, Deserialize)]
pub struct Issue {
pub id: u64,
// pub id: u64,
pub number: u64,
pub title: String,
pub body: String,
@@ -14,14 +13,9 @@ pub struct Issue {
pub html_url: String,
}
pub trait IssueClient {
fn get_issue(owner_or_organisation: &String, repo: &String, number: &u64) -> Option<Issue>;
}
impl IssueClient for Client {
fn get_issue(_owner_or_organisation: &String, _repo: &String, number: &u64) -> Option<Issue> {
todo!()
}
#[derive(Debug, Serialize)]
pub struct IssueWriteSetBody {
pub body: String,
}
pub fn issue_html_url_to_api(url: &Url) -> Result<Url, GeneralError> {

View File

@@ -2,8 +2,8 @@ use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Repository {
id: u64,
name: String,
owner: String,
// id: u64,
// name: String,
// owner: String,
pub full_name: String,
}

View File

@@ -2,13 +2,13 @@ use crate::cli::Issue2Work;
use crate::config::Config;
use crate::error::GeneralError;
use crate::gitlab::client::{client_for_url, has_client_for_url};
use crate::gitlab::issue::IssueBundle;
use crate::gitlab::issue::{Issue, IssueBundle, Project};
use crate::openproject::client::Client;
use crate::openproject::user::{GetMe, User};
use crate::openproject::work::WorkPackageWriterAssignee;
use crate::planning::Issue2WorkActionTrait;
use crate::planning::Issue2WorkResult;
use gitlab::api::{issues, projects, AsyncQuery};
use gitlab::{Issue, Project};
use url::Url;
#[derive(Debug)]
@@ -27,7 +27,12 @@ struct Issue2WorkPackageDTO {
pub(crate) struct GitlabAction {}
impl Issue2WorkActionTrait for GitlabAction {
async fn run(&self, url: &Url, config: &Config, args: &Issue2Work) -> Result<(), GeneralError> {
async fn run(
&self,
url: &Url,
config: &Config,
args: &Issue2Work,
) -> Result<Issue2WorkResult, GeneralError> {
let client = client_for_url(&url, &config).await?;
let data = extract_issue_info(&url).unwrap();
@@ -41,7 +46,7 @@ impl Issue2WorkActionTrait for GitlabAction {
let issue = issues.first().unwrap();
let project_endpoint = projects::Project::builder()
.project(issue.project_id.value())
.project(issue.project_id)
.build()
.unwrap();
@@ -66,12 +71,15 @@ impl Issue2WorkActionTrait for GitlabAction {
.create_work_package(&(&dto).into(), &args.project_id)
.await?;
println!(
"new work package created: {:?}, edit at {}/projects/{}/work_packages/{}",
work_package.subject, config.openproject.base_url, args.project_id, work_package.id
);
let result = Issue2WorkResult {
work_package_url: format!(
"{}/projects/{}/work_packages/{}",
config.openproject.base_url, args.project_id, work_package.id
),
subject: work_package.subject,
};
Ok(())
Ok(result)
}
fn supports(&self, url: &Url, config: &Config, _args: &Issue2Work) -> bool {

View File

@@ -3,10 +3,6 @@ use crate::error::GeneralError;
use gitlab::{AsyncGitlab, GitlabBuilder};
use url::Url;
pub struct ClientProvider {}
impl ClientProvider {}
fn is_client_for_url(url: &Url, config: &GitlabConfig) -> bool {
if url.domain() == Some(config.domain.as_str()) {
return true;

View File

@@ -1,5 +1,18 @@
use gitlab::Issue;
use gitlab::Project;
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize)]
pub struct Issue {
pub iid: u64,
pub title: String,
pub web_url: String,
pub project_id: u64,
pub description: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Project {
pub name_with_namespace: String,
}
/// A struct which contains Issue and Project
#[derive(Debug)]

9
src/lib.rs Normal file
View File

@@ -0,0 +1,9 @@
// Regroupe la logique commune accessible par les deux binaires
pub mod cli;
pub mod config;
pub mod debug;
pub mod error;
pub mod gitea;
pub mod gitlab;
pub mod openproject;
pub mod planning;

View File

@@ -3,22 +3,11 @@ extern crate reqwest;
extern crate serde;
extern crate simple_home_dir;
mod cli;
mod config;
mod debug;
mod error;
mod gitea;
mod gitlab;
mod openproject;
mod planning;
use crate::cli::Commands::{Planning, Test};
use crate::cli::Planning::I2work;
use crate::error::GeneralError;
use cl_cli::cli::Cli;
use cl_cli::cli::Commands::{Planning, Test};
use cl_cli::cli::Planning::I2work;
use cl_cli::error::GeneralError;
use clap::Parser;
use cli::Cli;
use config::Config;
use std::fs;
use std::path::PathBuf;
use std::process::exit;
@@ -34,22 +23,20 @@ async fn main() {
None => &default_config_path,
};
let config_path_content = match fs::read_to_string(config_path) {
Ok(content) => content,
let config = match cl_cli::config::build_config(config_path) {
Ok(c) => c,
Err(e) => {
println!(
"Could not read config file at {:?}, error: {}",
config_path, e
);
let general_error: GeneralError = e.into();
println!("{}", general_error.description);
exit(1);
}
};
let config: Config = toml::from_str(&*config_path_content).expect("Could not parse config");
let result = match cli.command {
Some(Planning(I2work(args))) => planning::issue2work::issue2work(config, &args).await,
Some(Test) => debug::debug(config).await,
Some(Planning(I2work(args))) => {
cl_cli::planning::issue2work::issue2work_cli(config, &args).await
}
Some(Test) => cl_cli::debug::debug(config).await,
None => Err(GeneralError {
description: "No command launched".to_string(),
}),

View File

@@ -1,13 +1,13 @@
use serde::Deserialize;
#[derive(Deserialize, Debug)]
pub struct HalEntity {
#[serde(rename = "_type")]
pub d_type: String,
}
// #[derive(Deserialize, Debug)]
// pub struct HalEntity {
// // #[serde(rename = "_type")]
// // pub d_type: String,
// }
#[derive(Deserialize, Debug, Clone)]
pub struct Link {
pub href: String,
pub title: Option<String>,
// pub title: Option<String>,
}

View File

@@ -1,11 +1,11 @@
use crate::openproject::client::{handle_response_status, Client, OpenProjectError};
use crate::openproject::hal::HalEntity;
// use crate::openproject::hal::HalEntity;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
pub struct User {
pub href: String,
pub title: String,
// pub title: String,
}
#[derive(Deserialize, Debug)]
pub struct Links {
@@ -13,12 +13,12 @@ pub struct Links {
}
#[derive(Deserialize, Debug)]
pub struct Root {
#[serde(rename = "instanceName")]
pub instance_name: String,
// #[serde(rename = "instanceName")]
// pub instance_name: String,
#[serde(rename = "_links")]
pub links: Links,
#[serde(flatten)]
pub hal_entity: HalEntity,
// #[serde(flatten)]
// pub hal_entity: HalEntity,
}
pub trait RootClient {

View File

@@ -10,13 +10,10 @@ pub struct UserLink {
}
#[derive(Deserialize, Debug, Clone)]
pub struct User {
#[serde(rename = "_type")]
#[allow(unused_variables)]
pub d_type: String,
#[allow(unused_variables)]
pub id: u64,
#[allow(unused_variables)]
pub name: String,
// #[serde(rename = "_type")]
// pub d_type: String,
// pub id: u64,
// pub name: String,
#[serde(rename = "_links")]
pub d_links: UserLink,
}

View File

@@ -3,7 +3,7 @@ use crate::config::Config;
use crate::error::GeneralError;
use crate::gitea::action::GiteaAction;
use crate::gitlab::action::GitlabAction;
use crate::planning::Issue2WorkActionTrait;
use crate::planning::{Issue2WorkActionTrait, Issue2WorkResult};
use url::Url;
struct App {
@@ -11,20 +11,44 @@ struct App {
gitea_issue2work_action: GiteaAction,
}
pub(crate) async fn issue2work(config: Config, args: &Issue2Work) -> Result<(), GeneralError> {
pub async fn handle_issue2work(
config: Config,
args: &Issue2Work,
) -> Result<Issue2WorkResult, GeneralError> {
let url = Url::parse(&*args.issue_url).expect("issue_url is not valid");
let app = App {
gitlab_issue2work_action: GitlabAction {},
gitea_issue2work_action: GiteaAction {},
};
let result: Issue2WorkResult;
if app.gitlab_issue2work_action.supports(&url, &config, args) {
app.gitlab_issue2work_action.run(&url, &config, args).await
result = app
.gitlab_issue2work_action
.run(&url, &config, args)
.await?;
} else if app.gitea_issue2work_action.supports(&url, &config, args) {
app.gitea_issue2work_action.run(&url, &config, args).await
result = app.gitea_issue2work_action.run(&url, &config, args).await?
} else {
Err(GeneralError {
return Err(GeneralError {
description: format!("This action is not supported for this url: {}", url),
})
});
}
Ok(result)
}
pub async fn issue2work_cli(config: Config, args: &Issue2Work) -> Result<(), GeneralError> {
let result = handle_issue2work(config, args).await?;
println!(
"new work package created: {:?}, edit at {}",
result.subject, result.work_package_url
);
if let Err(e) = open::that(result.work_package_url.as_str()) {
println!("failed to open work package in browser: {}", e);
};
Ok(())
}

View File

@@ -3,10 +3,21 @@ use crate::config::Config;
use crate::error::GeneralError;
use url::Url;
pub(crate) mod issue2work;
pub mod issue2work;
pub mod utils;
pub trait Issue2WorkActionTrait {
async fn run(&self, url: &Url, config: &Config, args: &Issue2Work) -> Result<(), GeneralError>;
fn run(
&self,
url: &Url,
config: &Config,
args: &Issue2Work,
) -> impl std::future::Future<Output = Result<Issue2WorkResult, GeneralError>> + Send;
fn supports(&self, url: &Url, config: &Config, args: &Issue2Work) -> bool;
}
pub struct Issue2WorkResult {
pub work_package_url: String,
pub subject: String,
}

180
src/planning/utils.rs Normal file
View File

@@ -0,0 +1,180 @@
pub enum IssueRelated {
OpenProjectIssue(String),
}
pub fn append_related_issues(issue: &IssueRelated, content: &String) -> String {
let mut splitted = content.lines();
let mut new_content: Vec<String> = Vec::new();
let mut found = false;
let mut iteration_started = false;
while let Some(line) = splitted.next() {
if line.contains("## Related issues") {
found = true;
new_content.push(line.parse().unwrap());
// we go to the end of the section
while let Some(line) = splitted.next() {
if line.starts_with("-") || line.starts_with("*") {
iteration_started = true;
new_content.push(line.parse().unwrap());
} else if (line.trim().is_empty()) && iteration_started {
new_content.append(&mut add_related_issues_section(issue));
iteration_started = false;
found = true;
new_content.push(line.parse().unwrap());
break;
} else if line.starts_with("#") {
new_content.push("new title found".to_string());
found = true;
new_content.append(&mut add_related_issues_section(issue));
new_content.push("".to_string());
new_content.push(line.parse().unwrap());
break;
} else {
new_content.push(line.parse().unwrap());
}
}
} else {
new_content.push(line.parse().unwrap());
}
}
if !found || iteration_started {
if !found {
new_content.append(&mut add_related_issues_title());
}
new_content.append(&mut add_related_issues_section(issue));
}
new_content.join(&"\n")
}
fn add_related_issues_title() -> Vec<String> {
let mut previous: Vec<String> = Vec::new();
previous.push("".to_string());
previous.push("## Related issues".to_string());
previous.push("".to_string());
previous
}
fn add_related_issues_section(issue: &IssueRelated) -> Vec<String> {
let mut previous: Vec<String> = Vec::new();
previous.push(convert_issue_link_items(issue));
previous
}
fn convert_issue_link_items(issue: &IssueRelated) -> String {
match issue {
IssueRelated::OpenProjectIssue(issue_url) => format!("- [{}]({})", issue_url, issue_url),
}
}
#[cfg(test)]
mod tests {
use crate::planning::utils::{append_related_issues, IssueRelated};
#[test]
pub fn test_append_related_issues_content_empty() {
let issue = IssueRelated::OpenProjectIssue("https://example/wp/1".to_string());
assert_eq!(
r#"
## Related issues
- [https://example/wp/1](https://example/wp/1)"#,
append_related_issues(&issue, &("".to_string()))
);
}
#[test]
pub fn test_append_related_issues_content_not_empty() {
let issue = IssueRelated::OpenProjectIssue("https://example/wp/1".to_string());
assert_eq!(
r#"Something happens.
## Some title
Some content
## Related issues
- [https://example/wp/1](https://example/wp/1)"#,
append_related_issues(
&issue,
&"Something happens.\n\
\n\
## Some title\n\
\n\
Some content"
.to_string()
),
);
}
#[test]
pub fn test_append_related_issues_existing_section_related_issues() {
let issue = IssueRelated::OpenProjectIssue("https://example/wp/1".to_string());
assert_eq!(
r#"Something happens.
## Some title
Some content
## Related issues
- [https://example/wp/2](https://example/wp/2)
- [https://example/wp/1](https://example/wp/1)"#,
append_related_issues(
&issue,
&r#"Something happens.
## Some title
Some content
## Related issues
- [https://example/wp/2](https://example/wp/2)"#
.to_string()
),
);
}
#[test]
pub fn test_append_related_issues_existing_section_related_issues_not_last() {
let issue = IssueRelated::OpenProjectIssue("https://example/wp/1".to_string());
assert_eq!(
r#"Something happens.
## Some title
Some content
## Related issues
- [https://example/wp/2](https://example/wp/2)
- [https://example/wp/1](https://example/wp/1)
## Other content
Some other content"#,
append_related_issues(
&issue,
&r#"Something happens.
## Some title
Some content
## Related issues
- [https://example/wp/2](https://example/wp/2)
## Other content
Some other content"#
.to_string()
),
);
}
}

223
src/webext.rs Normal file
View File

@@ -0,0 +1,223 @@
use cl_cli::cli::Issue2Work as Issue2WorkCli;
use cl_cli::config::build_config;
use cl_cli::error::GeneralError;
use cl_cli::planning::issue2work::handle_issue2work;
use serde::{Deserialize, Serialize};
use std::io::{self, Read, Write};
use std::path::PathBuf;
use std::process;
#[derive(Debug, Deserialize)]
#[serde(tag = "type")]
enum InputMessage {
Issue2Work(Issue2WorkWebExtMessage),
}
#[derive(Debug, Serialize)]
#[serde(tag = "type")]
enum OutputContent {
Issue2Work(OutputIssue2Work),
}
#[derive(Debug, Serialize)]
#[serde(tag = "result", content = "data")]
enum OutputMessage {
Ok(OutputContent),
Error(String),
}
/// Input message for creating an issue.
#[derive(Debug, Deserialize)]
struct Issue2WorkWebExtMessage {
url: String,
project: String,
}
/// Output message returned to the web extension.
#[derive(Debug, Serialize)]
struct OutputIssue2Work {
created: String,
}
/// Reads a *raw* message from stdin (Native Messaging format).
/// - first reads 4 bytes (length)
/// - then reads exactly `len` bytes into a Vec<u8>
/// Returns Ok(None) if EOF (no more data).
fn read_message_bytes(stdin: &mut io::StdinLock<'_>) -> io::Result<Option<Vec<u8>>> {
let mut len_buf = [0u8; 4];
// Lire les 4 octets de longueur
match stdin.read_exact(&mut len_buf) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
// Plus de données : fin normale
return Ok(None);
}
Err(e) => return Err(e),
}
// Longueur annoncée (endianness native, comme dans la spec)
let len = u32::from_ne_bytes(len_buf) as usize;
// Ici, on peut recevoir un *nombre arbitraire d'octets* (dans la limite de u32).
let mut buf = vec![0u8; len];
stdin.read_exact(&mut buf)?;
Ok(Some(buf))
}
/// Write a JSON message to stdout with a header of 4 bytes.
fn write_json_message<T: serde::Serialize>(
stdout: &mut io::StdoutLock<'_>,
msg: &T,
) -> io::Result<()> {
let data =
serde_json::to_vec(msg).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let len = data.len() as u32;
let len_buf = len.to_ne_bytes();
stdout.write_all(&len_buf)?;
stdout.write_all(&data)?;
stdout.flush()?;
Ok(())
}
/// "Logique métier" : construit une URL "created" à partir de l'entrée.
async fn handle_business_logic(input: InputMessage) -> Result<OutputContent, GeneralError> {
let mut default_config_path = PathBuf::new();
default_config_path.push(simple_home_dir::home_dir().unwrap());
default_config_path.push(".config/cl-cli/config.toml");
// during dev: default_config_path.push("/tmp/cl-cli/config.toml");
let config = match build_config(&default_config_path) {
Ok(c) => c,
Err(e) => {
return Err(e.into());
}
};
let result = match input {
InputMessage::Issue2Work(i2w) => {
let arg = Issue2WorkCli {
issue_url: i2w.url,
project_id: i2w.project,
assign_to_me: false,
};
let result = handle_issue2work(config, &arg).await?;
OutputContent::Issue2Work(OutputIssue2Work {
created: result.work_package_url,
})
}
};
Ok(result)
}
#[tokio::main]
async fn main() {
eprintln!("Native host Rust démarré.");
let stdin = io::stdin();
let mut stdin_lock = stdin.lock();
let stdout = io::stdout();
let mut stdout_lock = stdout.lock();
loop {
match read_message_bytes(&mut stdin_lock) {
Ok(Some(raw_bytes)) => {
eprintln!("Reçu {} octets depuis l'extension.", raw_bytes.len());
// On parse le JSON *après* avoir lu tous les octets.
let input: InputMessage = match serde_json::from_slice(&raw_bytes) {
Ok(v) => v,
Err(e) => {
eprintln!("JSON invalide reçu: {e}");
// Option : renvoyer une erreur JSON au lieu d'abandonner
continue;
}
};
// Logique métier -> OutputMessage
let output = match handle_business_logic(input).await {
Ok(v) => OutputMessage::Ok(v),
Err(e) => OutputMessage::Error(e.description),
};
if let Err(e) = write_json_message(&mut stdout_lock, &output) {
eprintln!("Erreur lors de l'écriture de la réponse: {e}");
// Si on ne sait plus écrire, on termine.
process::exit(1);
}
}
Ok(None) => {
eprintln!("EOF sur stdin, arrêt du host.");
break;
}
Err(e) => {
eprintln!("Erreur de lecture: {e}");
process::exit(1);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn deserializes_issue2work_variant() {
// Given: a JSON with the external tag "type" matching the enum variant name
let payload = json!({
"type": "Issue2Work",
"url": "https://example.com/issues/123",
"project": "test"
});
// When: deserializing to InputMessage
let msg: InputMessage = serde_json::from_value(payload).expect("valid enum JSON");
// Then: we get the correct variant and fields
match msg {
InputMessage::Issue2Work(inner) => {
assert_eq!(inner.url, "https://example.com/issues/123");
assert_eq!(inner.project, "test");
}
}
}
#[test]
fn fails_on_unknown_type_tag() {
let payload = json!({
"type": "Unknown",
"url": "https://example.com/issues/123",
"project": "test"
});
let err = serde_json::from_value::<InputMessage>(payload).unwrap_err();
// Basic sanity check that it's a data error mentioning the unrecognized variant
let msg = err.to_string();
assert!(
msg.contains("unknown variant") || msg.contains("unknown") || msg.contains("expected"),
"unexpected error message: {msg}"
);
}
#[test]
fn fails_when_missing_required_fields() {
// Missing "url" and "project"
let payload = json!({
"type": "Issue2Work"
});
let err = serde_json::from_value::<InputMessage>(payload).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("missing field"),
"unexpected error message: {msg}"
);
}
}

8
web-extension/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
web-extension/.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/web-extension.iml" filepath="$PROJECT_DIR$/.idea/web-extension.iml" />
</modules>
</component>
</project>

6
web-extension/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

12
web-extension/.idea/web-extension.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

1
web-extension/.nvmrc Normal file
View File

@@ -0,0 +1 @@
24

3
web-extension/cl/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/*
dist/*
web-ext-artifacts/*

View File

@@ -0,0 +1,3 @@
# This file was created by https://github.com/mozilla/web-ext
# Your auto-generated extension ID for addons.mozilla.org is:
helper@champs-libres-coop

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

View File

@@ -0,0 +1,29 @@
{
"manifest_version": 3,
"name": "Champs-Libres Helper",
"version": "0.4",
"browser_specific_settings": {
"gecko": {
"id": "helper@champs-libres-coop",
"strict_min_version": "143.0",
"data_collection_permissions": {
"required": ["none"]
}
}
},
"description": "Aide pour les champs-libres",
"icons": {
"48": "icons/img-48.png"
},
"permissions": ["menus", "nativeMessaging", "tabs"],
"host_permissions": [
"*://*/*"
],
"background": {
"scripts": ["dist/background/background.js"],
"type": "module"
}
}

6194
web-extension/cl/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
{
"name": "web-extension",
"version": "0.4.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"check": "tsc --noEmit",
"build": "tsc",
"package": "tsc && web-ext build --overwrite-dest",
"sign": "tsc && web-ext build --overwrite-dest && web-ext sign --use-submission-api --api-key $AMO_API_KEY --api-secret $AMO_API_SECRET --channel unlisted"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@types/firefox-webext-browser": "^143.0.0",
"typescript": "^5.9.3",
"web-ext": "^7.6.0"
}
}

View File

@@ -0,0 +1,68 @@
import {isOutputMessageError, isOutputMessageSuccess} from "./types.js";
const projects: { name: string, slug: string }[] = [
//{name: "test (dev only)", slug: "test"},
{name: "Adminsys", slug: "champs-libres-adminsys"},
{name: "Be-Arbres", slug: "be-arbres"},
{name: "Chill", slug: "chill"},
{name: "Chill > AMLI", slug: "amli"},
{name: "Chill > Haute-Vienne", slug: "haute-vienne"},
{name: "Chill > Petits Chills", slug: "petits-chill"},
{name: "Chill > Samu Social", slug: "samu-social"},
{name: "Chill > Vendée", slug: "vendee"},
{name: "Osiris", slug: "osiris"},
{name: "Raponmap", slug: "raponmap"}
]
for (const project of projects) {
browser.menus.create(
{
id: "cl-issue-2-wp-" + project.slug,
title: project.name,
contexts: ["link"],
type: "radio"
},
);
}
browser.menus.onClicked.addListener(async function(info, tab) {
console.log("info", info);
if (typeof info.menuItemId === "string") {
console.log("menuItemId", info.menuItemId);
if (info.menuItemId.startsWith("cl-issue-2-wp-")) {
console.log("start with cl-issue-2-wp-");
const slug = info.menuItemId.replace("cl-issue-2-wp-", "");
console.log("slug", slug);
if (typeof info.linkUrl === "string") {
await convertToWorkPackage(slug, info.linkUrl);
} else {
console.error("linkUrl is not a string");
}
}
}
});
async function convertToWorkPackage(slug: string, url: string): Promise<void> {
const message = {
type: "Issue2Work",
url: url,
project: slug,
};
try {
const sending: unknown = await browser.runtime.sendNativeMessage("cl_cli", message);
if (isOutputMessageError(sending)) {
console.error("error while handling message", sending.message);
} else if (isOutputMessageSuccess(sending)) {
try {
await browser.tabs.create({
url: sending.data.created,
});
} catch (error) {
console.error("error while creating new tab", error);
}
}
} catch (error) {
console.error("error while handling message", error);
}
}

View File

@@ -0,0 +1,44 @@
export type OutputMessageError = {
result: "Error",
message: string,
}
export type OutputIssue2Work = {
type: "Issue2Work";
created: string;
}
export type OutputMessageSuccess = {
result: 'Ok'
data: OutputIssue2Work;
}
export type OutputMessage = OutputMessageError | OutputMessageSuccess;
export function isOutputMessage(value: unknown): value is OutputMessage {
return isOutputMessageError(value) || isOutputMessageSuccess(value);
}
export function isOutputMessageError(value: unknown): value is OutputMessageError {
return (
typeof value === 'object' &&
value !== null &&
'result' in value &&
value.result === 'Error' &&
'message' in value &&
typeof value.message === 'string'
);
}
export function isOutputMessageSuccess(value: unknown): value is OutputMessageSuccess {
return (
typeof value === 'object' &&
value !== null &&
'result' in value &&
value.result === 'Ok' &&
'data' in value &&
typeof value.data === 'object' &&
value.data !== null
);
}

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES6",
"module": "ESNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"outDir": "./dist",
"rootDir": "./src",
"declaration": false,
"types": [
"firefox-webext-browser"
]
},
"include": ["./src/**/*.ts"],
"exclude": ["./node_modules", "dist"]
}