gitasks 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/cli.js +132 -0
- package/dist/ui/app.js +1 -0
- package/dist/ui/index.html +142 -0
- package/dist/ui/styles.css +426 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gitasks contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Gitasks
|
|
2
|
+
|
|
3
|
+
A lightweight local task protocol for humans and coding agents, powered entirely by GitHub Issues.
|
|
4
|
+
|
|
5
|
+
Gitasks is a small, development-only CLI. GitHub Issues remain the single source of truth: there is no local task database, hosted backend, SaaS account, or additional credential store.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Node.js 20 or newer
|
|
10
|
+
- Git
|
|
11
|
+
- [GitHub CLI](https://cli.github.com/) (`gh`)
|
|
12
|
+
- A repository whose `origin` points to `github.com`
|
|
13
|
+
|
|
14
|
+
Authenticate once with GitHub CLI:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
gh auth login
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Gitasks uses that existing authentication. It never asks for or stores a personal access token.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -D gitasks
|
|
26
|
+
npx gitasks init
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`gitasks init` detects the current GitHub repository, verifies `gh` authentication, creates missing `status:*` labels, and adds the following files without replacing existing content or modifying issues:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
.gitasks/
|
|
33
|
+
config.json
|
|
34
|
+
protocol.md
|
|
35
|
+
AGENTS.md
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If `AGENTS.md` already exists, Gitasks appends a clearly marked task-management section. An existing `.gitasks/protocol.md` is never replaced. Re-running `init` is safe.
|
|
39
|
+
|
|
40
|
+
## Local task board
|
|
41
|
+
|
|
42
|
+
Start the lightweight local board from anywhere inside the target Git repository:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx gitasks ui
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The default address is `http://127.0.0.1:4317`. Choose another port when needed:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx gitasks ui --port 4400
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The board keeps the six Gitasks status columns and shows issues without a recognized status in a separate **Unclassified issues** section. Its **Open / Closed / All** filter defaults to **Open**; GitHub issue state remains independent from task status, so a manually closed `IN PROGRESS` issue stays `IN PROGRESS` under **Closed** or **All**. Search covers every issue loaded for the selected state scope. All pages are fetched, so the board does not silently truncate results. Pull requests are excluded.
|
|
55
|
+
|
|
56
|
+
The server listens only on `127.0.0.1`, validates the request host and origin, and requires a per-process CSRF token for mutations. Stop it with `Ctrl+C`.
|
|
57
|
+
|
|
58
|
+
Current limitations: one repository per server process, manual refresh only, no drag-and-drop, no comments or PR management, and no offline mode.
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
gitasks list
|
|
64
|
+
gitasks ui
|
|
65
|
+
gitasks ui --port 4400
|
|
66
|
+
gitasks list --status in-progress
|
|
67
|
+
gitasks list --status unclassified
|
|
68
|
+
gitasks list --state closed
|
|
69
|
+
gitasks list --state all
|
|
70
|
+
gitasks create "Implement login"
|
|
71
|
+
gitasks create "Implement login" --status todo
|
|
72
|
+
gitasks create "Implement login" --body "Add email/password authentication."
|
|
73
|
+
gitasks backlog 42
|
|
74
|
+
gitasks todo 42
|
|
75
|
+
gitasks start 42
|
|
76
|
+
gitasks review 42
|
|
77
|
+
gitasks done 42
|
|
78
|
+
gitasks block 42
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Issue numbers may be written as `42` or `#42`.
|
|
82
|
+
|
|
83
|
+
### Task lifecycle
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
BACKLOG → TODO → IN PROGRESS → REVIEW → DONE
|
|
87
|
+
↘ BLOCKED
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| Command | Result |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `backlog` | Moves the issue to `BACKLOG` |
|
|
93
|
+
| `todo` | Moves the issue to `TODO` |
|
|
94
|
+
| `start` | Moves the issue to `IN PROGRESS` |
|
|
95
|
+
| `review` | Moves the issue to `REVIEW` |
|
|
96
|
+
| `done` | Moves the issue to `DONE` and closes it |
|
|
97
|
+
| `block` | Moves the issue to `BLOCKED` |
|
|
98
|
+
|
|
99
|
+
Moving a completed task back to an active state reopens its GitHub Issue. Assigning any of the six statuses to an unclassified issue adds the title prefix and canonical label at that explicit point; reading, refreshing, opening the board, and `init` never classify or otherwise mutate existing issues. Transitions add and remove only `status:*` labels instead of replacing the complete label collection, so unrelated labels added concurrently are preserved.
|
|
100
|
+
|
|
101
|
+
## Issue protocol
|
|
102
|
+
|
|
103
|
+
Each task has one canonical status label and a matching title:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
status:backlog [BACKLOG] Implement login
|
|
107
|
+
status:todo [TODO] Implement login
|
|
108
|
+
status:in-progress [IN PROGRESS] Implement login
|
|
109
|
+
status:review [REVIEW] Implement login
|
|
110
|
+
status:done [DONE] Implement login
|
|
111
|
+
status:blocked [BLOCKED] Implement login
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Labels are machine-readable and canonical. Titles mirror them for humans. If a manually created issue has no status label, Gitasks infers a recognized title prefix. If neither exists, the issue is **unclassified**—not a seventh task status—and remains unchanged until a user explicitly assigns one of the six statuses. New tasks created through Gitasks still default to `BACKLOG`.
|
|
115
|
+
|
|
116
|
+
If multiple status labels conflict, a matching title prefix wins. Without a matching prefix, Gitasks resolves the first status in lifecycle definition order: `BACKLOG`, `TODO`, `IN PROGRESS`, `REVIEW`, `DONE`, then `BLOCKED`. The next explicit transition removes all conflicting `status:*` labels.
|
|
117
|
+
|
|
118
|
+
`gitasks list` defaults to all open issues and prints task status (or `UNCLASSIFIED`) separately from GitHub `OPEN`/`CLOSED` state. Use `--state open`, `--state closed`, or `--state all` to choose the issue scope, and combine it with any `--status` filter. Gitasks fetches every REST page and excludes pull requests; results are not silently capped.
|
|
119
|
+
|
|
120
|
+
## Coding agents
|
|
121
|
+
|
|
122
|
+
Run `gitasks init`, then point coding agents to `AGENTS.md` and `.gitasks/protocol.md`. The generated protocol requires agents to:
|
|
123
|
+
|
|
124
|
+
1. search existing issues for the same work before opening a new issue;
|
|
125
|
+
2. reuse the relevant GitHub Issue when one exists;
|
|
126
|
+
3. explicitly classify an unclassified issue when appropriate;
|
|
127
|
+
4. move it to `IN PROGRESS` before implementation;
|
|
128
|
+
5. move completed implementation to `REVIEW`;
|
|
129
|
+
6. use `BLOCKED` when an external dependency prevents progress;
|
|
130
|
+
7. avoid marking work `DONE` unless completion is explicit and appropriate.
|
|
131
|
+
|
|
132
|
+
Because every transition updates GitHub directly, humans and agents share one task state.
|
|
133
|
+
|
|
134
|
+
Within one local UI server process, Gitasks serializes complete status-transition attempts per issue number, including the GitHub update and the recovery read after a failure. Different issues, separate CLI/server processes, task creation, and read-only requests are not part of that queue.
|
|
135
|
+
|
|
136
|
+
### Ambiguous create recovery
|
|
137
|
+
|
|
138
|
+
If GitHub may have accepted a create request but `gh` returned no usable response, Gitasks does not offer a blind create retry. Run `gitasks list --state all` or open the repository's Issues page, search for the exact proposed title, and reuse the issue if it exists. Retry creation only after confirming that it does not.
|
|
139
|
+
|
|
140
|
+
## Repository detection
|
|
141
|
+
|
|
142
|
+
Gitasks works from any directory inside a Git worktree. It reads `origin` and supports common GitHub remote forms:
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
https://github.com/owner/repo.git
|
|
146
|
+
git@github.com:owner/repo.git
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
No repository configuration is stored locally.
|
|
150
|
+
|
|
151
|
+
## Development
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
npm install
|
|
155
|
+
npm run typecheck
|
|
156
|
+
npm test
|
|
157
|
+
npm run build
|
|
158
|
+
npm pack --dry-run
|
|
159
|
+
node dist/cli.js --help
|
|
160
|
+
node dist/cli.js ui
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
To run this checkout without confusing it with any registry package, use `node dist/cli.js <command>` after `npm run build`, or install the exact local tarball produced by `npm pack`:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npm install -D C:/path/to/gitasks/gitasks-<version>.tgz
|
|
167
|
+
npx gitasks --version
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The production bundle contains the executable Node.js shebang and targets Node.js 20+ ESM. `npm pack` and `npm publish` run the `prepack` build automatically, so a clean checkout does not require a committed `dist/` directory.
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
[MIT](LICENSE)
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{Command as Xt}from"commander";var a=class extends Error{exitCode;constructor(e,s=1){super(e),this.name="UserError",this.exitCode=s}},S=class extends a{issue;constructor(e,s){super(e),this.name="PartialCreateError",this.issue=s}},C=class extends a{title;recoveryUrl;constructor(e,s,r){super(e),this.name="AmbiguousCreateError",this.title=s,this.recoveryUrl=r}},b=class extends Error{command;exitCode;stdout;stderr;causeCode;constructor(e){let s=e.stderr.trim()||e.stdout.trim();super(s||`Command failed: ${e.command}`),this.name="CommandError",this.command=e.command,this.exitCode=e.exitCode,this.stdout=e.stdout,this.stderr=e.stderr,e.causeCode!==void 0&&(this.causeCode=e.causeCode)}};function m(t){return t instanceof Error?t.message:String(t)}var y=[{name:"BACKLOG",slug:"backlog",label:"status:backlog",color:"BFD4F2"},{name:"TODO",slug:"todo",label:"status:todo",color:"FBCA04"},{name:"IN PROGRESS",slug:"in-progress",label:"status:in-progress",color:"1D76DB"},{name:"REVIEW",slug:"review",label:"status:review",color:"A371F7"},{name:"DONE",slug:"done",label:"status:done",color:"0E8A16"},{name:"BLOCKED",slug:"blocked",label:"status:blocked",color:"D73A4A"}],V=y.map(({name:t})=>t);function W(t){return t.trim().toLowerCase().replace(/[\s_]+/g,"-")}function D(t){let e=W(t);return y.find(({name:s,slug:r,label:n})=>e===W(s)||e===r||e===n)?.name}function w(t){let e=D(t);if(e===void 0)throw new a(`Invalid status: ${t}
|
|
3
|
+
|
|
4
|
+
Valid statuses:
|
|
5
|
+
${y.map(({slug:s})=>`- ${s}`).join(`
|
|
6
|
+
`)}`);return e}function wt(t){return y.find(({name:e})=>e===t)}function F(t){return wt(t).label}function X(t){return y.find(({label:e})=>e.toLowerCase()===t.trim().toLowerCase())?.name}function I(t){return t.trim().toLowerCase().startsWith("status:")}var kt=/^\s*\[\s*([^\]]+?)\s*\]\s*/;function q(t){let e=kt.exec(t);if(e===null)return{title:t.trim()};let s=e[1];if(s===void 0)return{title:t.trim()};let r=D(s);return r===void 0?{title:t.trim()}:{status:r,title:t.slice(e[0].length).trim()}}function $(t){let e=t.trim();for(;;){let s=q(e);if(s.status===void 0)return e;e=s.title}}function j(t,e){let s=$(t);return s.length===0?`[${e}]`:`[${e}] ${s}`}function Y(t,e){let s=[];for(let n of t){let o=X(n);o!==void 0&&!s.includes(o)&&s.push(o)}let r=q(e).status;return s.length===0||r!==void 0&&s.includes(r)?r:y.find(({name:n})=>s.includes(n)).name}function Tt(t){let e=/^#?([1-9]\d*)$/.exec(t.trim());if(e?.[1]===void 0)throw new a(`Invalid issue number: ${t}
|
|
7
|
+
|
|
8
|
+
Use a positive number such as 42 or #42.`);return Number(e[1])}function G(t){return $(t.title)}function O(t){return Y(t.labels,t.title)}function R(t){let e=t.trim().toLowerCase();if(e==="open"||e==="closed"||e==="all")return e;throw new a(`Invalid issue state: ${t}
|
|
9
|
+
|
|
10
|
+
Valid states:
|
|
11
|
+
- open
|
|
12
|
+
- closed
|
|
13
|
+
- all`)}async function P(t,e,s){let r=$(e);if(r.length===0)throw new a("Task title cannot be empty.");let n=w(s.status??"backlog");return t.createIssue({title:j(r,n),body:s.body??"",label:F(n),state:n==="DONE"?"closed":"open"})}async function L(t,e,s){let r=Tt(e),n=await t.getIssue(r),o=j(n.title,s);return t.transitionIssue(r,{title:o,previousStatusLabels:n.labels.filter(I),nextStatusLabel:F(s),state:s==="DONE"?"closed":"open"})}async function Q(t,e,s){let r=await P(t,e,s);return`Created task #${r.number}
|
|
14
|
+
${r.title}
|
|
15
|
+
${r.url}`}import{mkdir as St,readFile as Ct,writeFile as B}from"fs/promises";import{join as H}from"path";var et="<!-- gitasks:start -->",It="<!-- gitasks:end -->",xt=`# Gitasks Task Protocol
|
|
16
|
+
|
|
17
|
+
GitHub Issues are this repository's task management source of truth. Do not create or maintain a separate task database.
|
|
18
|
+
|
|
19
|
+
## Rules
|
|
20
|
+
|
|
21
|
+
- Every development task should have a GitHub Issue.
|
|
22
|
+
- Before opening a new issue, search existing issues for the same work and reuse a suitable issue when one exists.
|
|
23
|
+
- Issue titles and labels must follow the Gitasks status protocol.
|
|
24
|
+
- Before starting a task, run \`gitasks start <issue-number>\` to move it to \`IN PROGRESS\`.
|
|
25
|
+
- When implementation is complete and ready for review, run \`gitasks review <issue-number>\`.
|
|
26
|
+
- Move a task to \`DONE\` only when the work is truly complete.
|
|
27
|
+
- If progress depends on missing information or an external dependency, run \`gitasks block <issue-number>\`.
|
|
28
|
+
- Do not silently change issue status outside this protocol.
|
|
29
|
+
- Preserve unrelated issue labels during status changes.
|
|
30
|
+
- If an issue has conflicting status labels, its matching title prefix wins; otherwise Gitasks uses the first status in protocol order.
|
|
31
|
+
- Issues without a recognized status label or title prefix are unclassified; assign a status explicitly before starting them.
|
|
32
|
+
- GitHub open/closed state is separate from task status. Lists default to open issues; use \`--state closed\` or \`--state all\` to change scope.
|
|
33
|
+
|
|
34
|
+
## Statuses
|
|
35
|
+
|
|
36
|
+
\`BACKLOG\` \u2192 \`TODO\` \u2192 \`IN PROGRESS\` \u2192 \`REVIEW\` \u2192 \`DONE\`
|
|
37
|
+
|
|
38
|
+
Use \`BLOCKED\` when work cannot continue. A blocked task may return to any active status when its blocker is removed.
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
\`\`\`bash
|
|
43
|
+
gitasks list
|
|
44
|
+
gitasks list --status unclassified
|
|
45
|
+
gitasks list --state all
|
|
46
|
+
gitasks create "Describe the task"
|
|
47
|
+
gitasks todo 42
|
|
48
|
+
gitasks start 42
|
|
49
|
+
gitasks review 42
|
|
50
|
+
gitasks done 42
|
|
51
|
+
gitasks block 42
|
|
52
|
+
gitasks backlog 42
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
Gitasks synchronizes each issue's \`status:*\` label, title prefix, and open/closed state. GitHub remains authoritative.
|
|
56
|
+
`,Z=`${et}
|
|
57
|
+
## Gitasks Task Management
|
|
58
|
+
|
|
59
|
+
This repository uses GitHub Issues as its task management source of truth.
|
|
60
|
+
|
|
61
|
+
Before starting development work:
|
|
62
|
+
|
|
63
|
+
1. Read \`.gitasks/protocol.md\`.
|
|
64
|
+
2. Run \`gitasks list\` and search existing issues for the same work.
|
|
65
|
+
3. Reuse the relevant issue, or create one only when none exists.
|
|
66
|
+
4. Run \`gitasks start <issue-number>\`.
|
|
67
|
+
|
|
68
|
+
When implementation is ready for review, run:
|
|
69
|
+
|
|
70
|
+
\`gitasks review <issue-number>\`
|
|
71
|
+
|
|
72
|
+
Do not mark tasks as \`DONE\` automatically unless the work is truly complete and doing so is explicitly appropriate. If an external dependency prevents progress, move the task to \`BLOCKED\`.
|
|
73
|
+
${It}
|
|
74
|
+
`;async function tt(t,e){try{return await B(t,e,{encoding:"utf8",flag:"wx"}),!0}catch(s){if(s.code==="EEXIST")return!1;throw s}}async function Et(t){let e;try{e=await Ct(t,"utf8")}catch(r){if(r.code==="ENOENT")return await B(t,`${Z}
|
|
75
|
+
`,"utf8"),"created";throw r}if(e.includes(et))return"unchanged";let s=e.length===0||e.endsWith(`
|
|
76
|
+
|
|
77
|
+
`)?"":e.endsWith(`
|
|
78
|
+
`)?`
|
|
79
|
+
`:`
|
|
80
|
+
|
|
81
|
+
`;return await B(t,`${e}${s}${Z}
|
|
82
|
+
`,"utf8"),"updated"}async function st(t,e){let s=await t.ensureStatusLabels(),r=H(e,".gitasks");await St(r,{recursive:!0});let n=`${JSON.stringify({version:1,statuses:V},null,2)}
|
|
83
|
+
`,o=[];await tt(H(r,"config.json"),n)&&o.push(".gitasks/config.json"),await tt(H(r,"protocol.md"),xt)&&o.push(".gitasks/protocol.md");let i=await Et(H(e,"AGENTS.md")),u=[];return i==="created"?o.push("AGENTS.md"):i==="updated"&&u.push("AGENTS.md"),{createdLabels:s,createdFiles:o,updatedFiles:u}}async function rt(t,e){let s=e.status===void 0?void 0:e.status.trim().toLowerCase()==="unclassified"?null:w(e.status),r=R(e.state??"open"),o=(await t.listIssues(r)).map(c=>({issue:c,status:O(c)??null,title:G(c)})).filter(({status:c})=>s===void 0||c===s);if(o.length===0)return"No tasks found.";let i=Math.max(...o.map(({issue:c})=>`#${c.number}`.length)),u=14;return o.map(({issue:c,status:l,title:f})=>`${`#${c.number}`.padEnd(i)} ${`[${l??"UNCLASSIFIED"}]`.padEnd(u)} [${c.state}] ${f}`).join(`
|
|
84
|
+
`)}async function nt(t,e,s){let r=await L(t,e,s);return`Task #${r.number} moved to ${s}
|
|
85
|
+
${r.title}`}import{once as Wt}from"events";function x(t){return{number:t.number,title:t.title,state:t.state.toUpperCase()==="CLOSED"?"CLOSED":"OPEN",labels:t.labels.map(({name:e})=>e),url:t.html_url??t.url??"",body:t.body??"",assignees:t.assignees?.map(({login:e})=>e)??[]}}var N=class{constructor(e,s){this.repository=e;this.runner=s}repository;runner;async run(e,s){try{return(await this.runner("gh",e)).stdout}catch(r){throw new a(`${s}
|
|
86
|
+
|
|
87
|
+
${m(r)}`)}}async runJson(e,s){let r=await this.run(e,s);try{return JSON.parse(r)}catch{throw new a(`${s}
|
|
88
|
+
|
|
89
|
+
GitHub CLI returned an unexpected response.`)}}async removeLabel(e,s){try{await this.runner("gh",["api","--method","DELETE",`repos/${this.repository}/issues/${e}/labels/${encodeURIComponent(s)}`])}catch(r){let n=m(r);if(/\b404\b|label does not exist/i.test(n))return;throw new a(`Could not remove status label ${s} from issue #${e}.
|
|
90
|
+
|
|
91
|
+
${n}`)}}async ensureStatusLabels(){let e=await this.runJson(["label","list","--repo",this.repository,"--limit","1000","--json","name"],"Could not read repository labels."),s=new Set(e.map(({name:n})=>n.toLowerCase())),r=[];for(let n of y)s.has(n.label)||(await this.run(["label","create",n.label,"--repo",this.repository,"--color",n.color,"--description",`Gitasks status: ${n.name}`],`Could not create label ${n.label}.`),r.push(n.label));return r}async listIssues(e){return(await this.runJson(["api",`repos/${this.repository}/issues?state=${e}&per_page=100`,"--paginate","--slurp"],"Could not list GitHub issues.")).flat().filter(r=>r.pull_request===void 0&&(e==="all"||r.state.toLowerCase()===e)).map(x)}async getIssue(e){let s=await this.runJson(["api",`repos/${this.repository}/issues/${e}`],`Could not load issue #${e}. Make sure it exists and is accessible.`);if(s.pull_request!==void 0)throw new a(`#${e} is a pull request, not an issue.`);return x(s)}async createIssue(e){let s=["api","--method","POST",`repos/${this.repository}/issues`,"--raw-field",`title=${e.title}`,"--raw-field",`body=${e.body}`,"--raw-field",`labels[]=${e.label}`],r;try{r=(await this.runner("gh",s)).stdout}catch(o){let i=m(o);throw o instanceof b&&o.causeCode==="ENOENT"||/\bHTTP\s+4\d\d\b|\b(400|401|403|404|409|410|422|429)\b/.test(i)?new a(`Could not create the GitHub issue.
|
|
92
|
+
|
|
93
|
+
${i}`):this.ambiguousCreateError(e.title,i)}let n;try{let o=JSON.parse(r);if(!Number.isSafeInteger(o.number)||typeof o.title!="string"||typeof o.state!="string"||!Array.isArray(o.labels))throw new Error("Invalid issue response.");n=o}catch{throw this.ambiguousCreateError(e.title,"GitHub CLI returned an unexpected response.")}if(e.state==="open")return x(n);try{let o=await this.runJson(["api","--method","PATCH",`repos/${this.repository}/issues/${n.number}`,"--raw-field","state=closed"],`Created issue #${n.number} but could not close it.`);return x(o)}catch(o){throw new S(m(o),x(n))}}ambiguousCreateError(e,s){let r=`https://github.com/${this.repository}/issues`;return new C(`Could not confirm whether GitHub created "${e}". Do not retry yet: run \`gitasks list --state all\` or open ${r} and search for the exact title. Reuse it if found; retry only after confirming it does not exist.
|
|
94
|
+
|
|
95
|
+
${s}`,e,r)}async transitionIssue(e,s){await this.runJson(["api","--method","POST",`repos/${this.repository}/issues/${e}/labels`,"--raw-field",`labels[]=${s.nextStatusLabel}`],`Could not add status label ${s.nextStatusLabel} to issue #${e}.`),await this.runJson(["api","--method","PATCH",`repos/${this.repository}/issues/${e}`,"--raw-field",`title=${s.title}`,"--raw-field",`state=${s.state}`],`Could not update issue #${e}.`);for(let o of s.previousStatusLabels)o.toLowerCase()!==s.nextStatusLabel.toLowerCase()&&await this.removeLabel(e,o);let r=await this.getIssue(e),n=r.labels.filter(o=>I(o)&&o.toLowerCase()!==s.nextStatusLabel.toLowerCase());for(let o of n)await this.removeLabel(e,o);return n.length>0&&(r=await this.getIssue(e)),r}};async function vt(t){try{await t("gh",["--version"])}catch(e){throw e instanceof b&&e.causeCode==="ENOENT"?new a(`GitHub CLI is not installed.
|
|
96
|
+
|
|
97
|
+
Install it from:
|
|
98
|
+
https://cli.github.com/`):new a(`GitHub CLI could not be executed.
|
|
99
|
+
|
|
100
|
+
Install or repair it using:
|
|
101
|
+
https://cli.github.com/`)}}async function $t(t){try{await t("gh",["auth","status","--hostname","github.com"])}catch{throw new a(`GitHub authentication not found.
|
|
102
|
+
|
|
103
|
+
Run:
|
|
104
|
+
gh auth login`)}}async function ot(t){await vt(t),await $t(t)}function Gt(t){let e=t.trim(),s=[/^https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i,/^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?$/i,/^ssh:\/\/git@github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i];for(let r of s){let n=r.exec(e);if(n?.[1]!==void 0&&n[2]!==void 0)return`${n[1]}/${n[2]}`}}async function it(t,e=process.cwd()){try{let{stdout:n}=await t("git",["rev-parse","--is-inside-work-tree"],{cwd:e});if(n.trim()!=="true")throw new a("Not inside a git repository.")}catch(n){throw n instanceof a?n:n instanceof b&&n.causeCode==="ENOENT"?new a("Git is not installed or is not available on PATH."):new a("Not inside a git repository.")}let s;try{s=(await t("git",["remote","get-url","origin"],{cwd:e})).stdout}catch{throw new a(`No origin remote found.
|
|
105
|
+
|
|
106
|
+
Add a GitHub remote, for example:
|
|
107
|
+
git remote add origin https://github.com/owner/repo.git`)}let r=Gt(s);if(r===void 0)throw new a(`The origin remote is not a supported GitHub URL:
|
|
108
|
+
${s.trim()}
|
|
109
|
+
|
|
110
|
+
Expected an HTTPS or SSH github.com remote.`);return r}import{execFile as Ot}from"child_process";import{promisify as Rt}from"util";var Pt=Rt(Ot),at=async(t,e,s={})=>{try{let{stdout:r,stderr:n}=await Pt(t,[...e],{...s,encoding:"utf8",maxBuffer:10485760,windowsHide:!0});return{stdout:r,stderr:n}}catch(r){let n=r,o=typeof n.code=="string"?n.code:void 0;throw new b({command:[t,...e].join(" "),exitCode:typeof n.code=="number"?n.code:null,stdout:n.stdout??"",stderr:n.stderr??"",...o===void 0?{}:{causeCode:o}})}};async function E(t=process.cwd(),e=at){let s=await it(e,t);return await ot(e),{repository:s,client:new N(s,e)}}import{randomBytes as Lt,timingSafeEqual as Ht}from"crypto";import{once as ut}from"events";import{existsSync as Nt}from"fs";import{readFile as _}from"fs/promises";import{createServer as At}from"http";import{dirname as Ut,join as v}from"path";import{fileURLToPath as Dt}from"url";function k(t){return{number:t.number,status:O(t)??null,title:G(t),fullTitle:t.title,body:t.body,state:t.state,labels:t.labels.filter(e=>!I(e)),assignees:t.assignees,url:t.url}}var dt=4317,ct="127.0.0.1",mt=64*1024,pt="__GITASKS_CSRF_TOKEN__",d=class extends Error{constructor(s,r){super(r);this.statusCode=s;this.name="HttpError"}statusCode};function ft(t){if(!/^\d+$/.test(t))throw new a(`Invalid port: ${t}
|
|
111
|
+
|
|
112
|
+
Use an integer between 1 and 65535.`);let e=Number(t);if(!Number.isSafeInteger(e)||e<1||e>65535)throw new a(`Invalid port: ${t}
|
|
113
|
+
|
|
114
|
+
Use an integer between 1 and 65535.`);return e}function Ft(){let t=Ut(Dt(import.meta.url)),e=v(t,"ui");return Nt(e)?e:v(t,"..","..","dist","ui")}async function jt(t){try{let[e,s,r]=await Promise.all([_(v(t,"index.html"),"utf8"),_(v(t,"app.js")),_(v(t,"styles.css"))]);if(!e.includes(pt))throw new Error("index.html is missing its CSRF placeholder");return{html:e,javascript:s,css:r}}catch(e){throw new a(`Gitasks UI assets could not be loaded from ${t}.
|
|
115
|
+
|
|
116
|
+
Run:
|
|
117
|
+
npm run build
|
|
118
|
+
|
|
119
|
+
${m(e)}`)}}function M(t){t.setHeader("Content-Security-Policy","default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'"),t.setHeader("Referrer-Policy","no-referrer"),t.setHeader("X-Content-Type-Options","nosniff"),t.setHeader("X-Frame-Options","DENY")}function h(t,e,s){let r=JSON.stringify(s);M(t),t.writeHead(e,{"Cache-Control":"no-store","Content-Type":"application/json; charset=utf-8","Content-Length":Buffer.byteLength(r)}),t.end(r)}function J(t,e,s,r,n){M(t),t.writeHead(e,{"Cache-Control":n,"Content-Type":s,"Content-Length":Buffer.byteLength(r)}),t.end(r)}function A(t,e){let s=t.headers[e];return Array.isArray(s)?s[0]:s}function Bt(t,e){if(t===void 0)return!1;let s=Buffer.from(t),r=Buffer.from(e);return s.length===r.length&&Ht(s,r)}function _t(t,e,s){if(A(t,"host")?.toLowerCase()!==e)throw new d(403,"Request host is not allowed.");let r=A(t,"origin"),n=`http://${e}`;if(r!==void 0&&r!==n)throw new d(403,"Cross-origin requests are not allowed.");if(t.method!=="GET"&&t.method!=="HEAD"){if(r!==n)throw new d(403,"Mutating requests require the local UI origin.");if(!Bt(A(t,"x-gitasks-csrf"),s))throw new d(403,"Invalid or missing CSRF token.")}}async function lt(t){if(A(t,"content-type")?.split(";",1)[0]?.trim()!=="application/json")throw new d(415,"Content-Type must be application/json.");let s=[],r=0;for await(let o of t){let i=Buffer.isBuffer(o)?o:Buffer.from(o);if(r+=i.length,r>mt)throw new d(413,"Request body is too large.");s.push(i)}let n;try{n=JSON.parse(Buffer.concat(s).toString("utf8"))}catch{throw new d(400,"Request body must contain valid JSON.")}if(n===null||typeof n!="object"||Array.isArray(n))throw new d(400,"Request body must be a JSON object.");return n}function gt(t,e){let s=Object.keys(t).find(r=>!e.includes(r));if(s!==void 0)throw new d(400,`Unknown field: ${s}`)}function Jt(t){if(gt(t,["title","body","status"]),typeof t.title!="string"||t.title.trim().length===0)throw new d(400,"Task title is required.");if(t.title.trim().length>256)throw new d(400,"Task title must be 256 characters or fewer.");if(t.body!==void 0&&typeof t.body!="string")throw new d(400,"Task description must be a string.");if(typeof t.body=="string"&&Buffer.byteLength(t.body)>mt)throw new d(400,"Task description is too large.");if(t.status!==void 0&&typeof t.status!="string")throw new d(400,"Task status must be a string.");let e=typeof t.status=="string"?t.status:"backlog";try{w(e)}catch(s){throw new d(400,m(s))}return{title:t.title.trim(),body:typeof t.body=="string"?t.body:"",status:e}}function Mt(t){if(gt(t,["status"]),typeof t.status!="string")throw new d(400,"Task status is required.");try{return w(t.status)}catch(e){throw new d(400,m(e))}}function zt(t,e,s){return{repository:t,state:e,scope:`${e==="all"?"Open and closed":e==="open"?"Open":"Closed"} GitHub Issues; search covers every loaded issue; pull requests excluded`,complete:!0,statuses:y.map(({name:r,slug:n,color:o})=>({name:r,slug:n,color:o})),tasks:s.map(k)}}function Kt(t,e,s,r){let n=new Map,o=(i,u)=>{let l=(n.get(i)??Promise.resolve()).then(u),f=l.then(()=>{},()=>{});return n.set(i,f),l.finally(()=>{n.get(i)===f&&n.delete(i)})};return async(i,u)=>{try{_t(i,r(),s);let c=new URL(i.url??"/",`http://${r()}`),l=c.pathname;if(i.method==="GET"&&l==="/api/board"){let g;try{g=R(c.searchParams.get("state")??"open")}catch(T){throw new d(400,m(T))}let p=await t.gateway.listIssues(g);h(u,200,zt(t.repository,g,p));return}let f=/^\/api\/issues\/([1-9]\d*)$/.exec(l);if(i.method==="GET"&&f?.[1]!==void 0){let g=await t.gateway.getIssue(Number(f[1]));h(u,200,{task:k(g)});return}if(i.method==="POST"&&l==="/api/issues"){let g=Jt(await lt(i));try{let p=await P(t.gateway,g.title,{body:g.body,status:g.status});h(u,201,{task:k(p)})}catch(p){p instanceof S?h(u,502,{error:{message:p.message,retryable:!0},task:k(p.issue),repair:{issueNumber:p.issue.number,status:"DONE"}}):p instanceof C?h(u,502,{error:{message:p.message,retryable:!1},recovery:{kind:"ambiguous-create",title:p.title,issuesUrl:p.recoveryUrl}}):h(u,502,{error:{message:m(p),retryable:!0}})}return}let K=/^\/api\/issues\/([1-9]\d*)\/status$/.exec(l);if(i.method==="POST"&&K?.[1]!==void 0){let g=Number(K[1]),p=Mt(await lt(i));await o(g,async()=>{try{let T=await L(t.gateway,String(g),p);h(u,200,{task:k(T)})}catch(T){try{let bt=await t.gateway.getIssue(g);h(u,502,{error:{message:m(T),retryable:!0,stateVerified:!0},task:k(bt)})}catch{h(u,502,{error:{message:m(T),retryable:!0,stateVerified:!1}})}}});return}if(i.method==="GET"&&(l==="/"||l==="/index.html")){J(u,200,"text/html; charset=utf-8",e.html.replace(pt,s),"no-store");return}if(i.method==="GET"&&l==="/app.js"){J(u,200,"text/javascript; charset=utf-8",e.javascript,"no-cache");return}if(i.method==="GET"&&l==="/styles.css"){J(u,200,"text/css; charset=utf-8",e.css,"no-cache");return}if(i.method==="GET"&&l==="/favicon.ico"){M(u),u.writeHead(204,{"Cache-Control":"no-store"}),u.end();return}h(u,404,{error:{message:"Not found."}})}catch(c){if(c instanceof d){h(u,c.statusCode,{error:{message:c.message}});return}if(c instanceof a){h(u,502,{error:{message:c.message,retryable:!0}});return}h(u,500,{error:{message:`Unexpected server error: ${m(c)}`}})}}}async function ht(t){let e=t.assetDirectory??Ft(),s=await jt(e),r=t.csrfToken??Lt(24).toString("hex"),n="",o=Kt(t,s,r,()=>n),i=At((l,f)=>{o(l,f)});i.on("clientError",(l,f)=>{f.end(`HTTP/1.1 400 Bad Request\r
|
|
120
|
+
Connection: close\r
|
|
121
|
+
\r
|
|
122
|
+
`)});let u=ut(i,"listening");i.listen(t.port,ct);try{await u}catch(l){let f=l.code;throw f==="EADDRINUSE"?new a(`Port ${t.port} is already in use.
|
|
123
|
+
|
|
124
|
+
Choose another port:
|
|
125
|
+
gitasks ui --port ${t.port+1}`):f==="EACCES"?new a(`Permission denied while binding to port ${t.port}.`):new a(`Could not start the local UI server.
|
|
126
|
+
|
|
127
|
+
${m(l)}`)}let c=i.address();if(c===null||typeof c=="string")throw i.close(),new a("Could not determine the local UI address.");return n=`${ct}:${c.port}`,{server:i,port:c.port,url:`http://${n}`,csrfToken:r,async close(){if(!i.listening)return;let l=ut(i,"close");i.close(),await l}}}async function Vt(t,e,s){let r=ft(e),n=await ht({repository:t.repository,gateway:t.client,port:r});s(`Gitasks UI running at ${n.url}`),s(`Repository: ${t.repository}`),s("Press Ctrl+C to stop.");let o=Wt(n.server,"close"),i=!1,u=()=>{i||(i=!0,s("Stopping Gitasks UI..."),n.close())};process.once("SIGINT",u),process.once("SIGTERM",u);try{await o}finally{process.removeListener("SIGINT",u),process.removeListener("SIGTERM",u)}}function yt(t){t.command("ui").description("Start the local Gitasks task board").option("-p, --port <port>","local server port",String(dt)).action(async e=>{let s=await E();await Vt(s,e.port,r=>{process.stdout.write(`${r}
|
|
128
|
+
`)})})}var qt="0.3.0";function U(t){process.stdout.write(`${t}
|
|
129
|
+
`)}async function z(t){let e=await E();return t(e)}function Yt(t){t.command("init").description("Initialize Gitasks labels and agent protocol files").action(async()=>{let e=await E(),s=await st(e.client,process.cwd()),r=[`Initialized Gitasks for ${e.repository}`];r.push(s.createdLabels.length>0?`Created labels: ${s.createdLabels.join(", ")}`:"Status labels already exist."),s.createdFiles.length>0&&r.push(`Created files: ${s.createdFiles.join(", ")}`),s.updatedFiles.length>0&&r.push(`Updated files: ${s.updatedFiles.join(", ")}`),s.createdFiles.length===0&&s.updatedFiles.length===0&&r.push("Protocol files already exist; nothing was overwritten."),U(r.join(`
|
|
130
|
+
`))})}function Qt(t){t.command("list").description("List GitHub Issues as Gitasks tasks").option("-s, --status <status>","filter by backlog, todo, in-progress, review, done, blocked, or unclassified").option("--state <state>","GitHub issue state: open, closed, or all","open").action(async e=>{U(await z(({client:s})=>rt(s,e)))})}function Zt(t){t.command("create <title>").description("Create a standardized GitHub Issue task").option("-s, --status <status>","initial task status","backlog").option("-b, --body <body>","issue body","").action(async(e,s)=>{U(await z(({client:r})=>Q(r,e,s)))})}function te(t){let e=[{command:"backlog",status:"BACKLOG",description:"Move a task to BACKLOG"},{command:"todo",status:"TODO",description:"Move a task to TODO"},{command:"start",status:"IN PROGRESS",description:"Move a task to IN PROGRESS"},{command:"review",status:"REVIEW",description:"Move a task to REVIEW"},{command:"done",status:"DONE",description:"Move a task to DONE and close it"},{command:"block",status:"BLOCKED",description:"Move a task to BLOCKED"}];for(let s of e)t.command(`${s.command} <issue>`).description(s.description).action(async r=>{U(await z(({client:n})=>nt(n,r,s.status)))})}async function ee(){let t=new Xt().name("gitasks").description("Lightweight task management for humans and coding agents, powered by GitHub Issues").version(qt).showHelpAfterError();Yt(t),Qt(t),Zt(t),te(t),yt(t),await t.parseAsync(process.argv)}try{await ee()}catch(t){t instanceof a?(process.stderr.write(`${t.message}
|
|
131
|
+
`),process.exitCode=t.exitCode):(process.stderr.write(`Unexpected error: ${m(t)}
|
|
132
|
+
`),process.exitCode=1)}
|
package/dist/ui/app.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var GitasksUi=(()=>{var le=Object.defineProperty;var ce=(e,t,n)=>t in e?le(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var X=(e,t,n)=>ce(e,typeof t!="symbol"?t+"":t,n);var h=class extends Error{constructor(n,s={}){super(n);X(this,"payload");this.name="ApiRequestError",this.payload=s}};function a(e){let t=document.getElementById(e);if(t===null)throw new Error(`Missing UI element: ${e}`);return t}var ee=document.querySelector('meta[name="gitasks-csrf"]')?.content;if(!ee)throw new Error("Missing Gitasks CSRF token.");var de=ee,L=a("board"),R=a("loading-state"),ue=a("search-empty"),me=a("repository"),fe=a("scope-text"),pe=a("status-message"),C=a("state-filter"),z=a("refresh-time"),U=a("unclassified-panel"),F=a("unclassified-list"),ge=a("unclassified-count"),j=a("refresh-button"),H=a("search-input"),Y=a("connection-error"),Ee=a("connection-error-message"),E=a("create-dialog"),M=a("create-form"),k=a("create-submit"),B=a("create-error"),K=a("task-status"),u=a("detail-dialog"),he=a("detail-number"),Te=a("detail-title"),$=a("detail-content"),p=a("toast"),te=a("toast-message"),J=a("toast-retry"),ne=a("toast-title"),b=[],f=[],se="",N="open",y,S=new Set,O;async function P(e,t){let n=new Headers(t?.headers);t?.body!==void 0&&n.set("Content-Type","application/json"),t?.method!==void 0&&t.method!=="GET"&&n.set("X-Gitasks-CSRF",de);let s;try{s=await fetch(e,{...t,headers:n})}catch(r){throw new h(r instanceof Error?r.message:"Could not reach the local Gitasks server.")}let o;try{o=await s.json()}catch{throw new h(`The local server returned HTTP ${s.status}.`)}if(!s.ok){let r=o;throw new h(r.error?.message??`Request failed with HTTP ${s.status}.`,r)}return o}function _(e){let t=b.map(n=>{let s=document.createElement("option");return s.value=n.name,s.textContent=n.name,s.selected=n.name===e,s});if(e===null){let n=document.createElement("option");n.value="",n.textContent="Assign status\u2026",n.selected=!0,n.disabled=!0,t.unshift(n)}return t}function be(e){return`#${b.find(({name:t})=>t===e)?.color??"667085"}`}function x(e){let t=f.findIndex(({number:n})=>n===e.number);t===-1?f.unshift(e):f[t]=e}function D(e,t){p.classList.remove("success"),ne.textContent="Action failed",te.textContent=e,O=t,J.hidden=t===void 0,(u.open?u:E.open?E:document.body).append(p),p.hidden=!1}function ye(e){p.classList.add("success"),ne.textContent="Success",te.textContent=e,O=void 0,J.hidden=!0,document.body.append(p),p.hidden=!1}function w(){p.hidden=!0,O=void 0,p.parentElement!==document.body&&document.body.append(p)}function oe(e){let t=document.createElement("div");t.className="label-list";for(let n of e){let s=document.createElement("span");s.className="label-chip",s.textContent=n,t.append(s)}return t}function Le(e,t){let n=document.querySelector(`[data-issue="${e}"]`);n?.classList.toggle("is-pending",t);for(let s of n?.querySelectorAll("button, select")??[])s.disabled=t;y===e&&$.querySelector("#detail-status-select")?.toggleAttribute("disabled",t)}function Z(e){let t=document.createElement("article");t.className="task-card",t.dataset.issue=String(e.number),t.style.setProperty("--status-color",be(e.status));let n=S.has(e.number);t.classList.toggle("is-pending",n);let s=document.createElement("div");s.className="card-topline";let o=document.createElement("span");o.className="issue-number",o.textContent=`#${e.number}`;let r=document.createElement("span");r.className=`state-badge ${e.state==="CLOSED"?"closed":""}`,r.textContent=e.state,s.append(o,r);let i=document.createElement("button");if(i.className="card-title-button",i.type="button",i.disabled=n,i.textContent=e.title||"Untitled task",i.addEventListener("click",()=>{ae(e.number)}),t.append(s,i),e.assignees.length>0){let l=document.createElement("p");l.className="assignee",l.textContent=`Assigned to ${e.assignees.map(T=>`@${T}`).join(", ")}`,t.append(l)}e.labels.length>0&&t.append(oe(e.labels));let m=document.createElement("div");m.className="card-footer";let g=document.createElement("label");g.textContent="Move to";let d=document.createElement("select");d.setAttribute("aria-label",`Change status for issue #${e.number}`),d.disabled=n,d.append(..._(e.status)),d.addEventListener("change",()=>{I(e.number,d.value)}),g.append(d);let c=document.createElement("a");return c.className="github-link",c.href=e.url,c.target="_blank",c.rel="noreferrer",c.textContent="Open in GitHub \u2197",m.append(g,c),t.append(m),t}function ve(){let e=f.filter(s=>N==="all"||s.state.toLowerCase()===N),t=H.value.trim().toLowerCase();if(t.length===0)return e;let n=t.startsWith("#")?t.slice(1):t;return e.filter(s=>s.title.toLowerCase().includes(t)||s.fullTitle.toLowerCase().includes(t)||String(s.number)===n)}function v(){let e=ve();L.replaceChildren();for(let n of b){let s=document.createElement("section");s.className="column",s.style.setProperty("--status-color",`#${n.color}`),s.setAttribute("aria-labelledby",`column-${n.slug}`);let o=document.createElement("header");o.className="column-header";let r=document.createElement("div");r.className="column-title";let i=document.createElement("span");i.className="status-dot",i.setAttribute("aria-hidden","true");let m=document.createElement("h2");m.id=`column-${n.slug}`,m.textContent=n.name,r.append(i,m);let g=e.filter(l=>l.status===n.name),d=document.createElement("span");d.className="count",d.textContent=String(g.length),o.append(r,d);let c=document.createElement("div");if(c.className="card-list",g.length===0){let l=document.createElement("p");l.className="column-empty",l.textContent=H.value.trim()?"No matches":"No tasks",c.append(l)}else c.append(...g.map(Z));s.append(o,c),L.append(s)}let t=e.filter(n=>n.status===null);if(ge.textContent=String(t.length),F.replaceChildren(),t.length===0){let n=document.createElement("p");n.className="column-empty",n.textContent=H.value.trim()?"No matches":"No unclassified issues",F.append(n)}else F.append(...t.map(Z));U.hidden=!1,ue.hidden=H.value.trim().length===0||e.length>0,fe.textContent=`${se} \xB7 ${f.length} issue${f.length===1?"":"s"}`}function V(e){he.textContent=`Issue #${e.number}`,Te.textContent=e.fullTitle,$.replaceChildren();let t=document.createElement("section");t.className="detail-section";let n=document.createElement("h3");n.textContent="Overview",t.append(n);let s=[["Task status",e.status??"Unclassified"],["GitHub state",e.state],["Assignee",e.assignees.length>0?e.assignees.map(q=>`@${q}`).join(", "):"Unassigned"]];for(let[q,ie]of s){let G=document.createElement("div");G.className="detail-row";let Q=document.createElement("span");Q.textContent=q;let W=document.createElement("strong");W.textContent=ie,G.append(Q,W),t.append(G)}let o=document.createElement("section");o.className="detail-section";let r=document.createElement("h3");r.textContent="Description";let i=document.createElement("p");i.className="detail-body",i.textContent=e.body.trim()||"No description provided.",o.append(r,i);let m=document.createElement("section");m.className="detail-section";let g=document.createElement("h3");g.textContent="Labels",m.append(g),m.append(e.labels.length>0?oe(e.labels):document.createTextNode("No additional labels."));let d=document.createElement("section");d.className="detail-status";let c=document.createElement("label");c.htmlFor="detail-status-select",c.textContent="Change task status";let l=document.createElement("select");l.id="detail-status-select",l.disabled=S.has(e.number),l.append(..._(e.status)),l.addEventListener("change",()=>{I(e.number,l.value)}),d.append(c,l);let T=document.createElement("a");T.className="button secondary detail-link",T.href=e.url,T.target="_blank",T.rel="noreferrer",T.textContent="Open in GitHub \u2197",$.append(t,o,m,d,T)}async function ae(e){y=e;let t=f.find(({number:n})=>n===e);t!==void 0&&V(t),u.open||(u.showModal(),u.querySelector(".icon-button")?.focus());try{let{task:n}=await P(`/api/issues/${e}`);x(n),y===e&&u.open&&V(n)}catch(n){D(n instanceof Error?n.message:"Could not load issue details.",()=>{ae(e)})}}async function I(e,t){if(S.has(e))return;let n=y===e&&u.open,s=!1;S.add(e),Le(e,!0);try{let{task:o}=await P(`/api/issues/${e}/status`,{method:"POST",body:JSON.stringify({status:t})});x(o),s=!0}catch(o){o instanceof h&&o.payload.task!==void 0&&x(o.payload.task);let r=o instanceof h&&o.payload.error?.stateVerified===!1,i=`${o instanceof Error?o.message:"Status change failed."}${r?" The issue state could not be verified; refresh before continuing.":""}`;D(i,()=>{I(e,t)})}finally{S.delete(e),v();let o=f.find(({number:r})=>r===e);if(y===e&&o!==void 0&&u.open?(V(o),n&&$.querySelector("#detail-status-select")?.focus()):document.querySelector(`[data-issue="${e}"] .card-title-button`)?.focus(),s){pe.textContent=`Issue #${e} moved to ${t}.`;let r=f.find(({number:i})=>i===e);t==="DONE"&&r?.state==="CLOSED"&&N==="open"&&ye(`Issue #${e} moved to DONE and closed. It is hidden by the Open filter.`)}}}async function A(){j.disabled=!0,C.disabled=!0,Y.hidden=!0,f.length===0&&(R.hidden=!1,L.hidden=!0,U.hidden=!0);try{let e=C.value,t=await P(`/api/board?state=${encodeURIComponent(e)}`);b=t.statuses,f=t.tasks,se=t.scope,N=t.state,C.value=t.state,me.textContent=t.repository;let n=new Date;z.dateTime=n.toISOString(),z.textContent=n.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"}),K.replaceChildren(..._(b[0]?.name??"BACKLOG")),L.hidden=!1,R.hidden=!0,v()}catch(e){R.hidden=!0,f.length===0&&(L.hidden=!0,U.hidden=!0),Ee.textContent=e instanceof Error?e.message:"Could not read GitHub Issues.",Y.hidden=!1}finally{j.disabled=!1,C.disabled=!1}}async function re(){if(!M.reportValidity())return;let e=new FormData(M),t={title:String(e.get("title")??"").trim(),body:String(e.get("body")??""),status:String(e.get("status")??"BACKLOG")};k.disabled=!0,k.textContent="Creating\u2026",B.hidden=!0;try{let{task:n}=await P("/api/issues",{method:"POST",body:JSON.stringify(t)});x(n),v(),E.close(),M.reset(),K.value=b[0]?.name??"BACKLOG"}catch(n){let s=n instanceof Error?n.message:"Task creation failed.";if(n instanceof h&&n.payload.task!==void 0&&n.payload.repair!==void 0){let{task:o,repair:r}=n.payload;x(o),v(),E.close(),M.reset(),K.value=b[0]?.name??"BACKLOG",D(s,()=>{I(r.issueNumber,r.status)})}else{B.textContent=s,B.hidden=!1;let o=!(n instanceof h)||n.payload.error?.retryable!==!1;D(s,o?()=>{re()}:void 0)}}finally{k.disabled=!1,k.textContent="Create task"}}j.addEventListener("click",()=>{A()});C.addEventListener("change",()=>{A()});a("connection-retry").addEventListener("click",()=>{A()});a("new-task-button").addEventListener("click",()=>{B.hidden=!0,E.showModal()});H.addEventListener("input",v);M.addEventListener("submit",e=>{e.preventDefault(),re()});for(let e of document.querySelectorAll("[data-close]"))e.addEventListener("click",()=>{let t=e.dataset.close;t!==void 0&&a(t).close()});for(let e of[E,u])e.addEventListener("click",t=>{t.target===e&&e.close()});u.addEventListener("close",()=>{let e=y;y=void 0,p.parentElement===u&&w(),v(),e!==void 0&&window.setTimeout(()=>{L.querySelector(`[data-issue="${e}"] .card-title-button`)?.focus()},0)});E.addEventListener("close",()=>{p.parentElement===E&&w()});J.addEventListener("click",()=>{let e=O;w(),e?.()});a("toast-dismiss").addEventListener("click",w);document.addEventListener("keydown",e=>{e.key==="Escape"&&(w(),u.open?u.close():E.open&&E.close())});A();})();
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="gitasks-csrf" content="__GITASKS_CSRF_TOKEN__">
|
|
7
|
+
<meta name="color-scheme" content="light">
|
|
8
|
+
<title>Gitasks</title>
|
|
9
|
+
<link rel="stylesheet" href="/styles.css">
|
|
10
|
+
<script src="/app.js" defer></script>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div class="app-shell">
|
|
14
|
+
<header class="app-header">
|
|
15
|
+
<div class="brand-block">
|
|
16
|
+
<div class="brand-mark" aria-hidden="true">G</div>
|
|
17
|
+
<div>
|
|
18
|
+
<p class="eyebrow">Local task board</p>
|
|
19
|
+
<h1>Gitasks</h1>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="repo-block">
|
|
23
|
+
<span class="repo-label">Repository</span>
|
|
24
|
+
<strong id="repository">Connecting…</strong>
|
|
25
|
+
</div>
|
|
26
|
+
</header>
|
|
27
|
+
|
|
28
|
+
<section class="toolbar" aria-label="Board controls">
|
|
29
|
+
<label class="search-field" for="search-input">
|
|
30
|
+
<span class="visually-hidden">Search by title or issue number</span>
|
|
31
|
+
<svg aria-hidden="true" viewBox="0 0 20 20"><path d="m14.4 13.2 3.2 3.2-1.2 1.2-3.2-3.2a6.5 6.5 0 1 1 1.2-1.2ZM9.5 14.3a4.8 4.8 0 1 0 0-9.6 4.8 4.8 0 0 0 0 9.6Z"/></svg>
|
|
32
|
+
<input id="search-input" type="search" placeholder="Search title or #issue" autocomplete="off">
|
|
33
|
+
</label>
|
|
34
|
+
<div class="toolbar-actions">
|
|
35
|
+
<label class="state-filter" for="state-filter">
|
|
36
|
+
<span>GitHub state</span>
|
|
37
|
+
<select id="state-filter">
|
|
38
|
+
<option value="open" selected>Open</option>
|
|
39
|
+
<option value="closed">Closed</option>
|
|
40
|
+
<option value="all">All</option>
|
|
41
|
+
</select>
|
|
42
|
+
</label>
|
|
43
|
+
<button id="refresh-button" class="button secondary" type="button">
|
|
44
|
+
<svg aria-hidden="true" viewBox="0 0 20 20"><path d="M15.7 6.2A6.5 6.5 0 1 0 16 13l-1.6-.8a4.7 4.7 0 1 1-.2-4.8h-2.5V5.7h5.5v5.5h-1.7V7.6l.2-1.4Z"/></svg>
|
|
45
|
+
Refresh
|
|
46
|
+
</button>
|
|
47
|
+
<button id="new-task-button" class="button primary" type="button">
|
|
48
|
+
<span aria-hidden="true">+</span>
|
|
49
|
+
New task
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
53
|
+
|
|
54
|
+
<section class="board-meta" aria-live="polite">
|
|
55
|
+
<p id="scope-text">Loading issue scope…</p>
|
|
56
|
+
<p>Last refreshed <time id="refresh-time">—</time></p>
|
|
57
|
+
</section>
|
|
58
|
+
<p id="status-message" class="visually-hidden" role="status" aria-live="polite"></p>
|
|
59
|
+
|
|
60
|
+
<section id="connection-error" class="state-panel error-panel" role="alert" hidden>
|
|
61
|
+
<div>
|
|
62
|
+
<strong>Could not load the board</strong>
|
|
63
|
+
<p id="connection-error-message">Check your internet connection and GitHub CLI authentication.</p>
|
|
64
|
+
</div>
|
|
65
|
+
<button id="connection-retry" class="button secondary" type="button">Retry</button>
|
|
66
|
+
</section>
|
|
67
|
+
|
|
68
|
+
<main>
|
|
69
|
+
<p id="board-scroll-hint" class="visually-hidden">Scroll the board horizontally and vertically to view every status column and task.</p>
|
|
70
|
+
<section id="loading-state" class="state-panel loading-panel" aria-live="polite">
|
|
71
|
+
<span class="loading-dot" aria-hidden="true"></span>
|
|
72
|
+
Reading GitHub Issues…
|
|
73
|
+
</section>
|
|
74
|
+
<section id="search-empty" class="state-panel" hidden>
|
|
75
|
+
<strong>No matching tasks</strong>
|
|
76
|
+
<p>Try a different title or issue number.</p>
|
|
77
|
+
</section>
|
|
78
|
+
<section id="unclassified-panel" class="unclassified-panel" aria-labelledby="unclassified-title" hidden>
|
|
79
|
+
<header class="unclassified-header">
|
|
80
|
+
<div>
|
|
81
|
+
<h2 id="unclassified-title">Unclassified issues</h2>
|
|
82
|
+
<p>No recognized Gitasks status label or title prefix. Assigning a status updates GitHub.</p>
|
|
83
|
+
</div>
|
|
84
|
+
<span id="unclassified-count" class="count">0</span>
|
|
85
|
+
</header>
|
|
86
|
+
<div id="unclassified-list" class="unclassified-list"></div>
|
|
87
|
+
</section>
|
|
88
|
+
<div id="board" class="board" tabindex="0" aria-label="Gitasks status board" aria-describedby="board-scroll-hint" hidden></div>
|
|
89
|
+
</main>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<dialog id="create-dialog" class="modal" aria-labelledby="create-dialog-title">
|
|
93
|
+
<form id="create-form" method="dialog">
|
|
94
|
+
<header class="dialog-header">
|
|
95
|
+
<div>
|
|
96
|
+
<p class="eyebrow">GitHub Issue</p>
|
|
97
|
+
<h2 id="create-dialog-title">Create a task</h2>
|
|
98
|
+
</div>
|
|
99
|
+
<button class="icon-button" type="button" data-close="create-dialog" aria-label="Close create task form">×</button>
|
|
100
|
+
</header>
|
|
101
|
+
<div class="form-body">
|
|
102
|
+
<label for="task-title">Title</label>
|
|
103
|
+
<input id="task-title" name="title" type="text" maxlength="256" required autofocus placeholder="What needs to be done?">
|
|
104
|
+
|
|
105
|
+
<label for="task-body">Description <span>optional</span></label>
|
|
106
|
+
<textarea id="task-body" name="body" rows="7" placeholder="Add context, acceptance criteria, or implementation notes."></textarea>
|
|
107
|
+
|
|
108
|
+
<label for="task-status">Initial status</label>
|
|
109
|
+
<select id="task-status" name="status"></select>
|
|
110
|
+
<p class="form-note">Creates a GitHub Issue immediately.</p>
|
|
111
|
+
<p id="create-error" class="inline-error" role="alert" hidden></p>
|
|
112
|
+
</div>
|
|
113
|
+
<footer class="dialog-footer">
|
|
114
|
+
<button class="button secondary" type="button" data-close="create-dialog">Cancel</button>
|
|
115
|
+
<button id="create-submit" class="button primary" type="submit">Create task</button>
|
|
116
|
+
</footer>
|
|
117
|
+
</form>
|
|
118
|
+
</dialog>
|
|
119
|
+
|
|
120
|
+
<dialog id="detail-dialog" class="drawer" aria-labelledby="detail-title">
|
|
121
|
+
<article>
|
|
122
|
+
<header class="dialog-header detail-header">
|
|
123
|
+
<div>
|
|
124
|
+
<p id="detail-number" class="eyebrow">Issue</p>
|
|
125
|
+
<h2 id="detail-title">Task details</h2>
|
|
126
|
+
</div>
|
|
127
|
+
<button class="icon-button" type="button" data-close="detail-dialog" aria-label="Close task details">×</button>
|
|
128
|
+
</header>
|
|
129
|
+
<div id="detail-content" class="detail-content"></div>
|
|
130
|
+
</article>
|
|
131
|
+
</dialog>
|
|
132
|
+
|
|
133
|
+
<div id="toast" class="toast" role="alert" hidden>
|
|
134
|
+
<div>
|
|
135
|
+
<strong id="toast-title">Action failed</strong>
|
|
136
|
+
<p id="toast-message"></p>
|
|
137
|
+
</div>
|
|
138
|
+
<button id="toast-retry" class="button compact" type="button" hidden>Retry</button>
|
|
139
|
+
<button id="toast-dismiss" class="icon-button" type="button" aria-label="Dismiss message">×</button>
|
|
140
|
+
</div>
|
|
141
|
+
</body>
|
|
142
|
+
</html>
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light;
|
|
3
|
+
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
4
|
+
color: #172033;
|
|
5
|
+
background: #f4f6f8;
|
|
6
|
+
font-synthesis: none;
|
|
7
|
+
--ink: #172033;
|
|
8
|
+
--muted: #697386;
|
|
9
|
+
--line: #dfe4ea;
|
|
10
|
+
--surface: #ffffff;
|
|
11
|
+
--surface-subtle: #f8fafb;
|
|
12
|
+
--accent: #246bfd;
|
|
13
|
+
--accent-strong: #1854cc;
|
|
14
|
+
--danger: #b42318;
|
|
15
|
+
--danger-bg: #fff3f2;
|
|
16
|
+
--shadow: 0 12px 34px rgba(27, 39, 61, 0.12);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
* { box-sizing: border-box; }
|
|
20
|
+
|
|
21
|
+
body {
|
|
22
|
+
margin: 0;
|
|
23
|
+
min-width: 320px;
|
|
24
|
+
min-height: 100vh;
|
|
25
|
+
background: #f4f6f8;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
button, input, textarea, select { font: inherit; }
|
|
29
|
+
button, select { cursor: pointer; }
|
|
30
|
+
button:disabled, select:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
31
|
+
a { color: inherit; }
|
|
32
|
+
|
|
33
|
+
:focus-visible {
|
|
34
|
+
outline: 2px solid var(--accent-strong);
|
|
35
|
+
outline-offset: 2px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.visually-hidden {
|
|
39
|
+
position: absolute;
|
|
40
|
+
width: 1px;
|
|
41
|
+
height: 1px;
|
|
42
|
+
padding: 0;
|
|
43
|
+
margin: -1px;
|
|
44
|
+
overflow: hidden;
|
|
45
|
+
clip: rect(0, 0, 0, 0);
|
|
46
|
+
white-space: nowrap;
|
|
47
|
+
border: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.app-shell {
|
|
51
|
+
height: 100vh;
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.app-header {
|
|
58
|
+
min-height: 78px;
|
|
59
|
+
padding: 18px 28px;
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: space-between;
|
|
63
|
+
gap: 24px;
|
|
64
|
+
background: var(--surface);
|
|
65
|
+
border-bottom: 1px solid var(--line);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.brand-block, .repo-block, .toolbar, .toolbar-actions, .board-meta,
|
|
69
|
+
.card-topline, .card-footer, .label-list, .detail-row, .toast {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.brand-block { gap: 12px; }
|
|
75
|
+
.brand-mark {
|
|
76
|
+
width: 40px;
|
|
77
|
+
height: 40px;
|
|
78
|
+
display: grid;
|
|
79
|
+
place-items: center;
|
|
80
|
+
border-radius: 10px;
|
|
81
|
+
color: #fff;
|
|
82
|
+
background: #172033;
|
|
83
|
+
font-weight: 750;
|
|
84
|
+
letter-spacing: -0.04em;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
h1, h2, h3, p { margin: 0; }
|
|
88
|
+
h1 { font-size: 20px; line-height: 1.1; letter-spacing: -0.025em; }
|
|
89
|
+
h2 { font-size: 20px; line-height: 1.25; letter-spacing: -0.02em; }
|
|
90
|
+
.eyebrow, .repo-label {
|
|
91
|
+
margin-bottom: 3px;
|
|
92
|
+
color: var(--muted);
|
|
93
|
+
font-size: 11px;
|
|
94
|
+
font-weight: 700;
|
|
95
|
+
letter-spacing: 0.08em;
|
|
96
|
+
text-transform: uppercase;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.repo-block {
|
|
100
|
+
align-items: flex-end;
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
min-width: 0;
|
|
103
|
+
}
|
|
104
|
+
.repo-block strong {
|
|
105
|
+
max-width: min(50vw, 420px);
|
|
106
|
+
overflow: hidden;
|
|
107
|
+
color: #33405a;
|
|
108
|
+
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
|
109
|
+
font-size: 13px;
|
|
110
|
+
text-overflow: ellipsis;
|
|
111
|
+
white-space: nowrap;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.toolbar {
|
|
115
|
+
padding: 20px 28px 10px;
|
|
116
|
+
justify-content: space-between;
|
|
117
|
+
gap: 16px;
|
|
118
|
+
}
|
|
119
|
+
.toolbar-actions { gap: 10px; }
|
|
120
|
+
.state-filter {
|
|
121
|
+
min-height: 40px;
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 8px;
|
|
125
|
+
padding: 0 8px 0 12px;
|
|
126
|
+
border: 1px solid #cfd6df;
|
|
127
|
+
border-radius: 8px;
|
|
128
|
+
color: var(--muted);
|
|
129
|
+
background: var(--surface);
|
|
130
|
+
font-size: 11px;
|
|
131
|
+
font-weight: 700;
|
|
132
|
+
text-transform: uppercase;
|
|
133
|
+
}
|
|
134
|
+
.state-filter select {
|
|
135
|
+
border: 0;
|
|
136
|
+
outline: 0;
|
|
137
|
+
color: var(--ink);
|
|
138
|
+
background: transparent;
|
|
139
|
+
font-weight: 650;
|
|
140
|
+
text-transform: none;
|
|
141
|
+
}
|
|
142
|
+
.search-field {
|
|
143
|
+
width: min(440px, 52vw);
|
|
144
|
+
min-height: 42px;
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
gap: 10px;
|
|
148
|
+
padding: 0 13px;
|
|
149
|
+
border: 1px solid #cfd6df;
|
|
150
|
+
border-radius: 9px;
|
|
151
|
+
background: var(--surface);
|
|
152
|
+
}
|
|
153
|
+
.search-field:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(36, 107, 253, 0.12); }
|
|
154
|
+
.search-field svg, .button svg { width: 18px; height: 18px; fill: currentColor; flex: 0 0 auto; }
|
|
155
|
+
.search-field svg { color: #8992a2; }
|
|
156
|
+
.search-field input {
|
|
157
|
+
width: 100%;
|
|
158
|
+
border: 0;
|
|
159
|
+
outline: 0;
|
|
160
|
+
color: var(--ink);
|
|
161
|
+
background: transparent;
|
|
162
|
+
}
|
|
163
|
+
.search-field input::placeholder { color: var(--muted); }
|
|
164
|
+
|
|
165
|
+
.button, .icon-button {
|
|
166
|
+
border: 1px solid transparent;
|
|
167
|
+
border-radius: 8px;
|
|
168
|
+
font-weight: 650;
|
|
169
|
+
}
|
|
170
|
+
.button {
|
|
171
|
+
min-height: 40px;
|
|
172
|
+
padding: 0 15px;
|
|
173
|
+
display: inline-flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
justify-content: center;
|
|
176
|
+
gap: 8px;
|
|
177
|
+
}
|
|
178
|
+
.button.primary { color: #fff; background: var(--accent); border-color: var(--accent); }
|
|
179
|
+
.button.primary:hover:not(:disabled) { background: var(--accent-strong); }
|
|
180
|
+
.button.secondary { color: #344054; background: var(--surface); border-color: #cfd6df; }
|
|
181
|
+
.button.secondary:hover:not(:disabled) { background: #f8fafb; }
|
|
182
|
+
.button.compact { min-height: 32px; padding: 0 10px; color: #fff; background: transparent; border-color: rgba(255,255,255,.45); }
|
|
183
|
+
.icon-button {
|
|
184
|
+
width: 34px;
|
|
185
|
+
height: 34px;
|
|
186
|
+
padding: 0;
|
|
187
|
+
color: #667085;
|
|
188
|
+
background: transparent;
|
|
189
|
+
font-size: 24px;
|
|
190
|
+
line-height: 1;
|
|
191
|
+
}
|
|
192
|
+
.icon-button:hover { background: #f0f2f5; color: var(--ink); }
|
|
193
|
+
|
|
194
|
+
.board-meta {
|
|
195
|
+
min-height: 34px;
|
|
196
|
+
padding: 0 28px 12px;
|
|
197
|
+
justify-content: space-between;
|
|
198
|
+
gap: 16px;
|
|
199
|
+
color: var(--muted);
|
|
200
|
+
font-size: 12px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.state-panel {
|
|
204
|
+
margin: 14px 28px;
|
|
205
|
+
padding: 18px;
|
|
206
|
+
border: 1px dashed #cfd6df;
|
|
207
|
+
border-radius: 10px;
|
|
208
|
+
color: var(--muted);
|
|
209
|
+
background: rgba(255,255,255,.65);
|
|
210
|
+
text-align: center;
|
|
211
|
+
}
|
|
212
|
+
.state-panel strong { display: block; margin-bottom: 4px; color: var(--ink); }
|
|
213
|
+
.error-panel { display: flex; justify-content: space-between; text-align: left; border-style: solid; border-color: #f2b8b5; background: var(--danger-bg); }
|
|
214
|
+
.error-panel strong, .inline-error { color: var(--danger); }
|
|
215
|
+
.loading-panel { display: flex; justify-content: center; align-items: center; gap: 10px; }
|
|
216
|
+
.loading-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--accent); box-shadow: 14px 0 #9bb9fb, -14px 0 #d4e1ff; }
|
|
217
|
+
|
|
218
|
+
main { min-width: 0; min-height: 0; flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
|
219
|
+
.board {
|
|
220
|
+
min-height: 0;
|
|
221
|
+
flex: 1;
|
|
222
|
+
display: grid;
|
|
223
|
+
grid-auto-flow: column;
|
|
224
|
+
grid-auto-columns: minmax(286px, 1fr);
|
|
225
|
+
gap: 14px;
|
|
226
|
+
min-width: 100%;
|
|
227
|
+
padding: 2px 28px 30px;
|
|
228
|
+
overflow: auto;
|
|
229
|
+
overscroll-behavior: contain;
|
|
230
|
+
}
|
|
231
|
+
.board:focus-visible { outline-offset: -2px; }
|
|
232
|
+
.column {
|
|
233
|
+
min-height: 100%;
|
|
234
|
+
padding: 10px;
|
|
235
|
+
border: 1px solid var(--line);
|
|
236
|
+
border-radius: 11px;
|
|
237
|
+
background: #eef1f4;
|
|
238
|
+
}
|
|
239
|
+
.column-header {
|
|
240
|
+
position: sticky;
|
|
241
|
+
top: 0;
|
|
242
|
+
z-index: 1;
|
|
243
|
+
background: #eef1f4;
|
|
244
|
+
display: flex;
|
|
245
|
+
align-items: center;
|
|
246
|
+
justify-content: space-between;
|
|
247
|
+
min-height: 42px;
|
|
248
|
+
padding: 0 3px 10px;
|
|
249
|
+
}
|
|
250
|
+
.column-title { display: flex; align-items: center; gap: 8px; }
|
|
251
|
+
.status-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--status-color); }
|
|
252
|
+
.column h2 { font-size: 12px; letter-spacing: 0.045em; }
|
|
253
|
+
.count {
|
|
254
|
+
min-width: 24px;
|
|
255
|
+
padding: 3px 7px;
|
|
256
|
+
border: 1px solid #d5dbe3;
|
|
257
|
+
border-radius: 999px;
|
|
258
|
+
color: #667085;
|
|
259
|
+
background: rgba(255,255,255,.72);
|
|
260
|
+
font-size: 11px;
|
|
261
|
+
font-weight: 700;
|
|
262
|
+
text-align: center;
|
|
263
|
+
}
|
|
264
|
+
.card-list { display: grid; gap: 9px; }
|
|
265
|
+
.column-empty { padding: 22px 10px; color: #596579; font-size: 12px; text-align: center; }
|
|
266
|
+
|
|
267
|
+
.unclassified-panel {
|
|
268
|
+
flex: 0 0 auto;
|
|
269
|
+
max-height: min(260px, 38vh);
|
|
270
|
+
margin: 2px 28px 16px;
|
|
271
|
+
padding: 12px;
|
|
272
|
+
border: 1px solid #cfd6df;
|
|
273
|
+
border-left: 3px solid #667085;
|
|
274
|
+
border-radius: 11px;
|
|
275
|
+
background: #eef1f4;
|
|
276
|
+
overflow: auto;
|
|
277
|
+
}
|
|
278
|
+
.unclassified-header {
|
|
279
|
+
position: sticky;
|
|
280
|
+
top: -12px;
|
|
281
|
+
z-index: 1;
|
|
282
|
+
display: flex;
|
|
283
|
+
align-items: flex-start;
|
|
284
|
+
justify-content: space-between;
|
|
285
|
+
gap: 16px;
|
|
286
|
+
margin: -12px -12px 10px;
|
|
287
|
+
padding: 12px;
|
|
288
|
+
background: #eef1f4;
|
|
289
|
+
}
|
|
290
|
+
.unclassified-header h2 { font-size: 13px; }
|
|
291
|
+
.unclassified-header p { margin-top: 3px; color: var(--muted); font-size: 11px; }
|
|
292
|
+
.unclassified-list {
|
|
293
|
+
display: grid;
|
|
294
|
+
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
|
295
|
+
gap: 9px;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.task-card {
|
|
299
|
+
position: relative;
|
|
300
|
+
padding: 13px;
|
|
301
|
+
border: 1px solid #d9dee6;
|
|
302
|
+
border-left: 3px solid var(--status-color);
|
|
303
|
+
border-radius: 9px;
|
|
304
|
+
background: var(--surface);
|
|
305
|
+
box-shadow: 0 1px 2px rgba(16, 24, 40, .035);
|
|
306
|
+
}
|
|
307
|
+
.task-card.is-pending { background: #f7f9fb; }
|
|
308
|
+
.card-topline { justify-content: space-between; gap: 8px; margin-bottom: 8px; }
|
|
309
|
+
.issue-number { color: #667085; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; font-weight: 700; }
|
|
310
|
+
.state-badge { padding: 2px 6px; border-radius: 5px; color: #596579; background: #edf0f3; font-size: 10px; font-weight: 750; letter-spacing: .04em; }
|
|
311
|
+
.state-badge.closed { color: #7a271a; background: #fee4e2; }
|
|
312
|
+
.card-title-button {
|
|
313
|
+
width: 100%;
|
|
314
|
+
padding: 0;
|
|
315
|
+
border: 0;
|
|
316
|
+
color: var(--ink);
|
|
317
|
+
background: transparent;
|
|
318
|
+
font-weight: 680;
|
|
319
|
+
line-height: 1.42;
|
|
320
|
+
text-align: left;
|
|
321
|
+
overflow-wrap: anywhere;
|
|
322
|
+
}
|
|
323
|
+
.card-title-button:hover { color: var(--accent-strong); text-decoration: underline; text-underline-offset: 2px; }
|
|
324
|
+
.assignee { margin-top: 9px; color: #657086; font-size: 12px; overflow-wrap: anywhere; }
|
|
325
|
+
.label-list { flex-wrap: wrap; gap: 5px; margin-top: 10px; }
|
|
326
|
+
.label-chip { max-width: 100%; padding: 3px 7px; border-radius: 999px; color: #4d5a70; background: #edf1f5; font-size: 10px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
327
|
+
.card-footer { align-items: flex-end; justify-content: space-between; gap: 8px; margin-top: 12px; }
|
|
328
|
+
.card-footer label { flex: 1; color: var(--muted); font-size: 10px; font-weight: 700; text-transform: uppercase; }
|
|
329
|
+
.card-footer select { width: 100%; min-height: 32px; margin-top: 4px; padding: 0 28px 0 8px; border: 1px solid #d1d7e0; border-radius: 7px; color: #344054; background: #fff; font-size: 11px; font-weight: 650; }
|
|
330
|
+
.github-link { padding: 7px 2px; color: #4f5c71; font-size: 11px; font-weight: 650; text-decoration: none; }
|
|
331
|
+
.github-link:hover { color: var(--accent-strong); text-decoration: underline; }
|
|
332
|
+
|
|
333
|
+
[hidden] { display: none !important; }
|
|
334
|
+
|
|
335
|
+
dialog {
|
|
336
|
+
padding: 0;
|
|
337
|
+
border: 0;
|
|
338
|
+
color: var(--ink);
|
|
339
|
+
background: var(--surface);
|
|
340
|
+
box-shadow: var(--shadow);
|
|
341
|
+
}
|
|
342
|
+
dialog::backdrop { background: rgba(18, 27, 43, .38); }
|
|
343
|
+
.modal { width: min(560px, calc(100vw - 32px)); border-radius: 13px; }
|
|
344
|
+
.modal form { margin: 0; }
|
|
345
|
+
.dialog-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; padding: 20px 22px; border-bottom: 1px solid var(--line); }
|
|
346
|
+
.form-body { display: grid; padding: 20px 22px; }
|
|
347
|
+
.form-body label { margin: 14px 0 7px; color: #344054; font-size: 12px; font-weight: 700; }
|
|
348
|
+
.form-body label:first-child { margin-top: 0; }
|
|
349
|
+
.form-body label span { color: var(--muted); font-weight: 500; }
|
|
350
|
+
.form-body input, .form-body textarea, .form-body select {
|
|
351
|
+
width: 100%;
|
|
352
|
+
border: 1px solid #cfd6df;
|
|
353
|
+
border-radius: 8px;
|
|
354
|
+
color: var(--ink);
|
|
355
|
+
background: #fff;
|
|
356
|
+
}
|
|
357
|
+
.form-body input, .form-body select { min-height: 42px; padding: 0 11px; }
|
|
358
|
+
.form-body textarea { padding: 10px 11px; resize: vertical; line-height: 1.45; }
|
|
359
|
+
.form-note { margin-top: 9px; color: var(--muted); font-size: 11px; }
|
|
360
|
+
.inline-error { margin-top: 12px; padding: 10px; border-radius: 7px; background: var(--danger-bg); font-size: 12px; }
|
|
361
|
+
.dialog-footer { display: flex; justify-content: flex-end; gap: 9px; padding: 15px 22px; border-top: 1px solid var(--line); background: var(--surface-subtle); }
|
|
362
|
+
|
|
363
|
+
.drawer {
|
|
364
|
+
position: fixed;
|
|
365
|
+
inset: 0 0 0 auto;
|
|
366
|
+
z-index: 10;
|
|
367
|
+
border-left: 1px solid var(--line);
|
|
368
|
+
width: min(520px, 100vw);
|
|
369
|
+
max-width: none;
|
|
370
|
+
height: 100vh;
|
|
371
|
+
max-height: none;
|
|
372
|
+
margin: 0 0 0 auto;
|
|
373
|
+
overflow-y: auto;
|
|
374
|
+
}
|
|
375
|
+
.drawer article { min-height: 100%; }
|
|
376
|
+
.detail-header { position: sticky; top: 0; z-index: 1; background: #fff; }
|
|
377
|
+
.detail-header h2 { max-width: 410px; overflow-wrap: anywhere; }
|
|
378
|
+
.detail-content { display: grid; gap: 22px; padding: 22px; }
|
|
379
|
+
.detail-section h3 { margin-bottom: 8px; color: #667085; font-size: 11px; letter-spacing: .07em; text-transform: uppercase; }
|
|
380
|
+
.detail-body { max-height: 42vh; overflow: auto; padding: 13px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-subtle); color: #344054; font-family: inherit; font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
381
|
+
.detail-row { justify-content: space-between; gap: 18px; padding: 9px 0; border-bottom: 1px solid #edf0f3; font-size: 13px; }
|
|
382
|
+
.detail-row span:first-child { color: var(--muted); }
|
|
383
|
+
.detail-row strong { text-align: right; overflow-wrap: anywhere; }
|
|
384
|
+
.detail-status label { display: block; margin-bottom: 7px; color: var(--muted); font-size: 11px; font-weight: 700; text-transform: uppercase; }
|
|
385
|
+
.detail-status select { width: 100%; min-height: 42px; padding: 0 10px; border: 1px solid #cfd6df; border-radius: 8px; background: #fff; }
|
|
386
|
+
.detail-link { width: 100%; }
|
|
387
|
+
|
|
388
|
+
.toast {
|
|
389
|
+
position: fixed;
|
|
390
|
+
right: 20px;
|
|
391
|
+
bottom: 20px;
|
|
392
|
+
z-index: 20;
|
|
393
|
+
max-width: min(520px, calc(100vw - 40px));
|
|
394
|
+
align-items: flex-start;
|
|
395
|
+
gap: 14px;
|
|
396
|
+
padding: 14px 14px 14px 16px;
|
|
397
|
+
border-radius: 10px;
|
|
398
|
+
color: #fff;
|
|
399
|
+
background: #712018;
|
|
400
|
+
box-shadow: var(--shadow);
|
|
401
|
+
}
|
|
402
|
+
.toast > div { flex: 1; }
|
|
403
|
+
.toast p { margin-top: 3px; color: #fddbd8; font-size: 12px; line-height: 1.4; }
|
|
404
|
+
.toast .icon-button { color: #fff; }
|
|
405
|
+
.toast .icon-button:hover { background: rgba(255,255,255,.1); }
|
|
406
|
+
.toast.success { background: #067647; }
|
|
407
|
+
.toast.success p { color: #d1fadf; }
|
|
408
|
+
|
|
409
|
+
@media (max-width: 720px) {
|
|
410
|
+
.app-header, .toolbar, .board-meta { padding-left: 16px; padding-right: 16px; }
|
|
411
|
+
.app-header { align-items: flex-start; }
|
|
412
|
+
.repo-block { align-items: flex-start; }
|
|
413
|
+
.toolbar { align-items: stretch; flex-direction: column; }
|
|
414
|
+
.search-field { width: 100%; }
|
|
415
|
+
.toolbar-actions { flex-wrap: wrap; justify-content: flex-end; }
|
|
416
|
+
.state-filter { margin-right: auto; }
|
|
417
|
+
.board-meta { align-items: flex-start; flex-direction: column; gap: 4px; }
|
|
418
|
+
.board { grid-auto-columns: minmax(274px, 84vw); padding-left: 16px; padding-right: 16px; }
|
|
419
|
+
.unclassified-panel { margin-left: 16px; margin-right: 16px; }
|
|
420
|
+
.unclassified-list { grid-template-columns: minmax(0, 1fr); }
|
|
421
|
+
.state-panel { margin-left: 16px; margin-right: 16px; }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
@media (prefers-reduced-motion: reduce) {
|
|
425
|
+
*, *::before, *::after { scroll-behavior: auto !important; }
|
|
426
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gitasks",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "A lightweight local task protocol for humans and coding agents, powered entirely by GitHub Issues.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gitasks": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup src/cli.ts --format esm --platform node --target node20 --clean --minify --no-sourcemap && tsup src/ui/client/app.ts --format iife --platform browser --target es2020 --out-dir dist/ui --minify --no-sourcemap --global-name GitasksUi && node scripts/copy-ui-assets.mjs",
|
|
19
|
+
"dev": "tsx src/cli.ts",
|
|
20
|
+
"test": "tsx --test test/index.test.ts test/ui.test.ts",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"prepack": "npm run build",
|
|
23
|
+
"prepublishOnly": "npm run typecheck && npm test"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"github-issues",
|
|
27
|
+
"task-management",
|
|
28
|
+
"cli",
|
|
29
|
+
"coding-agents"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/bilalnrts/gitasks.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/bilalnrts/gitasks/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/bilalnrts/gitasks#readme",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"commander": "^14.0.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^24.3.0",
|
|
45
|
+
"tsup": "^8.5.0",
|
|
46
|
+
"tsx": "^4.20.5",
|
|
47
|
+
"typescript": "^5.9.2"
|
|
48
|
+
}
|
|
49
|
+
}
|