create-workframe 0.1.0 → 0.1.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/LICENSE +201 -201
- package/NOTICE +12 -12
- package/README.md +8 -92
- package/SECURITY.md +40 -40
- package/bin/workframe.js +329 -329
- package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +1 -1
- package/docs/workspace-instructions/WORKFRAME_ROUTING.md +8 -8
- package/package.json +3 -6
- package/profiles/architect/AGENTS.md +29 -29
- package/profiles/architect/SOUL.md +2 -2
- package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/designer/AGENTS.md +26 -26
- package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/dev/AGENTS.md +28 -28
- package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/docs/AGENTS.md +27 -27
- package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/research/AGENTS.md +26 -26
- package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/visionary/AGENTS.md +25 -25
- package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/workframe-agent/AGENTS.md +37 -37
- package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -85
- package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -58
- package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -54
- package/rules/workspace-README.md +5 -5
- package/scripts/apply-update-hermes.sh +17 -17
- package/scripts/apply-update-workframe.sh +77 -77
- package/scripts/bootstrap-workspace-link.sh +8 -8
- package/scripts/bundle-workframe-ui.mjs +3 -3
- package/scripts/compose-docker-host.sh +37 -37
- package/scripts/ensure-compose-host-paths.mjs +51 -51
- package/scripts/fix-zk-encryption-key.sh +35 -35
- package/scripts/lib/install-identity.mjs +212 -212
- package/scripts/restart-gateway-hermes.sh +12 -12
- package/scripts/set-compose-public-url.mjs +92 -92
- package/scripts/setup-stack-secrets.sh +50 -50
- package/scripts/sync-canonical-to-package.mjs +8 -7
- package/scripts/verify-public-deploy.sh +105 -105
- package/shared/WORKFRAME_AGENT_LIBRARY.md +17 -17
- package/shared/WORKFRAME_AGENT_OPERATIONS.md +15 -15
- package/shared/WORKFRAME_AGENT_PACKS.json +18 -18
- package/shared/WORKFRAME_AGENT_PACKS.yaml +8 -8
- package/shared/WORKFRAME_SKILL_CURATION.md +4 -4
- package/workframe-api/README.md +28 -28
- package/workframe-api/action_proxy.py +131 -131
- package/workframe-api/auth_rate_limit.py +49 -49
- package/workframe-api/credential_vault.py +445 -445
- package/workframe-api/data/avatar-catalog.json +41 -41
- package/workframe-api/email_sender.py +220 -220
- package/workframe-api/google_auth.py +90 -90
- package/workframe-api/install_api.py +359 -359
- package/workframe-api/internal_proxy_auth.py +150 -150
- package/workframe-api/llm_proxy.py +277 -277
- package/workframe-api/oidc_jwt.py +108 -108
- package/workframe-api/package.json +12 -13
- package/workframe-api/public/assets/index-DPXu_lGn.css +1 -1
- package/workframe-api/public/assets/index-DYnLrCZZ.js +8 -8
- package/workframe-api/requirements.txt +2 -2
- package/workframe-api/site_meta.py +271 -271
- package/workframe-api/stack_config.py +427 -427
- package/workframe-api/time-bind-chat.py +99 -99
- package/workframe-api/turn_credentials.py +226 -226
- package/workframe-api/updates.py +417 -417
- package/workframe-api/vault_kek.py +159 -159
- package/workframe-api/zk_auth.py +633 -633
- package/workframe-supervisor/Dockerfile +11 -11
- package/workframe-supervisor/server.py +787 -787
- package/workframe-ui/docker/nginx.conf +85 -85
- package/workframe-ui/public/assets/{arc-CBDYvkAF.js → arc-COAT3laO.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-DUyH3hWG.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-XnBRKeW0.js → architectureDiagram-3BPJPVTR-BFjWV24l.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-VYHUfVhd.js → blockDiagram-GPEHLZMM-DSQLPfrj.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BTjUcJpm.js → c4Diagram-AAUBKEIU-DKEHv1t2.js} +1 -1
- package/workframe-ui/public/assets/channel-g7r_RGaY.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-w7uu7R-b.js → chunk-2J33WTMH-DHZg-DUi.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-Cb9LtnDX.js → chunk-3OPIFGDE-BB-OYTfp.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-DiQ-qCwH.js → chunk-4BX2VUAB-C93q0YIm.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-C-mLFr7z.js → chunk-55IACEB6-MAYniqik.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-DOesfiCI.js → chunk-5ZQYHXKU-ChgN6YJs.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-BJ3oBZuz.js → chunk-727SXJPM-B_FYwdAv.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-CCA6xpGs.js → chunk-AQP2D5EJ-1_Hw_h1A.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-a0cMNFb2.js → chunk-BSJP7CBP-CFiDQ1Rv.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-kuqN8EIY.js → chunk-CSCIHK7Q-DZ9UMTlB.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-DyPgYHCg.js → chunk-FMBD7UC4-DlMlyFgw.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-CdUuvR0V.js → chunk-KSCS5N6A-DHXtQ_Hf.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-Dq9NoWmK.js → chunk-L5ZTLDWV-CuQzg-QG.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-p74rddlO.js → chunk-LZXEDZCA-BHzjzCGg.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-DBD2u1Gz.js → chunk-ND2GUHAM-DHXx05n2.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-BeIeYFnd.js → chunk-NZK2D7GU-CV5pmDM_.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-ClHc56ib.js → chunk-O5CBEL6O-6tkCHxsV.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-C5UQehWY.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-DhWllrI8.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-EFr8R_1p.js → chunk-XPW4576I-BClwIiCp.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BBM_8T8E.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BBM_8T8E.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-C7aPBODd.js → cose-bilkent-S5V4N54A-DOrGV6DQ.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-BdU1Rv-H.js → dagre-BM42HDAG-DXTPvJkX.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-DWowSo85.js → diagram-2AECGRRQ-xX_v-pbf.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-MnxBbceO.js → diagram-5GNKFQAL-Cd2pXbBe.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-DQaLRXFf.js → diagram-KO2AKTUF-Df3XvUtk.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-CQaBud9k.js → diagram-LMA3HP47-CsijIPaD.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-D8bAXbY9.js → diagram-OG6HWLK6-aq5fmfHd.js} +1 -1
- package/workframe-ui/public/assets/{dist-DGpTLHr_.js → dist-D1c0mkbB.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-1E-xSvBK.js → erDiagram-TEJ5UH35-DnFysVRY.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Ci8mdb44.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-CgOVD5hu.js → flowDiagram-I6XJVG4X-C6Ebi3su.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-JFYAIauo.js → ganttDiagram-6RSMTGT7-BQXQtUpa.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-B9REenIl.js → gitGraph-WXDBUCRP-Dt0zIs_M.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BQ7NcMSn.js → gitGraphDiagram-PVQCEYII-BF8gHzRn.js} +1 -1
- package/workframe-ui/public/assets/index-DpoUZAxh.css +1 -0
- package/workframe-ui/public/assets/{index-Dnw6vjqb.js → index-lRpzpNPT.js} +2 -2
- package/workframe-ui/public/assets/{info-J43DQDTF-CL6-eTjH.js → info-J43DQDTF-CSmszQJT.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-LJTODW4W.js → infoDiagram-5YYISTIA-CVTKGW6p.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-bchrQVuo.js → ishikawaDiagram-YF4QCWOH-Z8pT09Lv.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-DkrvYuxP.js → journeyDiagram-JHISSGLW-r3wD68_T.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-DFRbj0IG.js → kanban-definition-UN3LZRKU-Il8VglqN.js} +1 -1
- package/workframe-ui/public/assets/{line-Vd48P7-O.js → line-oyjpfz2A.js} +1 -1
- package/workframe-ui/public/assets/{linear-Ckizh2G7.js → linear-Cf7p5tVp.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-Bkimsnqj.js → mermaid-parser.core-YmbZ-AfY.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-x0TvVuPo.js → mermaid.core-BFdCAqCo.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-6ykAFPEz.js → mindmap-definition-RKZ34NQL-Cy2iCtEl.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-Dw3xgMDt.js → packet-YPE3B663-DwOBZL6K.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-DATysawG.js → pie-LRSECV5Y-04PPhnKK.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-SJKD1S0S.js → pieDiagram-4H26LBE5-LxIpgHqi.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-BrYDZX8q.js → quadrantDiagram-W4KKPZXB-0nBYfYm4.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-BmWYPCds.js → radar-GUYGQ44K-D2-vBqps.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DwL9Mc8e.js → requirementDiagram-4Y6WPE33-DbuU0nlu.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DYIFsL8h.js → sankeyDiagram-5OEKKPKP-B2hQ6B2x.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-0-FPkFk8.js → sequenceDiagram-3UESZ5HK-BBrU30e1.js} +1 -1
- package/workframe-ui/public/assets/{src-B_od6b6h.js → src-BJEDmV70.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BQCiBk6u.js → stateDiagram-AJRCARHV-7FGO4kkH.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DLTSizMg.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DS3tFcXj.js → timeline-definition-PNZ67QCA-ptDm4rCN.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-DSyUCKLY.js → treeView-BLDUP644-CS6Z-0q8.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-CEZaNh5Y.js → treemap-LRROVOQU-DqV4Y2VA.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CD-Vc9NF.js → vennDiagram-CIIHVFJN-C0UrZJYt.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-Drq5w1Mc.js → wardley-L42UT6IY-bNDN3_Sa.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DouXDJoF.js → wardleyDiagram-YWT4CUSO-jWiJsefM.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DDf_Lol5.js → xychartDiagram-2RQKCTM6-Dsh_fLCy.js} +1 -1
- package/workframe-ui/public/favicon.svg +7 -7
- package/workframe-ui/public/index.html +50 -50
- package/workframe-ui/public/workframe-config.json +3 -3
- package/scripts/security_audit.py +0 -156
- package/scripts/test-scaffold.mjs +0 -390
- package/workframe-api/tests/__init__.py +0 -0
- package/workframe-api/tests/db_setup.py +0 -13
- package/workframe-api/tests/test_admin_updates_gated.py +0 -30
- package/workframe-api/tests/test_agent_dm_bootstrap.py +0 -196
- package/workframe-api/tests/test_agent_profile_sync.py +0 -76
- package/workframe-api/tests/test_auth_email.py +0 -222
- package/workframe-api/tests/test_auth_hole_fix_selfcheck.py +0 -99
- package/workframe-api/tests/test_auth_rate_limit.py +0 -19
- package/workframe-api/tests/test_avatar_resolve.py +0 -77
- package/workframe-api/tests/test_child_soul_template.py +0 -71
- package/workframe-api/tests/test_credential_canary.py +0 -135
- package/workframe-api/tests/test_credential_isolation.py +0 -448
- package/workframe-api/tests/test_credential_resolution.py +0 -206
- package/workframe-api/tests/test_device_oauth.py +0 -108
- package/workframe-api/tests/test_doctor_repair.py +0 -103
- package/workframe-api/tests/test_ensure_profile_api.py +0 -77
- package/workframe-api/tests/test_gateway_compose_security.py +0 -136
- package/workframe-api/tests/test_install_secure_host.py +0 -39
- package/workframe-api/tests/test_internal_proxy_auth.py +0 -125
- package/workframe-api/tests/test_invite_runtime_bootstrap.py +0 -72
- package/workframe-api/tests/test_kanban_delegation.py +0 -185
- package/workframe-api/tests/test_llm_proxy.py +0 -155
- package/workframe-api/tests/test_login_access_policy.py +0 -183
- package/workframe-api/tests/test_mvp_model_bootstrap.py +0 -75
- package/workframe-api/tests/test_onboarding_bootstrap.py +0 -248
- package/workframe-api/tests/test_platform_auth.py +0 -47
- package/workframe-api/tests/test_profile_config_path.py +0 -56
- package/workframe-api/tests/test_profile_config_yaml_repair.py +0 -63
- package/workframe-api/tests/test_profile_create.py +0 -72
- package/workframe-api/tests/test_profile_identity_overlay.py +0 -61
- package/workframe-api/tests/test_profile_install_health.py +0 -45
- package/workframe-api/tests/test_profile_secret_policy.py +0 -57
- package/workframe-api/tests/test_profile_workspace_cwd.py +0 -34
- package/workframe-api/tests/test_provider_bootstrap.py +0 -75
- package/workframe-api/tests/test_provider_connect.py +0 -54
- package/workframe-api/tests/test_room_crud.py +0 -192
- package/workframe-api/tests/test_room_tenancy.py +0 -701
- package/workframe-api/tests/test_runtime_identity_backfill.py +0 -34
- package/workframe-api/tests/test_site_meta.py +0 -81
- package/workframe-api/tests/test_soul_stub.py +0 -42
- package/workframe-api/tests/test_space_member_sync.py +0 -99
- package/workframe-api/tests/test_stripe_stack_config.py +0 -37
- package/workframe-api/tests/test_supervisor_lifecycle.py +0 -52
- package/workframe-api/tests/test_turn_credential_vault.py +0 -125
- package/workframe-api/tests/test_updates.py +0 -176
- package/workframe-api/tests/test_user_cohort.py +0 -113
- package/workframe-api/tests/test_vault_envelope.py +0 -110
- package/workframe-api/tests/test_workspace_members.py +0 -183
- package/workframe-api/tests/test_workspace_messaging_sync.py +0 -125
- package/workframe-api/tests/test_workspace_provider_list.py +0 -57
- package/workframe-supervisor/tests/test_exec_guard.py +0 -42
- package/workframe-supervisor/tests/test_server_import.py +0 -21
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CtbQKTuT.js +0 -1
- package/workframe-ui/public/assets/channel-Dy4Z4-jn.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-CtBEchFK.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-B9pBtriN.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BMAEA8jI.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BMAEA8jI.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-D75cstNT.js +0 -1
- package/workframe-ui/public/assets/index-DpAGxump.css +0 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-B89jAMFF.js +0 -1
|
@@ -1,212 +1,212 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-install identity: slot-based localhost ports + stable install id for cookies.
|
|
3
|
-
*
|
|
4
|
-
* Slot N (1–99) → host ports N*10000 + tail:
|
|
5
|
-
* gateway 8642, dashboard 9119, api 9120, ui 8644, supervisor 8090
|
|
6
|
-
* slot 1 → 18642, 19119, 19120, 18644, 18090
|
|
7
|
-
* slot 2 → 28642, 29119, 29120, 28644, 28090
|
|
8
|
-
*
|
|
9
|
-
* ponytail: cookies use WORKFRAME_INSTALL_ID (not display name); auth DB is per install so same email can sign into many.
|
|
10
|
-
*/
|
|
11
|
-
import crypto from 'node:crypto';
|
|
12
|
-
import fs from 'node:fs';
|
|
13
|
-
import net from 'node:net';
|
|
14
|
-
import os from 'node:os';
|
|
15
|
-
import path from 'node:path';
|
|
16
|
-
|
|
17
|
-
export const PORT_TAIL = Object.freeze({
|
|
18
|
-
gateway: 8642,
|
|
19
|
-
dashboard: 9119,
|
|
20
|
-
api: 9120,
|
|
21
|
-
ui: 8644,
|
|
22
|
-
supervisor: 8090,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
export function portsForSlot(slot) {
|
|
26
|
-
const n = Number(slot);
|
|
27
|
-
if (!Number.isInteger(n) || n < 1 || n > 99) {
|
|
28
|
-
throw new Error(`WORKFRAME_SLOT must be 1–99, got ${slot}`);
|
|
29
|
-
}
|
|
30
|
-
const base = n * 10_000;
|
|
31
|
-
return {
|
|
32
|
-
slot: n,
|
|
33
|
-
gateway: base + PORT_TAIL.gateway,
|
|
34
|
-
dashboard: base + PORT_TAIL.dashboard,
|
|
35
|
-
api: base + PORT_TAIL.api,
|
|
36
|
-
ui: base + PORT_TAIL.ui,
|
|
37
|
-
supervisor: base + PORT_TAIL.supervisor,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function generateSupervisorToken() {
|
|
42
|
-
return crypto.randomBytes(32).toString('hex');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function generateProxyToken() {
|
|
46
|
-
return crypto.randomBytes(32).toString('base64url');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function generateApiToken() {
|
|
50
|
-
return crypto.randomBytes(32).toString('base64url');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function generateDashboardAuthPassword() {
|
|
54
|
-
return crypto.randomBytes(18).toString('base64url');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function generateDashboardAuthSecret() {
|
|
58
|
-
return crypto.randomBytes(24).toString('hex');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function generateVaultKekB64() {
|
|
62
|
-
return crypto.randomBytes(32).toString('base64');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function generateInstallId() {
|
|
66
|
-
return `wf_${crypto.randomBytes(6).toString('hex')}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function sessionCookieNameFromEnv(env = process.env) {
|
|
70
|
-
const installId = String(env.WORKFRAME_INSTALL_ID || '').trim();
|
|
71
|
-
if (installId) {
|
|
72
|
-
const safe = installId.replace(/[^a-zA-Z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
|
|
73
|
-
if (safe) return `${safe}_session`;
|
|
74
|
-
}
|
|
75
|
-
const slug = String(env.WORKFRAME_PROJECT || 'workframe')
|
|
76
|
-
.toLowerCase()
|
|
77
|
-
.replace(/[^a-z0-9]+/g, '_')
|
|
78
|
-
.replace(/^_+|_+$/g, '') || 'workframe';
|
|
79
|
-
return `wf_${slug}_session`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function portTaken(port, host = '127.0.0.1') {
|
|
83
|
-
return new Promise((resolve) => {
|
|
84
|
-
const socket = net.createConnection({ port, host });
|
|
85
|
-
const done = (taken) => {
|
|
86
|
-
socket.removeAllListeners();
|
|
87
|
-
socket.destroy();
|
|
88
|
-
resolve(taken);
|
|
89
|
-
};
|
|
90
|
-
socket.setTimeout(400);
|
|
91
|
-
socket.once('connect', () => done(true));
|
|
92
|
-
socket.once('timeout', () => done(false));
|
|
93
|
-
socket.once('error', () => done(false));
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function slotPortsFree(slot, host = '127.0.0.1') {
|
|
98
|
-
const ports = portsForSlot(slot);
|
|
99
|
-
for (const key of ['gateway', 'dashboard', 'api', 'ui']) {
|
|
100
|
-
if (await portTaken(ports[key], host)) return false;
|
|
101
|
-
}
|
|
102
|
-
return true;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Pick the first free slot, or use preferredSlot when its ports are free.
|
|
107
|
-
*/
|
|
108
|
-
export async function allocateInstall({
|
|
109
|
-
projectName,
|
|
110
|
-
preferredSlot = null,
|
|
111
|
-
maxSlot = 9,
|
|
112
|
-
host = '127.0.0.1',
|
|
113
|
-
installId = null,
|
|
114
|
-
} = {}) {
|
|
115
|
-
if (preferredSlot != null) {
|
|
116
|
-
const slot = Number(preferredSlot);
|
|
117
|
-
if (!await slotPortsFree(slot, host)) {
|
|
118
|
-
throw new Error(
|
|
119
|
-
`WORKFRAME_SLOT ${slot} ports are in use (${JSON.stringify(portsForSlot(slot))})`,
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
return {
|
|
123
|
-
installId: installId || generateInstallId(),
|
|
124
|
-
projectName,
|
|
125
|
-
slot,
|
|
126
|
-
ports: portsForSlot(slot),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
for (let slot = 1; slot <= maxSlot; slot++) {
|
|
131
|
-
if (await slotPortsFree(slot, host)) {
|
|
132
|
-
return {
|
|
133
|
-
installId: installId || generateInstallId(),
|
|
134
|
-
projectName,
|
|
135
|
-
slot,
|
|
136
|
-
ports: portsForSlot(slot),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
throw new Error(`No free Workframe install slot (1–${maxSlot}) on ${host}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function envFileLines(install, { example = false, nativeProfile = '', deploy = 'docker', hermesHome = '' } = {}) {
|
|
144
|
-
const header = example
|
|
145
|
-
? '# Copy to .env — one install = one slot + one WORKFRAME_INSTALL_ID.\n# Same email may be used across installs; each has its own auth DB.\n'
|
|
146
|
-
: '# Local Workframe install identity (.env is gitignored).\n';
|
|
147
|
-
const { ports, installId, slot } = install;
|
|
148
|
-
const deployLine = deploy === 'native' ? 'WORKFRAME_DEPLOY=native\n' : 'WORKFRAME_DEPLOY=docker\n';
|
|
149
|
-
const hermesLine = hermesHome ? `HERMES_DATA=${hermesHome.replace(/\\/g, '/')}\n` : '';
|
|
150
|
-
return `${header}${deployLine}${hermesLine}WORKFRAME_INSTALL_ID=${installId}
|
|
151
|
-
WORKFRAME_SLOT=${slot}
|
|
152
|
-
WORKFRAME_PROJECT=${install.projectName}
|
|
153
|
-
WORKFRAME_GATEWAY_PORT=${ports.gateway}
|
|
154
|
-
WORKFRAME_DASHBOARD_PORT=${ports.dashboard}
|
|
155
|
-
WORKFRAME_UI_PORT=${ports.ui}
|
|
156
|
-
WORKFRAME_UI_STATIC_DIR=./workframe-ui/public
|
|
157
|
-
WORKFRAME_API_PORT=${ports.api}
|
|
158
|
-
WORKFRAME_SUPERVISOR_PORT=${ports.supervisor}
|
|
159
|
-
WORKFRAME_MISSION_PORT=${ports.api}
|
|
160
|
-
WORKFRAME_NATIVE_PROFILE=${nativeProfile}
|
|
161
|
-
SECURE_MODE=true
|
|
162
|
-
DEV_LOCAL_UNSAFE=false
|
|
163
|
-
WORKFRAME_DEPLOYMENT_MODE=trusted_team
|
|
164
|
-
WORKFRAME_API_TOKEN=${example ? '' : generateApiToken()}
|
|
165
|
-
WORKFRAME_SUPERVISOR_TOKEN=${example ? '' : generateSupervisorToken()}
|
|
166
|
-
WORKFRAME_PROXY_TOKEN=${example ? '' : generateProxyToken()}
|
|
167
|
-
HERMES_DASHBOARD_BASIC_AUTH_USERNAME=workframe
|
|
168
|
-
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD=${example ? '' : generateDashboardAuthPassword()}
|
|
169
|
-
HERMES_DASHBOARD_BASIC_AUTH_SECRET=${example ? '' : generateDashboardAuthSecret()}
|
|
170
|
-
# Credential vault KEK (32-byte base64). Required when WORKFRAME_DEPLOYMENT_MODE=public_multi_user.
|
|
171
|
-
# Generate: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
|
|
172
|
-
WORKFRAME_VAULT_KEK=${example ? '' : ''}
|
|
173
|
-
APP_BASE_URL=http://127.0.0.1:${ports.ui}
|
|
174
|
-
`;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Existing host Hermes install (never overwrites). Windows: %LOCALAPPDATA%\\hermes */
|
|
178
|
-
export function detectHermesHome() {
|
|
179
|
-
const fromEnv = String(process.env.HERMES_HOME || '').trim();
|
|
180
|
-
if (fromEnv && fs.existsSync(path.join(fromEnv, 'config.yaml'))) return fromEnv;
|
|
181
|
-
const candidates = [];
|
|
182
|
-
if (process.platform === 'win32') {
|
|
183
|
-
const local = process.env.LOCALAPPDATA;
|
|
184
|
-
if (local) candidates.push(path.join(local, 'hermes'));
|
|
185
|
-
} else if (process.platform === 'darwin') {
|
|
186
|
-
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'hermes'));
|
|
187
|
-
}
|
|
188
|
-
candidates.push(path.join(os.homedir(), '.hermes'));
|
|
189
|
-
for (const dir of candidates) {
|
|
190
|
-
if (fs.existsSync(path.join(dir, 'config.yaml'))) return dir;
|
|
191
|
-
}
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function resolveDeployMode(requested = 'auto') {
|
|
196
|
-
const mode = String(requested || 'auto').trim().toLowerCase();
|
|
197
|
-
if (mode === 'native' || mode === 'docker') return mode;
|
|
198
|
-
return detectHermesHome() ? 'native' : 'docker';
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ponytail: runnable self-check — node scripts/lib/install-identity.mjs
|
|
202
|
-
import { pathToFileURL } from 'node:url';
|
|
203
|
-
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
204
|
-
if (isMain) {
|
|
205
|
-
const p1 = portsForSlot(1);
|
|
206
|
-
const p2 = portsForSlot(2);
|
|
207
|
-
console.assert(p1.ui === 18644 && p1.api === 19120 && p1.supervisor === 18090, 'slot 1 ports');
|
|
208
|
-
console.assert(p2.ui === 28644 && p2.gateway === 28642, 'slot 2 ports');
|
|
209
|
-
console.assert(sessionCookieNameFromEnv({ WORKFRAME_INSTALL_ID: 'wf_abc123' }) === 'wf_abc123_session');
|
|
210
|
-
allocateInstall({ projectName: 'Test', preferredSlot: 99 }).catch(() => {});
|
|
211
|
-
console.log('install-identity ok');
|
|
212
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Per-install identity: slot-based localhost ports + stable install id for cookies.
|
|
3
|
+
*
|
|
4
|
+
* Slot N (1–99) → host ports N*10000 + tail:
|
|
5
|
+
* gateway 8642, dashboard 9119, api 9120, ui 8644, supervisor 8090
|
|
6
|
+
* slot 1 → 18642, 19119, 19120, 18644, 18090
|
|
7
|
+
* slot 2 → 28642, 29119, 29120, 28644, 28090
|
|
8
|
+
*
|
|
9
|
+
* ponytail: cookies use WORKFRAME_INSTALL_ID (not display name); auth DB is per install so same email can sign into many.
|
|
10
|
+
*/
|
|
11
|
+
import crypto from 'node:crypto';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import net from 'node:net';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
export const PORT_TAIL = Object.freeze({
|
|
18
|
+
gateway: 8642,
|
|
19
|
+
dashboard: 9119,
|
|
20
|
+
api: 9120,
|
|
21
|
+
ui: 8644,
|
|
22
|
+
supervisor: 8090,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export function portsForSlot(slot) {
|
|
26
|
+
const n = Number(slot);
|
|
27
|
+
if (!Number.isInteger(n) || n < 1 || n > 99) {
|
|
28
|
+
throw new Error(`WORKFRAME_SLOT must be 1–99, got ${slot}`);
|
|
29
|
+
}
|
|
30
|
+
const base = n * 10_000;
|
|
31
|
+
return {
|
|
32
|
+
slot: n,
|
|
33
|
+
gateway: base + PORT_TAIL.gateway,
|
|
34
|
+
dashboard: base + PORT_TAIL.dashboard,
|
|
35
|
+
api: base + PORT_TAIL.api,
|
|
36
|
+
ui: base + PORT_TAIL.ui,
|
|
37
|
+
supervisor: base + PORT_TAIL.supervisor,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function generateSupervisorToken() {
|
|
42
|
+
return crypto.randomBytes(32).toString('hex');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function generateProxyToken() {
|
|
46
|
+
return crypto.randomBytes(32).toString('base64url');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function generateApiToken() {
|
|
50
|
+
return crypto.randomBytes(32).toString('base64url');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function generateDashboardAuthPassword() {
|
|
54
|
+
return crypto.randomBytes(18).toString('base64url');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function generateDashboardAuthSecret() {
|
|
58
|
+
return crypto.randomBytes(24).toString('hex');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function generateVaultKekB64() {
|
|
62
|
+
return crypto.randomBytes(32).toString('base64');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function generateInstallId() {
|
|
66
|
+
return `wf_${crypto.randomBytes(6).toString('hex')}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function sessionCookieNameFromEnv(env = process.env) {
|
|
70
|
+
const installId = String(env.WORKFRAME_INSTALL_ID || '').trim();
|
|
71
|
+
if (installId) {
|
|
72
|
+
const safe = installId.replace(/[^a-zA-Z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
|
|
73
|
+
if (safe) return `${safe}_session`;
|
|
74
|
+
}
|
|
75
|
+
const slug = String(env.WORKFRAME_PROJECT || 'workframe')
|
|
76
|
+
.toLowerCase()
|
|
77
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
78
|
+
.replace(/^_+|_+$/g, '') || 'workframe';
|
|
79
|
+
return `wf_${slug}_session`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function portTaken(port, host = '127.0.0.1') {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const socket = net.createConnection({ port, host });
|
|
85
|
+
const done = (taken) => {
|
|
86
|
+
socket.removeAllListeners();
|
|
87
|
+
socket.destroy();
|
|
88
|
+
resolve(taken);
|
|
89
|
+
};
|
|
90
|
+
socket.setTimeout(400);
|
|
91
|
+
socket.once('connect', () => done(true));
|
|
92
|
+
socket.once('timeout', () => done(false));
|
|
93
|
+
socket.once('error', () => done(false));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function slotPortsFree(slot, host = '127.0.0.1') {
|
|
98
|
+
const ports = portsForSlot(slot);
|
|
99
|
+
for (const key of ['gateway', 'dashboard', 'api', 'ui']) {
|
|
100
|
+
if (await portTaken(ports[key], host)) return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Pick the first free slot, or use preferredSlot when its ports are free.
|
|
107
|
+
*/
|
|
108
|
+
export async function allocateInstall({
|
|
109
|
+
projectName,
|
|
110
|
+
preferredSlot = null,
|
|
111
|
+
maxSlot = 9,
|
|
112
|
+
host = '127.0.0.1',
|
|
113
|
+
installId = null,
|
|
114
|
+
} = {}) {
|
|
115
|
+
if (preferredSlot != null) {
|
|
116
|
+
const slot = Number(preferredSlot);
|
|
117
|
+
if (!await slotPortsFree(slot, host)) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`WORKFRAME_SLOT ${slot} ports are in use (${JSON.stringify(portsForSlot(slot))})`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
installId: installId || generateInstallId(),
|
|
124
|
+
projectName,
|
|
125
|
+
slot,
|
|
126
|
+
ports: portsForSlot(slot),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (let slot = 1; slot <= maxSlot; slot++) {
|
|
131
|
+
if (await slotPortsFree(slot, host)) {
|
|
132
|
+
return {
|
|
133
|
+
installId: installId || generateInstallId(),
|
|
134
|
+
projectName,
|
|
135
|
+
slot,
|
|
136
|
+
ports: portsForSlot(slot),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`No free Workframe install slot (1–${maxSlot}) on ${host}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function envFileLines(install, { example = false, nativeProfile = '', deploy = 'docker', hermesHome = '' } = {}) {
|
|
144
|
+
const header = example
|
|
145
|
+
? '# Copy to .env — one install = one slot + one WORKFRAME_INSTALL_ID.\n# Same email may be used across installs; each has its own auth DB.\n'
|
|
146
|
+
: '# Local Workframe install identity (.env is gitignored).\n';
|
|
147
|
+
const { ports, installId, slot } = install;
|
|
148
|
+
const deployLine = deploy === 'native' ? 'WORKFRAME_DEPLOY=native\n' : 'WORKFRAME_DEPLOY=docker\n';
|
|
149
|
+
const hermesLine = hermesHome ? `HERMES_DATA=${hermesHome.replace(/\\/g, '/')}\n` : '';
|
|
150
|
+
return `${header}${deployLine}${hermesLine}WORKFRAME_INSTALL_ID=${installId}
|
|
151
|
+
WORKFRAME_SLOT=${slot}
|
|
152
|
+
WORKFRAME_PROJECT=${install.projectName}
|
|
153
|
+
WORKFRAME_GATEWAY_PORT=${ports.gateway}
|
|
154
|
+
WORKFRAME_DASHBOARD_PORT=${ports.dashboard}
|
|
155
|
+
WORKFRAME_UI_PORT=${ports.ui}
|
|
156
|
+
WORKFRAME_UI_STATIC_DIR=./workframe-ui/public
|
|
157
|
+
WORKFRAME_API_PORT=${ports.api}
|
|
158
|
+
WORKFRAME_SUPERVISOR_PORT=${ports.supervisor}
|
|
159
|
+
WORKFRAME_MISSION_PORT=${ports.api}
|
|
160
|
+
WORKFRAME_NATIVE_PROFILE=${nativeProfile}
|
|
161
|
+
SECURE_MODE=true
|
|
162
|
+
DEV_LOCAL_UNSAFE=false
|
|
163
|
+
WORKFRAME_DEPLOYMENT_MODE=trusted_team
|
|
164
|
+
WORKFRAME_API_TOKEN=${example ? '' : generateApiToken()}
|
|
165
|
+
WORKFRAME_SUPERVISOR_TOKEN=${example ? '' : generateSupervisorToken()}
|
|
166
|
+
WORKFRAME_PROXY_TOKEN=${example ? '' : generateProxyToken()}
|
|
167
|
+
HERMES_DASHBOARD_BASIC_AUTH_USERNAME=workframe
|
|
168
|
+
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD=${example ? '' : generateDashboardAuthPassword()}
|
|
169
|
+
HERMES_DASHBOARD_BASIC_AUTH_SECRET=${example ? '' : generateDashboardAuthSecret()}
|
|
170
|
+
# Credential vault KEK (32-byte base64). Required when WORKFRAME_DEPLOYMENT_MODE=public_multi_user.
|
|
171
|
+
# Generate: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
|
|
172
|
+
WORKFRAME_VAULT_KEK=${example ? '' : ''}
|
|
173
|
+
APP_BASE_URL=http://127.0.0.1:${ports.ui}
|
|
174
|
+
`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Existing host Hermes install (never overwrites). Windows: %LOCALAPPDATA%\\hermes */
|
|
178
|
+
export function detectHermesHome() {
|
|
179
|
+
const fromEnv = String(process.env.HERMES_HOME || '').trim();
|
|
180
|
+
if (fromEnv && fs.existsSync(path.join(fromEnv, 'config.yaml'))) return fromEnv;
|
|
181
|
+
const candidates = [];
|
|
182
|
+
if (process.platform === 'win32') {
|
|
183
|
+
const local = process.env.LOCALAPPDATA;
|
|
184
|
+
if (local) candidates.push(path.join(local, 'hermes'));
|
|
185
|
+
} else if (process.platform === 'darwin') {
|
|
186
|
+
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'hermes'));
|
|
187
|
+
}
|
|
188
|
+
candidates.push(path.join(os.homedir(), '.hermes'));
|
|
189
|
+
for (const dir of candidates) {
|
|
190
|
+
if (fs.existsSync(path.join(dir, 'config.yaml'))) return dir;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function resolveDeployMode(requested = 'auto') {
|
|
196
|
+
const mode = String(requested || 'auto').trim().toLowerCase();
|
|
197
|
+
if (mode === 'native' || mode === 'docker') return mode;
|
|
198
|
+
return detectHermesHome() ? 'native' : 'docker';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ponytail: runnable self-check — node scripts/lib/install-identity.mjs
|
|
202
|
+
import { pathToFileURL } from 'node:url';
|
|
203
|
+
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
204
|
+
if (isMain) {
|
|
205
|
+
const p1 = portsForSlot(1);
|
|
206
|
+
const p2 = portsForSlot(2);
|
|
207
|
+
console.assert(p1.ui === 18644 && p1.api === 19120 && p1.supervisor === 18090, 'slot 1 ports');
|
|
208
|
+
console.assert(p2.ui === 28644 && p2.gateway === 28642, 'slot 2 ports');
|
|
209
|
+
console.assert(sessionCookieNameFromEnv({ WORKFRAME_INSTALL_ID: 'wf_abc123' }) === 'wf_abc123_session');
|
|
210
|
+
allocateInstall({ projectName: 'Test', preferredSlot: 99 }).catch(() => {});
|
|
211
|
+
console.log('install-identity ok');
|
|
212
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Restart Hermes gateway container only — no image pull. Preserves runtime/Agents.
|
|
3
|
-
set -euo pipefail
|
|
4
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
-
# shellcheck source=compose-docker-host.sh
|
|
6
|
-
source "$SCRIPT_DIR/compose-docker-host.sh"
|
|
7
|
-
|
|
8
|
-
echo "=== Hermes gateway restart ==="
|
|
9
|
-
workframe_compose_prepare
|
|
10
|
-
echo "Compose dir: $compose_cd"
|
|
11
|
-
workframe_compose up -d --force-recreate --no-deps gateway
|
|
12
|
-
echo "=== Gateway restart complete ==="
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Restart Hermes gateway container only — no image pull. Preserves runtime/Agents.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
# shellcheck source=compose-docker-host.sh
|
|
6
|
+
source "$SCRIPT_DIR/compose-docker-host.sh"
|
|
7
|
+
|
|
8
|
+
echo "=== Hermes gateway restart ==="
|
|
9
|
+
workframe_compose_prepare
|
|
10
|
+
echo "Compose dir: $compose_cd"
|
|
11
|
+
workframe_compose up -d --force-recreate --no-deps gateway
|
|
12
|
+
echo "=== Gateway restart complete ==="
|
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Set public URL keys in compose .env (APP_BASE_URL, WORKFRAME_PUBLIC_HOST, CORS, ALLOWED_HOSTS).
|
|
4
|
-
* HERMES_DASHBOARD_PUBLIC_URL is derived from APP_BASE_URL in docker-compose.yml.
|
|
5
|
-
* Usage: node set-compose-public-url.mjs https://dev.example.com [--env path/to/.env]
|
|
6
|
-
*/
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import { fileURLToPath } from 'node:url';
|
|
10
|
-
|
|
11
|
-
const args = process.argv.slice(2);
|
|
12
|
-
|
|
13
|
-
if (args.includes('--self-check')) {
|
|
14
|
-
function normalizePublicUrl(raw) {
|
|
15
|
-
let u = String(raw || '').trim();
|
|
16
|
-
if (!u) throw new Error('url required');
|
|
17
|
-
if (!/^https?:\/\//i.test(u)) u = `https://${u}`;
|
|
18
|
-
const parsed = new URL(u);
|
|
19
|
-
if (!parsed.hostname) throw new Error('invalid hostname');
|
|
20
|
-
return `https://${parsed.hostname}`;
|
|
21
|
-
}
|
|
22
|
-
if (normalizePublicUrl('dev.example.com') !== 'https://dev.example.com') {
|
|
23
|
-
throw new Error('normalizePublicUrl failed');
|
|
24
|
-
}
|
|
25
|
-
console.log('self-check ok');
|
|
26
|
-
process.exit(0);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const envFlag = args.indexOf('--env');
|
|
30
|
-
let envPath =
|
|
31
|
-
envFlag >= 0 ? args[envFlag + 1] : path.join(path.dirname(fileURLToPath(import.meta.url)), '../../infra/compose/workframe/.env');
|
|
32
|
-
const urlArg = args.find((a) => !a.startsWith('--') && a !== envPath);
|
|
33
|
-
|
|
34
|
-
if (!urlArg?.trim()) {
|
|
35
|
-
console.error('Usage: node set-compose-public-url.mjs <https://host> [--env path/to/.env]');
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function normalizePublicUrl(raw) {
|
|
40
|
-
let u = String(raw || '').trim();
|
|
41
|
-
if (!u) throw new Error('url required');
|
|
42
|
-
if (!/^https?:\/\//i.test(u)) u = `https://${u}`;
|
|
43
|
-
const parsed = new URL(u);
|
|
44
|
-
if (!parsed.hostname) throw new Error('invalid hostname');
|
|
45
|
-
return `https://${parsed.hostname}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function hostnameFromUrl(url) {
|
|
49
|
-
return new URL(url).hostname;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function setKv(text, key, val) {
|
|
53
|
-
const line = `${key}=${val}`;
|
|
54
|
-
const re = new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}=.*$`, 'm');
|
|
55
|
-
if (re.test(text)) return text.replace(re, line);
|
|
56
|
-
return `${text}${text.endsWith('\n') || !text ? '' : '\n'}${line}\n`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const publicUrl = normalizePublicUrl(urlArg);
|
|
60
|
-
const host = hostnameFromUrl(publicUrl);
|
|
61
|
-
|
|
62
|
-
if (!fs.existsSync(envPath)) {
|
|
63
|
-
const example = `${envPath}.example`;
|
|
64
|
-
if (fs.existsSync(example)) {
|
|
65
|
-
fs.mkdirSync(path.dirname(envPath), { recursive: true });
|
|
66
|
-
fs.copyFileSync(example, envPath);
|
|
67
|
-
console.log(`Created ${envPath} from example`);
|
|
68
|
-
} else {
|
|
69
|
-
throw new Error(`Missing env file: ${envPath}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let text = fs.readFileSync(envPath, 'utf8');
|
|
74
|
-
text = setKv(text, 'APP_BASE_URL', publicUrl);
|
|
75
|
-
text = setKv(text, 'WORKFRAME_PUBLIC_HOST', host);
|
|
76
|
-
text = setKv(text, 'ALLOWED_HOSTS', host);
|
|
77
|
-
text = setKv(text, 'CORS_ALLOW_ORIGIN', publicUrl);
|
|
78
|
-
fs.writeFileSync(envPath, text);
|
|
79
|
-
|
|
80
|
-
console.log(
|
|
81
|
-
JSON.stringify(
|
|
82
|
-
{
|
|
83
|
-
ok: true,
|
|
84
|
-
env: envPath,
|
|
85
|
-
app_base_url: publicUrl,
|
|
86
|
-
hermes_dashboard_public_url: `${publicUrl}/hermes-dashboard`,
|
|
87
|
-
host,
|
|
88
|
-
},
|
|
89
|
-
null,
|
|
90
|
-
2,
|
|
91
|
-
),
|
|
92
|
-
);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Set public URL keys in compose .env (APP_BASE_URL, WORKFRAME_PUBLIC_HOST, CORS, ALLOWED_HOSTS).
|
|
4
|
+
* HERMES_DASHBOARD_PUBLIC_URL is derived from APP_BASE_URL in docker-compose.yml.
|
|
5
|
+
* Usage: node set-compose-public-url.mjs https://dev.example.com [--env path/to/.env]
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
|
|
13
|
+
if (args.includes('--self-check')) {
|
|
14
|
+
function normalizePublicUrl(raw) {
|
|
15
|
+
let u = String(raw || '').trim();
|
|
16
|
+
if (!u) throw new Error('url required');
|
|
17
|
+
if (!/^https?:\/\//i.test(u)) u = `https://${u}`;
|
|
18
|
+
const parsed = new URL(u);
|
|
19
|
+
if (!parsed.hostname) throw new Error('invalid hostname');
|
|
20
|
+
return `https://${parsed.hostname}`;
|
|
21
|
+
}
|
|
22
|
+
if (normalizePublicUrl('dev.example.com') !== 'https://dev.example.com') {
|
|
23
|
+
throw new Error('normalizePublicUrl failed');
|
|
24
|
+
}
|
|
25
|
+
console.log('self-check ok');
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const envFlag = args.indexOf('--env');
|
|
30
|
+
let envPath =
|
|
31
|
+
envFlag >= 0 ? args[envFlag + 1] : path.join(path.dirname(fileURLToPath(import.meta.url)), '../../infra/compose/workframe/.env');
|
|
32
|
+
const urlArg = args.find((a) => !a.startsWith('--') && a !== envPath);
|
|
33
|
+
|
|
34
|
+
if (!urlArg?.trim()) {
|
|
35
|
+
console.error('Usage: node set-compose-public-url.mjs <https://host> [--env path/to/.env]');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizePublicUrl(raw) {
|
|
40
|
+
let u = String(raw || '').trim();
|
|
41
|
+
if (!u) throw new Error('url required');
|
|
42
|
+
if (!/^https?:\/\//i.test(u)) u = `https://${u}`;
|
|
43
|
+
const parsed = new URL(u);
|
|
44
|
+
if (!parsed.hostname) throw new Error('invalid hostname');
|
|
45
|
+
return `https://${parsed.hostname}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hostnameFromUrl(url) {
|
|
49
|
+
return new URL(url).hostname;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function setKv(text, key, val) {
|
|
53
|
+
const line = `${key}=${val}`;
|
|
54
|
+
const re = new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}=.*$`, 'm');
|
|
55
|
+
if (re.test(text)) return text.replace(re, line);
|
|
56
|
+
return `${text}${text.endsWith('\n') || !text ? '' : '\n'}${line}\n`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const publicUrl = normalizePublicUrl(urlArg);
|
|
60
|
+
const host = hostnameFromUrl(publicUrl);
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(envPath)) {
|
|
63
|
+
const example = `${envPath}.example`;
|
|
64
|
+
if (fs.existsSync(example)) {
|
|
65
|
+
fs.mkdirSync(path.dirname(envPath), { recursive: true });
|
|
66
|
+
fs.copyFileSync(example, envPath);
|
|
67
|
+
console.log(`Created ${envPath} from example`);
|
|
68
|
+
} else {
|
|
69
|
+
throw new Error(`Missing env file: ${envPath}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let text = fs.readFileSync(envPath, 'utf8');
|
|
74
|
+
text = setKv(text, 'APP_BASE_URL', publicUrl);
|
|
75
|
+
text = setKv(text, 'WORKFRAME_PUBLIC_HOST', host);
|
|
76
|
+
text = setKv(text, 'ALLOWED_HOSTS', host);
|
|
77
|
+
text = setKv(text, 'CORS_ALLOW_ORIGIN', publicUrl);
|
|
78
|
+
fs.writeFileSync(envPath, text);
|
|
79
|
+
|
|
80
|
+
console.log(
|
|
81
|
+
JSON.stringify(
|
|
82
|
+
{
|
|
83
|
+
ok: true,
|
|
84
|
+
env: envPath,
|
|
85
|
+
app_base_url: publicUrl,
|
|
86
|
+
hermes_dashboard_public_url: `${publicUrl}/hermes-dashboard`,
|
|
87
|
+
host,
|
|
88
|
+
},
|
|
89
|
+
null,
|
|
90
|
+
2,
|
|
91
|
+
),
|
|
92
|
+
);
|