feat: add project export and import functionality

- Implemented `pcli project export` command to export project hierarchy as JSON.
- Added `pcli project import` command to import project data from JSON.
- Created user client to fetch user details for comment attribution.
- Introduced new data structures for export and import processes.
- Ensured name-based references in exports and handled conflicts during imports.
- Added versioning and progress reporting for import operations.
- Updated documentation and specifications for new features.
This commit is contained in:
2026-03-04 19:53:55 +00:00
parent e352fd530f
commit e973b2ce20
49 changed files with 1492 additions and 3303 deletions
+85
View File
@@ -0,0 +1,85 @@
# project-export Spec
## Purpose
Enable users to export a complete Planka project hierarchy (boards, lists, cards, labels, tasks, comments) to a portable JSON format for backup, migration, or documentation purposes.
## Requirements
### Requirement: Export project hierarchy to JSON
The system SHALL export a complete project hierarchy as a JSON object to stdout. The export SHALL include: project metadata, boards, lists, labels, cards (with list and label name references), task lists, tasks, and comments.
#### Scenario: Export entire project
- **WHEN** user runs `pcli project export <project-id-or-name>`
- **THEN** the system outputs a JSON object to stdout containing the project and all its boards with their full hierarchy
#### Scenario: Export with board filter
- **WHEN** user runs `pcli project export <project-id-or-name> --board <board-name-or-id>`
- **THEN** the system outputs a JSON object containing only the specified board (and its full hierarchy) under the project
#### Scenario: Board filter does not match
- **WHEN** user runs `pcli project export <project> --board <nonexistent>`
- **THEN** the system exits with an error indicating the board was not found
### Requirement: Export format uses version envelope
The export JSON SHALL contain a `version` field (integer, currently `1`), an `exportedAt` timestamp (ISO 8601), and a `project` object.
#### Scenario: Export envelope structure
- **WHEN** an export is generated
- **THEN** the root JSON object contains exactly `version`, `exportedAt`, and `project` keys
- **THEN** `version` is `1`
- **THEN** `exportedAt` is a valid ISO 8601 timestamp
### Requirement: Export uses name-based references
The export SHALL use names instead of IDs for all cross-references. Cards SHALL reference their list by `listName` and their labels by `labelNames` (array of label name strings). No Planka IDs SHALL appear in the export.
#### Scenario: Card references list by name
- **WHEN** a card belongs to a list named "In Progress"
- **THEN** the exported card has `"listName": "In Progress"` instead of a list ID
#### Scenario: Card references labels by name
- **WHEN** a card has labels "Bug" and "Urgent"
- **THEN** the exported card has `"labelNames": ["Bug", "Urgent"]`
### Requirement: Export preserves ordering
The export SHALL include `position` fields for boards, lists, labels, and cards to maintain their display ordering on import.
#### Scenario: List ordering preserved
- **WHEN** a board has lists "Todo" (position 1), "Doing" (position 2), "Done" (position 3)
- **THEN** the exported lists include their position values
### Requirement: Export includes comment attribution
Each exported comment SHALL have its text prefixed with `(Original comment by <username>)\n` where `<username>` is resolved from the comment's userId. If the userId cannot be resolved, the prefix SHALL use `unknown user`.
#### Scenario: Comment with known author
- **WHEN** a comment was authored by user "jsmith"
- **THEN** the exported comment text starts with `(Original comment by jsmith)\n`
#### Scenario: Comment with unresolvable author
- **WHEN** a comment's userId does not match any known user
- **THEN** the exported comment text starts with `(Original comment by unknown user)\n`
### Requirement: Export excludes user-specific data
The export SHALL NOT include cardMemberships, creatorUserId, or userId fields. Only textual comment attribution (as a text prefix) SHALL preserve user information.
#### Scenario: Card memberships excluded
- **WHEN** a card has members assigned
- **THEN** the exported card does not contain membership data
### Requirement: Export includes task lists and tasks
The export SHALL include task lists and their tasks for each card. Each task list SHALL contain its tasks inline. Tasks SHALL include `name` and `isCompleted` fields.
#### Scenario: Card with task lists
- **WHEN** a card has a task list "Checklist" with tasks "Item 1" (complete) and "Item 2" (incomplete)
- **THEN** the exported card includes the task list with both tasks and their completion status
### Requirement: Project identified by name or ID
The `<project-id-or-name>` argument SHALL accept either a Planka project ID or a project name. If a name is provided, the system SHALL look up the project by name. If multiple projects match the name, the system SHALL exit with an error.
#### Scenario: Project by name
- **WHEN** user provides a project name that matches exactly one project
- **THEN** the system exports that project
#### Scenario: Ambiguous project name
- **WHEN** user provides a project name that matches multiple projects
- **THEN** the system exits with an error listing the matches
+89
View File
@@ -0,0 +1,89 @@
# project-import Spec
## Purpose
Enable users to import a Planka project hierarchy from a JSON export file, creating all resources (boards, lists, cards, labels, tasks, comments) on the target Planka instance with fresh IDs while preserving structure and ordering.
## Requirements
### Requirement: Import project hierarchy from JSON
The system SHALL read a JSON export file and create all resources (project, boards, lists, labels, cards, card-labels, task lists, tasks, comments) on the target Planka instance with fresh IDs.
#### Scenario: Import from stdin
- **WHEN** user runs `pcli project import < backup.json`
- **THEN** the system reads the JSON from stdin and creates all resources
#### Scenario: Import from file flag
- **WHEN** user runs `pcli project import --file backup.json`
- **THEN** the system reads the JSON from the specified file and creates all resources
#### Scenario: No input provided
- **WHEN** user runs `pcli project import` with no stdin and no --file flag
- **THEN** the system exits with an error indicating input is required
### Requirement: Import creates project if not exists
The system SHALL look up the project by name. If no project with that name exists, the system SHALL create it. If the project already exists, the system SHALL use the existing project.
#### Scenario: Project does not exist
- **WHEN** importing a project named "proj1" and no project with that name exists
- **THEN** the system creates a new project named "proj1"
#### Scenario: Project already exists
- **WHEN** importing a project named "proj1" and a project with that name already exists
- **THEN** the system uses the existing project (does not create a duplicate)
### Requirement: Import fails if board name conflicts
Before creating any resources, the system SHALL check that none of the boards in the export file have names that conflict with existing boards in the target project. If any board name already exists under the target project, the system SHALL exit with an error naming the conflicting board(s) and create nothing.
#### Scenario: No board conflict
- **WHEN** importing boards "board1" and "board2" into project "proj1" which has no boards
- **THEN** the import proceeds and creates both boards
#### Scenario: Board name conflict
- **WHEN** importing board "board1" into project "proj1" which already has a board named "board1"
- **THEN** the system exits with an error: board "board1" already exists in project "proj1"
- **THEN** no resources are created
#### Scenario: Partial conflict
- **WHEN** importing boards "board1" and "board2" into project "proj1" which already has "board2"
- **THEN** the system exits with an error about "board2" conflicting
- **THEN** no resources are created (not even "board1")
### Requirement: Import creates resources in dependency order
The system SHALL create resources top-down: board → lists → labels → cards → card-label associations → task lists → tasks → comments. Name-to-ID maps SHALL be maintained at each level to resolve references for child resources.
#### Scenario: Card references resolved on import
- **WHEN** a card in the export has `"listName": "In Progress"` and `"labelNames": ["Bug"]`
- **THEN** the system creates the card in the list named "In Progress" and associates it with the label named "Bug" using the IDs generated during import
### Requirement: Import preserves ordering
The system SHALL use position values from the export to maintain the original display ordering of boards, lists, labels, and cards.
#### Scenario: List positions preserved
- **WHEN** the export contains lists with positions 1, 2, 3
- **THEN** the imported lists are created with those same position values
### Requirement: Import validates export version
The system SHALL check the `version` field of the export JSON. If the version is not supported (currently only version `1`), the system SHALL exit with an error.
#### Scenario: Supported version
- **WHEN** the export file has `"version": 1`
- **THEN** the import proceeds normally
#### Scenario: Unsupported version
- **WHEN** the export file has `"version": 99`
- **THEN** the system exits with an error indicating the version is not supported
### Requirement: Import reports progress
The system SHALL output progress information to stderr as it creates resources, including the count of each resource type created.
#### Scenario: Progress reporting
- **WHEN** an import completes successfully
- **THEN** stderr shows a summary: boards created, lists created, cards created, etc.
### Requirement: Import handles missing list reference
If a card references a `listName` that does not exist in the board's lists, the system SHALL exit with an error identifying the card and the missing list name.
#### Scenario: Invalid list reference
- **WHEN** a card references `"listName": "Nonexistent"` and no list with that name exists in the board
- **THEN** the system exits with an error: card "<name>" references unknown list "Nonexistent"