mirror of
https://github.com/penpot/penpot.git
synced 2026-09-01 17:49:08 +00:00
Merge branch 'staging' into niwinz-upgrade-deps
This commit is contained in:
commit
4cd289c567
@ -3,11 +3,8 @@ description: Execute a ready plan end-to-end — create a GitHub issue, branch i
|
||||
agent: build
|
||||
---
|
||||
|
||||
# Implement Plan
|
||||
|
||||
This command is run once a plan is ready (for example, from plan mode). Execute
|
||||
the plan already prepared in the current session context — it does not take
|
||||
extra arguments. Follow these steps in order.
|
||||
the plan already prepared in the current session context. Follow these steps in order.
|
||||
|
||||
## 1. Create the issue
|
||||
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
Act as a senior software engineer and perform a thorough review.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Determine what is being reviewed** from the provided context:
|
||||
- **If it is a plan** (implementation plan, design document, task breakdown) → load the **`plan-review`** skill.
|
||||
- **If it is code** (diff, PR, code change) → load the **`code-review`** skill.
|
||||
|
||||
2. Read `AGENTS.md` and follow its instructions for finding and reading all related testing documentation from memories before reviewing.
|
||||
|
||||
3. **Skip generated files, lockfile-only changes, and unrelated modifications** unless they introduce security risks.
|
||||
|
||||
4. Follow the loaded skill's process and produce its output format.
|
||||
|
||||
## Strong Rules
|
||||
|
||||
1. Do not invent problems. Every finding must be real and actionable.
|
||||
2. Do not modify any code and do not create a commit — this command only reviews.
|
||||
3. Be specific and constructive. "This could be better" is not helpful — explain why and how.
|
||||
4. Prioritize by impact. One structural issue outweighs ten nits.
|
||||
5. Missing tests are an issue, not a suggestion. Report as a severity-tagged finding — never as a recommendation.
|
||||
|
||||
## Context
|
||||
|
||||
$ARGUMENTS
|
||||
@ -106,6 +106,8 @@ For detailed security guidance, see `security-and-hardening`.
|
||||
| **Low:** | Minor, optional | Author may ignore — formatting, style preferences |
|
||||
| **Suggestion:** | Worth considering | Not required, but improves the code |
|
||||
|
||||
**Unique finding IDs.** Assign every finding a stable identifier: `F1`, `F2`, `F3`, … numbered in order of severity (Critical first, then High, Medium, Low, Suggestion). Use the ID everywhere the finding is mentioned — in section headers, in the verdict, in follow-up discussion. Never renumber within a review. Example: `**F3 (High)** — `app/validate.cljs:42` — duplicate branch logic…`.
|
||||
|
||||
For each finding, describe the circumstances under which it could fail: specific inputs, load conditions, timing, or user actions that trigger the problem. "This crashes when input is null" is actionable; "this might crash" is not.
|
||||
|
||||
Lead with what matters: correctness and security first, then structural issues, then everything else. A few high-conviction comments beat a long list.
|
||||
@ -122,11 +124,11 @@ Briefly explain what the code does and give an overall assessment.
|
||||
|
||||
### Critical and High-Priority Issues
|
||||
|
||||
List problems that could cause security incidents, data loss, crashes, incorrect behavior, or major performance degradation. For each: state the severity, identify the file/function/code section, explain why it's a problem, describe failure circumstances, and provide a concrete improvement with corrected code when useful.
|
||||
List problems that could cause security incidents, data loss, crashes, incorrect behavior, or major performance degradation. Each finding gets its unique ID (`F1`, `F2`, …). For each: state the severity, identify the file/function/code section, explain why it's a problem, describe failure circumstances, and provide a concrete improvement with corrected code when useful.
|
||||
|
||||
### Other Findings
|
||||
|
||||
List medium- and low-priority issues, including maintainability and design concerns.
|
||||
List medium- and low-priority issues, including maintainability and design concerns. Continue the ID sequence started above (`F3`, `F4`, …).
|
||||
|
||||
### Suggested Refactoring
|
||||
|
||||
@ -148,6 +150,8 @@ Choose one:
|
||||
- **Approve with minor changes** — Good to merge after addressing low/medium issues
|
||||
- **Request changes** — Critical or high issues must be resolved before merge
|
||||
|
||||
List the finding IDs the verdict depends on (e.g. "Request changes: F1, F4").
|
||||
|
||||
## Change Sizing
|
||||
|
||||
Small, focused changes are easier to review, faster to merge, and safer to deploy.
|
||||
@ -231,25 +235,13 @@ For supply-chain risk triage, follow the `security-and-hardening` skill.
|
||||
|
||||
## Verification
|
||||
|
||||
After review is complete:
|
||||
Before emitting the verdict, verify the change as it stands. This is the reviewer's own due diligence — it covers the state of the code at review time, not the later resolution of findings (fixing findings is the author's job; confirming them is a new review):
|
||||
|
||||
- [ ] All Critical issues are resolved
|
||||
- [ ] All Required (no-prefix) changes are resolved or explicitly deferred with justification
|
||||
- [ ] Tests pass
|
||||
- [ ] Tests pass — run them yourself, don't trust the claim
|
||||
- [ ] Build succeeds
|
||||
- [ ] The verification story is documented (what changed, how it was verified)
|
||||
- [ ] Dependency upgrades reviewed against changelog, isolated per package, verified by green suite
|
||||
|
||||
## Multi-Model Review Pattern
|
||||
|
||||
Use different models for different review perspectives:
|
||||
|
||||
```
|
||||
Model A writes the code → Model B reviews → Model A addresses feedback → Human makes the final call
|
||||
```
|
||||
|
||||
Different models have different blind spots.
|
||||
|
||||
## See Also
|
||||
|
||||
- For detailed security review guidance, see `security-and-hardening`
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
---
|
||||
name: planner
|
||||
description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan (Context, Affected modules, Approach, Risks, Testing). Always output to the user; additionally save to .opencode/plans/YYYY-MM-DD-<title>.md.
|
||||
description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan with task breakdown, acceptance criteria, sizing, and checkpoints. Always output to the user and save to .opencode/plans/YYYY-MM-DD-<title>.md.
|
||||
---
|
||||
|
||||
# Planner
|
||||
|
||||
Read-only senior software architect role for Penpot. Produces structured
|
||||
implementation plans that engineers or other agents can execute. Never writes
|
||||
or modifies code.
|
||||
implementation plans with task breakdowns that engineers or other agents can
|
||||
execute. Never writes or modifies code.
|
||||
|
||||
## When to Use
|
||||
|
||||
@ -18,24 +18,29 @@ or modifies code.
|
||||
- The user asks "how would I implement X?" or "what's involved in fixing Y?".
|
||||
- The user is about to start non-trivial work and wants a bite-sized task
|
||||
breakdown.
|
||||
- A task feels too large or vague to start.
|
||||
- Work needs to be parallelized across multiple agents or sessions.
|
||||
|
||||
Do **not** use this skill to actually implement anything — it is read-only.
|
||||
|
||||
**When NOT to use:** Single-file changes with obvious scope, or when the spec
|
||||
already contains well-defined tasks.
|
||||
|
||||
## Role
|
||||
|
||||
You are a Senior Software Architect working on Penpot, an open-source design
|
||||
tool. Your sole responsibility is planning and analysis — you do NOT write or
|
||||
modify code.
|
||||
You help users understand the Penpot codebase, design solutions, and produce
|
||||
implementation plans that other agents or developers can execute. The plan
|
||||
tells them what to build and how to verify it, task by task.
|
||||
|
||||
You help users understand the codebase, design solutions, and create detailed
|
||||
implementation plans that other agents or developers can execute. Document
|
||||
everything they need to know: which files to touch for each task, code patterns,
|
||||
tests, and how to verify correctness. Apply DRY and KISS principles.
|
||||
The implementer reads the project's agent docs (`AGENTS.md`, project memories
|
||||
such as `mem:critical-info`, `mem:testing`, and each module's core memory)
|
||||
before working. Reference those memories instead of re-explaining tooling,
|
||||
conventions, or test design — explain in the plan only what they do not cover.
|
||||
|
||||
Do **not** suggest commit messages or commit names anywhere in your plans or
|
||||
responses — committing is the developer's responsibility.
|
||||
responses — committing is the implementer's responsibility.
|
||||
|
||||
## Required Reading Before Planning
|
||||
## CRITICAL: Required Reading Before Planning
|
||||
|
||||
Before drafting any plan, work through the project's own guidance:
|
||||
|
||||
@ -50,6 +55,8 @@ Before drafting any plan, work through the project's own guidance:
|
||||
|
||||
Skipping this step is the #1 cause of incorrect or incomplete plans.
|
||||
|
||||
---
|
||||
|
||||
## The Planning Process
|
||||
|
||||
### Phase 1: Architecture Analysis
|
||||
@ -64,16 +71,42 @@ Skipping this step is the #1 cause of incorrect or incomplete plans.
|
||||
|
||||
### Phase 2: Task Breakdown
|
||||
|
||||
Implementation order follows the monorepo's dependency graph:
|
||||
`frontend -> common`, `backend -> common`, `exporter -> common`,
|
||||
`frontend -> render-wasm`. Build shared foundations first, then layer
|
||||
consumers on top.
|
||||
#### Identify the Dependency Graph
|
||||
|
||||
Map what depends on what, following the monorepo's module dependency graph:
|
||||
|
||||
```
|
||||
common (shared types, schemas — no deps)
|
||||
│
|
||||
├── backend (depends common)
|
||||
│ ├── RPC handlers
|
||||
│ └── persistence / migrations
|
||||
│
|
||||
├── frontend (depends common, render-wasm)
|
||||
│ ├── UI components
|
||||
│ └── state / API integration
|
||||
│
|
||||
├── exporter (depends common)
|
||||
│
|
||||
└── render-wasm (consumed by frontend)
|
||||
```
|
||||
|
||||
Implementation order follows the dependency graph bottom-up: build shared
|
||||
foundations first, then layer consumers on top.
|
||||
|
||||
#### Slice Vertically
|
||||
|
||||
Instead of building all of common, then all of backend, then all of frontend —
|
||||
build one complete feature path at a time:
|
||||
|
||||
**Bad (horizontal slicing):**
|
||||
```
|
||||
Task 1: Build all common types
|
||||
Task 2: Build all backend handlers
|
||||
Task 3: Build all frontend components
|
||||
```
|
||||
|
||||
**Good (vertical slicing):**
|
||||
```
|
||||
Task 1: common data types + schema ← foundation
|
||||
Task 2: backend RPC handler + persistence
|
||||
@ -89,39 +122,58 @@ Each task follows this structure:
|
||||
```markdown
|
||||
## Task [N]: [Short descriptive title]
|
||||
|
||||
**Description:** One paragraph explaining what this task accomplishes.
|
||||
**Description:** One or two paragraphs explaining what this task accomplishes.
|
||||
Should be clear and concise.
|
||||
|
||||
**Rationale:** Why this task exists and why this approach over the obvious
|
||||
alternatives — design decisions, trade-offs, constraints discovered during
|
||||
analysis. One or two sentences; skip only if genuinely trivial.
|
||||
|
||||
**Code sketch (optional):** Signature-, type-, or shape-level example when the
|
||||
intended interface is non-obvious. Keep it short — a skeleton that fixes the
|
||||
contract (function signature, model fields, error shape), never a full
|
||||
implementation. Omit when the task is mechanical.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] [Specific, testable condition]
|
||||
- [ ] [Specific, testable condition]
|
||||
|
||||
**Verification:**
|
||||
- [ ] Tests pass (module-specific test command)
|
||||
- [ ] Lint/formatter passes (module-specific check command)
|
||||
- [ ] Relevant tests pass (module-specific test command).
|
||||
- [ ] Lint/formatter passes (module-specific check command), if applicable.
|
||||
- [ ] The core flow works end-to-end, if applicable.
|
||||
|
||||
**Dependencies:** [Task numbers this depends on, or "None"]
|
||||
|
||||
**Files likely touched:**
|
||||
- `path/to/file.clj`
|
||||
- `path/to/file_test.clj`
|
||||
|
||||
**Estimated scope:** [XS: 1 file | S: 1-2 files | M: 3-5 files | L: 5+ files]
|
||||
```
|
||||
|
||||
Replace "module-specific test command" with the actual commands for the module
|
||||
(e.g. `clojure -M:dev:test` for backend/common, `npx shadow-cljs compile test && npx karma start` for frontend,
|
||||
or the commands noted in the module's core memory).
|
||||
(e.g. `clojure -M:dev:test` for backend/common,
|
||||
`npx shadow-cljs compile test && npx karma start` for frontend, or the
|
||||
commands noted in the module's core memory).
|
||||
|
||||
When possible, design each task with TDD in mind: acceptance criteria double
|
||||
as a test list, and the natural first step of the task is writing those tests
|
||||
before the implementation. Some tasks resist this (config, migrations, pure
|
||||
wiring) — for those, keep the usual verification steps.
|
||||
|
||||
#### Estimate Scope
|
||||
|
||||
| Size | Files | Scope |
|
||||
|------|-------|-------|
|
||||
| **XS** | 1 | Single function, config change, or schema tweak |
|
||||
| **S** | 1-2 | One handler or component method |
|
||||
| **M** | 3-5 | One vertical feature slice |
|
||||
| **L** | 5-8 | Multi-component feature |
|
||||
| **XL** | 8+ | **Too large — break it down further** |
|
||||
| Size | Files | Scope | Example |
|
||||
|------|-------|-------|---------|
|
||||
| **XS** | 1 | Single function, config change, or schema tweak | Add a validation rule |
|
||||
| **S** | 1-2 | One handler or component method | Add a new RPC endpoint |
|
||||
| **M** | 3-5 | One vertical feature slice | Bookmark CRUD with tests |
|
||||
| **L** | 5-8 | Multi-component feature | Search with filtering and pagination |
|
||||
| **XL** | 8+ | **Too large — break it down further** | — |
|
||||
|
||||
If a task is L or larger, break it into smaller tasks. Agents perform best on
|
||||
S and M tasks.
|
||||
If a task is XL, it should be broken into smaller tasks. Agents perform best
|
||||
on S and M tasks.
|
||||
|
||||
**When to break a task down further:**
|
||||
- It would take more than one focused session
|
||||
@ -141,11 +193,11 @@ Arrange tasks so that:
|
||||
Add explicit checkpoints with the relevant module commands:
|
||||
|
||||
```markdown
|
||||
## Checkpoint: After Tasks 1-3
|
||||
- [ ] All tests pass (module-specific command)
|
||||
- [ ] Lint/format passes (module-specific command)
|
||||
- [ ] Core flow works end-to-end
|
||||
- [ ] Review with human before proceeding
|
||||
### Checkpoint: After Tasks 1-3
|
||||
- [ ] Relevant tests pass (module-specific command).
|
||||
- [ ] The relevant build or compilation passes, if applicable.
|
||||
- [ ] The core flow works end-to-end.
|
||||
- [ ] Review with human before proceeding.
|
||||
```
|
||||
|
||||
## Requirements
|
||||
@ -159,7 +211,7 @@ Add explicit checkpoints with the relevant module commands:
|
||||
- Apply DRY and KISS principles to the proposed implementation.
|
||||
- Define a testing strategy aligned with each affected module's tooling.
|
||||
- Every task must have acceptance criteria and verification steps.
|
||||
- Checkpoints must exist between major phases.
|
||||
- Checkpoints must exist after every 2-3 tasks.
|
||||
|
||||
## Constraints
|
||||
|
||||
@ -168,7 +220,8 @@ Add explicit checkpoints with the relevant module commands:
|
||||
`.opencode/plans/`.
|
||||
- You do **not** run builds, tests, linters, or any commands that modify state.
|
||||
- You do **not** create git commits or interact with version control.
|
||||
- You do **not** execute shell commands beyond read-only searches.
|
||||
- You do **not** execute shell commands beyond read-only searches (`rg`, `ls`,
|
||||
`find`, `cat`, `bat`).
|
||||
- Your output is a structured plan or analysis, ready for handoff to an
|
||||
engineer agent or developer.
|
||||
|
||||
@ -188,8 +241,9 @@ slug is lowercase, hyphen-separated, and a short summary of the task
|
||||
(e.g. `add-batch-get-profiles-for-file-comments`). Create the
|
||||
`.opencode/plans/` directory if it does not exist.
|
||||
|
||||
Always attempt the write. If the user explicitly provides a target file path,
|
||||
use that path instead of the default.
|
||||
IMPORTANT: The plan agent has write permission specifically for
|
||||
`.opencode/plans/` — always attempt the write. If the user explicitly provides
|
||||
a target file path, use that path instead of the default.
|
||||
|
||||
### Plan Document Template
|
||||
|
||||
@ -212,41 +266,75 @@ use that path instead of the default.
|
||||
security implications.]
|
||||
|
||||
## Approach
|
||||
[Step-by-step implementation plan with file paths, function names, and code
|
||||
shape where applicable. Group steps into atomic, ordered tasks.]
|
||||
[A short strategy summary: 3-5 sentences describing the overall approach and
|
||||
the shape of the dependency graph (what depends on what, what gets built
|
||||
first). High-level only — the task-by-task detail lives in the Task List.]
|
||||
|
||||
## Task List
|
||||
|
||||
### Phase 1: Foundation
|
||||
- [ ] Task 1: ...
|
||||
- [ ] Task 2: ...
|
||||
Each task uses the full task structure defined in
|
||||
[Write Tasks](#write-tasks) — description, rationale, acceptance criteria,
|
||||
verification, dependencies, files, estimated scope, and optional code sketch.
|
||||
Never reduce a task to a one-line checkbox; the plan must be self-contained
|
||||
and executable without other context.
|
||||
|
||||
### Checkpoint: Phase 1
|
||||
- [ ] Tests pass, lint/formatter clean (module-specific commands)
|
||||
Tasks are a flat, ordered list — a plan is not a roadmap. Do not group tasks
|
||||
into phases, milestones, or sprints; ordering and dependencies are already
|
||||
captured per task. Insert a checkpoint after every 2-3 tasks.
|
||||
|
||||
### Phase 2: Core Features
|
||||
- [ ] Task 3: ...
|
||||
- [ ] Task 4: ...
|
||||
## Task 1: [Short descriptive title]
|
||||
|
||||
### Checkpoint: Phase 2
|
||||
- [ ] End-to-end flow works
|
||||
**Description:** [What this task accomplishes.]
|
||||
|
||||
### Phase 3: Polish
|
||||
- [ ] Task 5: ...
|
||||
- [ ] Task 6: ...
|
||||
**Rationale:** [Why this approach over the alternatives.]
|
||||
|
||||
### Checkpoint: Complete
|
||||
- [ ] All acceptance criteria met
|
||||
- [ ] Ready for review
|
||||
**Acceptance criteria:**
|
||||
- [ ] [Specific, testable condition]
|
||||
|
||||
## Testing Strategy
|
||||
[How to verify: which test commands to run per module, what cases to cover,
|
||||
manual verification steps, lint/format checks. Consult each module's core
|
||||
memory for the exact commands.]
|
||||
**Verification:**
|
||||
- [ ] Relevant tests pass (module-specific command).
|
||||
|
||||
**Dependencies:** None
|
||||
|
||||
**Files likely touched:**
|
||||
- `path/to/file`
|
||||
|
||||
**Estimated scope:** [XS: 1 file | S: 1-2 files | M: 3-5 files | L: 5+ files]
|
||||
|
||||
**Code sketch (optional):** [Short contract-level example, only if the shape
|
||||
is non-obvious.]
|
||||
|
||||
## Task 2: [Short descriptive title]
|
||||
|
||||
[Same structure as Task 1.]
|
||||
|
||||
## Task 3: [Short descriptive title]
|
||||
|
||||
[Same structure as Task 1.]
|
||||
|
||||
### Checkpoint: After Tasks 1-3
|
||||
- [ ] Relevant tests pass (module-specific command).
|
||||
- [ ] The relevant build or compilation passes, if applicable.
|
||||
- [ ] The core flow works end-to-end.
|
||||
- [ ] Review with human before proceeding.
|
||||
|
||||
## Task 4: [Short descriptive title]
|
||||
|
||||
[Same structure as Task 1.]
|
||||
|
||||
## Task 5: [Short descriptive title]
|
||||
|
||||
[Same structure as Task 1.]
|
||||
|
||||
## Verification & Testing
|
||||
[How to verify each task and the whole plan: the project's real test, lint,
|
||||
build, and run commands (extracted during Required Reading), coverage
|
||||
expectations, and manual checks. Consult each module's core memory for the
|
||||
exact commands.]
|
||||
|
||||
## Parallelization Opportunities
|
||||
- **Safe to parallelize:** Independent feature slices across separate
|
||||
modules, tests for already-implemented features
|
||||
modules, tests for already-implemented features, documentation
|
||||
- **Must be sequential:** Shared common schema changes, database migrations
|
||||
- **Needs coordination:** Features that share a contract (define the contract
|
||||
first, then parallelize)
|
||||
@ -259,13 +347,31 @@ When the plan is purely analytical (e.g. a code review or feasibility study
|
||||
with no implementation), skip the **Approach** and **Task List** sections and
|
||||
lead with **Findings** instead, keeping the rest of the structure.
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "I'll figure it out as I go" | That's how you end up with a tangled mess and rework. 10 minutes of planning saves hours. |
|
||||
| "The tasks are obvious" | Write them down anyway. Explicit tasks surface hidden dependencies and forgotten edge cases. |
|
||||
| "Planning is overhead" | Planning is the task. Implementation without a plan is just typing. |
|
||||
| "I can hold it all in my head" | Context windows are finite. Written plans survive session boundaries and compaction. |
|
||||
|
||||
## Red Flags
|
||||
|
||||
- Delivering prose without a task breakdown
|
||||
- Tasks that say "implement the feature" without acceptance criteria
|
||||
- No verification steps in the plan
|
||||
- All tasks are XL-sized
|
||||
- No checkpoints between tasks
|
||||
- Dependency order isn't considered
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before starting implementation, confirm:
|
||||
Before delivering the plan, confirm:
|
||||
|
||||
- [ ] Every task has acceptance criteria
|
||||
- [ ] Every task has a verification step
|
||||
- [ ] Task dependencies are identified and ordered correctly
|
||||
- [ ] No task touches more than ~5 files
|
||||
- [ ] Checkpoints exist between major phases
|
||||
- [ ] The human has reviewed and approved the plan
|
||||
- [ ] No task is XL or larger — break it down instead
|
||||
- [ ] Checkpoints exist after every 2-3 tasks
|
||||
- [ ] The plan is ready for human review
|
||||
|
||||
@ -6,6 +6,7 @@ Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Par
|
||||
|
||||
- `penpotdev-infra`: shared `postgres`, `minio`, `minio-setup`, `mailer`, `ldap`. File: `docker-compose.infra.yml`.
|
||||
- `penpotdev-wsN` (N=0,1,…): per-instance `main` + `redis` (Valkey). File: `docker-compose.main.yml`. ws0 (a.k.a. `main`) binds `$PWD`; ws1+ bind clones at `${PENPOT_WORKSPACES_DIR}/wsN/` (default `~/.penpot/penpot_workspaces/`), maintained by the developer.
|
||||
- Optional overlay `docker-compose.opencode.yml`: added by `instance-compose` as an extra `-f` only when `PENPOT_OPENCODE_CONFIG_DIR` is set (i.e. `run-devenv --opencode-config-dir DIR` ran in this process). Bind-mounts the host dir at `/home/penpot/.config/opencode` (`:z`). Flag-only, per-call; not read from ambient env. Parser `parse-opencode-config-dir` absolutizes (`~`, realpath) because compose resolves relative bind sources against the compose file's dir. Only instances brought up with the flag get the mount.
|
||||
- All projects join external network `penpot_shared`. Created idempotently by `ensure-devenv-network`, never removed by lifecycle commands.
|
||||
|
||||
## Source-of-truth files
|
||||
@ -65,7 +66,7 @@ No `--delete` on the working-tree pass: gitignored caches in the workspace survi
|
||||
|
||||
## CLI surface
|
||||
|
||||
- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet).
|
||||
- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX] [--opencode-config-dir DIR]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet). `--opencode-config-dir DIR` bind-mounts DIR at `~/.config/opencode` in-container via the optional overlay above; mount applies at container creation, so changing it requires stop + re-run.
|
||||
- `stop-devenv [--ws main|0|wsN|N] [--all]`: stop instances. Flags mutually exclusive. `--ws N` stops just that workspace. `--ws 0` or no flag stops ws0; shared infra shuts down only if no other instances remain. `--all` stops every ws highest-first then ws0, then infra.
|
||||
- `run-devenv`: legacy alias, ws0 non-agentic attached.
|
||||
- `attach-devenv [--ws main|0|wsN|N]`: pure attach. Fails fast if instance/session missing.
|
||||
|
||||
@ -1045,3 +1045,18 @@ RETURNING id, deleted_at;")
|
||||
(update acc :created conj email)))))
|
||||
{:created [] :skipped []}
|
||||
emails)))))
|
||||
|
||||
;; ---- API: get-air-gapped
|
||||
|
||||
(def ^:private schema:get-air-gapped-result
|
||||
[:map
|
||||
[:air-gapped ::sm/boolean]])
|
||||
|
||||
(sv/defmethod ::get-air-gapped
|
||||
"Returns whether this Penpot instance runs in air-gapped mode."
|
||||
{::doc/added "2.18"
|
||||
::sm/params [:map]
|
||||
::sm/result schema:get-air-gapped-result
|
||||
::rpc/auth false}
|
||||
[_cfg _params]
|
||||
{:air-gapped (contains? cf/flags :air-gapped-conf)})
|
||||
|
||||
@ -194,6 +194,15 @@
|
||||
(string? (get version k)))))
|
||||
(t/is (= cf/version version))))
|
||||
|
||||
(t/deftest get-air-gapped
|
||||
(let [out (th/management-command! {::th/type :get-air-gapped})]
|
||||
(t/is (th/success? out))
|
||||
(t/is (false? (-> out :result :air-gapped))))
|
||||
(binding [cf/flags (conj cf/flags :air-gapped-conf)]
|
||||
(let [out (th/management-command! {::th/type :get-air-gapped})]
|
||||
(t/is (th/success? out))
|
||||
(t/is (true? (-> out :result :air-gapped))))))
|
||||
|
||||
(t/deftest get-teams-returns-only-owned-non-default-non-deleted
|
||||
(with-mocks [nitrate-mock {:target 'app.nitrate/call :return nil}]
|
||||
(let [profile (th/create-profile* 1 {:is-active true})
|
||||
|
||||
10
docker/devenv/docker-compose.opencode.yml
Normal file
10
docker/devenv/docker-compose.opencode.yml
Normal file
@ -0,0 +1,10 @@
|
||||
# Optional compose overlay, included by manage.sh's instance-compose ONLY
|
||||
# when PENPOT_OPENCODE_CONFIG_DIR is set (run-devenv --opencode-config-dir
|
||||
# DIR). Bind-mounts a host directory over the container's opencode global
|
||||
# config dir (~/.config/opencode) so personal agents/prompts/skills kept in
|
||||
# a separate repo are available inside the devenv without committing them
|
||||
# here. Without the flag this file is never referenced.
|
||||
services:
|
||||
main:
|
||||
volumes:
|
||||
- "${PENPOT_OPENCODE_CONFIG_DIR}:/home/penpot/.config/opencode:z"
|
||||
@ -148,10 +148,15 @@ automatically, so regular users never run this.
|
||||
```bash
|
||||
./manage.sh run-devenv --agentic \
|
||||
[--ws N] [--sync] [--serena-context CTX] \
|
||||
[--opencode-config-dir DIR] \
|
||||
[--git-user-name NAME] [--git-user-email EMAIL]
|
||||
```
|
||||
|
||||
Brings one agentic instance up. Errors out if the target is already running.
|
||||
`--opencode-config-dir DIR` bind-mounts DIR over the container's
|
||||
`~/.config/opencode` so personal agents/prompts/skills kept in a separate
|
||||
repository are available to the coding agent; see the
|
||||
[Dev environment guide](./devenv.md#personal-opencode-config-inside-the-container).
|
||||
|
||||
`--ws N` (N ≥ 1) brings that workspace up independently — workspaces can be
|
||||
started and stopped in any order. Per-instance ports
|
||||
|
||||
@ -139,6 +139,27 @@ until you set an identity. The values are applied every time
|
||||
`run-devenv` brings an instance up (idempotent), so re-running
|
||||
with different flags is the way to change the in-container identity.
|
||||
|
||||
### Personal opencode config inside the container
|
||||
|
||||
`run-devenv --opencode-config-dir DIR` bind-mounts a host directory over the
|
||||
container's `~/.config/opencode` (opencode's global config dir). This is how
|
||||
you keep personal agents, prompts, and skills in a separate repository and
|
||||
use them inside the devenv without committing them here or leaving untracked
|
||||
files in the repo:
|
||||
|
||||
```bash
|
||||
./manage.sh run-devenv --agentic --opencode-config-dir ../penpot-opencode
|
||||
```
|
||||
|
||||
The path must be an existing directory; `~` is expanded and the value is
|
||||
resolved to an absolute path automatically. The mount is applied at container
|
||||
creation, so changing it requires stopping and re-running `run-devenv` for
|
||||
that instance, and it applies only to instances brought up with the flag —
|
||||
other workspaces mount nothing. The directory is shared read-write with the
|
||||
container (same UID mapping as the source tree). Opencode's own state
|
||||
(sessions, `auth.json`) lives in `~/.local/share/opencode`, which stays in
|
||||
the container's data volume regardless of this flag.
|
||||
|
||||
### Shared state and workers
|
||||
|
||||
All instances share one Penpot database and one MinIO bucket; users, teams,
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
[app.main.ui.auth.login :refer [login-page*]]
|
||||
[app.main.ui.auth.recovery :refer [recovery-page*]]
|
||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page*]]
|
||||
[app.main.ui.auth.register :refer [register-page* register-success-page* register-validate-page* terms-register*]]
|
||||
[app.main.ui.auth.register :refer [register-page* register-success-page* register-validate-page* terms-service-privacy-policy*]]
|
||||
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*] :as raw-svg]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.util.dom :as dom]
|
||||
@ -74,7 +74,7 @@
|
||||
[:> recovery-page* {:params params}])
|
||||
|
||||
(when (= section :auth-register)
|
||||
[:> terms-register*])]]))
|
||||
[:> terms-service-privacy-policy*])]]))
|
||||
|
||||
|
||||
(mf/defc auth-page*
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "./ds/mixins.scss" as *;
|
||||
|
||||
.auth-section {
|
||||
display: grid;
|
||||
@ -26,6 +27,8 @@
|
||||
}
|
||||
|
||||
.auth-section.register {
|
||||
@include custom-scrollbar;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@ -4,56 +4,15 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
|
||||
.auth-form-wrapper {
|
||||
inline-size: 100%;
|
||||
padding-block-end: 0;
|
||||
display: grid;
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-m);
|
||||
|
||||
// Native <form> inside auth-form-wrapper — no class available
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-m);
|
||||
margin-block-start: var(--sp-m);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-title-wrapper {
|
||||
inline-size: 100%;
|
||||
padding-block-end: 0;
|
||||
display: grid;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-color: var(--color-background-quaternary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
@include use-typography("title-large");
|
||||
|
||||
line-height: 1.2;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.auth-subtitle {
|
||||
@include use-typography("title-small");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.auth-tagline {
|
||||
@include use-typography("title-small");
|
||||
|
||||
margin: 0;
|
||||
color: var(--color-foreground-secondary);
|
||||
margin: var(--sp-m) 0;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
@ -62,36 +21,40 @@
|
||||
--input-min-width: 100%;
|
||||
}
|
||||
|
||||
.buttons-stack {
|
||||
display: grid;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
.form-submit-btn {
|
||||
--button-bg-color: var(--color-accent-primary);
|
||||
--button-border-color: var(--color-accent-primary);
|
||||
--button-fg-color: var(--color-background-secondary);
|
||||
|
||||
.login-button,
|
||||
.login-ldap-button {
|
||||
@include use-typography("headline-small");
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-accent-primary);
|
||||
border: $b-1 solid var(--color-accent-primary);
|
||||
color: var(--color-background-secondary);
|
||||
background-color: var(--button-bg-color);
|
||||
border: $b-1 solid var(--button-border-color);
|
||||
color: var(--button-fg-color);
|
||||
border-radius: $br-8;
|
||||
min-block-size: $sz-32;
|
||||
block-size: $sz-40;
|
||||
inline-size: 100%;
|
||||
|
||||
&:hover {
|
||||
--button-bg-color: var(--color-accent-tertiary);
|
||||
--button-border-color: var(--color-accent-tertiary);
|
||||
--button-fg-color: var(--color-background-secondary);
|
||||
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--color-background-quaternary);
|
||||
border: 1px solid var(--color-background-quaternary);
|
||||
color: var(--color-foreground-disabled);
|
||||
cursor: unset;
|
||||
--button-bg-color: var(--color-background-quaternary);
|
||||
--button-border-color: var(--color-background-quaternary);
|
||||
--button-fg-color: var(--color-foreground-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.go-back {
|
||||
.go-back-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-m);
|
||||
@ -100,19 +63,68 @@
|
||||
}
|
||||
|
||||
.go-back-link {
|
||||
--button-bg-color: var(--color-background-tertiary);
|
||||
--button-border-color: var(--color-background-tertiary);
|
||||
--button-fg-color: var(--color-foreground-secondary);
|
||||
|
||||
@include use-typography("headline-small");
|
||||
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-tertiary);
|
||||
border: $b-1 solid var(--color-background-tertiary);
|
||||
color: var(--color-foreground-secondary);
|
||||
|
||||
@include use-typography("headline-small");
|
||||
|
||||
background-color: var(--button-bg-color);
|
||||
border: $b-1 solid var(--button-border-color);
|
||||
color: var(--button-fg-color);
|
||||
block-size: $sz-40;
|
||||
|
||||
&:hover {
|
||||
--button-bg-color: var(--color-background-quaternary);
|
||||
--button-border-color: var(--color-background-quaternary);
|
||||
--button-fg-color: var(--color-accent-primary);
|
||||
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-color: var(--color-background-quaternary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
inline-size: 100%;
|
||||
padding-block-end: 0;
|
||||
display: grid;
|
||||
gap: var(--sp-m);
|
||||
}
|
||||
|
||||
.title-wrapper {
|
||||
inline-size: 100%;
|
||||
padding-block-end: 0;
|
||||
display: grid;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.title {
|
||||
@include use-typography("title-large");
|
||||
|
||||
line-height: 1.2;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@include use-typography("title-small");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.tagline {
|
||||
@include use-typography("title-small");
|
||||
|
||||
margin: 0;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.links {
|
||||
@ -120,104 +132,10 @@
|
||||
gap: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.register,
|
||||
.account,
|
||||
.recovery-request,
|
||||
.demo-account {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--sp-s);
|
||||
padding: 0;
|
||||
}
|
||||
.notification-email {
|
||||
@include use-typography("title-medium");
|
||||
|
||||
.register-text,
|
||||
.account-text,
|
||||
.recovery-text,
|
||||
.demo-account-text {
|
||||
@include use-typography("title-small");
|
||||
|
||||
text-align: right;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.register-link,
|
||||
.account-link,
|
||||
.recovery-link,
|
||||
.forgot-pass-link,
|
||||
.demo-account-link {
|
||||
@include use-typography("title-small");
|
||||
|
||||
text-align: left;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
display: inline;
|
||||
line-height: 1.2;
|
||||
color: var(--color-accent-primary);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.submit-btn,
|
||||
.register-btn,
|
||||
.recover-btn {
|
||||
@include use-typography("headline-small");
|
||||
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--color-accent-primary);
|
||||
border: $b-1 solid var(--color-accent-primary);
|
||||
color: var(--color-background-secondary);
|
||||
border-radius: $br-8;
|
||||
min-block-size: $sz-32;
|
||||
block-size: $sz-40;
|
||||
inline-size: 100%;
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--color-background-quaternary);
|
||||
border: $b-1 solid var(--color-background-quaternary);
|
||||
color: var(--color-foreground-disabled);
|
||||
cursor: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
@include use-typography("title-small");
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: px2rem(6);
|
||||
inline-size: 100%;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-tertiary);
|
||||
color: var(--color-foreground-primary);
|
||||
|
||||
span {
|
||||
padding-block-start: var(--sp-xxs);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--color-foreground-primary);
|
||||
background-color: var(--color-background-quaternary);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--color-background-quaternary);
|
||||
border: 1px solid var(--color-background-quaternary);
|
||||
color: var(--color-foreground-disabled);
|
||||
cursor: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-buttons {
|
||||
display: flex;
|
||||
gap: var(--sp-s);
|
||||
margin-inline: $sz-36;
|
||||
}
|
||||
|
||||
@ -71,10 +71,17 @@
|
||||
(mf/defc login-form*
|
||||
[{:keys [params handle-redirect on-success-callback on-recovery-request origin] :as props}]
|
||||
(let [initial (mf/with-memo [params] params)
|
||||
error (mf/use-state false)
|
||||
|
||||
form (fm/use-form :schema schema:login-form
|
||||
:initial initial)
|
||||
|
||||
error (mf/use-state false)
|
||||
|
||||
show-password-field* (mf/use-state #(not (contains? cf/flags :login-with-custom-sso)))
|
||||
show-password-field? (deref show-password-field*)
|
||||
|
||||
callback-url (:callback-url params)
|
||||
|
||||
on-error
|
||||
(fn [cause]
|
||||
(let [cause (ex-data cause)]
|
||||
@ -102,12 +109,6 @@
|
||||
:else
|
||||
(reset! error (tr "errors.generic")))))
|
||||
|
||||
show-password-field*
|
||||
(mf/use-state #(not (contains? cf/flags :login-with-custom-sso)))
|
||||
|
||||
show-password-field?
|
||||
(deref show-password-field*)
|
||||
|
||||
on-success
|
||||
(fn [data]
|
||||
(when (fn? on-success-callback)
|
||||
@ -130,7 +131,8 @@
|
||||
(->> (rp/cmd! :get-sso-provider {:email (:email params)})
|
||||
(rx/map :id)
|
||||
(rx/catch (fn [cause]
|
||||
(log/error :hint "error on retrieving sso provider" :cause cause)
|
||||
(log/error :hint "error on retrieving sso provider"
|
||||
:cause cause)
|
||||
(rx/of nil)))
|
||||
(rx/subs! (fn [sso-provider-id]
|
||||
(if sso-provider-id
|
||||
@ -139,7 +141,7 @@
|
||||
(reset! show-password-field* true))))))))))
|
||||
|
||||
on-submit-ldap
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps form)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
@ -168,91 +170,102 @@
|
||||
{:level :error} message])
|
||||
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:class (stl/css :login-form)
|
||||
:class (stl/css :form)
|
||||
:form form}
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/input
|
||||
{:name :email
|
||||
:type "email"
|
||||
:label (tr "auth.work-email")
|
||||
:class (stl/css :form-field)}]]
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:name :email
|
||||
:type "email"
|
||||
:label (tr "auth.work-email")
|
||||
:class (stl/css :form-field)}]]
|
||||
|
||||
(when show-password-field?
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/input
|
||||
{:type "password"
|
||||
:name :password
|
||||
:auto-focus? true
|
||||
:label (tr "auth.password")
|
||||
:class (stl/css :form-field)}]])
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:type "password"
|
||||
:name :password
|
||||
:auto-focus? true
|
||||
:label (tr "auth.password")
|
||||
:class (stl/css :form-field)}]])
|
||||
|
||||
(when (and (not= origin :viewer)
|
||||
(or (contains? cf/flags :login)
|
||||
(contains? cf/flags :login-with-password)))
|
||||
[:div {:class (stl/css :fields-row :forgot-password)}
|
||||
[:div {:class (stl/css :form-row :forgot-password-row)}
|
||||
[:> lk/link* {:action on-recovery-request
|
||||
:class (stl/css :forgot-pass-link)
|
||||
:class (stl/css :forgot-password-link)
|
||||
:data-testid "forgot-password"}
|
||||
(tr "auth.forgot-password")]])
|
||||
|
||||
[:div {:class (stl/css :buttons-stack)}
|
||||
[:div {:class (stl/css :form-submit-buttons)}
|
||||
(when (or (contains? cf/flags :login)
|
||||
(contains? cf/flags :login-with-password))
|
||||
[:> fm/submit-button*
|
||||
{:label (tr "labels.continue")
|
||||
:data-testid "login-submit"
|
||||
:class (stl/css :login-button)}])
|
||||
[:> fm/submit-button* {:label (tr "labels.continue")
|
||||
:data-testid "login-submit"
|
||||
:class (stl/css :form-submit-btn)}])
|
||||
|
||||
(when (contains? cf/flags :login-with-ldap)
|
||||
[:> fm/submit-button*
|
||||
{:label (tr "auth.login-with-ldap-submit")
|
||||
:class (stl/css :login-ldap-button)
|
||||
:on-click on-submit-ldap}])]]]))
|
||||
[:> fm/submit-button* {:label (tr "auth.login-with-ldap-submit")
|
||||
:class (stl/css :form-submit-btn)
|
||||
:on-click on-submit-ldap}])]]]))
|
||||
|
||||
(defn raw-icon
|
||||
[id]
|
||||
(mf/html
|
||||
[:> raw-svg* {:id id :class (stl/css :sso-icon)}]))
|
||||
|
||||
(mf/defc login-sso-buttons*
|
||||
(mf/defc sso-buttons*
|
||||
[{:keys [params] :as props}]
|
||||
(let [login-with-google (mf/use-fn (mf/deps params) #(login-with-sso "google" params))
|
||||
login-with-github (mf/use-fn (mf/deps params) #(login-with-sso "github" params))
|
||||
login-with-gitlab (mf/use-fn (mf/deps params) #(login-with-sso "gitlab" params))
|
||||
login-with-oidc (mf/use-fn (mf/deps params) #(login-with-sso "oidc" params))]
|
||||
(let [login-with-google
|
||||
(mf/use-fn
|
||||
(mf/deps params)
|
||||
#(login-with-sso "google" params))
|
||||
|
||||
[:div {:class (stl/css :auth-buttons)}
|
||||
login-with-github
|
||||
(mf/use-fn
|
||||
(mf/deps params)
|
||||
#(login-with-sso "github" params))
|
||||
|
||||
login-with-gitlab
|
||||
(mf/use-fn
|
||||
(mf/deps params)
|
||||
#(login-with-sso "gitlab" params))
|
||||
|
||||
login-with-oidc
|
||||
(mf/use-fn
|
||||
(mf/deps params)
|
||||
#(login-with-sso "oidc" params))]
|
||||
|
||||
[:div {:class (stl/css :sso-row)}
|
||||
(when (contains? cf/flags :login-with-google)
|
||||
[:> bl/button-link* {:on-click login-with-google
|
||||
:icon (raw-icon raw-icons/brand-google)
|
||||
:label (tr "auth.login-with-google-submit")
|
||||
:class (stl/css :login-btn :btn-google-auth)}])
|
||||
:class (stl/css :sso-btn)}])
|
||||
|
||||
(when (contains? cf/flags :login-with-github)
|
||||
[:> bl/button-link* {:on-click login-with-github
|
||||
:icon (raw-icon raw-icons/brand-github)
|
||||
:label (tr "auth.login-with-github-submit")
|
||||
:class (stl/css :login-btn :btn-github-auth)}])
|
||||
:class (stl/css :sso-btn)}])
|
||||
|
||||
(when (contains? cf/flags :login-with-gitlab)
|
||||
[:> bl/button-link* {:on-click login-with-gitlab
|
||||
:icon (raw-icon raw-icons/brand-gitlab)
|
||||
:label (tr "auth.login-with-gitlab-submit")
|
||||
:class (stl/css :login-btn :btn-gitlab-auth)}])
|
||||
:class (stl/css :sso-btn)}])
|
||||
|
||||
(when (contains? cf/flags :login-with-oidc)
|
||||
[:> bl/button-link* {:on-click login-with-oidc
|
||||
:icon (raw-icon raw-icons/brand-openid)
|
||||
:label (or (not-empty cf/oidc-name) (tr "auth.login-with-oidc-submit"))
|
||||
:class (stl/css :login-btn :btn-oidc-auth)}])]))
|
||||
:class (stl/css :sso-btn)}])]))
|
||||
|
||||
(mf/defc login-dialog*
|
||||
[{:keys [params] :as props}]
|
||||
[:*
|
||||
(when show-sso-login-buttons?
|
||||
[:*
|
||||
[:> login-sso-buttons* {:params params}]
|
||||
[:> sso-buttons* {:params params}]
|
||||
|
||||
(when (or (contains? cf/flags :login)
|
||||
(contains? cf/flags :login-with-password)
|
||||
@ -270,11 +283,11 @@
|
||||
(mf/use-fn
|
||||
#(st/emit! (rt/nav :auth-register params)))]
|
||||
|
||||
[:div {:class (stl/css :auth-form-wrapper)}
|
||||
[:h1 {:class (stl/css :auth-title)
|
||||
[:div {:class (stl/css :wrapper)}
|
||||
[:h1 {:class (stl/css :title)
|
||||
:data-testid "login-title"} (tr "auth.login-account-title")]
|
||||
|
||||
[:p {:class (stl/css :auth-tagline)}
|
||||
[:p {:class (stl/css :tagline)}
|
||||
(tr "auth.login-tagline")]
|
||||
|
||||
(when (contains? cf/flags :demo-warning)
|
||||
@ -286,7 +299,7 @@
|
||||
|
||||
[:div {:class (stl/css :links)}
|
||||
(when (contains? cf/flags :registration)
|
||||
[:div {:class (stl/css :register)}
|
||||
[:div {:class (stl/css :register-row)}
|
||||
[:span {:class (stl/css :register-text)}
|
||||
(tr "auth.register") " "]
|
||||
[:> lk/link* {:action go-register
|
||||
|
||||
@ -6,8 +6,88 @@
|
||||
|
||||
@use "./common";
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
|
||||
.forgot-password-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.forgot-password-link {
|
||||
@include use-typography("title-small");
|
||||
|
||||
text-align: left;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
display: inline;
|
||||
color: var(--color-accent-primary);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.form-submit-buttons {
|
||||
display: grid;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.sso-icon {
|
||||
max-inline-size: px2rem(26);
|
||||
max-block-size: px2rem(26);
|
||||
}
|
||||
|
||||
.sso-row {
|
||||
display: flex;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.sso-btn {
|
||||
--button-bg-color: var(--color-background-tertiary);
|
||||
--button-fg-color: var(--color-foreground-primary);
|
||||
|
||||
@include use-typography("title-small");
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: px2rem(6);
|
||||
inline-size: 100%;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--button-bg-color);
|
||||
color: var(--button-fg-color);
|
||||
|
||||
&:hover {
|
||||
--button-bg-color: var(--color-background-quaternary);
|
||||
--button-fg-color: var(--color-foreground-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.register-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--sp-s);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.register-text {
|
||||
@include use-typography("title-small");
|
||||
|
||||
text-align: right;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.register-link {
|
||||
@include use-typography("title-small");
|
||||
|
||||
text-align: left;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
display: inline;
|
||||
color: var(--color-accent-primary);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,39 +60,38 @@
|
||||
:initial params)]
|
||||
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:class (stl/css :recovery-form)
|
||||
:class (stl/css :form)
|
||||
:form form}
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:type "password"
|
||||
:name :password-1
|
||||
:show-success? true
|
||||
:label (tr "auth.new-password")
|
||||
:class (stl/css :form-field)}]]
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:type "password"
|
||||
:name :password-2
|
||||
:show-success? true
|
||||
:label (tr "auth.confirm-password")
|
||||
:class (stl/css :form-field)}]]
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:label (tr "auth.recovery-submit")
|
||||
:class (stl/css :submit-btn)}]]))
|
||||
[:> fm/submit-button* {:label (tr "auth.recovery-submit")
|
||||
:class (stl/css :form-submit-btn)}]]))
|
||||
|
||||
;; --- Recovery Request Page
|
||||
|
||||
(mf/defc recovery-page*
|
||||
[{:keys [params]}]
|
||||
[:div {:class (stl/css :auth-form-wrapper)}
|
||||
[:h1 {:class (stl/css :auth-title)} "Forgot your password?"]
|
||||
[:div {:class (stl/css :auth-subtitle)} "Please enter your new password"]
|
||||
[:div {:class (stl/css :wrapper)}
|
||||
[:h1 {:class (stl/css :title)} (tr "auth.recovery-request-title")]
|
||||
[:div {:class (stl/css :subtitle)} (tr "auth.recovery-request-subtitle")]
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
[:> recovery-form* {:params params}]
|
||||
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :go-back)}
|
||||
[:div {:class (stl/css :go-back-row)}
|
||||
[:a {:on-click #(st/emit! (rt/nav :auth-login))
|
||||
:class (stl/css :go-back-link)}
|
||||
(tr "profile.recovery.go-to-login")]]]])
|
||||
|
||||
@ -5,7 +5,3 @@
|
||||
// Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
@use "./common";
|
||||
|
||||
.submit-btn {
|
||||
margin-block-start: var(--sp-l);
|
||||
}
|
||||
|
||||
@ -70,18 +70,17 @@
|
||||
(st/emit! (du/request-profile-recovery params)))))]
|
||||
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:class (stl/css :recovery-request-form)
|
||||
:class (stl/css :form)
|
||||
:form form}
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:name :email
|
||||
:label (tr "auth.work-email")
|
||||
:type "text"
|
||||
:class (stl/css :form-field)}]]
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:label (tr "auth.recovery-request-submit")
|
||||
:data-testid "recovery-resquest-submit"
|
||||
:class (stl/css :recover-btn)}]]))
|
||||
[:> fm/submit-button* {:label (tr "auth.recovery-request-submit")
|
||||
:data-testid "recovery-resquest-submit"
|
||||
:class (stl/css :form-submit-btn)}]]))
|
||||
|
||||
|
||||
;; --- Recovery Request Page
|
||||
@ -90,14 +89,14 @@
|
||||
[{:keys [params on-success-callback go-back-callback]}]
|
||||
(let [default-go-back #(st/emit! (rt/nav :auth-login))
|
||||
go-back (or go-back-callback default-go-back)]
|
||||
[:div {:class (stl/css :auth-form-wrapper)}
|
||||
[:h1 {:class (stl/css :auth-title)} (tr "auth.recovery-request-title")]
|
||||
[:div {:class (stl/css :auth-subtitle)} (tr "auth.recovery-request-subtitle")]
|
||||
[:div {:class (stl/css :wrapper)}
|
||||
[:h1 {:class (stl/css :title)} (tr "auth.recovery-request-title")]
|
||||
[:div {:class (stl/css :subtitle)} (tr "auth.recovery-request-subtitle")]
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
|
||||
[:> recovery-form* {:params params :on-success-callback on-success-callback}]
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
[:div {:class (stl/css :go-back)}
|
||||
[:div {:class (stl/css :go-back-row)}
|
||||
[:> lk/link* {:action go-back
|
||||
:class (stl/css :go-back-link)
|
||||
:data-testid "go-back-link"}
|
||||
@ -106,11 +105,10 @@
|
||||
|
||||
(mf/defc recovery-sent-page*
|
||||
[{:keys [email]}]
|
||||
[:div {:class (stl/css :auth-form-wrapper :register-success)}
|
||||
[:div {:class (stl/css :auth-title-wrapper)}
|
||||
[:h2 {:class (stl/css :auth-title)}
|
||||
[:div {:class (stl/css :wrapper :register-success)}
|
||||
[:div {:class (stl/css :title-wrapper)}
|
||||
[:h2 {:class (stl/css :title)}
|
||||
(tr "auth.check-email")]
|
||||
[:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery")]]
|
||||
[:div {:class (stl/css :notification-text-email)} email]
|
||||
[:div {:class (stl/css :notification-email)} email]
|
||||
[:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery-check")]])
|
||||
|
||||
|
||||
@ -4,19 +4,4 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
@use "./common";
|
||||
|
||||
.fields-row {
|
||||
margin-block-end: var(--sp-s);
|
||||
}
|
||||
|
||||
.notification-text-email {
|
||||
@include use-typography("title-medium");
|
||||
|
||||
line-height: 1.2;
|
||||
color: var(--color-accent-primary);
|
||||
margin-inline: $sz-36;
|
||||
}
|
||||
|
||||
@ -26,39 +26,38 @@
|
||||
|
||||
;; --- PAGE: Register
|
||||
|
||||
(mf/defc newsletter-options*
|
||||
(mf/defc newsletter-checkbox*
|
||||
{::mf/private true}
|
||||
[]
|
||||
(let [updates-label
|
||||
(let [newsletter-label
|
||||
(mf/html
|
||||
[:> i18n/tr-html*
|
||||
{:tag-name "div"
|
||||
:content (tr "onboarding-v2.newsletter.updates")}])]
|
||||
[:div {:class (stl/css :fields-row :input-visible :newsletter-option-wrapper)}
|
||||
[:> i18n/tr-html* {:tag-name "div"
|
||||
:content (tr "onboarding-v2.newsletter.updates")}])]
|
||||
|
||||
[:div {:class (stl/css :form-row :input-visible :newsletter-option-wrapper)}
|
||||
[:& fm/input {:name :accept-newsletter-updates
|
||||
:class (stl/css :checkbox-newsletter-updates)
|
||||
:type "checkbox"
|
||||
:default-checked false
|
||||
:label updates-label}]]))
|
||||
:label newsletter-label}]]))
|
||||
|
||||
(mf/defc terms-and-privacy*
|
||||
(mf/defc terms-and-privacy-checkbox*
|
||||
{::mf/private true}
|
||||
[]
|
||||
(let [terms-label
|
||||
(let [terms-and-privacy-label
|
||||
(mf/html
|
||||
[:> i18n/tr-html*
|
||||
{:tag-name "div"
|
||||
:content (tr "auth.terms-and-privacy-agreement"
|
||||
cf/terms-of-service-uri
|
||||
cf/privacy-policy-uri)}])]
|
||||
[:> i18n/tr-html* {:tag-name "div"
|
||||
:content (tr "auth.terms-and-privacy-agreement"
|
||||
cf/terms-of-service-uri
|
||||
cf/privacy-policy-uri)}])]
|
||||
|
||||
[:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)}
|
||||
[:div {:class (stl/css :form-row :input-visible :accept-terms-and-privacy-wrapper)}
|
||||
[:& fm/input {:name :accept-terms-and-privacy
|
||||
:show-error false
|
||||
:class (stl/css :checkbox-terms-and-privacy)
|
||||
:type "checkbox"
|
||||
:default-checked false
|
||||
:label terms-label}]]))
|
||||
:label terms-and-privacy-label}]]))
|
||||
|
||||
(def ^:private schema:register-form
|
||||
[:map {:title "RegisterForm"}
|
||||
@ -76,8 +75,7 @@
|
||||
form (fm/use-form :schema schema:register-form
|
||||
:initial initial)
|
||||
|
||||
submitted?
|
||||
(mf/use-state false)
|
||||
submitted? (mf/use-state false)
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
@ -167,22 +165,24 @@
|
||||
(->> (rp/cmd! :prepare-register-profile cdata)
|
||||
(rx/subs! on-register-profile on-error #(reset! submitted? false))))))]
|
||||
|
||||
[:& fm/form {:on-submit on-submit :form form}
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:form form
|
||||
:class (stl/css :form)}
|
||||
[:div {:class (stl/css :form-row)}
|
||||
|
||||
[:& fm/input {:name :fullname
|
||||
:label (tr "auth.fullname")
|
||||
:type "text"
|
||||
:show-success? true
|
||||
:class (stl/css :form-field)}]]
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:type "text"
|
||||
:name :email
|
||||
:label (tr "auth.work-email")
|
||||
:data-testid "email-input"
|
||||
:show-success? true
|
||||
:class (stl/css :form-field)}]]
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:name :password
|
||||
:hint (tr "auth.password-length-hint")
|
||||
:label (tr "auth.password")
|
||||
@ -191,21 +191,20 @@
|
||||
:class (stl/css :form-field)}]]
|
||||
|
||||
(when (contains? cf/flags :terms-and-privacy-checkbox)
|
||||
[:> terms-and-privacy*])
|
||||
[:> terms-and-privacy-checkbox*])
|
||||
|
||||
[:> newsletter-options*]
|
||||
[:> newsletter-checkbox*]
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:label (tr "auth.register-submit")
|
||||
:disabled @submitted?
|
||||
:data-testid "register-form-submit"
|
||||
:class (stl/css :register-btn)}]]))
|
||||
[:> fm/submit-button* {:label (tr "auth.register-submit")
|
||||
:disabled @submitted?
|
||||
:data-testid "register-form-submit"
|
||||
:class (stl/css :form-submit-btn)}]]))
|
||||
|
||||
(mf/defc register-methods*
|
||||
[{:keys [params hide-separator on-success-callback]}]
|
||||
[:*
|
||||
(when login/show-sso-login-buttons?
|
||||
[:> login/login-sso-buttons* {:params params}])
|
||||
[:> login/sso-buttons* {:params params}])
|
||||
(when (or login/show-sso-login-buttons? (false? hide-separator))
|
||||
[:hr {:class (stl/css :separator)}])
|
||||
(when (contains? cf/flags :login-with-password)
|
||||
@ -213,8 +212,8 @@
|
||||
|
||||
(mf/defc register-page*
|
||||
[{:keys [params]}]
|
||||
[:div {:class (stl/css :auth-form-wrapper :register-form)}
|
||||
[:h1 {:class (stl/css :auth-title)
|
||||
[:div {:class (stl/css :wrapper :register-form)}
|
||||
[:h1 {:class (stl/css :title)
|
||||
:data-testid "registration-title"} (tr "auth.register-title")]
|
||||
|
||||
(when (contains? cf/flags :demo-warning)
|
||||
@ -223,7 +222,7 @@
|
||||
[:> register-methods* {:params params}]
|
||||
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :account)}
|
||||
[:div {:class (stl/css :account-row)}
|
||||
[:span {:class (stl/css :account-text)} (tr "auth.already-have-account") " "]
|
||||
[:> lk/link* {:action #(st/emit! (rt/nav :auth-login params))
|
||||
:class (stl/css :account-link)
|
||||
@ -233,9 +232,9 @@
|
||||
(when (contains? cf/flags :demo-users)
|
||||
[:*
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
[:div {:class (stl/css :demo-account)}
|
||||
[:div {:class (stl/css :account-row)}
|
||||
[:> lk/link* {:action login/create-demo-profile
|
||||
:class (stl/css :demo-account-link)}
|
||||
:class (stl/css :account-link)}
|
||||
(tr "auth.create-demo-account")]]])]])
|
||||
|
||||
|
||||
@ -244,31 +243,31 @@
|
||||
(mf/defc register-success-page*
|
||||
[{:keys [params]}]
|
||||
(let [email (or (:email params) (::email storage/user))]
|
||||
[:div {:class (stl/css :auth-form-wrapper :register-success)}
|
||||
[:div {:class (stl/css :auth-title-wrapper)}
|
||||
[:h2 {:class (stl/css :auth-title)}
|
||||
[:div {:class (stl/css :wrapper :register-success)}
|
||||
[:div {:class (stl/css :title-wrapper)}
|
||||
[:h2 {:class (stl/css :register-success-title)}
|
||||
(tr "auth.check-email")]
|
||||
[:div {:class (stl/css :notification-text)} (tr "auth.verification-sent-email")]]
|
||||
[:div {:class (stl/css :notification-text-email)} email]]))
|
||||
[:div {:class (stl/css :notification-email)} email]]))
|
||||
|
||||
(mf/defc terms-register*
|
||||
(mf/defc terms-service-privacy-policy*
|
||||
[]
|
||||
(let [show-all? (and cf/terms-of-service-uri cf/privacy-policy-uri)
|
||||
show-terms? (some? cf/terms-of-service-uri)
|
||||
show-privacy? (some? cf/privacy-policy-uri)]
|
||||
|
||||
(when show-all?
|
||||
[:div {:class (stl/css :terms-register)}
|
||||
[:div {:class (stl/css :terms)}
|
||||
(when show-terms?
|
||||
[:a {:href cf/terms-of-service-uri :target "_blank" :class (stl/css :auth-link)}
|
||||
[:a {:href cf/terms-of-service-uri :target "_blank" :class (stl/css :terms-link)}
|
||||
(tr "auth.terms-of-service")])
|
||||
|
||||
(when show-all?
|
||||
[:span {:class (stl/css :and-text)}
|
||||
[:span {:class (stl/css :terms-and)}
|
||||
(dm/str " " (tr "labels.and") " ")])
|
||||
|
||||
(when show-privacy?
|
||||
[:a {:href cf/privacy-policy-uri :target "_blank" :class (stl/css :auth-link)}
|
||||
[:a {:href cf/privacy-policy-uri :target "_blank" :class (stl/css :terms-link)}
|
||||
(tr "auth.privacy-policy")])])))
|
||||
|
||||
;; --- PAGE: register validation
|
||||
@ -334,9 +333,9 @@
|
||||
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:form form
|
||||
:class (stl/css :register-validate-form)}
|
||||
:class (stl/css :auth-form)}
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:name :fullname
|
||||
:label (tr "auth.fullname")
|
||||
:type "text"
|
||||
@ -344,30 +343,28 @@
|
||||
:class (stl/css :form-field)}]]
|
||||
|
||||
(when (contains? cf/flags :terms-and-privacy-checkbox)
|
||||
[:> terms-and-privacy*])
|
||||
[:> terms-and-privacy-checkbox*])
|
||||
|
||||
[:> newsletter-options*]
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:label (tr "auth.register-submit")
|
||||
:disabled @submitted?
|
||||
:class (stl/css :register-btn)}]]))
|
||||
[:> newsletter-checkbox*]
|
||||
|
||||
[:> fm/submit-button* {:label (tr "auth.register-submit")
|
||||
:disabled @submitted?
|
||||
:class (stl/css :form-submit-btn)}]]))
|
||||
|
||||
(mf/defc register-validate-page*
|
||||
[{:keys [params]}]
|
||||
[:div {:class (stl/css :auth-form-wrapper :register-form)}
|
||||
[:div {:class (stl/css :wrapper :register-form)}
|
||||
|
||||
|
||||
[:div {:class (stl/css :auth-title-wrapper)}
|
||||
[:h2 {:class (stl/css :auth-title)
|
||||
[:div {:class (stl/css :title-wrapper)}
|
||||
[:h2 {:class (stl/css :title)
|
||||
:data-testid "register-title"} (tr "auth.register-account-title")]
|
||||
[:div {:class (stl/css :auth-subtitle)} (tr "auth.register-account-tagline")]]
|
||||
[:div {:class (stl/css :subtitle)} (tr "auth.register-account-tagline")]]
|
||||
|
||||
[:> register-validate-form* {:params params}]
|
||||
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :go-back)}
|
||||
[:div {:class (stl/css :go-back-row)}
|
||||
[:> lk/link* {:action #(st/emit! (rt/nav :auth-register {}))
|
||||
:class (stl/css :go-back-link)}
|
||||
(tr "labels.go-back")]]]])
|
||||
|
||||
@ -4,57 +4,64 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
@use "./common";
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
@use "./common";
|
||||
|
||||
.checkbox-terms-and-privacy,
|
||||
.checkbox-newsletter-updates {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.checkbox-terms-and-privacy {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.register-form {
|
||||
gap: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.account-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--sp-s);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.account-text {
|
||||
@include use-typography("title-small");
|
||||
|
||||
text-align: right;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.account-link {
|
||||
@include use-typography("title-small");
|
||||
|
||||
text-align: left;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
display: inline;
|
||||
color: var(--color-accent-primary);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.register-success {
|
||||
gap: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.register-success .auth-title {
|
||||
.register-success-title {
|
||||
@include use-typography("title-medium");
|
||||
|
||||
line-height: 1.2;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.notification-text {
|
||||
@include use-typography("body-medium");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.notification-text-email {
|
||||
@include use-typography("title-medium");
|
||||
|
||||
line-height: 1.2;
|
||||
color: var(--color-accent-primary);
|
||||
margin-inline: $sz-36;
|
||||
}
|
||||
|
||||
.logo-btn {
|
||||
block-size: $sz-40;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
inline-size: $sz-120;
|
||||
margin-block-end: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.terms-register {
|
||||
.terms {
|
||||
@include use-typography("body-small");
|
||||
|
||||
display: flex;
|
||||
@ -63,15 +70,15 @@
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.and-text {
|
||||
border-block-end: $b-1 solid transparent;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
.terms-link {
|
||||
color: var(--color-accent-primary);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.terms-and {
|
||||
border-block-end: $b-1 solid transparent;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
@ -839,7 +839,8 @@
|
||||
(mf/with-effect [handle-unmount] handle-unmount)
|
||||
|
||||
[:div {:class [class (stl/css-case :input-wrapper true
|
||||
:resizable (not is-token-applied?))]
|
||||
:resizable (and (not is-token-applied?)
|
||||
(not disabled)))]
|
||||
:ref wrapper-ref
|
||||
:on-pointer-down on-scrub-pointer-down
|
||||
:on-pointer-move on-scrub-pointer-move
|
||||
|
||||
@ -22,9 +22,10 @@
|
||||
column-gap: var(--sp-xs);
|
||||
align-items: center;
|
||||
inline-size: 100%;
|
||||
block-size: var(--token-field-height);
|
||||
background: var(--token-field-bg-color);
|
||||
border-radius: $br-8;
|
||||
padding-inline-end: var(--sp-xs);
|
||||
padding: 0 var(--input-padding-size, var(--sp-s));
|
||||
outline: $b-1 solid var(--token-field-outline-color);
|
||||
position: relative;
|
||||
|
||||
|
||||
@ -114,6 +114,7 @@
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-block-size: 0;
|
||||
outline: $b-1 solid var(--tab-panel-outline-color);
|
||||
}
|
||||
|
||||
|
||||
@ -185,7 +185,7 @@
|
||||
:on-click set-section} (tr "auth.login-here")]]
|
||||
[:div {:class (stl/css :links)}
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
[:> register/terms-register*]]]
|
||||
[:> register/terms-service-privacy-policy*]]]
|
||||
|
||||
:register-validate
|
||||
[:div {:class (stl/css :form-container)}
|
||||
|
||||
@ -12,8 +12,10 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.auth.login :refer [login-dialog*]]
|
||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page*]]
|
||||
[app.main.ui.auth.register :refer [register-methods* register-success-page* terms-register* register-validate-form*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.auth.register :refer [register-methods* register-success-page*
|
||||
register-validate-form* terms-service-privacy-policy*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
@ -24,14 +26,15 @@
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :login-register}
|
||||
[_]
|
||||
(let [user-email (mf/use-state "")
|
||||
(let [user-email (mf/use-state "")
|
||||
register-token (mf/use-state "")
|
||||
|
||||
current-section* (mf/use-state :login)
|
||||
current-section (deref current-section*)
|
||||
current-section (deref current-section*)
|
||||
|
||||
set-current-section
|
||||
(mf/use-fn #(reset! current-section* %))
|
||||
(mf/use-fn
|
||||
#(reset! current-section* %))
|
||||
|
||||
set-section
|
||||
(mf/use-fn
|
||||
@ -41,7 +44,9 @@
|
||||
(keyword))]
|
||||
(set-current-section section))))
|
||||
|
||||
go-back-to-login (mf/use-fn #(set-current-section :login))
|
||||
go-back-to-login
|
||||
(mf/use-fn
|
||||
#(set-current-section :login))
|
||||
|
||||
main-section (or
|
||||
(= current-section :login)
|
||||
@ -51,13 +56,16 @@
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (modal/hide)))
|
||||
|
||||
success-email-sent
|
||||
(fn [email]
|
||||
(reset! user-email email)
|
||||
(set-current-section :email-sent))
|
||||
|
||||
success-login
|
||||
(fn []
|
||||
(.reload js/window.location true))
|
||||
|
||||
success-register
|
||||
(fn [data]
|
||||
(reset! register-token (:token data))
|
||||
@ -66,48 +74,47 @@
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h2 {:class (stl/css :modal-title)} (tr "labels.continue-with-penpot")]
|
||||
[:button {:class (stl/css :modal-close-btn)
|
||||
:title (tr "labels.close")
|
||||
:on-click close} deprecated-icon/close]]
|
||||
[:h2 {:class (stl/css :modal-header-title)} (tr "labels.continue-with-penpot")]
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:class (stl/css :modal-close)
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click close
|
||||
:icon i/close}]]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
(case current-section
|
||||
:login
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:> login-dialog*
|
||||
{:on-success-callback success-login
|
||||
:origin :viewer}]
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :recovery-request)}
|
||||
[:div {:class (stl/css :login-form)}
|
||||
[:> login-dialog* {:on-success-callback success-login
|
||||
:origin :viewer}]
|
||||
[:div {:class (stl/css :login-links)}
|
||||
[:div
|
||||
[:a {:on-click set-section
|
||||
:class (stl/css :recovery-link)
|
||||
:data-value "recovery-request"}
|
||||
(tr "auth.forgot-password")]]
|
||||
[:div {:class (stl/css :register)}
|
||||
[:span {:class (stl/css :register-text)}
|
||||
[:div
|
||||
[:span
|
||||
(tr "auth.register") " "]
|
||||
[:a {:on-click set-section
|
||||
:class (stl/css :register-link)
|
||||
:data-value "register"}
|
||||
(tr "auth.register-submit")]]]]
|
||||
|
||||
:register
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:div {:class (stl/css :login-form)}
|
||||
[:> register-methods* {:on-success-callback success-register}]
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :account)}
|
||||
[:div {:class (stl/css :login-links)}
|
||||
[:div
|
||||
[:span (tr "auth.already-have-account") " "]
|
||||
[:a {:on-click set-section
|
||||
:data-value "login"}
|
||||
(tr "auth.login-here")]]]]
|
||||
|
||||
:register-validate
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:div {:class (stl/css :login-form)}
|
||||
[:> register-validate-form* {:params {:token @register-token}
|
||||
:on-success-callback success-email-sent}]
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :register)}
|
||||
[:div {:class (stl/css :login-links)}
|
||||
[:div
|
||||
[:a {:on-click set-section
|
||||
:data-value "register"}
|
||||
(tr "labels.go-back")]]]]
|
||||
@ -115,10 +122,11 @@
|
||||
:recovery-request
|
||||
[:> recovery-request-page* {:go-back-callback go-back-to-login
|
||||
:on-success-callback success-email-sent}]
|
||||
|
||||
:email-sent
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:div {:class (stl/css :login-form)}
|
||||
[:> register-success-page* {:params {:email @user-email}}]])
|
||||
|
||||
(when main-section
|
||||
[:div {:class (stl/css :links)}
|
||||
[:> terms-register*]])]]]))
|
||||
[:div {:class (stl/css :login-links)}
|
||||
[:> terms-service-privacy-policy*]])]]]))
|
||||
|
||||
@ -4,75 +4,71 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
@use "ds/z-index.scss" as *;
|
||||
|
||||
.modal-overlay {
|
||||
@extend %modal-overlay-base;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
z-index: var(--z-index-set);
|
||||
background-color: var(--color-overlay-default);
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
@extend %modal-container-base;
|
||||
|
||||
width: deprecated.$s-368;
|
||||
position: relative;
|
||||
padding: var(--sp-xxxl);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-primary);
|
||||
border: $b-2 solid var(--color-background-quaternary);
|
||||
min-inline-size: $sz-364;
|
||||
min-block-size: $sz-192;
|
||||
max-inline-size: $sz-512;
|
||||
max-block-size: $sz-712;
|
||||
inline-size: $sz-364;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: deprecated.$s-24;
|
||||
margin-block-end: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include deprecated.uppercase-title-typography;
|
||||
.modal-header-title {
|
||||
@include use-typography("headline-small");
|
||||
|
||||
color: var(--modal-title-foreground-color);
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
@extend %modal-close-btn-base;
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
inset-block-start: var(--sp-s);
|
||||
inset-inline-end: var(--sp-s);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include deprecated.flex-column;
|
||||
@include deprecated.body-small-typography;
|
||||
@include use-typography("body-small");
|
||||
|
||||
gap: deprecated.$s-24;
|
||||
max-height: deprecated.$s-400;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxl);
|
||||
max-block-size: px2rem(576);
|
||||
overflow: hidden auto;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
.login-form {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-m);
|
||||
}
|
||||
|
||||
.links {
|
||||
.login-links {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.link-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: deprecated.$s-12;
|
||||
|
||||
span {
|
||||
text-align: center;
|
||||
font-size: deprecated.$fs-14;
|
||||
color: var(--modal-text-foreground-color);
|
||||
margin-top: deprecated.$s-12;
|
||||
}
|
||||
|
||||
a {
|
||||
@extend %button-secondary;
|
||||
|
||||
height: deprecated.$s-40;
|
||||
text-transform: uppercase;
|
||||
font-size: deprecated.$fs-11;
|
||||
}
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
@ -344,7 +344,7 @@
|
||||
(mf/defc view-menu*
|
||||
{::mf/private true
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [layout toggle-flag on-close]}]
|
||||
[{:keys [layout toggle-flag on-close on-close-all]}]
|
||||
(let [read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
|
||||
toggle-color-palette
|
||||
@ -365,11 +365,11 @@
|
||||
|
||||
toggle-comments-visibility
|
||||
(mf/use-fn
|
||||
(mf/deps on-close)
|
||||
(mf/deps on-close-all)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dwcm/toggle-comments-visibility {:origin "workspace:menu"}))
|
||||
(on-close)))]
|
||||
(on-close-all)))]
|
||||
|
||||
[:> dropdown-menu* {:show true
|
||||
:class (stl/css :base-menu :sub-menu :pos-3)
|
||||
@ -1139,7 +1139,8 @@
|
||||
:view
|
||||
[:> view-menu* {:layout layout
|
||||
:toggle-flag toggle-flag
|
||||
:on-close close-sub-menu}]
|
||||
:on-close close-sub-menu
|
||||
:on-close-all close-all-menus}]
|
||||
|
||||
:preferences
|
||||
[:> preferences-menu* {:layout layout
|
||||
|
||||
@ -97,9 +97,7 @@
|
||||
sitemap-height
|
||||
(if sitemap-collapsed? 32 height)]
|
||||
|
||||
[:article {:class (stl/css :layers-tab)
|
||||
:style {:--height (dm/str height "px")}}
|
||||
|
||||
[:article {:class (stl/css :layers-tab)}
|
||||
[:> sitemap* {:layout layout
|
||||
:height sitemap-height
|
||||
:collapsed sitemap-collapsed?
|
||||
|
||||
@ -79,13 +79,15 @@
|
||||
|
||||
.layers-tab {
|
||||
padding-block-start: var(--sp-xs);
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.layers-tab-resize-area {
|
||||
background-color: var(--color-background-primary);
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
flex: 0 0 auto;
|
||||
inline-size: 100%;
|
||||
padding: px2rem(3) 0 px2rem(1);
|
||||
block-size: $sz-6;
|
||||
@ -120,11 +122,16 @@
|
||||
.left-sidebar-content {
|
||||
grid-area: content;
|
||||
inset-inline-end: calc(-1 * var(--sp-s));
|
||||
min-block-size: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.left-sidebar-tabs {
|
||||
--tabs-nav-padding-inline-start: var(--sp-m);
|
||||
--tabs-nav-padding-inline-end: var(--sp-m);
|
||||
|
||||
block-size: 100%;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.left-sidebar-resize-area {
|
||||
|
||||
@ -31,11 +31,10 @@
|
||||
}
|
||||
|
||||
.tool-window-content {
|
||||
--calculated-height: calc(#{px2rem(136)} + var(--height, #{$sz-200}));
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
block-size: calc(100vh - var(--calculated-height));
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
inline-size: calc(var(--left-sidebar-width) + var(--depth) * var(--layer-indentation-size));
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable;
|
||||
@ -117,6 +116,10 @@
|
||||
|
||||
.layers {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.replace-wrapper {
|
||||
|
||||
@ -15,7 +15,8 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
flex: 0 0 auto;
|
||||
flex-shrink: 0;
|
||||
inline-size: 100%;
|
||||
block-size: var(--height, $sz-200);
|
||||
}
|
||||
|
||||
@ -149,16 +149,26 @@
|
||||
|
||||
on-main-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps open)
|
||||
(fn [event]
|
||||
(cond
|
||||
(kbd/space? event)
|
||||
(and open (kbd/esc? event))
|
||||
(reset! open* false)
|
||||
|
||||
(or (kbd/enter? event) (kbd/space? event))
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(if open
|
||||
(reset! open* false)
|
||||
(do
|
||||
(cancel-timer! close-timer*)
|
||||
(reset! open* true))))
|
||||
|
||||
(kbd/down-arrow? event)
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(cancel-timer! close-timer*)
|
||||
(reset! open* true))
|
||||
|
||||
(and open (kbd/esc? event))
|
||||
(reset! open* false))))
|
||||
(reset! open* true)))))
|
||||
|
||||
on-flyout-key-down
|
||||
(mf/use-fn
|
||||
@ -218,7 +228,10 @@
|
||||
:aria-expanded open
|
||||
:has-tooltip false
|
||||
:icon default-icon
|
||||
:on-click on-select-tool
|
||||
:on-click (fn [event]
|
||||
(cancel-timer! open-timer*)
|
||||
(cancel-timer! close-timer*)
|
||||
(on-select-tool event))
|
||||
:on-key-down on-main-key-down
|
||||
:data-tool (name default-tool)}]
|
||||
|
||||
|
||||
61
manage.sh
61
manage.sh
@ -223,8 +223,10 @@ function ensure-devenv-network {
|
||||
# those stale values would leak into substitution. And because Docker Compose
|
||||
# gives shell-env precedence over --env-file, the re-injected per-instance
|
||||
# overrides cleanly override the defaults.env baseline. Re-injected: HOME/PATH
|
||||
# (tooling), CURRENT_USER_ID/PENPOT_SOURCE_PATH (always per-call), and the
|
||||
# instance-env-overrides block.
|
||||
# (tooling), CURRENT_USER_ID/PENPOT_SOURCE_PATH (always per-call), the
|
||||
# optional PENPOT_OPENCODE_CONFIG_DIR (set only by run-devenv's
|
||||
# --opencode-config-dir within this process), and the instance-env-overrides
|
||||
# block.
|
||||
function infra-compose {
|
||||
env -i HOME="$HOME" PATH="$PATH" PWD="$PWD" \
|
||||
docker compose -p penpotdev-infra \
|
||||
@ -245,14 +247,26 @@ function instance-compose {
|
||||
# Per-instance overrides apply to all workspaces uniformly.
|
||||
mapfile -t overrides < <(instance-env-overrides "$instance")
|
||||
|
||||
# Optional personal-opencode-config overlay: the extra -f and variable
|
||||
# are only present when run-devenv --opencode-config-dir resolved a host
|
||||
# directory into PENPOT_OPENCODE_CONFIG_DIR; when unset neither the file
|
||||
# nor the variable is referenced, so default behaviour is unchanged.
|
||||
local -a compose_files=(-f docker/devenv/docker-compose.main.yml)
|
||||
local -a opencode_env=()
|
||||
if [[ -n "${PENPOT_OPENCODE_CONFIG_DIR:-}" ]]; then
|
||||
compose_files+=(-f docker/devenv/docker-compose.opencode.yml)
|
||||
opencode_env=("PENPOT_OPENCODE_CONFIG_DIR=${PENPOT_OPENCODE_CONFIG_DIR}")
|
||||
fi
|
||||
|
||||
env -i HOME="$HOME" PATH="$PATH" PWD="$PWD" \
|
||||
CURRENT_USER_ID="${CURRENT_USER_ID:-$(id -u)}" \
|
||||
PENPOT_SOURCE_PATH="$source_path" \
|
||||
DEVENV_TAG="$DEVENV_TAG" \
|
||||
"${opencode_env[@]}" \
|
||||
"${overrides[@]}" \
|
||||
docker compose -p "penpotdev-${instance}" \
|
||||
--env-file "$DEVENV_DEFAULTS_FILE" \
|
||||
-f docker/devenv/docker-compose.main.yml \
|
||||
"${compose_files[@]}" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
@ -658,6 +672,26 @@ function parse-ws-integer {
|
||||
echo "ws$raw"
|
||||
}
|
||||
|
||||
# Strict parser for --opencode-config-dir. Resolves the value to an absolute
|
||||
# host directory (docker compose resolves relative bind-mount sources against
|
||||
# the compose file's directory, not $PWD, so relative values would be
|
||||
# misinterpreted) and verifies it exists. Echoes the absolute path; anything
|
||||
# else fails fast.
|
||||
function parse-opencode-config-dir {
|
||||
local raw="$1"
|
||||
if [[ -z "$raw" ]]; then
|
||||
echo "Invalid --opencode-config-dir: value is empty." >&2
|
||||
return 1
|
||||
fi
|
||||
raw="${raw/#\~/$HOME}"
|
||||
local abs
|
||||
if ! abs=$(realpath -e "$raw" 2>/dev/null) || [[ ! -d "$abs" ]]; then
|
||||
echo "Invalid --opencode-config-dir: '$raw' is not an existing directory." >&2
|
||||
return 1
|
||||
fi
|
||||
echo "$abs"
|
||||
}
|
||||
|
||||
|
||||
# Bring a single instance up: compose up + detached tmux start. When agentic
|
||||
# is true (the default) the tmux session gets MCP + Serena enabled; when false
|
||||
@ -764,6 +798,7 @@ function run-devenv {
|
||||
local serena_context="desktop-app"
|
||||
local git_user_name=""
|
||||
local git_user_email=""
|
||||
local opencode_config_dir=""
|
||||
local -a extra_env_args=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@ -778,6 +813,8 @@ function run-devenv {
|
||||
do_attach=true; shift;;
|
||||
--serena-context)
|
||||
serena_context="$2"; shift 2;;
|
||||
--opencode-config-dir)
|
||||
opencode_config_dir="$(parse-opencode-config-dir "$2")" || return 1; shift 2;;
|
||||
--git-user-name)
|
||||
git_user_name="$2"; shift 2;;
|
||||
--git-user-email)
|
||||
@ -787,13 +824,16 @@ function run-devenv {
|
||||
-e*)
|
||||
extra_env_args+=(-e "${1#-e}"); shift;;
|
||||
-h|--help)
|
||||
echo "Usage: run-devenv [--ws N] [--sync] [--attach] [--agentic] [--serena-context CTX] [--git-user-name NAME] [--git-user-email EMAIL] [-e KEY=VAL]"
|
||||
echo "Usage: run-devenv [--ws N] [--sync] [--attach] [--agentic] [--serena-context CTX] [--opencode-config-dir DIR] [--git-user-name NAME] [--git-user-email EMAIL] [-e KEY=VAL]"
|
||||
echo " Bring a single workspace up."
|
||||
echo " --ws N target workspace (default: 0)."
|
||||
echo " --sync re-seed the wsN clone from the live repo (forbidden on ws0)."
|
||||
echo " --attach attach to the tmux session after startup."
|
||||
echo " --agentic enable MCP + Serena (AI-agent mode)."
|
||||
echo " --serena-context CTX context passed to Serena (default: desktop-app)."
|
||||
echo " --opencode-config-dir DIR bind-mount DIR at ~/.config/opencode inside the"
|
||||
echo " container (personal agents/prompts/skills kept in a"
|
||||
echo " separate repo). Applied at container creation."
|
||||
echo " --git-user-name NAME git author name inside the container (default: host git config)."
|
||||
echo " --git-user-email EMAIL git author email inside the container."
|
||||
echo " -e KEY=VAL forward env var to docker exec on attach."
|
||||
@ -863,6 +903,14 @@ function run-devenv {
|
||||
write-instance-mcp-configs "$target"
|
||||
fi
|
||||
|
||||
# Scope the personal opencode config to this process only: instance-compose
|
||||
# includes the overlay compose file and the variable just when it is set,
|
||||
# so instances brought up without the flag mount nothing.
|
||||
if [[ -n "$opencode_config_dir" ]]; then
|
||||
echo "[$target] mounting personal opencode config: $opencode_config_dir -> ~/.config/opencode"
|
||||
export PENPOT_OPENCODE_CONFIG_DIR="$opencode_config_dir"
|
||||
fi
|
||||
|
||||
echo "Starting $target..."
|
||||
start-instance "$target" "$serena_context" "$git_user_name" "$git_user_email" "$agentic"
|
||||
print-instance-info "$target"
|
||||
@ -1338,6 +1386,11 @@ function usage {
|
||||
echo " --attach attach to the tmux session after startup."
|
||||
echo " --agentic enable MCP + Serena (AI-agent mode)."
|
||||
echo " --serena-context CTX passed to Serena (default: desktop-app)."
|
||||
echo " --opencode-config-dir DIR"
|
||||
echo " bind-mount DIR over the container's"
|
||||
echo " ~/.config/opencode (personal opencode"
|
||||
echo " agents/prompts/skills kept outside this"
|
||||
echo " repo; applied at container creation)."
|
||||
echo " -e KEY=VAL forwarded to 'docker exec' on attach."
|
||||
echo " --git-user-name NAME / --git-user-email EMAIL"
|
||||
echo " identity wired into the container's git config"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user