@dylanpiercey/work 0.0.119 → 0.0.121
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/README.md +35 -8
- package/bin/work.mjs +380 -41
- package/dist/_B1lVb01g.js +2 -0
- package/dist/_Bjh5JEm7.js +2 -0
- package/dist/_CfwTkTbL.js +352 -0
- package/dist/{_BOIUqihs.js → _CivNK9fo.js} +20 -18
- package/dist/{_DE2RUdbF.js → _Cr71Xd0d.js} +8 -3
- package/dist/_DJyHc1w-.js +1739 -0
- package/dist/{_BIhoUpsW.js → _DnaZVHJ7.js} +95 -94
- package/dist/_Dpa8WFuv.js +2 -0
- package/dist/index.mjs +4301 -4236
- package/dist/public/assets/{_BSsp7Z4i2.js → _-zdUyUYM2.js} +1 -1
- package/dist/public/assets/_-zdUyUYM2.js.br +0 -0
- package/dist/public/assets/{_LUNsNRnJ2.js → _0L2K8sFq2.js} +1 -1
- package/dist/public/assets/_0L2K8sFq2.js.br +0 -0
- package/dist/public/assets/{_BE3wNPJo.js → _42jdam49.js} +1 -1
- package/dist/public/assets/_42jdam49.js.br +0 -0
- package/dist/public/assets/{_CIFN7c1t2.js → _9dJ8BnyZ2.js} +1 -1
- package/dist/public/assets/_9dJ8BnyZ2.js.br +0 -0
- package/dist/public/assets/{_CqDb111T.js → _B03G1Nvs.js} +1 -1
- package/dist/public/assets/_B03G1Nvs.js.br +0 -0
- package/dist/public/assets/{_Dzy9r1lm.js → _B0dZ7_qK.js} +1 -1
- package/dist/public/assets/_B0dZ7_qK.js.br +0 -0
- package/dist/public/assets/{_c1YZLlfE.js → _B1VuWiKK.js} +1 -1
- package/dist/public/assets/_B1VuWiKK.js.br +0 -0
- package/dist/public/assets/{_Bp1b0XWB2.js → _B5nrQZIq2.js} +1 -1
- package/dist/public/assets/_B5nrQZIq2.js.br +0 -0
- package/dist/public/assets/{_Bq2fn_8D.js → _B8ZQ8inF.js} +1 -1
- package/dist/public/assets/_B8ZQ8inF.js.br +0 -0
- package/dist/public/assets/{_CvnxxlPb.js → _B9e51G1K.js} +1 -1
- package/dist/public/assets/_B9e51G1K.js.br +0 -0
- package/dist/public/assets/{_CUjnrp_u2.js → _BH015W2i2.js} +1 -1
- package/dist/public/assets/_BH015W2i2.js.br +0 -0
- package/dist/public/assets/{_B62GzYdT.js → _BHjcfVXB.js} +6 -6
- package/dist/public/assets/_BHjcfVXB.js.br +0 -0
- package/dist/public/assets/{_DMHXo8uX.js → _BNIilCsC.js} +1 -1
- package/dist/public/assets/_BNIilCsC.js.br +0 -0
- package/dist/public/assets/{_szXVEFuj2.js → _BPgL37mi2.js} +1 -1
- package/dist/public/assets/_BPgL37mi2.js.br +0 -0
- package/dist/public/assets/{_Ctuh4IDL2.js → _BR3D4nzj2.js} +1 -1
- package/dist/public/assets/_BR3D4nzj2.js.br +0 -0
- package/dist/public/assets/{_BkTJUq-u.js → _BRSJwfZ3.js} +1 -1
- package/dist/public/assets/{_DS628Dnv.js → _BSFENb4l.js} +1 -1
- package/dist/public/assets/_BSFENb4l.js.br +0 -0
- package/dist/public/assets/{_Btt4k5w3.js → _BSWpz0dK.js} +1 -1
- package/dist/public/assets/_BSWpz0dK.js.br +0 -0
- package/dist/public/assets/{_N0vzdKCD.js → _BUOWar32.js} +1 -1
- package/dist/public/assets/_BUOWar32.js.br +0 -0
- package/dist/public/assets/{_1bmjwuWH.js → _BZWlvMSJ.js} +1 -1
- package/dist/public/assets/_BZWlvMSJ.js.br +0 -0
- package/dist/public/assets/{_BlVrxgss2.js → _B_dMglA92.js} +1 -1
- package/dist/public/assets/_B_dMglA92.js.br +0 -0
- package/dist/public/assets/{_XgOGaUKD.js → _Beb6Lb1w.js} +1 -1
- package/dist/public/assets/_Beb6Lb1w.js.br +0 -0
- package/dist/public/assets/{_CXQ_K2Vh2.js → _BjIMoOV-2.js} +1 -1
- package/dist/public/assets/_BjIMoOV-2.js.br +0 -0
- package/dist/public/assets/{_Dhu480me.js → _BlLuOKRT.js} +1 -1
- package/dist/public/assets/_BlLuOKRT.js.br +0 -0
- package/dist/public/assets/{_BijwdXvp.js → _BlXwius8.js} +1 -1
- package/dist/public/assets/_BlXwius8.js.br +0 -0
- package/dist/public/assets/{_DBJ43cPF.js → _Bmt76eWT.js} +1 -1
- package/dist/public/assets/_Bmt76eWT.js.br +0 -0
- package/dist/public/assets/{_CkuVE2zK.js → _BoS5LU5l.js} +1 -1
- package/dist/public/assets/_BoS5LU5l.js.br +0 -0
- package/dist/public/assets/{_DtLQVzKB2.js → _BsNKabYu2.js} +1 -1
- package/dist/public/assets/_BsNKabYu2.js.br +0 -0
- package/dist/public/assets/{_CEWjw4lq.js → _C0OXw3Vy.js} +1 -1
- package/dist/public/assets/_C0OXw3Vy.js.br +0 -0
- package/dist/public/assets/{_CpRKjPVO2.js → _CBUJNdeq2.js} +1 -1
- package/dist/public/assets/_CBUJNdeq2.js.br +0 -0
- package/dist/public/assets/{_CZzZOY4P2.js → _CCNhM32c2.js} +1 -1
- package/dist/public/assets/_CCNhM32c2.js.br +0 -0
- package/dist/public/assets/{_BgnvPND_.js → _CFBaZNUT.js} +1 -1
- package/dist/public/assets/_CFBaZNUT.js.br +0 -0
- package/dist/public/assets/{_CJT7ujcv2.js → _CJHqzl--2.js} +1 -1
- package/dist/public/assets/_CJHqzl--2.js.br +0 -0
- package/dist/public/assets/{_duTaTgEk2.js → _CMGgQ-4H2.js} +1 -1
- package/dist/public/assets/_CMGgQ-4H2.js.br +0 -0
- package/dist/public/assets/{_5YlmpA6X.js → _CPX0D-my.js} +1 -1
- package/dist/public/assets/_CPX0D-my.js.br +0 -0
- package/dist/public/assets/{_BYYDEuFR2.js → _CRuHDn_O2.js} +1 -1
- package/dist/public/assets/_CRuHDn_O2.js.br +0 -0
- package/dist/public/assets/{_CnKbN1QV.js → _CUOao_Hj.js} +1 -1
- package/dist/public/assets/_CUOao_Hj.js.br +0 -0
- package/dist/public/assets/{_CZejW_fq.js → _CUVw5V4z.js} +1 -1
- package/dist/public/assets/_CUVw5V4z.js.br +0 -0
- package/dist/public/assets/{_DO4n-NwM.js → _CYMGOhCb.js} +1 -1
- package/dist/public/assets/_CYMGOhCb.js.br +0 -0
- package/dist/public/assets/{_ER-_pvil.js → _CZfap_Dj.js} +1 -1
- package/dist/public/assets/_CZfap_Dj.js.br +0 -0
- package/dist/public/assets/{_DYngS949.js → _CbNmSe5k.js} +1 -1
- package/dist/public/assets/_CbNmSe5k.js.br +0 -0
- package/dist/public/assets/{_DKciAPQl.js → _CdtuhZSc.js} +1 -1
- package/dist/public/assets/_CdtuhZSc.js.br +0 -0
- package/dist/public/assets/{_B7w9Jp5h.js → _ClhApln3.js} +1 -1
- package/dist/public/assets/_ClhApln3.js.br +0 -0
- package/dist/public/assets/{_BD9TBsnM2.js → _CvA5IOAW2.js} +1 -1
- package/dist/public/assets/_CvA5IOAW2.js.br +0 -0
- package/dist/public/assets/{_Cp-yobQE2.js → _CwZYmUdt2.js} +1 -1
- package/dist/public/assets/{_MofKWwY9.js → _CxrcWZPE.js} +1 -1
- package/dist/public/assets/_CxrcWZPE.js.br +0 -0
- package/dist/public/assets/{_RhYe7Uqa.js → _D-KjGTSW.js} +24 -21
- package/dist/public/assets/_D-KjGTSW.js.br +0 -0
- package/dist/public/assets/{_BRiOyxeF.js → _D4u4SpiK.js} +1 -1
- package/dist/public/assets/_D4u4SpiK.js.br +0 -0
- package/dist/public/assets/{_D8raMSvy.js → _D6CjH3JN.js} +1 -1
- package/dist/public/assets/_D6CjH3JN.js.br +0 -0
- package/dist/public/assets/{_Cym2lUeD2.js → _D7cQLLGK2.js} +1 -1
- package/dist/public/assets/_D7cQLLGK2.js.br +0 -0
- package/dist/public/assets/{_C8DWM9rr.js → _DBoABI85.js} +1 -1
- package/dist/public/assets/_DBoABI85.js.br +0 -0
- package/dist/public/assets/{_Rf_7ZEPL.js → _DNcd1xtp.js} +1 -1
- package/dist/public/assets/_DNcd1xtp.js.br +0 -0
- package/dist/public/assets/{_Bhwci4Zy.js → _DQGTProt.js} +1 -1
- package/dist/public/assets/_DQGTProt.js.br +0 -0
- package/dist/public/assets/{_B2g0gJmZ.js → _DSIH6_Li.js} +1 -1
- package/dist/public/assets/_DSIH6_Li.js.br +0 -0
- package/dist/public/assets/{_T2999saD2.js → _DXHf4WHs2.js} +1 -1
- package/dist/public/assets/_DXHf4WHs2.js.br +0 -0
- package/dist/public/assets/{_CoStmjbL.js → _D_qDhyn2.js} +1 -1
- package/dist/public/assets/_D_qDhyn2.js.br +0 -0
- package/dist/public/assets/{_CGOYaUuV.js → _DcMfCkmd.js} +1 -1
- package/dist/public/assets/_DcMfCkmd.js.br +0 -0
- package/dist/public/assets/{_Cv9Hgy8P.js → _DgDEdxEu.js} +1 -1
- package/dist/public/assets/_DgDEdxEu.js.br +0 -0
- package/dist/public/assets/{_Bsk9ewWF.js → _DlhDim1D.js} +1 -1
- package/dist/public/assets/_DlhDim1D.js.br +0 -0
- package/dist/public/assets/{_BNQgBkwF.js → _DqLib98x.js} +1 -1
- package/dist/public/assets/_DqLib98x.js.br +0 -0
- package/dist/public/assets/{_l9vJoVeP.js → _Ds0Qa0YX.js} +1 -1
- package/dist/public/assets/_Ds0Qa0YX.js.br +0 -0
- package/dist/public/assets/{_iDn41XPe2.js → _DyUvL09k2.js} +1 -1
- package/dist/public/assets/_DyUvL09k2.js.br +0 -0
- package/dist/public/assets/{_AqmX0pBH2.js → _J6xlcydH2.js} +1 -1
- package/dist/public/assets/_J6xlcydH2.js.br +0 -0
- package/dist/public/assets/{_Dni1s_3Q2.js → _MInulccf2.js} +1 -1
- package/dist/public/assets/{_BsOtzbIC.js → _QMVFINxi.js} +1 -1
- package/dist/public/assets/_QMVFINxi.js.br +0 -0
- package/dist/public/assets/{_CWNv7bdi.js → _S9QQtt68.js} +1 -1
- package/dist/public/assets/_S9QQtt68.js.br +0 -0
- package/dist/public/assets/{_DlZyjzE_.js → _XCJLLGxV.js} +1 -1
- package/dist/public/assets/_XCJLLGxV.js.br +0 -0
- package/dist/public/assets/{_B2LXCPdE.js → _Y-bTh0dW.js} +1 -1
- package/dist/public/assets/_Y-bTh0dW.js.br +0 -0
- package/dist/public/assets/{_zPAwS_-H.js → _Y8TOk1pR.js} +1 -1
- package/dist/public/assets/_Y8TOk1pR.js.br +0 -0
- package/dist/public/assets/{_CzI2QhH4.js → _YetFhXkM.js} +1 -1
- package/dist/public/assets/_YetFhXkM.js.br +0 -0
- package/dist/public/assets/{_O8R4l7yw2.js → _YmJopjX92.js} +1 -1
- package/dist/public/assets/_YmJopjX92.js.br +0 -0
- package/dist/public/assets/{_Du5pjG7G.js → _mbzaOqik.js} +1 -1
- package/dist/public/assets/_mbzaOqik.js.br +0 -0
- package/dist/public/assets/{_qj_i_cqe.js → _rMcfapaa.js} +1 -1
- package/dist/public/assets/_rMcfapaa.js.br +0 -0
- package/dist/public/assets/{_yTOoKAfM2.js → _rRtbbfKJ2.js} +1 -1
- package/dist/public/assets/_rRtbbfKJ2.js.br +0 -0
- package/dist/public/assets/{_BD68UZIf.js → _sSWgnHeU.js} +1 -1
- package/dist/public/assets/_sSWgnHeU.js.br +0 -0
- package/dist/public/assets/{_CegJB2vl.js → _xOeYy_3v.js} +1 -1
- package/dist/public/assets/_xOeYy_3v.js.br +0 -0
- package/dist/public/assets/app-page-BWf0w6W9.css +1 -0
- package/dist/public/assets/app-page-BWf0w6W9.css.br +0 -0
- package/dist/public/assets/diff-view-BZaYYEDK.css +1 -0
- package/dist/public/assets/diff-view-BZaYYEDK.css.br +0 -0
- package/dist/public/assets/new-K-7z0qE8.js +1 -0
- package/dist/public/assets/settings-K-7z0qE8.js +1 -0
- package/dist/public/assets/w-K-7z0qE8.js +1 -0
- package/dist/public/assets/w._-D5d4klUv.js +1 -0
- package/dist/public/assets/w._-K-7z0qE8.js +1 -0
- package/lib/review/markdown.mjs +6 -2
- package/lib/setup/catalog.d.mts +3 -1
- package/lib/setup/catalog.mjs +11 -2
- package/lib/setup/doctor.mjs +96 -20
- package/lib/setup/install.d.mts +9 -3
- package/lib/setup/install.mjs +68 -39
- package/lib/share/manifest.d.mts +27 -0
- package/lib/share/manifest.mjs +144 -0
- package/package.json +6 -2
- package/scripts/migrate.mjs +101 -30
- package/dist/_BIDn_RW1.js +0 -2
- package/dist/public/assets/_1bmjwuWH.js.br +0 -0
- package/dist/public/assets/_5YlmpA6X.js.br +0 -0
- package/dist/public/assets/_AqmX0pBH2.js.br +0 -0
- package/dist/public/assets/_B2LXCPdE.js.br +0 -0
- package/dist/public/assets/_B2g0gJmZ.js.br +0 -0
- package/dist/public/assets/_B62GzYdT.js.br +0 -0
- package/dist/public/assets/_B7w9Jp5h.js.br +0 -0
- package/dist/public/assets/_BD68UZIf.js.br +0 -0
- package/dist/public/assets/_BD9TBsnM2.js.br +0 -0
- package/dist/public/assets/_BE3wNPJo.js.br +0 -0
- package/dist/public/assets/_BNQgBkwF.js.br +0 -0
- package/dist/public/assets/_BRiOyxeF.js.br +0 -0
- package/dist/public/assets/_BSsp7Z4i2.js.br +0 -0
- package/dist/public/assets/_BYYDEuFR2.js.br +0 -0
- package/dist/public/assets/_BgnvPND_.js.br +0 -0
- package/dist/public/assets/_Bhwci4Zy.js.br +0 -0
- package/dist/public/assets/_BijwdXvp.js.br +0 -0
- package/dist/public/assets/_BlVrxgss2.js.br +0 -0
- package/dist/public/assets/_Bp1b0XWB2.js.br +0 -0
- package/dist/public/assets/_Bq2fn_8D.js.br +0 -0
- package/dist/public/assets/_BsOtzbIC.js.br +0 -0
- package/dist/public/assets/_Bsk9ewWF.js.br +0 -0
- package/dist/public/assets/_Btt4k5w3.js.br +0 -0
- package/dist/public/assets/_C8DWM9rr.js.br +0 -0
- package/dist/public/assets/_CEWjw4lq.js.br +0 -0
- package/dist/public/assets/_CGOYaUuV.js.br +0 -0
- package/dist/public/assets/_CIFN7c1t2.js.br +0 -0
- package/dist/public/assets/_CJT7ujcv2.js.br +0 -0
- package/dist/public/assets/_CUjnrp_u2.js.br +0 -0
- package/dist/public/assets/_CWNv7bdi.js.br +0 -0
- package/dist/public/assets/_CXQ_K2Vh2.js.br +0 -0
- package/dist/public/assets/_CZejW_fq.js.br +0 -0
- package/dist/public/assets/_CZzZOY4P2.js.br +0 -0
- package/dist/public/assets/_CegJB2vl.js.br +0 -0
- package/dist/public/assets/_CkuVE2zK.js.br +0 -0
- package/dist/public/assets/_CnKbN1QV.js.br +0 -0
- package/dist/public/assets/_CoStmjbL.js.br +0 -0
- package/dist/public/assets/_CpRKjPVO2.js.br +0 -0
- package/dist/public/assets/_CqDb111T.js.br +0 -0
- package/dist/public/assets/_Ctuh4IDL2.js.br +0 -0
- package/dist/public/assets/_Cv9Hgy8P.js.br +0 -0
- package/dist/public/assets/_CvnxxlPb.js.br +0 -0
- package/dist/public/assets/_Cym2lUeD2.js.br +0 -0
- package/dist/public/assets/_CzI2QhH4.js.br +0 -0
- package/dist/public/assets/_D8raMSvy.js.br +0 -0
- package/dist/public/assets/_DBJ43cPF.js.br +0 -0
- package/dist/public/assets/_DKciAPQl.js.br +0 -0
- package/dist/public/assets/_DMHXo8uX.js.br +0 -0
- package/dist/public/assets/_DO4n-NwM.js.br +0 -0
- package/dist/public/assets/_DS628Dnv.js.br +0 -0
- package/dist/public/assets/_DYngS949.js.br +0 -0
- package/dist/public/assets/_Dhu480me.js.br +0 -0
- package/dist/public/assets/_DlZyjzE_.js.br +0 -0
- package/dist/public/assets/_DtLQVzKB2.js.br +0 -0
- package/dist/public/assets/_Du5pjG7G.js.br +0 -0
- package/dist/public/assets/_Dzy9r1lm.js.br +0 -0
- package/dist/public/assets/_ER-_pvil.js.br +0 -0
- package/dist/public/assets/_LUNsNRnJ2.js.br +0 -0
- package/dist/public/assets/_MofKWwY9.js.br +0 -0
- package/dist/public/assets/_N0vzdKCD.js.br +0 -0
- package/dist/public/assets/_O8R4l7yw2.js.br +0 -0
- package/dist/public/assets/_Rf_7ZEPL.js.br +0 -0
- package/dist/public/assets/_RhYe7Uqa.js.br +0 -0
- package/dist/public/assets/_T2999saD2.js.br +0 -0
- package/dist/public/assets/_XgOGaUKD.js.br +0 -0
- package/dist/public/assets/_c1YZLlfE.js.br +0 -0
- package/dist/public/assets/_duTaTgEk2.js.br +0 -0
- package/dist/public/assets/_iDn41XPe2.js.br +0 -0
- package/dist/public/assets/_l9vJoVeP.js.br +0 -0
- package/dist/public/assets/_qj_i_cqe.js.br +0 -0
- package/dist/public/assets/_szXVEFuj2.js.br +0 -0
- package/dist/public/assets/_yTOoKAfM2.js.br +0 -0
- package/dist/public/assets/_zPAwS_-H.js.br +0 -0
- package/dist/public/assets/app-page-BXbewHHv.css +0 -1
- package/dist/public/assets/app-page-BXbewHHv.css.br +0 -0
- package/dist/public/assets/diff-view-BJ1RAVYB.css +0 -1
- package/dist/public/assets/diff-view-BJ1RAVYB.css.br +0 -0
- package/dist/public/assets/new-DUXTU7jB.js +0 -1
- package/dist/public/assets/settings-DUXTU7jB.js +0 -1
- package/dist/public/assets/w-DUXTU7jB.js +0 -1
- package/dist/public/assets/w._-DUXTU7jB.js +0 -1
- package/dist/public/assets/w._-bwrmfhu0.js +0 -1
package/bin/work.mjs
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
// work repos [id|name] a workspace's repos with base branch and open PRs
|
|
12
12
|
// work procs [id|name] a workspace's running processes and ports
|
|
13
13
|
// work review [id|name] pending review comments as markdown (--json for rows)
|
|
14
|
+
// work export [file] write a backup manifest (workspaces, settings, skills, tool pins)
|
|
15
|
+
// work restore <file> bring a manifest's workspaces and machine setup back (--yes to run)
|
|
16
|
+
// work share [id|name] print a workspace's invite (paste into New → Join / work join)
|
|
17
|
+
// work join [file] join a workspace from an invite (file, or stdin)
|
|
14
18
|
// work version print the installed version
|
|
15
19
|
//
|
|
16
20
|
// The read commands ask the running server (same data as the UI) and take
|
|
@@ -25,10 +29,13 @@ import { createRequire } from "node:module";
|
|
|
25
29
|
import { doctor, formatDoctor, setupPath } from "../lib/setup/doctor.mjs";
|
|
26
30
|
import { installTool } from "../lib/setup/install.mjs";
|
|
27
31
|
import { commentMarkdown } from "../lib/review/markdown.mjs";
|
|
32
|
+
import { formatInvite, parseInvite } from "../lib/share/manifest.mjs";
|
|
28
33
|
import { duration, pctText } from "../lib/usage/format.mjs";
|
|
29
34
|
|
|
30
35
|
const pkgRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
31
|
-
const pkg = JSON.parse(
|
|
36
|
+
const pkg = JSON.parse(
|
|
37
|
+
fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8"),
|
|
38
|
+
);
|
|
32
39
|
const distEntry = path.join(pkgRoot, "dist", "index.mjs");
|
|
33
40
|
const appPort = process.env.WORK_APP_PORT || "7171";
|
|
34
41
|
|
|
@@ -48,7 +55,19 @@ const sh = (cmd, args, opts = {}) =>
|
|
|
48
55
|
const [, , command = "help", ...args] = process.argv;
|
|
49
56
|
const yes = args.includes("--yes") || args.includes("-y");
|
|
50
57
|
const jsonOut = args.includes("--json");
|
|
51
|
-
|
|
58
|
+
/** `--name <value>` style flags take the next arg; keep it out of the
|
|
59
|
+
* positionals so `work join --name x` still reads stdin. */
|
|
60
|
+
const VALUED = new Set(["--name"]);
|
|
61
|
+
const positional = args.filter(
|
|
62
|
+
(a, i) => a === "-" || (!a.startsWith("-") && !VALUED.has(args[i - 1])),
|
|
63
|
+
);
|
|
64
|
+
const flagValue = (flag) => {
|
|
65
|
+
const i = args.indexOf(flag);
|
|
66
|
+
if (i < 0) return undefined;
|
|
67
|
+
if (i + 1 >= args.length || args[i + 1].startsWith("-"))
|
|
68
|
+
die(`${flag} needs a value`);
|
|
69
|
+
return args[i + 1];
|
|
70
|
+
};
|
|
52
71
|
|
|
53
72
|
switch (command) {
|
|
54
73
|
case "start":
|
|
@@ -57,6 +76,16 @@ switch (command) {
|
|
|
57
76
|
case "setup":
|
|
58
77
|
await setup();
|
|
59
78
|
break;
|
|
79
|
+
case "install-service":
|
|
80
|
+
// Service installation only — no doctor/fix, no migrate. Used by
|
|
81
|
+
// scripts/setup-prod.sh so npm and source installs share one
|
|
82
|
+
// definition; WORK_SERVICE_EXEC points it at a checkout's run-prod.sh.
|
|
83
|
+
if (installService()) {
|
|
84
|
+
log(`done — http://localhost:${serviceEnv().PORT}`);
|
|
85
|
+
} else {
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
60
89
|
case "doctor":
|
|
61
90
|
await doctorCmd();
|
|
62
91
|
break;
|
|
@@ -82,6 +111,18 @@ switch (command) {
|
|
|
82
111
|
case "review":
|
|
83
112
|
await reviewCmd();
|
|
84
113
|
break;
|
|
114
|
+
case "export":
|
|
115
|
+
await exportCmd();
|
|
116
|
+
break;
|
|
117
|
+
case "restore":
|
|
118
|
+
await restoreCmd();
|
|
119
|
+
break;
|
|
120
|
+
case "share":
|
|
121
|
+
await shareCmd();
|
|
122
|
+
break;
|
|
123
|
+
case "join":
|
|
124
|
+
await joinCmd();
|
|
125
|
+
break;
|
|
85
126
|
case "version":
|
|
86
127
|
case "--version":
|
|
87
128
|
case "-v":
|
|
@@ -89,7 +130,7 @@ switch (command) {
|
|
|
89
130
|
break;
|
|
90
131
|
default:
|
|
91
132
|
console.log(
|
|
92
|
-
`work ${pkg.version}\n\nusage: work <setup|doctor|start|update|usage|workspaces|workspace|repos|procs|review|version> [--json]`,
|
|
133
|
+
`work ${pkg.version}\n\nusage: work <setup|install-service|doctor|start|update|usage|workspaces|workspace|repos|procs|review|export|restore|share|join|version> [--json]`,
|
|
93
134
|
);
|
|
94
135
|
}
|
|
95
136
|
|
|
@@ -116,7 +157,11 @@ async function usage() {
|
|
|
116
157
|
const agents = data.agents || [];
|
|
117
158
|
if (!agents.length) return console.log("no agent accounts found");
|
|
118
159
|
for (const agent of agents) {
|
|
119
|
-
const head = [
|
|
160
|
+
const head = [
|
|
161
|
+
agent.agent,
|
|
162
|
+
agent.plan?.replace(/^default_/, "").replaceAll("_", " "),
|
|
163
|
+
agent.account,
|
|
164
|
+
]
|
|
120
165
|
.filter(Boolean)
|
|
121
166
|
.join(" · ");
|
|
122
167
|
console.log(`\n${head}`);
|
|
@@ -143,7 +188,9 @@ async function usage() {
|
|
|
143
188
|
async function apiGet(path) {
|
|
144
189
|
const port = serviceEnv().PORT;
|
|
145
190
|
try {
|
|
146
|
-
const res = await fetch(`http://127.0.0.1:${port}${path}`, {
|
|
191
|
+
const res = await fetch(`http://127.0.0.1:${port}${path}`, {
|
|
192
|
+
signal: AbortSignal.timeout(30_000),
|
|
193
|
+
});
|
|
147
194
|
if (!res.ok) die(`server answered ${res.status} for ${path}`);
|
|
148
195
|
return await res.json();
|
|
149
196
|
} catch (err) {
|
|
@@ -159,14 +206,19 @@ async function apiGet(path) {
|
|
|
159
206
|
* a workspace, so no argument is the common case.
|
|
160
207
|
*/
|
|
161
208
|
async function resolveWorkspace(arg) {
|
|
162
|
-
const [list, status] = await Promise.all([
|
|
209
|
+
const [list, status] = await Promise.all([
|
|
210
|
+
apiGet("/api/workspaces"),
|
|
211
|
+
apiGet("/api/status"),
|
|
212
|
+
]);
|
|
163
213
|
let ws;
|
|
164
214
|
if (arg) {
|
|
165
215
|
ws = list.find((w) => w.id === arg || w.name === arg);
|
|
166
216
|
if (!ws) die(`no workspace named ${arg}`);
|
|
167
217
|
} else if (process.env.WORK_SESSION_ID) {
|
|
168
218
|
const sid = process.env.WORK_SESSION_ID;
|
|
169
|
-
ws = list.find((w) =>
|
|
219
|
+
ws = list.find((w) =>
|
|
220
|
+
(status[w.id]?.sessions || []).some((s) => s.id === sid),
|
|
221
|
+
);
|
|
170
222
|
}
|
|
171
223
|
if (!ws) {
|
|
172
224
|
// Compare real paths on both sides — /home may be a symlink to /var/home.
|
|
@@ -183,8 +235,12 @@ async function resolveWorkspace(arg) {
|
|
|
183
235
|
return cwd === root || cwd.startsWith(root + path.sep);
|
|
184
236
|
});
|
|
185
237
|
}
|
|
186
|
-
if (!ws)
|
|
187
|
-
|
|
238
|
+
if (!ws)
|
|
239
|
+
die("not inside a workspace. Pass its name or id (see: work workspaces)");
|
|
240
|
+
return {
|
|
241
|
+
ws,
|
|
242
|
+
status: status[ws.id] || { ports: [], repos: [], sessions: [] },
|
|
243
|
+
};
|
|
188
244
|
}
|
|
189
245
|
|
|
190
246
|
function pad(s, n) {
|
|
@@ -192,12 +248,17 @@ function pad(s, n) {
|
|
|
192
248
|
}
|
|
193
249
|
|
|
194
250
|
async function workspacesCmd() {
|
|
195
|
-
const [list, status] = await Promise.all([
|
|
251
|
+
const [list, status] = await Promise.all([
|
|
252
|
+
apiGet("/api/workspaces"),
|
|
253
|
+
apiGet("/api/status"),
|
|
254
|
+
]);
|
|
196
255
|
const rows = list.map((w) => ({ ...w, ...(status[w.id] || {}) }));
|
|
197
256
|
if (jsonOut) return console.log(JSON.stringify(rows, null, 2));
|
|
198
257
|
if (!rows.length) return console.log("no workspaces. Create one in the app");
|
|
199
258
|
const wName = Math.max(4, ...rows.map((w) => w.name.length));
|
|
200
|
-
console.log(
|
|
259
|
+
console.log(
|
|
260
|
+
`${pad("name", wName)} ${pad("status", 8)} ${pad("branch", 28)} repos agents ports`,
|
|
261
|
+
);
|
|
201
262
|
for (const w of rows) {
|
|
202
263
|
const ports = (w.ports || []).map((p) => `:${p.port}`).join(" ");
|
|
203
264
|
console.log(
|
|
@@ -207,15 +268,30 @@ async function workspacesCmd() {
|
|
|
207
268
|
}
|
|
208
269
|
|
|
209
270
|
function prLine(pr) {
|
|
210
|
-
const state =
|
|
271
|
+
const state =
|
|
272
|
+
pr.state === "MERGED"
|
|
273
|
+
? "merged"
|
|
274
|
+
: pr.state === "CLOSED"
|
|
275
|
+
? "closed"
|
|
276
|
+
: pr.isDraft
|
|
277
|
+
? "draft"
|
|
278
|
+
: "open";
|
|
211
279
|
return `#${pr.number} ${state} ${pr.title} ${pr.url}`;
|
|
212
280
|
}
|
|
213
281
|
|
|
214
282
|
async function workspaceCmd() {
|
|
215
283
|
const { ws, status } = await resolveWorkspace(positional[0]);
|
|
216
284
|
const procs = await apiGet(`/api/workspaces/${ws.id}/procs`).catch(() => []);
|
|
217
|
-
const [sessions] = await Promise.all([
|
|
218
|
-
|
|
285
|
+
const [sessions] = await Promise.all([
|
|
286
|
+
apiGet(`/api/workspaces/${ws.id}/sessions`).catch(() => []),
|
|
287
|
+
]);
|
|
288
|
+
const out = {
|
|
289
|
+
...ws,
|
|
290
|
+
ports: status.ports,
|
|
291
|
+
repos: mergeRepos(ws, status),
|
|
292
|
+
sessions,
|
|
293
|
+
procs,
|
|
294
|
+
};
|
|
219
295
|
if (jsonOut) return console.log(JSON.stringify(out, null, 2));
|
|
220
296
|
console.log(`${ws.name} (${ws.status})`);
|
|
221
297
|
console.log(` path: ${ws.display_path || ws.path}`);
|
|
@@ -234,14 +310,20 @@ async function workspaceCmd() {
|
|
|
234
310
|
}
|
|
235
311
|
if (procs.length) {
|
|
236
312
|
console.log(` running:`);
|
|
237
|
-
for (const p of procs)
|
|
313
|
+
for (const p of procs)
|
|
314
|
+
console.log(
|
|
315
|
+
` ${pad(p.pid, 7)} ${p.cmd}${p.ports?.length ? " " + p.ports.map((x) => `:${x}`).join(" ") : ""}`,
|
|
316
|
+
);
|
|
238
317
|
}
|
|
239
318
|
}
|
|
240
319
|
|
|
241
320
|
/** Workspace repos joined with the status snapshot's PRs per repo. */
|
|
242
321
|
function mergeRepos(ws, status) {
|
|
243
322
|
const byName = new Map((status.repos || []).map((r) => [r.full_name, r]));
|
|
244
|
-
return ws.repos.map((r) => ({
|
|
323
|
+
return ws.repos.map((r) => ({
|
|
324
|
+
...r,
|
|
325
|
+
prs: byName.get(r.full_name)?.prs || [],
|
|
326
|
+
}));
|
|
245
327
|
}
|
|
246
328
|
|
|
247
329
|
async function reposCmd() {
|
|
@@ -249,7 +331,9 @@ async function reposCmd() {
|
|
|
249
331
|
const repos = mergeRepos(ws, status);
|
|
250
332
|
if (jsonOut) return console.log(JSON.stringify(repos, null, 2));
|
|
251
333
|
for (const r of repos) {
|
|
252
|
-
console.log(
|
|
334
|
+
console.log(
|
|
335
|
+
`${r.worktree}/ ${r.full_name} base:${r.base_branch}${r.default_branch !== r.base_branch ? ` default:${r.default_branch}` : ""}`,
|
|
336
|
+
);
|
|
253
337
|
for (const pr of r.prs) console.log(` ${prLine(pr)}`);
|
|
254
338
|
}
|
|
255
339
|
}
|
|
@@ -259,7 +343,10 @@ async function procsCmd() {
|
|
|
259
343
|
const procs = await apiGet(`/api/workspaces/${ws.id}/procs`);
|
|
260
344
|
if (jsonOut) return console.log(JSON.stringify(procs, null, 2));
|
|
261
345
|
if (!procs.length) return console.log("nothing running");
|
|
262
|
-
for (const p of procs)
|
|
346
|
+
for (const p of procs)
|
|
347
|
+
console.log(
|
|
348
|
+
`${pad(p.pid, 7)} ${p.cmd}${p.ports?.length ? " " + p.ports.map((x) => `:${x}`).join(" ") : ""}`,
|
|
349
|
+
);
|
|
263
350
|
}
|
|
264
351
|
|
|
265
352
|
/** Comments left on the review page but not yet sent: an agent can pull
|
|
@@ -273,6 +360,192 @@ async function reviewCmd() {
|
|
|
273
360
|
console.log(commentMarkdown(comments, null));
|
|
274
361
|
}
|
|
275
362
|
|
|
363
|
+
async function apiPost(path, body) {
|
|
364
|
+
const port = serviceEnv().PORT;
|
|
365
|
+
let res;
|
|
366
|
+
try {
|
|
367
|
+
res = await fetch(`http://127.0.0.1:${port}${path}`, {
|
|
368
|
+
method: "POST",
|
|
369
|
+
headers: { "content-type": "application/json" },
|
|
370
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
371
|
+
signal: AbortSignal.timeout(15 * 60_000),
|
|
372
|
+
});
|
|
373
|
+
} catch {
|
|
374
|
+
die(`no work server on port ${port}. Start one with: work start`);
|
|
375
|
+
}
|
|
376
|
+
const data = await res.json().catch(() => ({}));
|
|
377
|
+
if (!res.ok || data.error)
|
|
378
|
+
die(data.error || `server answered ${res.status} for ${path}`);
|
|
379
|
+
return data;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** One line per workspace/repo of what the file will not carry. */
|
|
383
|
+
function printPreflight(found) {
|
|
384
|
+
for (const w of found.workspaces) {
|
|
385
|
+
console.log(w.name);
|
|
386
|
+
for (const r of w.repos) {
|
|
387
|
+
const parts = [];
|
|
388
|
+
for (const b of r.unpushed)
|
|
389
|
+
parts.push(`${b.count} unpushed on ${b.branch}`);
|
|
390
|
+
if (r.modified.length) parts.push(`${r.modified.length} modified`);
|
|
391
|
+
if (r.untracked.length) parts.push(`${r.untracked.length} untracked`);
|
|
392
|
+
if (r.ignored.length) parts.push(`${r.ignored.length} ignored`);
|
|
393
|
+
if (r.more) parts.push(`+${r.more} more`);
|
|
394
|
+
console.log(` ${r.worktree}/ ${parts.join(", ")}`);
|
|
395
|
+
for (const f of r.modified) console.log(` M ${f}`);
|
|
396
|
+
for (const f of r.untracked) console.log(` ? ${f}`);
|
|
397
|
+
for (const f of r.ignored) console.log(` ! ${f}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Backup manifest to a file. Same pre-flight as Settings → Backup: work
|
|
403
|
+
* the file cannot carry is listed, and needs --yes (or --push, which
|
|
404
|
+
* pushes every workspace branch first) to go ahead. */
|
|
405
|
+
async function exportCmd() {
|
|
406
|
+
const found = await apiPost(
|
|
407
|
+
args.includes("--push") ? "/api/backup/push" : "/api/backup/preflight",
|
|
408
|
+
);
|
|
409
|
+
const c = found.contents;
|
|
410
|
+
if (c) {
|
|
411
|
+
log(
|
|
412
|
+
`in the backup: ${c.workspaces} workspaces` +
|
|
413
|
+
(c.archived ? ` (${c.archived} archived)` : "") +
|
|
414
|
+
`, ${c.tools} tool pins, ${c.skills} skills, ${c.agents} agents`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
if (found.workspaces.length) {
|
|
418
|
+
console.log("not in the backup:");
|
|
419
|
+
printPreflight(found);
|
|
420
|
+
// As in Settings: after --push, leftover uncommitted files are noted,
|
|
421
|
+
// not a stop; only unpushed commits still are.
|
|
422
|
+
if (!yes && (found.unpushed || !args.includes("--push"))) {
|
|
423
|
+
console.log("");
|
|
424
|
+
die(
|
|
425
|
+
found.unpushed
|
|
426
|
+
? "unpushed commits: re-run with --push to push them first, or --yes to export without"
|
|
427
|
+
: "uncommitted files: re-run with --yes to export without them",
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const port = serviceEnv().PORT;
|
|
432
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/backup/export`);
|
|
433
|
+
if (!res.ok) die(`server answered ${res.status} for /api/backup/export`);
|
|
434
|
+
const name =
|
|
435
|
+
positional[0] ||
|
|
436
|
+
/filename="([^"]+)"/.exec(
|
|
437
|
+
res.headers.get("content-disposition") || "",
|
|
438
|
+
)?.[1] ||
|
|
439
|
+
"work-backup.json";
|
|
440
|
+
fs.writeFileSync(name, await res.text());
|
|
441
|
+
log(`wrote ${name}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function readManifest(file) {
|
|
445
|
+
if (!file) die("usage: work restore <file> [--yes]");
|
|
446
|
+
let manifest;
|
|
447
|
+
try {
|
|
448
|
+
manifest = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
449
|
+
} catch {
|
|
450
|
+
die(`${file} is not a work backup file`);
|
|
451
|
+
}
|
|
452
|
+
return manifest;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Preview, then (with --yes) restore everything the file names that this
|
|
456
|
+
* machine does not have. Workspaces already here are skipped. */
|
|
457
|
+
async function restoreCmd() {
|
|
458
|
+
const manifest = readManifest(positional[0]);
|
|
459
|
+
const preview = await apiPost("/api/backup/preview", manifest);
|
|
460
|
+
if (jsonOut && !yes) return console.log(JSON.stringify(preview, null, 2));
|
|
461
|
+
const pick = (list) => list.filter((x) => !x.installed);
|
|
462
|
+
const say = jsonOut ? () => {} : console.log;
|
|
463
|
+
say(`from ${preview.from.host} on ${preview.from.exportedAt.slice(0, 10)}`);
|
|
464
|
+
const workspaces = preview.workspaces.filter(
|
|
465
|
+
(w) => !w.exists && !w.unknownHost,
|
|
466
|
+
);
|
|
467
|
+
for (const w of preview.workspaces) {
|
|
468
|
+
const note = w.exists
|
|
469
|
+
? "already here"
|
|
470
|
+
: w.unknownHost
|
|
471
|
+
? `unknown host ${w.unknownHost}`
|
|
472
|
+
: w.status;
|
|
473
|
+
say(` ${pad(w.name, 24)} ${pad(note, 14)} ${w.repos.join(", ")}`);
|
|
474
|
+
}
|
|
475
|
+
const tools = pick(preview.tools).map((t) => t.name);
|
|
476
|
+
const skills = pick(preview.skills).map((s) => s.id);
|
|
477
|
+
const agents = pick(preview.agents).map((a) => a.label);
|
|
478
|
+
const hosts = preview.hosts.filter((h) => !h.known).map((h) => h.host);
|
|
479
|
+
if (tools.length) say(` tools to install: ${tools.join(", ")}`);
|
|
480
|
+
if (skills.length) say(` skills to add: ${skills.join(", ")}`);
|
|
481
|
+
if (agents.length) say(` agents to install: ${agents.join(", ")}`);
|
|
482
|
+
if (hosts.length) say(` hosts to add: ${hosts.join(", ")}`);
|
|
483
|
+
if (preview.settings) say(` settings: ${preview.settings}`);
|
|
484
|
+
if (!yes) {
|
|
485
|
+
say("");
|
|
486
|
+
log("re-run with --yes to restore");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const result = await apiPost("/api/backup/restore", {
|
|
490
|
+
manifest,
|
|
491
|
+
selection: {
|
|
492
|
+
workspaces: workspaces.map((w) => w.id),
|
|
493
|
+
tools: true,
|
|
494
|
+
skills: true,
|
|
495
|
+
agents: true,
|
|
496
|
+
settings: true,
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
if (jsonOut) return console.log(JSON.stringify(result, null, 2));
|
|
500
|
+
log(
|
|
501
|
+
`restored ${result.restored.length} workspace(s)${result.skipped.length ? `, skipped ${result.skipped.length}` : ""} — repos clone in the background`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** The invite for a workspace — what New → Join and `work join` take. A
|
|
506
|
+
* joined workspace shares the branch it is a lane of, not the lane. */
|
|
507
|
+
async function shareCmd() {
|
|
508
|
+
const { ws } = await resolveWorkspace(positional[0]);
|
|
509
|
+
const invite = {
|
|
510
|
+
name: ws.name,
|
|
511
|
+
branch: ws.origin_branch || ws.branch,
|
|
512
|
+
repos: ws.repos.map((r) => r.full_name),
|
|
513
|
+
};
|
|
514
|
+
if (jsonOut) return console.log(JSON.stringify(invite, null, 2));
|
|
515
|
+
process.stdout.write(formatInvite(invite));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Join from an invite: a file, or stdin (`work share | ssh box work join`). */
|
|
519
|
+
async function joinCmd() {
|
|
520
|
+
const file = positional[0];
|
|
521
|
+
let text;
|
|
522
|
+
try {
|
|
523
|
+
text = fs.readFileSync(file && file !== "-" ? file : 0, "utf8");
|
|
524
|
+
} catch {
|
|
525
|
+
die(
|
|
526
|
+
file
|
|
527
|
+
? `cannot read ${file}`
|
|
528
|
+
: "usage: work join [file] (or pipe an invite in)",
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
let invite;
|
|
532
|
+
try {
|
|
533
|
+
invite = parseInvite(text);
|
|
534
|
+
} catch (err) {
|
|
535
|
+
die(`not an invite: ${err.message}`);
|
|
536
|
+
}
|
|
537
|
+
const name = flagValue("--name") ?? invite.name;
|
|
538
|
+
const ws = await apiPost("/api/workspaces", {
|
|
539
|
+
name,
|
|
540
|
+
repos: invite.repos.map((fullName) => ({ fullName })),
|
|
541
|
+
join: invite.branch,
|
|
542
|
+
});
|
|
543
|
+
if (jsonOut) return console.log(JSON.stringify(ws, null, 2));
|
|
544
|
+
log(
|
|
545
|
+
`joined ${invite.branch} as ${ws.branch} in ${ws.display_path || ws.path}`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
276
549
|
/** "4h" / "6d" — how long until an ISO timestamp. A reset already in the past
|
|
277
550
|
* means the window turned over and this row hasn't refreshed since, so there
|
|
278
551
|
* is no honest countdown to print. */
|
|
@@ -302,9 +575,18 @@ async function setup() {
|
|
|
302
575
|
if (running) log(`done — http://localhost:${port}`);
|
|
303
576
|
else {
|
|
304
577
|
// A fresh install's shell may not have mise on PATH yet — name the shim.
|
|
305
|
-
const shim = path.join(
|
|
578
|
+
const shim = path.join(
|
|
579
|
+
os.homedir(),
|
|
580
|
+
".local",
|
|
581
|
+
"share",
|
|
582
|
+
"mise",
|
|
583
|
+
"shims",
|
|
584
|
+
"work",
|
|
585
|
+
);
|
|
306
586
|
const cmd = fs.existsSync(shim) ? shim : "work";
|
|
307
|
-
log(
|
|
587
|
+
log(
|
|
588
|
+
`done — nothing is serving yet: run \`${cmd} start\` (foreground) and open http://localhost:${port}`,
|
|
589
|
+
);
|
|
308
590
|
}
|
|
309
591
|
}
|
|
310
592
|
|
|
@@ -336,7 +618,10 @@ async function doctorCmd() {
|
|
|
336
618
|
async function fix(d) {
|
|
337
619
|
for (const tool of d.tools.filter((t) => !t.installed)) {
|
|
338
620
|
if (!tool.installable) {
|
|
339
|
-
if (tool.required)
|
|
621
|
+
if (tool.required)
|
|
622
|
+
die(
|
|
623
|
+
`${tool.label} not found. Install it and re-run (the curl installer does this automatically)`,
|
|
624
|
+
);
|
|
340
625
|
continue;
|
|
341
626
|
}
|
|
342
627
|
log(`installing ${tool.label} via mise`);
|
|
@@ -350,13 +635,19 @@ async function fix(d) {
|
|
|
350
635
|
// GitHub sign-in is a UI job (Settings → GitHub runs gh's device flow and
|
|
351
636
|
// wires git credentials); here we only say so. GH_TOKEN still works headless.
|
|
352
637
|
if (!d.github.some((h) => h.authed)) {
|
|
353
|
-
if (process.env.GH_TOKEN || process.env.GITHUB_TOKEN)
|
|
354
|
-
|
|
638
|
+
if (process.env.GH_TOKEN || process.env.GITHUB_TOKEN)
|
|
639
|
+
log("gh will use GH_TOKEN from the environment");
|
|
640
|
+
else
|
|
641
|
+
warn(
|
|
642
|
+
"GitHub is not signed in. Open the app and use Settings → GitHub → Sign in",
|
|
643
|
+
);
|
|
355
644
|
}
|
|
356
645
|
// Agent CLIs are installed from the app (Settings → Agents), where each
|
|
357
646
|
// one is a click and the picker updates immediately — no prompt here.
|
|
358
647
|
if (!d.agents.some((a) => a.installed)) {
|
|
359
|
-
warn(
|
|
648
|
+
warn(
|
|
649
|
+
"no coding agent CLI found. Install one from Settings → Agents in the app",
|
|
650
|
+
);
|
|
360
651
|
}
|
|
361
652
|
}
|
|
362
653
|
|
|
@@ -420,12 +711,24 @@ function serviceEnv() {
|
|
|
420
711
|
// before later top-level consts initialise — functions hoist, consts do
|
|
421
712
|
// not.)
|
|
422
713
|
const PROXY_ENV = [
|
|
423
|
-
"HTTP_PROXY",
|
|
424
|
-
"
|
|
714
|
+
"HTTP_PROXY",
|
|
715
|
+
"HTTPS_PROXY",
|
|
716
|
+
"NO_PROXY",
|
|
717
|
+
"http_proxy",
|
|
718
|
+
"https_proxy",
|
|
719
|
+
"no_proxy",
|
|
720
|
+
"NODE_EXTRA_CA_CERTS",
|
|
721
|
+
"SSL_CERT_FILE",
|
|
722
|
+
"SSL_CERT_DIR",
|
|
425
723
|
];
|
|
426
724
|
const env = { PATH: servicePath() };
|
|
427
725
|
const installed = installedServiceEnv();
|
|
428
|
-
for (const key of [
|
|
726
|
+
for (const key of [
|
|
727
|
+
"WORK_APP_PORT",
|
|
728
|
+
"WORK_ROOT",
|
|
729
|
+
"WORK_DATA_DIR",
|
|
730
|
+
"WORK_ALLOWED_HOSTS",
|
|
731
|
+
]) {
|
|
429
732
|
// `in` rather than a truthy check, so `WORK_ALLOWED_HOSTS= work setup`
|
|
430
733
|
// can deliberately clear a setting the installed service still carries.
|
|
431
734
|
const value = key in process.env ? process.env[key] : installed[key];
|
|
@@ -442,7 +745,8 @@ function serviceEnv() {
|
|
|
442
745
|
const value = key in process.env ? process.env[key] : installed[key];
|
|
443
746
|
if (value) env[key] = value;
|
|
444
747
|
}
|
|
445
|
-
if (Object.keys(env).some((k) => /^https?_proxy$/i.test(k)))
|
|
748
|
+
if (Object.keys(env).some((k) => /^https?_proxy$/i.test(k)))
|
|
749
|
+
env.NODE_USE_ENV_PROXY = "1";
|
|
446
750
|
// launchd passes no locale, and under the C locale tmux mangles the \t
|
|
447
751
|
// separators the server splits pane listings on — every session read dead
|
|
448
752
|
// on macOS. The server defends itself (exec.ts utf8Locale), but the
|
|
@@ -451,7 +755,8 @@ function serviceEnv() {
|
|
|
451
755
|
const locale = ["LC_ALL", "LC_CTYPE", "LANG"]
|
|
452
756
|
.map((key) => (key in process.env ? process.env[key] : installed[key]))
|
|
453
757
|
.find((value) => /utf-?8/i.test(value || ""));
|
|
454
|
-
env.LC_CTYPE =
|
|
758
|
+
env.LC_CTYPE =
|
|
759
|
+
locale || (process.platform === "darwin" ? "UTF-8" : "C.UTF-8");
|
|
455
760
|
return env;
|
|
456
761
|
}
|
|
457
762
|
|
|
@@ -492,7 +797,9 @@ function pidsListening(port) {
|
|
|
492
797
|
),
|
|
493
798
|
];
|
|
494
799
|
}
|
|
495
|
-
const ss = spawnSync("ss", ["-ltnp", `sport = :${port}`], {
|
|
800
|
+
const ss = spawnSync("ss", ["-ltnp", `sport = :${port}`], {
|
|
801
|
+
encoding: "utf8",
|
|
802
|
+
});
|
|
496
803
|
const pids = [];
|
|
497
804
|
for (const m of (ss.stdout || "").matchAll(/pid=(\d+)/g)) {
|
|
498
805
|
const pid = Number(m[1]);
|
|
@@ -575,11 +882,23 @@ function installService() {
|
|
|
575
882
|
const port = serviceEnv().PORT;
|
|
576
883
|
stopOldCopies(port);
|
|
577
884
|
const cliPath = fileURLToPath(import.meta.url);
|
|
885
|
+
// One service definition serves both installers. A source install
|
|
886
|
+
// (scripts/setup-prod.sh) points the unit at this checkout's run-prod.sh
|
|
887
|
+
// instead of the CLI — that script carries its own sleep inhibitor and
|
|
888
|
+
// PORT handling, so no awake prefix is layered on top.
|
|
889
|
+
const execOverride = process.env.WORK_SERVICE_EXEC;
|
|
890
|
+
const startArgs = execOverride
|
|
891
|
+
? [execOverride]
|
|
892
|
+
: [...awakePrefix(), cliPath, "start"];
|
|
578
893
|
if (process.platform === "darwin") {
|
|
579
894
|
const plist = serviceFile();
|
|
580
895
|
fs.mkdirSync(path.dirname(plist), { recursive: true });
|
|
581
896
|
fs.mkdirSync(path.join(os.homedir(), "Library/Logs"), { recursive: true });
|
|
582
|
-
const xml = (v) =>
|
|
897
|
+
const xml = (v) =>
|
|
898
|
+
String(v)
|
|
899
|
+
.replace(/&/g, "&")
|
|
900
|
+
.replace(/</g, "<")
|
|
901
|
+
.replace(/>/g, ">");
|
|
583
902
|
const envXml = Object.entries(serviceEnv())
|
|
584
903
|
.map(([k, v]) => ` <key>${k}</key><string>${xml(v)}</string>`)
|
|
585
904
|
.join("\n");
|
|
@@ -591,7 +910,7 @@ function installService() {
|
|
|
591
910
|
<dict>
|
|
592
911
|
<key>Label</key><string>dev.work.app</string>
|
|
593
912
|
<key>ProgramArguments</key>
|
|
594
|
-
<array>${
|
|
913
|
+
<array>${startArgs.map((a) => `<string>${a}</string>`).join("")}</array>
|
|
595
914
|
<key>RunAtLoad</key><true/>
|
|
596
915
|
<key>KeepAlive</key><true/>
|
|
597
916
|
<key>StandardOutPath</key><string>${os.homedir()}/Library/Logs/work.log</string>
|
|
@@ -606,15 +925,29 @@ ${envXml}
|
|
|
606
925
|
);
|
|
607
926
|
log("installing launchd agent");
|
|
608
927
|
const uid = process.getuid();
|
|
609
|
-
spawnSync("launchctl", ["bootout", `gui/${uid}/dev.work.app`], {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
928
|
+
spawnSync("launchctl", ["bootout", `gui/${uid}/dev.work.app`], {
|
|
929
|
+
stdio: "ignore",
|
|
930
|
+
});
|
|
931
|
+
const boot = spawnSync("launchctl", ["bootstrap", `gui/${uid}`, plist], {
|
|
932
|
+
stdio: "ignore",
|
|
933
|
+
});
|
|
934
|
+
if (
|
|
935
|
+
boot.status !== 0 &&
|
|
936
|
+
spawnSync("launchctl", ["load", "-w", plist], { stdio: "ignore" })
|
|
937
|
+
.status !== 0
|
|
938
|
+
) {
|
|
939
|
+
warn(
|
|
940
|
+
`could not load the agent (no GUI launchd session?). Run from a local terminal: launchctl bootstrap gui/$UID ${plist}`,
|
|
941
|
+
);
|
|
613
942
|
warn("or start manually: work start");
|
|
614
943
|
return false;
|
|
615
944
|
}
|
|
616
|
-
spawnSync("launchctl", ["kickstart", "-k", `gui/${uid}/dev.work.app`], {
|
|
617
|
-
|
|
945
|
+
spawnSync("launchctl", ["kickstart", "-k", `gui/${uid}/dev.work.app`], {
|
|
946
|
+
stdio: "ignore",
|
|
947
|
+
});
|
|
948
|
+
log(
|
|
949
|
+
"manage with: launchctl {kickstart -k,print} gui/$UID/dev.work.app (logs: ~/Library/Logs/work.log)",
|
|
950
|
+
);
|
|
618
951
|
return true;
|
|
619
952
|
}
|
|
620
953
|
|
|
@@ -626,7 +959,7 @@ ${envXml}
|
|
|
626
959
|
.map(([k, v]) => `Environment=${k}=${v}`)
|
|
627
960
|
.join("\n");
|
|
628
961
|
// systemd splits ExecStart on whitespace, so quote the inhibitor's --why.
|
|
629
|
-
const execStart =
|
|
962
|
+
const execStart = startArgs
|
|
630
963
|
.map((a) => (a.includes(" ") ? `"${a}"` : a))
|
|
631
964
|
.join(" ");
|
|
632
965
|
fs.writeFileSync(
|
|
@@ -655,12 +988,18 @@ WantedBy=default.target
|
|
|
655
988
|
sh("systemctl", ["--user", "enable", "work.service"]);
|
|
656
989
|
sh("systemctl", ["--user", "restart", "work.service"]);
|
|
657
990
|
// Survive logout (best-effort; may need sudo on some systems).
|
|
658
|
-
spawnSync("loginctl", ["enable-linger", os.userInfo().username], {
|
|
991
|
+
spawnSync("loginctl", ["enable-linger", os.userInfo().username], {
|
|
992
|
+
stdio: "ignore",
|
|
993
|
+
});
|
|
659
994
|
log("manage with: systemctl --user {status,restart,stop} work.service");
|
|
660
995
|
return true;
|
|
661
996
|
}
|
|
662
997
|
|
|
663
|
-
warn(
|
|
998
|
+
warn(
|
|
999
|
+
execOverride
|
|
1000
|
+
? "no service manager found. Start manually with: scripts/run-prod.sh"
|
|
1001
|
+
: "no service manager found. Start manually with: work start",
|
|
1002
|
+
);
|
|
664
1003
|
return false;
|
|
665
1004
|
}
|
|
666
1005
|
|