teamcopilot 0.0.1
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/.env.example +10 -0
- package/LICENSE.md +21 -0
- package/README.md +131 -0
- package/bin/teamcopilot.js +281 -0
- package/dist/auth/index.js +189 -0
- package/dist/change-user-role.js +77 -0
- package/dist/chat/index.js +849 -0
- package/dist/constants.js +2 -0
- package/dist/create-user.js +98 -0
- package/dist/cronjob/index.js +16 -0
- package/dist/cronjob/resource-reconciliation.js +33 -0
- package/dist/delete-user.js +66 -0
- package/dist/frontend/assets/abap-CRCWOmpq.js +1 -0
- package/dist/frontend/assets/apex-DnsZk_dE.js +1 -0
- package/dist/frontend/assets/azcli-1IWB1ccx.js +1 -0
- package/dist/frontend/assets/bat-DPkNLes8.js +1 -0
- package/dist/frontend/assets/bicep-Corcdgou.js +2 -0
- package/dist/frontend/assets/cameligo-CGrWLZr3.js +1 -0
- package/dist/frontend/assets/clojure-D9WOWImG.js +1 -0
- package/dist/frontend/assets/codicon-DCmgc-ay.ttf +0 -0
- package/dist/frontend/assets/coffee-B7EJu28W.js +1 -0
- package/dist/frontend/assets/cpp-SEyurbux.js +1 -0
- package/dist/frontend/assets/csharp-BoL64M5l.js +1 -0
- package/dist/frontend/assets/csp-C46ZqvIl.js +1 -0
- package/dist/frontend/assets/css-DQU6DXDx.js +3 -0
- package/dist/frontend/assets/cssMode-BDT3WbVs.js +4 -0
- package/dist/frontend/assets/cypher-D84EuPTj.js +1 -0
- package/dist/frontend/assets/dart-D8lhlL1r.js +1 -0
- package/dist/frontend/assets/dockerfile-DLk6rpji.js +1 -0
- package/dist/frontend/assets/ecl-BO6FnfXk.js +1 -0
- package/dist/frontend/assets/editor.worker-B4pQIWZD.js +12 -0
- package/dist/frontend/assets/elixir-BRjLKONM.js +1 -0
- package/dist/frontend/assets/flow9-Cac8vKd7.js +1 -0
- package/dist/frontend/assets/freemarker2-C7-hEgID.js +3 -0
- package/dist/frontend/assets/fsharp-fd1GTHhf.js +1 -0
- package/dist/frontend/assets/go-O9LJTZXk.js +1 -0
- package/dist/frontend/assets/graphql-LQdxqEYJ.js +1 -0
- package/dist/frontend/assets/handlebars-4cwTkPir.js +1 -0
- package/dist/frontend/assets/hcl-DxDQ3s82.js +1 -0
- package/dist/frontend/assets/html-YNfE1Q0A.js +1 -0
- package/dist/frontend/assets/htmlMode-opTQ1HoB.js +4 -0
- package/dist/frontend/assets/index-DWyaVa1h.js +782 -0
- package/dist/frontend/assets/index-lXrsgeTF.css +1 -0
- package/dist/frontend/assets/ini-BvajGCUy.js +1 -0
- package/dist/frontend/assets/java-SYsfObOQ.js +1 -0
- package/dist/frontend/assets/javascript-BEwGzk7T.js +1 -0
- package/dist/frontend/assets/jsonMode-CGhIS5Al.js +10 -0
- package/dist/frontend/assets/julia-DQXNmw_w.js +1 -0
- package/dist/frontend/assets/kotlin-qQ0MG-9I.js +1 -0
- package/dist/frontend/assets/less-GGFNNJHn.js +2 -0
- package/dist/frontend/assets/lexon-Canl7DCW.js +1 -0
- package/dist/frontend/assets/liquid-QekTGCGJ.js +1 -0
- package/dist/frontend/assets/lua-D28Ae8-K.js +1 -0
- package/dist/frontend/assets/m3-DPitgjJI.js +1 -0
- package/dist/frontend/assets/markdown-B811l8j2.js +1 -0
- package/dist/frontend/assets/mdx-BAVDaB7v.js +1 -0
- package/dist/frontend/assets/mips-CdjsipkG.js +1 -0
- package/dist/frontend/assets/msdax-CYqgjx_P.js +1 -0
- package/dist/frontend/assets/mysql-BHd6q0vd.js +1 -0
- package/dist/frontend/assets/objective-c-B1aVtJYH.js +1 -0
- package/dist/frontend/assets/pascal-BhNW15KB.js +1 -0
- package/dist/frontend/assets/pascaligo-5jv8CcQD.js +1 -0
- package/dist/frontend/assets/perl-DlYyT36c.js +1 -0
- package/dist/frontend/assets/pgsql-Dy0bjov7.js +1 -0
- package/dist/frontend/assets/php-120yhfDK.js +1 -0
- package/dist/frontend/assets/pla-CjnFlu4u.js +1 -0
- package/dist/frontend/assets/postiats-CQpG440k.js +1 -0
- package/dist/frontend/assets/powerquery-DdJtto1Z.js +1 -0
- package/dist/frontend/assets/powershell-Bu_VLpJB.js +1 -0
- package/dist/frontend/assets/protobuf-IBS6jZEB.js +2 -0
- package/dist/frontend/assets/pug-kFxLfcjb.js +1 -0
- package/dist/frontend/assets/python-BQlHw7XO.js +1 -0
- package/dist/frontend/assets/qsharp-q7JyzKFN.js +1 -0
- package/dist/frontend/assets/r-BIFz-_sK.js +1 -0
- package/dist/frontend/assets/razor-Be3Wwc2E.js +1 -0
- package/dist/frontend/assets/redis-CHOsPHWR.js +1 -0
- package/dist/frontend/assets/redshift-CBifECDb.js +1 -0
- package/dist/frontend/assets/restructuredtext-CghPJEOS.js +1 -0
- package/dist/frontend/assets/ruby-CYWGW-b1.js +1 -0
- package/dist/frontend/assets/rust-DMDD0SHb.js +1 -0
- package/dist/frontend/assets/sb-BYAiYHFx.js +1 -0
- package/dist/frontend/assets/scala-Bqvq8jcR.js +1 -0
- package/dist/frontend/assets/scheme-Dhb-2j9p.js +1 -0
- package/dist/frontend/assets/scss-CTwUZ5N7.js +3 -0
- package/dist/frontend/assets/shell-CsDZo4DB.js +1 -0
- package/dist/frontend/assets/solidity-CME5AdoB.js +1 -0
- package/dist/frontend/assets/sophia-RYC1BQQz.js +1 -0
- package/dist/frontend/assets/sparql-KEyrF7De.js +1 -0
- package/dist/frontend/assets/sql-BdTr02Mf.js +1 -0
- package/dist/frontend/assets/st-C7iG7M4S.js +1 -0
- package/dist/frontend/assets/swift-D7IUmUK8.js +1 -0
- package/dist/frontend/assets/systemverilog-DgMryOEJ.js +1 -0
- package/dist/frontend/assets/tcl-PloMZuKG.js +1 -0
- package/dist/frontend/assets/tsMode-CIBFoN3z.js +11 -0
- package/dist/frontend/assets/twig-BfRIq3la.js +1 -0
- package/dist/frontend/assets/typescript-BuV9wEIE.js +1 -0
- package/dist/frontend/assets/typespec-CzxlYoT_.js +1 -0
- package/dist/frontend/assets/vb-BwAE3J76.js +1 -0
- package/dist/frontend/assets/wgsl-B_1kOXbF.js +298 -0
- package/dist/frontend/assets/xml-DcDKYaM4.js +1 -0
- package/dist/frontend/assets/yaml-CuBNmOuI.js +1 -0
- package/dist/frontend/index.html +14 -0
- package/dist/frontend/logo.svg +50 -0
- package/dist/index.js +169 -0
- package/dist/logging.js +30 -0
- package/dist/opencode-auth/index.js +122 -0
- package/dist/opencode-server.js +91 -0
- package/dist/prisma/client.js +38 -0
- package/dist/reset-password.js +73 -0
- package/dist/rotate-jwt-secret.js +20 -0
- package/dist/scripts/prisma-workspace.js +34 -0
- package/dist/skills/index.js +311 -0
- package/dist/types/permissions.js +2 -0
- package/dist/types/shared/permissions.js +17 -0
- package/dist/types/shared/skill.js +17 -0
- package/dist/types/shared/workflow-files.js +17 -0
- package/dist/types/shared/workflow.js +17 -0
- package/dist/types/skill.js +2 -0
- package/dist/types/workflow-files.js +2 -0
- package/dist/types/workflow.js +2 -0
- package/dist/users/index.js +22 -0
- package/dist/utils/approval-snapshot-common.js +596 -0
- package/dist/utils/assert.js +20 -0
- package/dist/utils/chat-session.js +44 -0
- package/dist/utils/cli-bootstrap.js +26 -0
- package/dist/utils/index.js +95 -0
- package/dist/utils/jwt-secret.js +63 -0
- package/dist/utils/opencode-auth.js +126 -0
- package/dist/utils/opencode-client.js +109 -0
- package/dist/utils/password-policy.js +12 -0
- package/dist/utils/permission-common.js +280 -0
- package/dist/utils/redact.js +108 -0
- package/dist/utils/resource-access.js +37 -0
- package/dist/utils/resource-file-routes.js +115 -0
- package/dist/utils/resource-files.js +572 -0
- package/dist/utils/runtime-paths.js +61 -0
- package/dist/utils/session-abort.js +52 -0
- package/dist/utils/skill-approval-snapshot.js +39 -0
- package/dist/utils/skill-files.js +17 -0
- package/dist/utils/skill-permissions.js +15 -0
- package/dist/utils/skill.js +217 -0
- package/dist/utils/user-role.js +14 -0
- package/dist/utils/workflow-approval-snapshot.js +38 -0
- package/dist/utils/workflow-files.js +17 -0
- package/dist/utils/workflow-interruption.js +50 -0
- package/dist/utils/workflow-permissions.js +27 -0
- package/dist/utils/workflow-runner.js +414 -0
- package/dist/utils/workflow.js +158 -0
- package/dist/utils/workspace-sync.js +204 -0
- package/dist/workflows/index.js +751 -0
- package/dist/workspace_files/.opencode/opencode.json +17 -0
- package/dist/workspace_files/.opencode/package.json +14 -0
- package/dist/workspace_files/.opencode/plugins/createSkill.ts +339 -0
- package/dist/workspace_files/.opencode/plugins/createWorkflow.ts +345 -0
- package/dist/workspace_files/.opencode/plugins/findSimilarWorkflow.ts +173 -0
- package/dist/workspace_files/.opencode/plugins/findSkill.ts +211 -0
- package/dist/workspace_files/.opencode/plugins/getSkillContent.ts +135 -0
- package/dist/workspace_files/.opencode/plugins/honeytoken-protection.ts +64 -0
- package/dist/workspace_files/.opencode/plugins/listAvailableSkills.ts +93 -0
- package/dist/workspace_files/.opencode/plugins/listAvailableWorkflows.ts +93 -0
- package/dist/workspace_files/.opencode/plugins/python-protection.ts +184 -0
- package/dist/workspace_files/.opencode/plugins/runWorkflow.ts +168 -0
- package/dist/workspace_files/.opencode/tsconfig.json +16 -0
- package/dist/workspace_files/AGENTS.md +483 -0
- package/dist/workspace_files/package-lock.json +167 -0
- package/dist/workspace_files/package.json +5 -0
- package/package.json +86 -0
- package/prisma/migrations/20260203040755_init/migration.sql +20 -0
- package/prisma/migrations/20260204034845_replace_google_auth_with_email_password/migration.sql +25 -0
- package/prisma/migrations/20260207022226_add_user_role/migration.sql +25 -0
- package/prisma/migrations/20260210161254_add_workflow_runs/migration.sql +16 -0
- package/prisma/migrations/20260211050606_adds_workflow_table/migration.sql +40 -0
- package/prisma/migrations/20260211050750_adds_fkey_constraint/migration.sql +21 -0
- package/prisma/migrations/20260211051912_removes_workflow_table/migration.sql +34 -0
- package/prisma/migrations/20260211052238_changes_workflow_id_to_slug/migration.sql +27 -0
- package/prisma/migrations/20260212051912_add_output_to_workflow_runs/migration.sql +2 -0
- package/prisma/migrations/20260213073006_add_chat_sessions/migration.sql +13 -0
- package/prisma/migrations/20260216053202_add_chat_sessions_opencode_session_id_idx/migration.sql +2 -0
- package/prisma/migrations/20260216053237_drop_redundant_chat_sessions_opencode_idx/migration.sql +2 -0
- package/prisma/migrations/20260219060705_makes/migration.sql +24 -0
- package/prisma/migrations/20260222040542_add_workflow_execution_permissions/migration.sql +18 -0
- package/prisma/migrations/20260222040815_remove_workflow_execution_permissions/migration.sql +10 -0
- package/prisma/migrations/20260222041348_add_workflow_execution_permissions_final/migration.sql +17 -0
- package/prisma/migrations/20260222041741_rename_to_tool_execution_permissions/migration.sql +30 -0
- package/prisma/migrations/20260222041826_simplify_tool_execution_permissions/migration.sql +29 -0
- package/prisma/migrations/20260222041950_add_fields_for_standalone_permissions/migration.sql +32 -0
- package/prisma/migrations/20260222042954_simplify_tool_permissions_table/migration.sql +27 -0
- package/prisma/migrations/20260223073902_add_workflow_run_permissions_tables/migration.sql +23 -0
- package/prisma/migrations/20260225025151_add_workflow_metadata/migration.sql +16 -0
- package/prisma/migrations/20260225031035_merge_workflow_permissions_into_metadata/migration.sql +44 -0
- package/prisma/migrations/20260225031752_removes_default_for_run_permission_mode/migration.sql +20 -0
- package/prisma/migrations/20260225033603_remove_workflow_metadata_user_fkeys/migration.sql +18 -0
- package/prisma/migrations/20260225043032_restore_workflow_metadata_user_fkeys/migration.sql +20 -0
- package/prisma/migrations/20260225091423_add_workflow_approved_snapshots/migration.sql +28 -0
- package/prisma/migrations/20260226032121_add_is_approved_to_workflow_metadata/migration.sql +21 -0
- package/prisma/migrations/20260226032444_undoes_last_db_change/migration.sql +26 -0
- package/prisma/migrations/20260227120000_remove_snapshot_hash_from_approved_snapshots/migration.sql +16 -0
- package/prisma/migrations/20260228071125_adds_workspace_path_to_snapshot_table/migration.sql +22 -0
- package/prisma/migrations/20260228071217_modifies_index_and_removes_default_value/migration.sql +22 -0
- package/prisma/migrations/20260228071710_undoes_previous/migration.sql +27 -0
- package/prisma/migrations/20260228105022_add_must_change_password_first_login/migration.sql +20 -0
- package/prisma/migrations/20260301115439_add_workflow_run_log_refs/migration.sql +8 -0
- package/prisma/migrations/20260301122557_add_workflow_aborted_sessions/migration.sql +5 -0
- package/prisma/migrations/20260302045545_move_workflow_run_log_refs_into_workflow_runs/migration.sql +17 -0
- package/prisma/migrations/20260303040318_add_skill_tables/migration.sql +61 -0
- package/prisma/migrations/20260303051533_unify_resource_permissions/migration.sql +97 -0
- package/prisma/migrations/20260303064255_unify_resource_metadata_and_snapshots/migration.sql +179 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +147 -0
package/.env.example
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Workspace directory (absolute path or relative to project root)
|
|
2
|
+
WORKSPACE_DIR=./my_workspaces
|
|
3
|
+
|
|
4
|
+
# Server Configuration
|
|
5
|
+
TEAMCOPILOT_HOST=0.0.0.0
|
|
6
|
+
TEAMCOPILOT_PORT=5124
|
|
7
|
+
|
|
8
|
+
# Opencode Server Configuration
|
|
9
|
+
OPENCODE_PORT=4096
|
|
10
|
+
OPENCODE_MODEL=openai/gpt-5.3-codex
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020-2026 Rishabh Poddar
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="frontend/public/logo.svg" alt="TeamCopilot Logo" width="220" align="middle" />
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
# TeamCopilot
|
|
6
|
+
TeamCopilot helps technical and non-technical teams become more productive by enabling safe sharing of custom AI agent skills and tools.
|
|
7
|
+
|
|
8
|
+
## What makes TeamCopilot different
|
|
9
|
+
|
|
10
|
+
It's like Claude code / OpenAI Codex, except that:
|
|
11
|
+
- Multi-user environment: everyone uses the same agent setup. Configure once, the whole team can use it.
|
|
12
|
+
- Skill & tool permissions: control who can use which skills and tools through the agent. Example: allow only certain people in the team to use a skill for making server config changes.
|
|
13
|
+
- Approval workflow: anyone can create tools/skills, but engineers in the team must approve them before the agent can even see them.
|
|
14
|
+
- Fully auditable: chat sessions can’t be deleted by users and are stored on your server.
|
|
15
|
+
- Use it anywhere: web UI lets you talk to the agent even when you're away from your work machine.
|
|
16
|
+
- You can pick either OpenAI or Anthropic as your AI provider.
|
|
17
|
+
|
|
18
|
+
## Dashboard View
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
## Quick Start (npm)
|
|
23
|
+
|
|
24
|
+
### Prerequisites
|
|
25
|
+
|
|
26
|
+
- Node.js 20+
|
|
27
|
+
- npm
|
|
28
|
+
- Python 3.10+
|
|
29
|
+
|
|
30
|
+
### 1) Initialize in the folder you want to use
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx teamcopilot init
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This writes or updates a local `.env` in the current directory. `WORKSPACE_DIR` defaults to the current directory as an absolute path. You can also provide values up front:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx teamcopilot init \
|
|
40
|
+
--workspace-dir /absolute/path/to/workspace \
|
|
41
|
+
--teamcopilot-port 5124 \
|
|
42
|
+
--opencode-port 4096 \
|
|
43
|
+
--opencode-model openai/gpt-5.3-codex
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2) Start the server
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx teamcopilot start
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Open: **http://localhost:5124**
|
|
53
|
+
|
|
54
|
+
### 3) Run admin commands from the same directory
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx teamcopilot create-user
|
|
58
|
+
npx teamcopilot change-user-role
|
|
59
|
+
npx teamcopilot delete-user
|
|
60
|
+
npx teamcopilot reset-password
|
|
61
|
+
npx teamcopilot rotate-jwt-secret
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
If `.env` is missing or incomplete, TeamCopilot will ask you to run `npx teamcopilot init` first.
|
|
65
|
+
|
|
66
|
+
## Docker Setup
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
git clone https://github.com/rishabhpoddar/teamcopilot
|
|
70
|
+
cd teamcopilot
|
|
71
|
+
docker build -t teamcopilot .
|
|
72
|
+
docker run -d \
|
|
73
|
+
--name teamcopilot \
|
|
74
|
+
-p 5124:5124 \
|
|
75
|
+
-v /path/to/some/folder:/app/workspaces \
|
|
76
|
+
teamcopilot
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Open: **http://localhost:5124**
|
|
80
|
+
|
|
81
|
+
## Common Environment Variables
|
|
82
|
+
|
|
83
|
+
| Variable | Description | Default |
|
|
84
|
+
|----------|-------------|---------|
|
|
85
|
+
| `WORKSPACE_DIR` | Directory where workflows are stored | `./my_workspaces` |
|
|
86
|
+
| `TEAMCOPILOT_HOST` | Server host | `0.0.0.0` |
|
|
87
|
+
| `TEAMCOPILOT_PORT` | Server port | `5124` |
|
|
88
|
+
| `OPENCODE_PORT` | Internal OpenCode server port | `4096` |
|
|
89
|
+
| `OPENCODE_MODEL` | Model used by OpenCode | `openai/gpt-5.3-codex` |
|
|
90
|
+
|
|
91
|
+
## User Management (CLI)
|
|
92
|
+
|
|
93
|
+
Create user:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx teamcopilot create-user
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Change user role:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npx teamcopilot change-user-role
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Delete user:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npx teamcopilot delete-user
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Reset password:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx teamcopilot reset-password
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Rotate JWT secret (invalidates existing tokens causing everyone to get logged out):
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npx teamcopilot rotate-jwt-secret
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Users sign in at `/login`.
|
|
124
|
+
|
|
125
|
+
## Contributing
|
|
126
|
+
|
|
127
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { spawn } = require("child_process");
|
|
6
|
+
const { createInterface } = require("node:readline/promises");
|
|
7
|
+
const dotenv = require("dotenv");
|
|
8
|
+
|
|
9
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
10
|
+
const currentDirectory = process.cwd();
|
|
11
|
+
const envExamplePath = path.join(packageRoot, ".env.example");
|
|
12
|
+
const envFilePath = path.join(currentDirectory, ".env");
|
|
13
|
+
|
|
14
|
+
const commandToScript = {
|
|
15
|
+
start: "index.js",
|
|
16
|
+
"create-user": "create-user.js",
|
|
17
|
+
"delete-user": "delete-user.js",
|
|
18
|
+
"change-user-role": "change-user-role.js",
|
|
19
|
+
"reset-password": "reset-password.js",
|
|
20
|
+
"rotate-jwt-secret": "rotate-jwt-secret.js",
|
|
21
|
+
prisma: path.join("scripts", "prisma-workspace.js"),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function printHelp() {
|
|
25
|
+
console.error(`Usage:
|
|
26
|
+
npx teamcopilot init [options]
|
|
27
|
+
npx teamcopilot start
|
|
28
|
+
npx teamcopilot create-user [args]
|
|
29
|
+
npx teamcopilot delete-user [args]
|
|
30
|
+
npx teamcopilot change-user-role [args]
|
|
31
|
+
npx teamcopilot reset-password [args]
|
|
32
|
+
npx teamcopilot rotate-jwt-secret
|
|
33
|
+
npx teamcopilot prisma -- <prisma args>
|
|
34
|
+
|
|
35
|
+
Init options:
|
|
36
|
+
--workspace-dir <path>
|
|
37
|
+
--teamcopilot-host <host>
|
|
38
|
+
--teamcopilot-port <port>
|
|
39
|
+
--opencode-port <port>
|
|
40
|
+
--opencode-model <model>
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function loadEnvExample() {
|
|
45
|
+
const parsed = dotenv.parse(fs.readFileSync(envExamplePath, "utf-8"));
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeInitFlag(flagName) {
|
|
50
|
+
return flagName.replace(/^--/, "").trim().toUpperCase().replace(/-/g, "_");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function serializeEnvValue(value) {
|
|
54
|
+
if (/^[A-Za-z0-9_./:@+-]+$/.test(value)) {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
return JSON.stringify(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseFlags(argv) {
|
|
61
|
+
const flags = new Map();
|
|
62
|
+
const passthrough = [];
|
|
63
|
+
|
|
64
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
65
|
+
const value = argv[index];
|
|
66
|
+
if (!value.startsWith("--")) {
|
|
67
|
+
passthrough.push(value);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (value === "--") {
|
|
72
|
+
passthrough.push(...argv.slice(index));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const equalsIndex = value.indexOf("=");
|
|
77
|
+
if (equalsIndex >= 0) {
|
|
78
|
+
const key = normalizeInitFlag(value.slice(0, equalsIndex));
|
|
79
|
+
flags.set(key, value.slice(equalsIndex + 1));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const nextValue = argv[index + 1];
|
|
84
|
+
if (!nextValue || nextValue.startsWith("--")) {
|
|
85
|
+
throw new Error(`Missing value for ${value}`);
|
|
86
|
+
}
|
|
87
|
+
flags.set(normalizeInitFlag(value), nextValue);
|
|
88
|
+
index += 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { flags, passthrough };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function validateInitValue(key, rawValue) {
|
|
95
|
+
const value = rawValue.trim();
|
|
96
|
+
if (value.length === 0) {
|
|
97
|
+
throw new Error(`${key} cannot be empty`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (key === "WORKSPACE_DIR") {
|
|
101
|
+
return path.resolve(currentDirectory, value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (key === "TEAMCOPILOT_PORT" || key === "OPENCODE_PORT") {
|
|
105
|
+
const parsedPort = Number(value);
|
|
106
|
+
if (!Number.isInteger(parsedPort) || parsedPort <= 0 || parsedPort > 65535) {
|
|
107
|
+
throw new Error(`${key} must be an integer between 1 and 65535`);
|
|
108
|
+
}
|
|
109
|
+
return String(parsedPort);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseExistingEnv() {
|
|
116
|
+
if (!fs.existsSync(envFilePath)) {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
return dotenv.parse(fs.readFileSync(envFilePath, "utf-8"));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function promptForEnvValues(defaultValues, existingValues, providedFlags) {
|
|
123
|
+
const rl = createInterface({
|
|
124
|
+
input: process.stdin,
|
|
125
|
+
output: process.stdout,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const resolvedValues = {};
|
|
130
|
+
for (const key of Object.keys(defaultValues)) {
|
|
131
|
+
const providedValue = providedFlags.get(key);
|
|
132
|
+
if (providedValue !== undefined) {
|
|
133
|
+
resolvedValues[key] = validateInitValue(key, providedValue);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const displayedDefault = existingValues[key] ?? defaultValues[key];
|
|
138
|
+
while (true) {
|
|
139
|
+
const answer = await rl.question(`${key} [${displayedDefault}]: `);
|
|
140
|
+
const candidate = answer.trim().length === 0 ? displayedDefault : answer;
|
|
141
|
+
try {
|
|
142
|
+
resolvedValues[key] = validateInitValue(key, candidate);
|
|
143
|
+
break;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(error.message);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return resolvedValues;
|
|
150
|
+
} finally {
|
|
151
|
+
rl.close();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function upsertEnvFile(values) {
|
|
156
|
+
const existingContent = fs.existsSync(envFilePath) ? fs.readFileSync(envFilePath, "utf-8") : "";
|
|
157
|
+
const lines = existingContent.length > 0 ? existingContent.split(/\r?\n/) : [];
|
|
158
|
+
const nextLines = [...lines];
|
|
159
|
+
const updatedKeys = new Set();
|
|
160
|
+
|
|
161
|
+
for (let index = 0; index < nextLines.length; index += 1) {
|
|
162
|
+
const line = nextLines[index];
|
|
163
|
+
const match = /^([A-Z0-9_]+)=.*$/.exec(line);
|
|
164
|
+
if (!match) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const key = match[1];
|
|
169
|
+
if (!(key in values)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
nextLines[index] = `${key}=${serializeEnvValue(values[key])}`;
|
|
174
|
+
updatedKeys.add(key);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const [key, value] of Object.entries(values)) {
|
|
178
|
+
if (!updatedKeys.has(key)) {
|
|
179
|
+
nextLines.push(`${key}=${serializeEnvValue(value)}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const serialized = `${nextLines.join("\n").replace(/\n*$/, "\n")}`;
|
|
184
|
+
fs.writeFileSync(envFilePath, serialized, "utf-8");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function loadAndValidateLocalEnv() {
|
|
188
|
+
if (!fs.existsSync(envFilePath)) {
|
|
189
|
+
throw new Error(`No .env file found in ${currentDirectory}. Run \`npx teamcopilot init\` first.`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const parsedEnv = dotenv.parse(fs.readFileSync(envFilePath, "utf-8"));
|
|
193
|
+
const requiredKeys = Object.keys(loadEnvExample());
|
|
194
|
+
const missingKeys = requiredKeys.filter((key) => {
|
|
195
|
+
const value = parsedEnv[key];
|
|
196
|
+
return typeof value !== "string" || value.trim().length === 0;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (missingKeys.length > 0) {
|
|
200
|
+
throw new Error(`Missing required variables in .env: ${missingKeys.join(", ")}. Run \`npx teamcopilot init\` first.`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return parsedEnv;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function runCompiledCommand(command, argv, envValues) {
|
|
207
|
+
const relativeScriptPath = commandToScript[command];
|
|
208
|
+
if (!relativeScriptPath) {
|
|
209
|
+
throw new Error(`Unknown command: ${command}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const scriptPath = path.join(packageRoot, "dist", relativeScriptPath);
|
|
213
|
+
if (!fs.existsSync(scriptPath)) {
|
|
214
|
+
throw new Error(`Built script not found: ${scriptPath}. Rebuild the package before publishing.`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const child = spawn(process.execPath, [scriptPath, ...argv], {
|
|
218
|
+
cwd: currentDirectory,
|
|
219
|
+
stdio: "inherit",
|
|
220
|
+
env: {
|
|
221
|
+
...process.env,
|
|
222
|
+
...envValues,
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
child.on("exit", (code, signal) => {
|
|
227
|
+
if (signal) {
|
|
228
|
+
process.kill(process.pid, signal);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
process.exit(code ?? 1);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function runInit(argv) {
|
|
236
|
+
const { flags, passthrough } = parseFlags(argv);
|
|
237
|
+
if (passthrough.length > 0) {
|
|
238
|
+
throw new Error(`Unexpected positional arguments for init: ${passthrough.join(" ")}`);
|
|
239
|
+
}
|
|
240
|
+
const defaultValues = {
|
|
241
|
+
...loadEnvExample(),
|
|
242
|
+
WORKSPACE_DIR: currentDirectory,
|
|
243
|
+
};
|
|
244
|
+
const allowedKeys = new Set(Object.keys(defaultValues));
|
|
245
|
+
for (const key of flags.keys()) {
|
|
246
|
+
if (!allowedKeys.has(key)) {
|
|
247
|
+
throw new Error(`Unknown init option: --${key.toLowerCase().replace(/_/g, "-")}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const existingValues = parseExistingEnv();
|
|
251
|
+
const values = await promptForEnvValues(defaultValues, existingValues, flags);
|
|
252
|
+
upsertEnvFile(values);
|
|
253
|
+
console.log(`Init completed. Please run "npx teamcopilot start" to start the service`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function main() {
|
|
257
|
+
const [command, ...argv] = process.argv.slice(2);
|
|
258
|
+
|
|
259
|
+
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
260
|
+
printHelp();
|
|
261
|
+
process.exit(command ? 0 : 1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (command === "init") {
|
|
265
|
+
await runInit(argv);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!(command in commandToScript)) {
|
|
270
|
+
printHelp();
|
|
271
|
+
throw new Error(`Unknown command: ${command}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const envValues = loadAndValidateLocalEnv();
|
|
275
|
+
runCompiledCommand(command, argv, envValues);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
main().catch((error) => {
|
|
279
|
+
console.error(error.message);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
8
|
+
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
|
9
|
+
const client_1 = __importDefault(require("../prisma/client"));
|
|
10
|
+
const index_1 = require("../utils/index");
|
|
11
|
+
const jwt_secret_1 = require("../utils/jwt-secret");
|
|
12
|
+
const password_policy_1 = require("../utils/password-policy");
|
|
13
|
+
const router = express_1.default.Router({ mergeParams: true });
|
|
14
|
+
function issueAccessToken(user) {
|
|
15
|
+
return jsonwebtoken_1.default.sign({ sub: user.id, email: user.email, name: user.name }, (0, jwt_secret_1.getJwtSecret)(), { expiresIn: '365d' });
|
|
16
|
+
}
|
|
17
|
+
function issuePasswordChangeToken(user) {
|
|
18
|
+
return jsonwebtoken_1.default.sign({ sub: user.id, email: user.email, name: user.name, token_use: 'password_change' }, (0, jwt_secret_1.getJwtSecret)(), { expiresIn: '10m' });
|
|
19
|
+
}
|
|
20
|
+
function parsePasswordChangeToken(rawToken) {
|
|
21
|
+
const decoded = jsonwebtoken_1.default.verify(rawToken, (0, jwt_secret_1.getJwtSecret)());
|
|
22
|
+
if (typeof decoded !== 'object' || decoded === null) {
|
|
23
|
+
throw {
|
|
24
|
+
status: 401,
|
|
25
|
+
message: 'Invalid password change challenge token'
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const payload = decoded;
|
|
29
|
+
if (typeof payload.sub !== 'string' || typeof payload.email !== 'string' || typeof payload.name !== 'string' || payload.token_use !== 'password_change') {
|
|
30
|
+
throw {
|
|
31
|
+
status: 401,
|
|
32
|
+
message: 'Invalid password change challenge token'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
sub: payload.sub,
|
|
37
|
+
email: payload.email,
|
|
38
|
+
name: payload.name,
|
|
39
|
+
token_use: 'password_change'
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
router.post('/signin', (async (req, res, next) => {
|
|
43
|
+
try {
|
|
44
|
+
const { email, password } = req.body;
|
|
45
|
+
if (!email || !password) {
|
|
46
|
+
throw {
|
|
47
|
+
status: 400,
|
|
48
|
+
message: 'Email and password are required'
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const user = await client_1.default.users.findUnique({ where: { email } });
|
|
52
|
+
if (!user) {
|
|
53
|
+
throw {
|
|
54
|
+
status: 401,
|
|
55
|
+
message: 'Invalid email or password'
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const valid = await bcryptjs_1.default.compare(password, user.password_hash);
|
|
59
|
+
if (!valid) {
|
|
60
|
+
throw {
|
|
61
|
+
status: 401,
|
|
62
|
+
message: 'Invalid email or password'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (user.must_change_password) {
|
|
66
|
+
const challengeToken = issuePasswordChangeToken(user);
|
|
67
|
+
res.json({
|
|
68
|
+
requires_password_change: true,
|
|
69
|
+
challenge_token: challengeToken
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const token = issueAccessToken(user);
|
|
74
|
+
res.json({ token });
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
next(err);
|
|
78
|
+
}
|
|
79
|
+
}));
|
|
80
|
+
router.post('/complete-password-change', (async (req, res, next) => {
|
|
81
|
+
try {
|
|
82
|
+
const { challengeToken, newPassword } = req.body;
|
|
83
|
+
if (!challengeToken || !newPassword) {
|
|
84
|
+
throw {
|
|
85
|
+
status: 400,
|
|
86
|
+
message: 'challengeToken and newPassword are required'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (typeof newPassword !== 'string' || !(0, password_policy_1.isPasswordValid)(newPassword)) {
|
|
90
|
+
throw {
|
|
91
|
+
status: 400,
|
|
92
|
+
message: (0, password_policy_1.getPasswordPolicyErrorMessage)()
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
let payload;
|
|
96
|
+
try {
|
|
97
|
+
payload = parsePasswordChangeToken(challengeToken);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof jsonwebtoken_1.default.JsonWebTokenError || error instanceof jsonwebtoken_1.default.TokenExpiredError) {
|
|
101
|
+
throw {
|
|
102
|
+
status: 401,
|
|
103
|
+
message: 'Invalid or expired password change challenge token'
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
const user = await client_1.default.users.findUnique({ where: { id: payload.sub } });
|
|
109
|
+
if (!user || user.email !== payload.email) {
|
|
110
|
+
throw {
|
|
111
|
+
status: 401,
|
|
112
|
+
message: 'Invalid password change challenge token'
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (!user.must_change_password) {
|
|
116
|
+
throw {
|
|
117
|
+
status: 400,
|
|
118
|
+
message: 'Password change is not required for this user'
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const password_hash = await bcryptjs_1.default.hash(newPassword, 12);
|
|
122
|
+
await client_1.default.users.update({
|
|
123
|
+
where: { id: user.id },
|
|
124
|
+
data: {
|
|
125
|
+
password_hash,
|
|
126
|
+
must_change_password: false,
|
|
127
|
+
reset_token: null,
|
|
128
|
+
reset_token_expires_at: null
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
const token = issueAccessToken(user);
|
|
132
|
+
res.json({ token });
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
next(err);
|
|
136
|
+
}
|
|
137
|
+
}));
|
|
138
|
+
router.post('/reset-password', (async (req, res, next) => {
|
|
139
|
+
try {
|
|
140
|
+
const { token, newPassword } = req.body;
|
|
141
|
+
if (!token || !newPassword) {
|
|
142
|
+
throw {
|
|
143
|
+
status: 400,
|
|
144
|
+
message: 'Token and newPassword are required'
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (typeof newPassword !== 'string' || !(0, password_policy_1.isPasswordValid)(newPassword)) {
|
|
148
|
+
throw {
|
|
149
|
+
status: 400,
|
|
150
|
+
message: (0, password_policy_1.getPasswordPolicyErrorMessage)()
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const user = await client_1.default.users.findFirst({
|
|
154
|
+
where: {
|
|
155
|
+
reset_token: token,
|
|
156
|
+
reset_token_expires_at: { gt: Date.now() }
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
if (!user) {
|
|
160
|
+
throw {
|
|
161
|
+
status: 400,
|
|
162
|
+
message: 'Invalid or expired reset token'
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const password_hash = await bcryptjs_1.default.hash(newPassword, 12);
|
|
166
|
+
await client_1.default.users.update({
|
|
167
|
+
where: { id: user.id },
|
|
168
|
+
data: {
|
|
169
|
+
password_hash,
|
|
170
|
+
must_change_password: false,
|
|
171
|
+
reset_token: null,
|
|
172
|
+
reset_token_expires_at: null
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
res.json({ message: 'Password reset successfully' });
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
next(err);
|
|
179
|
+
}
|
|
180
|
+
}));
|
|
181
|
+
router.get('/me', (0, index_1.apiHandler)(async (req, res) => {
|
|
182
|
+
res.json({
|
|
183
|
+
userId: req.userId,
|
|
184
|
+
email: req.email,
|
|
185
|
+
name: req.name,
|
|
186
|
+
role: req.role
|
|
187
|
+
});
|
|
188
|
+
}, true));
|
|
189
|
+
exports.default = router;
|