devvami 1.1.0 → 1.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/oclif.manifest.json +117 -117
- package/package.json +2 -1
- package/src/commands/open.js +1 -1
- package/src/commands/upgrade.js +5 -0
- package/src/services/clickup.js +15 -1
- package/src/services/config.js +2 -1
- package/src/services/prompts.js +20 -1
- package/src/services/speckit.js +4 -1
package/oclif.manifest.json
CHANGED
|
@@ -703,19 +703,14 @@
|
|
|
703
703
|
"search.js"
|
|
704
704
|
]
|
|
705
705
|
},
|
|
706
|
-
"
|
|
706
|
+
"pr:create": {
|
|
707
707
|
"aliases": [],
|
|
708
|
-
"args": {
|
|
709
|
-
|
|
710
|
-
"description": "ID del workflow run",
|
|
711
|
-
"name": "run-id",
|
|
712
|
-
"required": true
|
|
713
|
-
}
|
|
714
|
-
},
|
|
715
|
-
"description": "Log di un workflow run specifico",
|
|
708
|
+
"args": {},
|
|
709
|
+
"description": "Apri Pull Request precompilata con template, label e reviewer",
|
|
716
710
|
"examples": [
|
|
717
|
-
"<%= config.bin %>
|
|
718
|
-
"<%= config.bin %>
|
|
711
|
+
"<%= config.bin %> pr create",
|
|
712
|
+
"<%= config.bin %> pr create --draft",
|
|
713
|
+
"<%= config.bin %> pr create --title \"My PR\" --dry-run"
|
|
719
714
|
],
|
|
720
715
|
"flags": {
|
|
721
716
|
"json": {
|
|
@@ -725,17 +720,29 @@
|
|
|
725
720
|
"allowNo": false,
|
|
726
721
|
"type": "boolean"
|
|
727
722
|
},
|
|
728
|
-
"
|
|
729
|
-
"description": "
|
|
730
|
-
"name": "
|
|
723
|
+
"title": {
|
|
724
|
+
"description": "Titolo PR (default: auto-generated)",
|
|
725
|
+
"name": "title",
|
|
731
726
|
"hasDynamicHelp": false,
|
|
732
727
|
"multiple": false,
|
|
733
728
|
"type": "option"
|
|
729
|
+
},
|
|
730
|
+
"draft": {
|
|
731
|
+
"description": "Crea come draft",
|
|
732
|
+
"name": "draft",
|
|
733
|
+
"allowNo": false,
|
|
734
|
+
"type": "boolean"
|
|
735
|
+
},
|
|
736
|
+
"dry-run": {
|
|
737
|
+
"description": "Preview senza eseguire",
|
|
738
|
+
"name": "dry-run",
|
|
739
|
+
"allowNo": false,
|
|
740
|
+
"type": "boolean"
|
|
734
741
|
}
|
|
735
742
|
},
|
|
736
743
|
"hasDynamicHelp": false,
|
|
737
744
|
"hiddenAliases": [],
|
|
738
|
-
"id": "
|
|
745
|
+
"id": "pr:create",
|
|
739
746
|
"pluginAlias": "devvami",
|
|
740
747
|
"pluginName": "devvami",
|
|
741
748
|
"pluginType": "core",
|
|
@@ -745,18 +752,24 @@
|
|
|
745
752
|
"relativePath": [
|
|
746
753
|
"src",
|
|
747
754
|
"commands",
|
|
748
|
-
"
|
|
749
|
-
"
|
|
755
|
+
"pr",
|
|
756
|
+
"create.js"
|
|
750
757
|
]
|
|
751
758
|
},
|
|
752
|
-
"
|
|
759
|
+
"pr:detail": {
|
|
753
760
|
"aliases": [],
|
|
754
|
-
"args": {
|
|
755
|
-
|
|
761
|
+
"args": {
|
|
762
|
+
"number": {
|
|
763
|
+
"description": "Numero della PR",
|
|
764
|
+
"name": "number",
|
|
765
|
+
"required": true
|
|
766
|
+
}
|
|
767
|
+
},
|
|
768
|
+
"description": "Dettaglio PR con commenti QA e checklist degli step",
|
|
756
769
|
"examples": [
|
|
757
|
-
"<%= config.bin %>
|
|
758
|
-
"<%= config.bin %>
|
|
759
|
-
"<%= config.bin %>
|
|
770
|
+
"<%= config.bin %> pr detail 42",
|
|
771
|
+
"<%= config.bin %> pr detail 42 --repo devvami/my-api",
|
|
772
|
+
"<%= config.bin %> pr detail 42 --json"
|
|
760
773
|
],
|
|
761
774
|
"flags": {
|
|
762
775
|
"json": {
|
|
@@ -766,23 +779,17 @@
|
|
|
766
779
|
"allowNo": false,
|
|
767
780
|
"type": "boolean"
|
|
768
781
|
},
|
|
769
|
-
"
|
|
770
|
-
"description": "
|
|
771
|
-
"name": "
|
|
782
|
+
"repo": {
|
|
783
|
+
"description": "Repository nel formato owner/repo (default: rilevato da git remote)",
|
|
784
|
+
"name": "repo",
|
|
772
785
|
"hasDynamicHelp": false,
|
|
773
786
|
"multiple": false,
|
|
774
787
|
"type": "option"
|
|
775
|
-
},
|
|
776
|
-
"failed-only": {
|
|
777
|
-
"description": "Rilancia solo i job falliti",
|
|
778
|
-
"name": "failed-only",
|
|
779
|
-
"allowNo": false,
|
|
780
|
-
"type": "boolean"
|
|
781
788
|
}
|
|
782
789
|
},
|
|
783
790
|
"hasDynamicHelp": false,
|
|
784
791
|
"hiddenAliases": [],
|
|
785
|
-
"id": "
|
|
792
|
+
"id": "pr:detail",
|
|
786
793
|
"pluginAlias": "devvami",
|
|
787
794
|
"pluginName": "devvami",
|
|
788
795
|
"pluginType": "core",
|
|
@@ -792,18 +799,17 @@
|
|
|
792
799
|
"relativePath": [
|
|
793
800
|
"src",
|
|
794
801
|
"commands",
|
|
795
|
-
"
|
|
796
|
-
"
|
|
802
|
+
"pr",
|
|
803
|
+
"detail.js"
|
|
797
804
|
]
|
|
798
805
|
},
|
|
799
|
-
"
|
|
806
|
+
"pr:review": {
|
|
800
807
|
"aliases": [],
|
|
801
808
|
"args": {},
|
|
802
|
-
"description": "
|
|
809
|
+
"description": "Lista PR assegnate a te per la code review",
|
|
803
810
|
"examples": [
|
|
804
|
-
"<%= config.bin %>
|
|
805
|
-
"<%= config.bin %>
|
|
806
|
-
"<%= config.bin %> pipeline status --limit 20 --json"
|
|
811
|
+
"<%= config.bin %> pr review",
|
|
812
|
+
"<%= config.bin %> pr review --json"
|
|
807
813
|
],
|
|
808
814
|
"flags": {
|
|
809
815
|
"json": {
|
|
@@ -812,26 +818,11 @@
|
|
|
812
818
|
"name": "json",
|
|
813
819
|
"allowNo": false,
|
|
814
820
|
"type": "boolean"
|
|
815
|
-
},
|
|
816
|
-
"branch": {
|
|
817
|
-
"description": "Filtra per branch",
|
|
818
|
-
"name": "branch",
|
|
819
|
-
"hasDynamicHelp": false,
|
|
820
|
-
"multiple": false,
|
|
821
|
-
"type": "option"
|
|
822
|
-
},
|
|
823
|
-
"limit": {
|
|
824
|
-
"description": "Numero di run da mostrare",
|
|
825
|
-
"name": "limit",
|
|
826
|
-
"default": 10,
|
|
827
|
-
"hasDynamicHelp": false,
|
|
828
|
-
"multiple": false,
|
|
829
|
-
"type": "option"
|
|
830
821
|
}
|
|
831
822
|
},
|
|
832
823
|
"hasDynamicHelp": false,
|
|
833
824
|
"hiddenAliases": [],
|
|
834
|
-
"id": "
|
|
825
|
+
"id": "pr:review",
|
|
835
826
|
"pluginAlias": "devvami",
|
|
836
827
|
"pluginName": "devvami",
|
|
837
828
|
"pluginType": "core",
|
|
@@ -841,18 +832,18 @@
|
|
|
841
832
|
"relativePath": [
|
|
842
833
|
"src",
|
|
843
834
|
"commands",
|
|
844
|
-
"
|
|
845
|
-
"
|
|
835
|
+
"pr",
|
|
836
|
+
"review.js"
|
|
846
837
|
]
|
|
847
838
|
},
|
|
848
|
-
"pr:
|
|
839
|
+
"pr:status": {
|
|
849
840
|
"aliases": [],
|
|
850
841
|
"args": {},
|
|
851
|
-
"description": "
|
|
842
|
+
"description": "Stato delle tue PR aperte (come autore e come reviewer)",
|
|
852
843
|
"examples": [
|
|
853
|
-
"<%= config.bin %> pr
|
|
854
|
-
"<%= config.bin %> pr
|
|
855
|
-
"<%= config.bin %> pr
|
|
844
|
+
"<%= config.bin %> pr status",
|
|
845
|
+
"<%= config.bin %> pr status --author",
|
|
846
|
+
"<%= config.bin %> pr status --json"
|
|
856
847
|
],
|
|
857
848
|
"flags": {
|
|
858
849
|
"json": {
|
|
@@ -862,29 +853,22 @@
|
|
|
862
853
|
"allowNo": false,
|
|
863
854
|
"type": "boolean"
|
|
864
855
|
},
|
|
865
|
-
"
|
|
866
|
-
"description": "
|
|
867
|
-
"name": "
|
|
868
|
-
"hasDynamicHelp": false,
|
|
869
|
-
"multiple": false,
|
|
870
|
-
"type": "option"
|
|
871
|
-
},
|
|
872
|
-
"draft": {
|
|
873
|
-
"description": "Crea come draft",
|
|
874
|
-
"name": "draft",
|
|
856
|
+
"author": {
|
|
857
|
+
"description": "Solo PR dove sei autore",
|
|
858
|
+
"name": "author",
|
|
875
859
|
"allowNo": false,
|
|
876
860
|
"type": "boolean"
|
|
877
861
|
},
|
|
878
|
-
"
|
|
879
|
-
"description": "
|
|
880
|
-
"name": "
|
|
862
|
+
"reviewer": {
|
|
863
|
+
"description": "Solo PR dove sei reviewer",
|
|
864
|
+
"name": "reviewer",
|
|
881
865
|
"allowNo": false,
|
|
882
866
|
"type": "boolean"
|
|
883
867
|
}
|
|
884
868
|
},
|
|
885
869
|
"hasDynamicHelp": false,
|
|
886
870
|
"hiddenAliases": [],
|
|
887
|
-
"id": "pr:
|
|
871
|
+
"id": "pr:status",
|
|
888
872
|
"pluginAlias": "devvami",
|
|
889
873
|
"pluginName": "devvami",
|
|
890
874
|
"pluginType": "core",
|
|
@@ -895,23 +879,22 @@
|
|
|
895
879
|
"src",
|
|
896
880
|
"commands",
|
|
897
881
|
"pr",
|
|
898
|
-
"
|
|
882
|
+
"status.js"
|
|
899
883
|
]
|
|
900
884
|
},
|
|
901
|
-
"
|
|
885
|
+
"pipeline:logs": {
|
|
902
886
|
"aliases": [],
|
|
903
887
|
"args": {
|
|
904
|
-
"
|
|
905
|
-
"description": "
|
|
906
|
-
"name": "
|
|
888
|
+
"run-id": {
|
|
889
|
+
"description": "ID del workflow run",
|
|
890
|
+
"name": "run-id",
|
|
907
891
|
"required": true
|
|
908
892
|
}
|
|
909
893
|
},
|
|
910
|
-
"description": "
|
|
894
|
+
"description": "Log di un workflow run specifico",
|
|
911
895
|
"examples": [
|
|
912
|
-
"<%= config.bin %>
|
|
913
|
-
"<%= config.bin %>
|
|
914
|
-
"<%= config.bin %> pr detail 42 --json"
|
|
896
|
+
"<%= config.bin %> pipeline logs 12345",
|
|
897
|
+
"<%= config.bin %> pipeline logs 12345 --job test"
|
|
915
898
|
],
|
|
916
899
|
"flags": {
|
|
917
900
|
"json": {
|
|
@@ -921,9 +904,9 @@
|
|
|
921
904
|
"allowNo": false,
|
|
922
905
|
"type": "boolean"
|
|
923
906
|
},
|
|
924
|
-
"
|
|
925
|
-
"description": "
|
|
926
|
-
"name": "
|
|
907
|
+
"job": {
|
|
908
|
+
"description": "Filtra per job name",
|
|
909
|
+
"name": "job",
|
|
927
910
|
"hasDynamicHelp": false,
|
|
928
911
|
"multiple": false,
|
|
929
912
|
"type": "option"
|
|
@@ -931,7 +914,7 @@
|
|
|
931
914
|
},
|
|
932
915
|
"hasDynamicHelp": false,
|
|
933
916
|
"hiddenAliases": [],
|
|
934
|
-
"id": "
|
|
917
|
+
"id": "pipeline:logs",
|
|
935
918
|
"pluginAlias": "devvami",
|
|
936
919
|
"pluginName": "devvami",
|
|
937
920
|
"pluginType": "core",
|
|
@@ -941,17 +924,18 @@
|
|
|
941
924
|
"relativePath": [
|
|
942
925
|
"src",
|
|
943
926
|
"commands",
|
|
944
|
-
"
|
|
945
|
-
"
|
|
927
|
+
"pipeline",
|
|
928
|
+
"logs.js"
|
|
946
929
|
]
|
|
947
930
|
},
|
|
948
|
-
"
|
|
931
|
+
"pipeline:rerun": {
|
|
949
932
|
"aliases": [],
|
|
950
933
|
"args": {},
|
|
951
|
-
"description": "
|
|
934
|
+
"description": "Rilancia l'ultimo workflow fallito",
|
|
952
935
|
"examples": [
|
|
953
|
-
"<%= config.bin %>
|
|
954
|
-
"<%= config.bin %>
|
|
936
|
+
"<%= config.bin %> pipeline rerun",
|
|
937
|
+
"<%= config.bin %> pipeline rerun --failed-only",
|
|
938
|
+
"<%= config.bin %> pipeline rerun --run-id 12345"
|
|
955
939
|
],
|
|
956
940
|
"flags": {
|
|
957
941
|
"json": {
|
|
@@ -960,11 +944,24 @@
|
|
|
960
944
|
"name": "json",
|
|
961
945
|
"allowNo": false,
|
|
962
946
|
"type": "boolean"
|
|
947
|
+
},
|
|
948
|
+
"run-id": {
|
|
949
|
+
"description": "ID specifico del run",
|
|
950
|
+
"name": "run-id",
|
|
951
|
+
"hasDynamicHelp": false,
|
|
952
|
+
"multiple": false,
|
|
953
|
+
"type": "option"
|
|
954
|
+
},
|
|
955
|
+
"failed-only": {
|
|
956
|
+
"description": "Rilancia solo i job falliti",
|
|
957
|
+
"name": "failed-only",
|
|
958
|
+
"allowNo": false,
|
|
959
|
+
"type": "boolean"
|
|
963
960
|
}
|
|
964
961
|
},
|
|
965
962
|
"hasDynamicHelp": false,
|
|
966
963
|
"hiddenAliases": [],
|
|
967
|
-
"id": "
|
|
964
|
+
"id": "pipeline:rerun",
|
|
968
965
|
"pluginAlias": "devvami",
|
|
969
966
|
"pluginName": "devvami",
|
|
970
967
|
"pluginType": "core",
|
|
@@ -974,18 +971,18 @@
|
|
|
974
971
|
"relativePath": [
|
|
975
972
|
"src",
|
|
976
973
|
"commands",
|
|
977
|
-
"
|
|
978
|
-
"
|
|
974
|
+
"pipeline",
|
|
975
|
+
"rerun.js"
|
|
979
976
|
]
|
|
980
977
|
},
|
|
981
|
-
"
|
|
978
|
+
"pipeline:status": {
|
|
982
979
|
"aliases": [],
|
|
983
980
|
"args": {},
|
|
984
|
-
"description": "Stato
|
|
981
|
+
"description": "Stato GitHub Actions per il repo corrente",
|
|
985
982
|
"examples": [
|
|
986
|
-
"<%= config.bin %>
|
|
987
|
-
"<%= config.bin %>
|
|
988
|
-
"<%= config.bin %>
|
|
983
|
+
"<%= config.bin %> pipeline status",
|
|
984
|
+
"<%= config.bin %> pipeline status --branch main",
|
|
985
|
+
"<%= config.bin %> pipeline status --limit 20 --json"
|
|
989
986
|
],
|
|
990
987
|
"flags": {
|
|
991
988
|
"json": {
|
|
@@ -995,22 +992,25 @@
|
|
|
995
992
|
"allowNo": false,
|
|
996
993
|
"type": "boolean"
|
|
997
994
|
},
|
|
998
|
-
"
|
|
999
|
-
"description": "
|
|
1000
|
-
"name": "
|
|
1001
|
-
"
|
|
1002
|
-
"
|
|
995
|
+
"branch": {
|
|
996
|
+
"description": "Filtra per branch",
|
|
997
|
+
"name": "branch",
|
|
998
|
+
"hasDynamicHelp": false,
|
|
999
|
+
"multiple": false,
|
|
1000
|
+
"type": "option"
|
|
1003
1001
|
},
|
|
1004
|
-
"
|
|
1005
|
-
"description": "
|
|
1006
|
-
"name": "
|
|
1007
|
-
"
|
|
1008
|
-
"
|
|
1002
|
+
"limit": {
|
|
1003
|
+
"description": "Numero di run da mostrare",
|
|
1004
|
+
"name": "limit",
|
|
1005
|
+
"default": 10,
|
|
1006
|
+
"hasDynamicHelp": false,
|
|
1007
|
+
"multiple": false,
|
|
1008
|
+
"type": "option"
|
|
1009
1009
|
}
|
|
1010
1010
|
},
|
|
1011
1011
|
"hasDynamicHelp": false,
|
|
1012
1012
|
"hiddenAliases": [],
|
|
1013
|
-
"id": "
|
|
1013
|
+
"id": "pipeline:status",
|
|
1014
1014
|
"pluginAlias": "devvami",
|
|
1015
1015
|
"pluginName": "devvami",
|
|
1016
1016
|
"pluginType": "core",
|
|
@@ -1020,7 +1020,7 @@
|
|
|
1020
1020
|
"relativePath": [
|
|
1021
1021
|
"src",
|
|
1022
1022
|
"commands",
|
|
1023
|
-
"
|
|
1023
|
+
"pipeline",
|
|
1024
1024
|
"status.js"
|
|
1025
1025
|
]
|
|
1026
1026
|
},
|
|
@@ -1498,5 +1498,5 @@
|
|
|
1498
1498
|
]
|
|
1499
1499
|
}
|
|
1500
1500
|
},
|
|
1501
|
-
"version": "1.1.
|
|
1501
|
+
"version": "1.1.1"
|
|
1502
1502
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devvami",
|
|
3
3
|
"description": "DevEx CLI for developers and teams — manage repos, PRs, pipelines, tasks, and costs from the terminal",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"author": "",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"commit": "git-cz",
|
|
129
129
|
"release": "semantic-release",
|
|
130
130
|
"release:dry-run": "semantic-release --dry-run",
|
|
131
|
+
"version:sync": "node scripts/sync-version.js",
|
|
131
132
|
"lint": "eslint src/ tests/",
|
|
132
133
|
"lint:fix": "eslint src/ tests/ --fix",
|
|
133
134
|
"format": "prettier --write src/ tests/",
|
package/src/commands/open.js
CHANGED
package/src/commands/upgrade.js
CHANGED
|
@@ -22,6 +22,11 @@ export default class Upgrade extends Command {
|
|
|
22
22
|
const { hasUpdate, current, latest } = await checkForUpdate({ force: true })
|
|
23
23
|
spinner?.stop()
|
|
24
24
|
|
|
25
|
+
// Guard against malformed version strings from the GitHub Releases API
|
|
26
|
+
if (latest && !/^v?\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/.test(latest)) {
|
|
27
|
+
this.error(`Invalid version received from releases API: "${latest}" — update aborted`)
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
if (!hasUpdate) {
|
|
26
31
|
const msg = `You're already on the latest version (${current})`
|
|
27
32
|
if (isJson) return { currentVersion: current, latestVersion: latest, updated: false }
|
package/src/services/clickup.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http from 'node:http'
|
|
2
|
+
import { randomBytes } from 'node:crypto'
|
|
2
3
|
import { openBrowser } from '../utils/open-browser.js'
|
|
3
4
|
import { loadConfig } from './config.js'
|
|
4
5
|
|
|
@@ -45,6 +46,10 @@ export async function storeToken(token) {
|
|
|
45
46
|
await keytar.setPassword('devvami', TOKEN_KEY, token)
|
|
46
47
|
} catch {
|
|
47
48
|
// Fallback: store in config (less secure)
|
|
49
|
+
process.stderr.write(
|
|
50
|
+
'Warning: keytar unavailable. ClickUp token will be stored in plaintext.\n' +
|
|
51
|
+
'Run `dvmi auth logout` after this session on shared machines.\n',
|
|
52
|
+
)
|
|
48
53
|
const config = await loadConfig()
|
|
49
54
|
await saveConfig({ ...config, clickup: { ...config.clickup, token } })
|
|
50
55
|
}
|
|
@@ -57,11 +62,20 @@ export async function storeToken(token) {
|
|
|
57
62
|
* @returns {Promise<string>} Access token
|
|
58
63
|
*/
|
|
59
64
|
export async function oauthFlow(clientId, clientSecret) {
|
|
65
|
+
const csrfState = randomBytes(16).toString('hex')
|
|
60
66
|
return new Promise((resolve, reject) => {
|
|
61
67
|
const server = http.createServer(async (req, res) => {
|
|
62
68
|
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
63
69
|
const code = url.searchParams.get('code')
|
|
70
|
+
const returnedState = url.searchParams.get('state')
|
|
64
71
|
if (!code) return
|
|
72
|
+
if (!returnedState || returnedState !== csrfState) {
|
|
73
|
+
res.writeHead(400)
|
|
74
|
+
res.end('State mismatch — possible CSRF attack.')
|
|
75
|
+
server.close()
|
|
76
|
+
reject(new Error('OAuth state mismatch — possible CSRF attack'))
|
|
77
|
+
return
|
|
78
|
+
}
|
|
65
79
|
res.end('Authorization successful! You can close this tab.')
|
|
66
80
|
server.close()
|
|
67
81
|
try {
|
|
@@ -80,7 +94,7 @@ export async function oauthFlow(clientId, clientSecret) {
|
|
|
80
94
|
server.listen(0, async () => {
|
|
81
95
|
const addr = /** @type {import('node:net').AddressInfo} */ (server.address())
|
|
82
96
|
const callbackUrl = `http://localhost:${addr.port}/callback`
|
|
83
|
-
const authUrl = `https://app.clickup.com/api?client_id=${clientId}&redirect_uri=${encodeURIComponent(callbackUrl)}`
|
|
97
|
+
const authUrl = `https://app.clickup.com/api?client_id=${clientId}&redirect_uri=${encodeURIComponent(callbackUrl)}&state=${csrfState}`
|
|
84
98
|
await openBrowser(authUrl)
|
|
85
99
|
})
|
|
86
100
|
server.on('error', reject)
|
package/src/services/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
1
|
+
import { readFile, writeFile, mkdir, chmod } from 'node:fs/promises'
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { homedir } from 'node:os'
|
|
@@ -47,6 +47,7 @@ export async function saveConfig(config, configPath = CONFIG_PATH) {
|
|
|
47
47
|
await mkdir(dir, { recursive: true })
|
|
48
48
|
}
|
|
49
49
|
await writeFile(configPath, JSON.stringify(config, null, 2), 'utf8')
|
|
50
|
+
await chmod(configPath, 0o600)
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
/**
|
package/src/services/prompts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdir, writeFile, readFile, access } from 'node:fs/promises'
|
|
2
|
-
import { join, dirname } from 'node:path'
|
|
2
|
+
import { join, dirname, resolve, sep } from 'node:path'
|
|
3
3
|
import { execa } from 'execa'
|
|
4
4
|
import { createOctokit } from './github.js'
|
|
5
5
|
import { which } from './shell.js'
|
|
@@ -204,6 +204,15 @@ export async function fetchPromptByPath(relativePath) {
|
|
|
204
204
|
export async function downloadPrompt(relativePath, localDir, opts = {}) {
|
|
205
205
|
const destPath = join(localDir, relativePath)
|
|
206
206
|
|
|
207
|
+
// Prevent path traversal: destPath must remain within localDir
|
|
208
|
+
const safeBase = resolve(localDir) + sep
|
|
209
|
+
if (!resolve(destPath).startsWith(safeBase)) {
|
|
210
|
+
throw new DvmiError(
|
|
211
|
+
`Invalid prompt path: "${relativePath}"`,
|
|
212
|
+
'Path must stay within the prompts directory',
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
207
216
|
// Fast-path: skip without a network round-trip if file exists and no overwrite
|
|
208
217
|
if (!opts.overwrite) {
|
|
209
218
|
try {
|
|
@@ -245,6 +254,16 @@ export async function downloadPrompt(relativePath, localDir, opts = {}) {
|
|
|
245
254
|
*/
|
|
246
255
|
export async function resolveLocalPrompt(relativePath, localDir) {
|
|
247
256
|
const fullPath = join(localDir, relativePath)
|
|
257
|
+
|
|
258
|
+
// Prevent path traversal: fullPath must remain within localDir
|
|
259
|
+
const safeBase = resolve(localDir) + sep
|
|
260
|
+
if (!resolve(fullPath).startsWith(safeBase)) {
|
|
261
|
+
throw new DvmiError(
|
|
262
|
+
`Invalid prompt path: "${relativePath}"`,
|
|
263
|
+
'Path must stay within the prompts directory',
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
248
267
|
let raw
|
|
249
268
|
try {
|
|
250
269
|
raw = await readFile(fullPath, 'utf8')
|
package/src/services/speckit.js
CHANGED
|
@@ -2,7 +2,10 @@ import { execa } from 'execa'
|
|
|
2
2
|
import { which, exec } from './shell.js'
|
|
3
3
|
import { DvmiError } from '../utils/errors.js'
|
|
4
4
|
|
|
5
|
-
/** GitHub spec-kit package source for uv
|
|
5
|
+
/** GitHub spec-kit package source for uv.
|
|
6
|
+
* TODO: pin to a specific tagged release (e.g. #v1.x.x) once one is available upstream.
|
|
7
|
+
* Tracking: https://github.com/github/spec-kit/releases
|
|
8
|
+
*/
|
|
6
9
|
const SPECKIT_FROM = 'git+https://github.com/github/spec-kit.git'
|
|
7
10
|
|
|
8
11
|
/**
|