@pippit-dev/cli 0.0.10 → 0.0.12
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 +9 -7
- package/checksums.txt +6 -6
- package/cmd/root.go +3 -2
- package/cmd/short_drama/short_drama.go +0 -3
- package/cmd/short_drama_test.go +119 -22
- package/internal/common/client.go +95 -0
- package/internal/common/upload_file.go +79 -24
- package/package.json +1 -1
- package/cmd/version/version.go +0 -21
package/README.md
CHANGED
|
@@ -10,9 +10,9 @@ matching prebuilt binary for macOS, Linux, or Windows:
|
|
|
10
10
|
```bash
|
|
11
11
|
npx @pippit-dev/cli@latest install
|
|
12
12
|
export XYQ_ACCESS_KEY="<access-key>"
|
|
13
|
-
pippit-cli version
|
|
13
|
+
pippit-cli --version
|
|
14
14
|
pippit-cli short-drama +submit-run --message "写一个赛博朋克短剧开头"
|
|
15
|
-
pippit-cli short-drama +upload-file --path ./
|
|
15
|
+
pippit-cli short-drama +upload-file --path ./reference.doc
|
|
16
16
|
pippit-cli short-drama +get-thread --thread-id thread_123 --run-id run_456 --after-seq 0
|
|
17
17
|
pippit-cli short-drama +download-result --output-path ./thread_123/results/result.mp4 --url URL
|
|
18
18
|
```
|
|
@@ -24,9 +24,9 @@ Submit a Run task for the short drama scene:
|
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
export XYQ_ACCESS_KEY="<access-key>"
|
|
27
|
-
go run . version
|
|
27
|
+
go run . --version
|
|
28
28
|
go run . short-drama +submit-run --message "写一个赛博朋克短剧开头"
|
|
29
|
-
go run . short-drama +upload-file --path ./
|
|
29
|
+
go run . short-drama +upload-file --path ./reference.doc
|
|
30
30
|
go run . short-drama +get-thread --thread-id thread_123 --run-id run_456 --after-seq 0
|
|
31
31
|
go run . short-drama +download-result --output-path ./thread_123/results/result.mp4 --url URL
|
|
32
32
|
```
|
|
@@ -35,9 +35,11 @@ go run . short-drama +download-result --output-path ./thread_123/results/result.
|
|
|
35
35
|
`run_id`, and `web_thread_link`; `--message` is required.
|
|
36
36
|
`+get-thread` calls
|
|
37
37
|
`/api/biz/v1/skill/get_thread` and prints extracted `messages`.
|
|
38
|
+
`+upload-file` calls `/api/biz/v1/skill/upload_file` with
|
|
39
|
+
`multipart/form-data` and prints the returned `asset_id`.
|
|
40
|
+
Only `.doc` and `.txt` file extensions are supported.
|
|
38
41
|
`+download-result` downloads the result URL to
|
|
39
|
-
the `--output-path` file path.
|
|
40
|
-
its real service contract is wired.
|
|
42
|
+
the `--output-path` file path.
|
|
41
43
|
|
|
42
44
|
## HTTP Client
|
|
43
45
|
|
|
@@ -47,6 +49,6 @@ settings such as base URL, HTTP timeout, and API paths are loaded by
|
|
|
47
49
|
|
|
48
50
|
## Auth
|
|
49
51
|
|
|
50
|
-
`short-drama +submit-run` and `short-drama +
|
|
52
|
+
`short-drama +submit-run`, `short-drama +get-thread`, and `short-drama +upload-file` authenticate with
|
|
51
53
|
`Authorization: Bearer <XYQ_ACCESS_KEY>`. OAuth command code remains available,
|
|
52
54
|
but runtime short drama requests do not use it.
|
package/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
5062d1d4950aa9e0d8b1770e62476d9bb2076f4492cd24a541ad93b05a254f49 pippit-cli-0.0.12-darwin-amd64.tar.gz
|
|
2
|
+
5a0c8d044ebcb717d18954147433ac5f54f2dc89285e2e50f6be6755431ee99c pippit-cli-0.0.12-darwin-arm64.tar.gz
|
|
3
|
+
456420a71cd9909e152907faa96f35abb93e62079786021d1bee28cfac61e8ff pippit-cli-0.0.12-linux-amd64.tar.gz
|
|
4
|
+
1c6681fa8f8edaa19dac835dd9bdf4bc17c06936700316f7d74b68d55d572460 pippit-cli-0.0.12-linux-arm64.tar.gz
|
|
5
|
+
e7c156bb35fb373fa89d05c729252b20951a41c70f3bbd4e8794e4c5abe65e06 pippit-cli-0.0.12-windows-amd64.zip
|
|
6
|
+
2832786f9c13069f3580407e24037e95cc9452534e9996366dc6751f841fcd26 pippit-cli-0.0.12-windows-arm64.zip
|
package/cmd/root.go
CHANGED
|
@@ -7,7 +7,6 @@ import (
|
|
|
7
7
|
// authcmd "github.com/Pippit-dev/pippit-cli/cmd/auth"
|
|
8
8
|
"github.com/Pippit-dev/pippit-cli/cmd/short_drama"
|
|
9
9
|
updatecmd "github.com/Pippit-dev/pippit-cli/cmd/update"
|
|
10
|
-
versioncmd "github.com/Pippit-dev/pippit-cli/cmd/version"
|
|
11
10
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
12
11
|
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
13
12
|
"github.com/Pippit-dev/pippit-cli/internal/version"
|
|
@@ -30,15 +29,17 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
|
|
|
30
29
|
root := &cobra.Command{
|
|
31
30
|
Use: "pippit-cli",
|
|
32
31
|
Short: "Pippit CLI",
|
|
32
|
+
Long: "Pippit CLI submits short-drama workflows, downloads generated assets, and updates the installed CLI package.",
|
|
33
33
|
Version: version.Current(),
|
|
34
34
|
SilenceUsage: true,
|
|
35
35
|
SilenceErrors: true,
|
|
36
36
|
}
|
|
37
|
+
root.CompletionOptions.DisableDefaultCmd = true
|
|
38
|
+
root.SetVersionTemplate("{{.Version}}\n")
|
|
37
39
|
root.SetOut(stdout)
|
|
38
40
|
root.SetErr(stderr)
|
|
39
41
|
// root.AddCommand(authcmd.NewCommand(stdout, stderr, runner)) // temporarily disabled; auth is via access key injection
|
|
40
42
|
root.AddCommand(short_drama.NewCommand(stdout, stderr, runner))
|
|
41
43
|
root.AddCommand(updatecmd.NewCommand(stdout, stderr))
|
|
42
|
-
root.AddCommand(versioncmd.NewCommand(stdout))
|
|
43
44
|
return root
|
|
44
45
|
}
|
|
@@ -3,7 +3,6 @@ package short_drama
|
|
|
3
3
|
import (
|
|
4
4
|
"fmt"
|
|
5
5
|
"io"
|
|
6
|
-
"path/filepath"
|
|
7
6
|
"strings"
|
|
8
7
|
|
|
9
8
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
@@ -72,8 +71,6 @@ func newShortDramaUploadFileCommand(stdout, stderr io.Writer, runner *common.Run
|
|
|
72
71
|
if opts.Path == "" {
|
|
73
72
|
return fmt.Errorf("--path is required")
|
|
74
73
|
}
|
|
75
|
-
opts.FileName = filepath.Base(opts.Path)
|
|
76
|
-
opts.Mock = true
|
|
77
74
|
|
|
78
75
|
result, err := common.UploadFile(cmd.Context(), opts, runner)
|
|
79
76
|
if err != nil {
|
package/cmd/short_drama_test.go
CHANGED
|
@@ -91,28 +91,38 @@ func TestRootIncludesUpdateCommand(t *testing.T) {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
func
|
|
94
|
+
func TestRootDoesNotIncludeVersionCommand(t *testing.T) {
|
|
95
95
|
var stdout, stderr bytes.Buffer
|
|
96
96
|
root := NewRootCommand(&stdout, &stderr)
|
|
97
97
|
cmd, _, err := root.Find([]string{"version"})
|
|
98
|
-
if err
|
|
99
|
-
t.Fatalf("Find(version)
|
|
100
|
-
}
|
|
101
|
-
if cmd == nil || cmd.Name() != "version" {
|
|
102
|
-
t.Fatalf("Find(version) = %#v, want version command", cmd)
|
|
98
|
+
if err == nil || cmd != root {
|
|
99
|
+
t.Fatalf("Find(version) = (%#v, %v), want unknown command", cmd, err)
|
|
103
100
|
}
|
|
104
101
|
}
|
|
105
102
|
|
|
106
|
-
func
|
|
103
|
+
func TestRootHelpListsSupportedCommands(t *testing.T) {
|
|
107
104
|
var stdout, stderr bytes.Buffer
|
|
108
105
|
root := NewRootCommand(&stdout, &stderr)
|
|
109
|
-
root.SetArgs([]string{"
|
|
106
|
+
root.SetArgs([]string{"--help"})
|
|
110
107
|
|
|
111
108
|
if err := root.Execute(); err != nil {
|
|
112
109
|
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
113
110
|
}
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
got := stdout.String()
|
|
112
|
+
for _, want := range []string{
|
|
113
|
+
"Pippit CLI submits short-drama workflows",
|
|
114
|
+
"short-drama",
|
|
115
|
+
"update",
|
|
116
|
+
"--version",
|
|
117
|
+
} {
|
|
118
|
+
if !strings.Contains(got, want) {
|
|
119
|
+
t.Fatalf("help output = %q, want %q", got, want)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for _, unwanted := range []string{"completion", "version "} {
|
|
123
|
+
if strings.Contains(got, unwanted) {
|
|
124
|
+
t.Fatalf("help output = %q, should not contain %q", got, unwanted)
|
|
125
|
+
}
|
|
116
126
|
}
|
|
117
127
|
}
|
|
118
128
|
|
|
@@ -124,8 +134,8 @@ func TestVersionFlagPrintsVersion(t *testing.T) {
|
|
|
124
134
|
if err := root.Execute(); err != nil {
|
|
125
135
|
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
126
136
|
}
|
|
127
|
-
if got := stdout.String();
|
|
128
|
-
t.Fatalf("version flag output = %q, want
|
|
137
|
+
if got := strings.TrimSpace(stdout.String()); got != version.Current() {
|
|
138
|
+
t.Fatalf("version flag output = %q, want %s", got, version.Current())
|
|
129
139
|
}
|
|
130
140
|
}
|
|
131
141
|
|
|
@@ -166,23 +176,65 @@ func TestShortDramaSubmitRunRequiresAccessKey(t *testing.T) {
|
|
|
166
176
|
}
|
|
167
177
|
|
|
168
178
|
func TestShortDramaUploadFile(t *testing.T) {
|
|
179
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
180
|
+
if r.Method != http.MethodPost {
|
|
181
|
+
t.Fatalf("method = %s, want POST", r.Method)
|
|
182
|
+
}
|
|
183
|
+
if r.URL.Path != "/api/biz/v1/skill/upload_file" {
|
|
184
|
+
t.Fatalf("path = %s, want upload_file path", r.URL.Path)
|
|
185
|
+
}
|
|
186
|
+
if r.Header.Get("Authorization") != "Bearer test-token" {
|
|
187
|
+
t.Fatalf("Authorization = %q, want test bearer token", r.Header.Get("Authorization"))
|
|
188
|
+
}
|
|
189
|
+
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
|
190
|
+
t.Fatalf("ParseMultipartForm(): %v", err)
|
|
191
|
+
}
|
|
192
|
+
if got := r.FormValue("accessKey"); got != "" {
|
|
193
|
+
t.Fatalf("accessKey = %q, want empty (auth via header only)", got)
|
|
194
|
+
}
|
|
195
|
+
files := r.MultipartForm.File["file"]
|
|
196
|
+
if len(files) != 1 {
|
|
197
|
+
t.Fatalf("file parts = %d, want 1", len(files))
|
|
198
|
+
}
|
|
199
|
+
if files[0].Filename != "story.txt" {
|
|
200
|
+
t.Fatalf("filename = %q, want story.txt", files[0].Filename)
|
|
201
|
+
}
|
|
202
|
+
if got := files[0].Header.Get("Content-Type"); got != "text/plain; charset=utf-8" {
|
|
203
|
+
t.Fatalf("Content-Type = %q, want text/plain; charset=utf-8", got)
|
|
204
|
+
}
|
|
205
|
+
file, err := files[0].Open()
|
|
206
|
+
if err != nil {
|
|
207
|
+
t.Fatalf("Open multipart file: %v", err)
|
|
208
|
+
}
|
|
209
|
+
defer file.Close()
|
|
210
|
+
data, err := io.ReadAll(file)
|
|
211
|
+
if err != nil {
|
|
212
|
+
t.Fatalf("ReadAll multipart file: %v", err)
|
|
213
|
+
}
|
|
214
|
+
if string(data) != "txt-data" {
|
|
215
|
+
t.Fatalf("file content = %q, want txt-data", string(data))
|
|
216
|
+
}
|
|
217
|
+
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"pippit_asset_id":"asset_123"}}`))
|
|
218
|
+
}))
|
|
219
|
+
defer server.Close()
|
|
220
|
+
|
|
221
|
+
cwd := chdirTemp(t)
|
|
222
|
+
path := filepath.Join(cwd, "story.txt")
|
|
223
|
+
if err := os.WriteFile(path, []byte("txt-data"), 0o644); err != nil {
|
|
224
|
+
t.Fatalf("WriteFile(): %v", err)
|
|
225
|
+
}
|
|
226
|
+
|
|
169
227
|
var stdout, stderr bytes.Buffer
|
|
170
|
-
root :=
|
|
171
|
-
root.SetArgs([]string{"short-drama", "+upload-file", "--path",
|
|
228
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
229
|
+
root.SetArgs([]string{"short-drama", "+upload-file", "--path", path})
|
|
172
230
|
|
|
173
231
|
if err := root.Execute(); err != nil {
|
|
174
232
|
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
175
233
|
}
|
|
176
234
|
|
|
177
235
|
got := decodeJSON(t, stdout.Bytes())
|
|
178
|
-
if got["
|
|
179
|
-
t.Fatalf("
|
|
180
|
-
}
|
|
181
|
-
if got["status"] != "uploaded" {
|
|
182
|
-
t.Fatalf("status = %v, want uploaded", got["status"])
|
|
183
|
-
}
|
|
184
|
-
if !strings.HasPrefix(got["file_id"].(string), "file_mock_") {
|
|
185
|
-
t.Fatalf("file_id = %v, want mock file id", got["file_id"])
|
|
236
|
+
if got["asset_id"] != "asset_123" {
|
|
237
|
+
t.Fatalf("asset_id = %v, want asset_123", got["asset_id"])
|
|
186
238
|
}
|
|
187
239
|
}
|
|
188
240
|
|
|
@@ -200,6 +252,51 @@ func TestShortDramaUploadFileRequiresPath(t *testing.T) {
|
|
|
200
252
|
}
|
|
201
253
|
}
|
|
202
254
|
|
|
255
|
+
func TestShortDramaUploadFileRequiresAccessKey(t *testing.T) {
|
|
256
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
257
|
+
t.Fatal("server should not receive request without access key")
|
|
258
|
+
}))
|
|
259
|
+
defer server.Close()
|
|
260
|
+
|
|
261
|
+
cwd := chdirTemp(t)
|
|
262
|
+
path := filepath.Join(cwd, "story.txt")
|
|
263
|
+
if err := os.WriteFile(path, []byte("txt-data"), 0o644); err != nil {
|
|
264
|
+
t.Fatalf("WriteFile(): %v", err)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
var stdout, stderr bytes.Buffer
|
|
268
|
+
root := newTestRootCommandWithAccessKey(t, &stdout, &stderr, server.URL, "")
|
|
269
|
+
root.SetArgs([]string{"short-drama", "+upload-file", "--path", path})
|
|
270
|
+
|
|
271
|
+
err := root.Execute()
|
|
272
|
+
if err == nil {
|
|
273
|
+
t.Fatal("Execute() error = nil, want access key error")
|
|
274
|
+
}
|
|
275
|
+
if !strings.Contains(err.Error(), "XYQ_ACCESS_KEY is required") {
|
|
276
|
+
t.Fatalf("error = %q, want access key guidance", err)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
func TestShortDramaUploadFileRejectsUnsupportedFileType(t *testing.T) {
|
|
281
|
+
cwd := chdirTemp(t)
|
|
282
|
+
path := filepath.Join(cwd, "story.png")
|
|
283
|
+
if err := os.WriteFile(path, []byte("png-data"), 0o644); err != nil {
|
|
284
|
+
t.Fatalf("WriteFile(): %v", err)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
var stdout, stderr bytes.Buffer
|
|
288
|
+
root := NewRootCommand(&stdout, &stderr)
|
|
289
|
+
root.SetArgs([]string{"short-drama", "+upload-file", "--path", path})
|
|
290
|
+
|
|
291
|
+
err := root.Execute()
|
|
292
|
+
if err == nil {
|
|
293
|
+
t.Fatal("Execute() error = nil, want file type validation error")
|
|
294
|
+
}
|
|
295
|
+
if !strings.Contains(err.Error(), "only .doc and .txt uploads are supported") {
|
|
296
|
+
t.Fatalf("error = %q, want unsupported type validation", err)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
203
300
|
func TestShortDramaDownloadResult(t *testing.T) {
|
|
204
301
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
205
302
|
if r.Header.Get("User-Agent") != "Pippit-CLI/1.0" {
|
|
@@ -5,8 +5,12 @@ import (
|
|
|
5
5
|
"context"
|
|
6
6
|
"fmt"
|
|
7
7
|
"io"
|
|
8
|
+
"mime/multipart"
|
|
8
9
|
"net/http"
|
|
10
|
+
"net/textproto"
|
|
9
11
|
"net/url"
|
|
12
|
+
"os"
|
|
13
|
+
"path/filepath"
|
|
10
14
|
"reflect"
|
|
11
15
|
"strings"
|
|
12
16
|
"time"
|
|
@@ -16,12 +20,20 @@ import (
|
|
|
16
20
|
|
|
17
21
|
type Client interface {
|
|
18
22
|
SendRequest(ctx context.Context, path string, body any, out any) error
|
|
23
|
+
SendMultipartRequest(ctx context.Context, path string, fields map[string]string, file MultipartFile, out any) error
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
type RequestAuthorizer interface {
|
|
22
27
|
Inject(ctx context.Context, req *http.Request) error
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
type MultipartFile struct {
|
|
31
|
+
FieldName string
|
|
32
|
+
Path string
|
|
33
|
+
FileName string
|
|
34
|
+
ContentType string
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
type httpClient struct {
|
|
26
38
|
baseURL string
|
|
27
39
|
httpClient *http.Client
|
|
@@ -100,6 +112,89 @@ func (c *httpClient) SendRequest(ctx context.Context, path string, body any, out
|
|
|
100
112
|
return c.do(req, out)
|
|
101
113
|
}
|
|
102
114
|
|
|
115
|
+
func (c *httpClient) SendMultipartRequest(ctx context.Context, path string, fields map[string]string, file MultipartFile, out any) error {
|
|
116
|
+
reqURL, err := c.resolveURL(path, nil)
|
|
117
|
+
if err != nil {
|
|
118
|
+
return err
|
|
119
|
+
}
|
|
120
|
+
if file.FieldName == "" {
|
|
121
|
+
return fmt.Errorf("multipart file field name is required")
|
|
122
|
+
}
|
|
123
|
+
if file.FileName == "" {
|
|
124
|
+
file.FileName = filepath.Base(file.Path)
|
|
125
|
+
}
|
|
126
|
+
if file.ContentType == "" {
|
|
127
|
+
file.ContentType = "application/octet-stream"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
pr, pw := io.Pipe()
|
|
131
|
+
writer := multipart.NewWriter(pw)
|
|
132
|
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, pr)
|
|
133
|
+
if err != nil {
|
|
134
|
+
_ = pr.Close()
|
|
135
|
+
_ = pw.Close()
|
|
136
|
+
return fmt.Errorf("build POST request: %w", err)
|
|
137
|
+
}
|
|
138
|
+
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
139
|
+
req.Header.Set("Accept", "application/json")
|
|
140
|
+
|
|
141
|
+
if c.authorizer == nil {
|
|
142
|
+
_ = pr.Close()
|
|
143
|
+
_ = pw.Close()
|
|
144
|
+
return fmt.Errorf("authorized request requires authorizer")
|
|
145
|
+
}
|
|
146
|
+
if err := c.authorizer.Inject(ctx, req); err != nil {
|
|
147
|
+
_ = pr.Close()
|
|
148
|
+
_ = pw.Close()
|
|
149
|
+
return fmt.Errorf("inject auth headers: %w", err)
|
|
150
|
+
}
|
|
151
|
+
c.injectHeaders(req)
|
|
152
|
+
|
|
153
|
+
go func() {
|
|
154
|
+
err := writeMultipartBody(writer, fields, file)
|
|
155
|
+
closeErr := writer.Close()
|
|
156
|
+
if err != nil {
|
|
157
|
+
_ = pw.CloseWithError(err)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
_ = pw.CloseWithError(closeErr)
|
|
161
|
+
}()
|
|
162
|
+
|
|
163
|
+
return c.do(req, out)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func writeMultipartBody(writer *multipart.Writer, fields map[string]string, file MultipartFile) error {
|
|
167
|
+
for key, value := range fields {
|
|
168
|
+
if err := writer.WriteField(key, value); err != nil {
|
|
169
|
+
return err
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
f, err := os.Open(file.Path)
|
|
174
|
+
if err != nil {
|
|
175
|
+
return fmt.Errorf("open upload file: %w", err)
|
|
176
|
+
}
|
|
177
|
+
defer f.Close()
|
|
178
|
+
|
|
179
|
+
header := make(textproto.MIMEHeader)
|
|
180
|
+
header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(file.FieldName), escapeQuotes(file.FileName)))
|
|
181
|
+
header.Set("Content-Type", file.ContentType)
|
|
182
|
+
part, err := writer.CreatePart(header)
|
|
183
|
+
if err != nil {
|
|
184
|
+
return err
|
|
185
|
+
}
|
|
186
|
+
if _, err := io.Copy(part, f); err != nil {
|
|
187
|
+
return fmt.Errorf("write upload file: %w", err)
|
|
188
|
+
}
|
|
189
|
+
return nil
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
var quotesReplacer = strings.NewReplacer("\\", "\\\\", `"`, `\"`)
|
|
193
|
+
|
|
194
|
+
func escapeQuotes(s string) string {
|
|
195
|
+
return quotesReplacer.Replace(s)
|
|
196
|
+
}
|
|
197
|
+
|
|
103
198
|
func (c *httpClient) injectHeaders(req *http.Request) {
|
|
104
199
|
for k, values := range c.headers {
|
|
105
200
|
for _, v := range values {
|
|
@@ -2,48 +2,103 @@ package common
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
5
|
+
"fmt"
|
|
6
|
+
"mime"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"strings"
|
|
10
|
+
|
|
11
|
+
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
8
12
|
)
|
|
9
13
|
|
|
10
14
|
// UploadFileOptions is the stable command-facing request shape for file upload.
|
|
11
15
|
type UploadFileOptions struct {
|
|
12
16
|
Path string `json:"path"`
|
|
13
17
|
FileName string `json:"file_name"`
|
|
14
|
-
Purpose string `json:"purpose"`
|
|
15
|
-
Mock bool `json:"mock"`
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
// UploadFileResult is the JSON envelope printed by `pippit-cli short-drama +upload-file`.
|
|
19
21
|
type UploadFileResult struct {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
AssetID string `json:"asset_id"`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type uploadFileResponse struct {
|
|
26
|
+
Ret string `json:"ret"`
|
|
27
|
+
Errmsg string `json:"errmsg"`
|
|
28
|
+
SvrTime int64 `json:"svr_time"`
|
|
29
|
+
LogID string `json:"log_id"`
|
|
30
|
+
Data struct {
|
|
31
|
+
PippitAssetID string `json:"pippit_asset_id"`
|
|
32
|
+
AssetID string `json:"asset_id"`
|
|
33
|
+
} `json:"data"`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const uploadFileFieldName = "file"
|
|
37
|
+
|
|
38
|
+
var allowedUploadExtensions = map[string]bool{
|
|
39
|
+
".doc": true,
|
|
40
|
+
".txt": true,
|
|
25
41
|
}
|
|
26
42
|
|
|
27
|
-
func UploadFile(ctx context.Context, opts UploadFileOptions,
|
|
43
|
+
func UploadFile(ctx context.Context, opts UploadFileOptions, runner *Runner) (*UploadFileResult, error) {
|
|
28
44
|
if err := ctx.Err(); err != nil {
|
|
29
45
|
return nil, err
|
|
30
46
|
}
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
if runner == nil || runner.Client == nil {
|
|
48
|
+
return nil, fmt.Errorf("upload_file runner client is required")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
path := strings.TrimSpace(opts.Path)
|
|
52
|
+
if path == "" {
|
|
53
|
+
return nil, fmt.Errorf("upload file path is required")
|
|
54
|
+
}
|
|
55
|
+
info, err := os.Stat(path)
|
|
56
|
+
if err != nil {
|
|
57
|
+
return nil, fmt.Errorf("stat upload file: %w", err)
|
|
58
|
+
}
|
|
59
|
+
if info.IsDir() {
|
|
60
|
+
return nil, fmt.Errorf("upload path %q is a directory", path)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
ext := strings.ToLower(filepath.Ext(path))
|
|
64
|
+
if !allowedUploadExtensions[ext] {
|
|
65
|
+
return nil, fmt.Errorf("unsupported file extension %q; only .doc and .txt uploads are supported", ext)
|
|
66
|
+
}
|
|
67
|
+
fileName := filepath.Base(path)
|
|
68
|
+
contentType := mime.TypeByExtension(ext)
|
|
69
|
+
|
|
70
|
+
var resp uploadFileResponse
|
|
71
|
+
if err := runner.Client.SendMultipartRequest(ctx, uploadFilePath(runner), nil, MultipartFile{
|
|
72
|
+
FieldName: uploadFileFieldName,
|
|
73
|
+
Path: path,
|
|
74
|
+
FileName: fileName,
|
|
75
|
+
ContentType: contentType,
|
|
76
|
+
}, &resp); err != nil {
|
|
77
|
+
return nil, fmt.Errorf("upload_file request failed: %w", err)
|
|
78
|
+
}
|
|
79
|
+
if resp.Ret != "0" {
|
|
80
|
+
if resp.Errmsg == "" {
|
|
81
|
+
resp.Errmsg = "unknown error"
|
|
82
|
+
}
|
|
83
|
+
return nil, fmt.Errorf("upload_file failed: ret=%s errmsg=%s", resp.Ret, resp.Errmsg)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
assetID := strings.TrimSpace(resp.Data.PippitAssetID)
|
|
87
|
+
if assetID == "" {
|
|
88
|
+
assetID = strings.TrimSpace(resp.Data.AssetID)
|
|
89
|
+
}
|
|
90
|
+
if assetID == "" {
|
|
91
|
+
return nil, fmt.Errorf("upload_file response missing pippit_asset_id")
|
|
92
|
+
}
|
|
93
|
+
|
|
33
94
|
return &UploadFileResult{
|
|
34
|
-
|
|
35
|
-
FileID: "file_mock_" + id,
|
|
36
|
-
Status: "uploaded",
|
|
37
|
-
Uploaded: time.Now().UTC().Format(time.RFC3339),
|
|
38
|
-
Request: opts,
|
|
95
|
+
AssetID: assetID,
|
|
39
96
|
}, nil
|
|
40
97
|
}
|
|
41
98
|
|
|
42
|
-
func
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if n > len(id) {
|
|
46
|
-
return id
|
|
99
|
+
func uploadFilePath(runner *Runner) string {
|
|
100
|
+
if runner != nil && runner.Config != nil && runner.Config.Paths != nil && runner.Config.Paths.UploadFile != "" {
|
|
101
|
+
return runner.Config.Paths.UploadFile
|
|
47
102
|
}
|
|
48
|
-
return
|
|
103
|
+
return config.UploadFilePath
|
|
49
104
|
}
|
package/package.json
CHANGED
package/cmd/version/version.go
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
package versioncmd
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"fmt"
|
|
5
|
-
"io"
|
|
6
|
-
|
|
7
|
-
"github.com/Pippit-dev/pippit-cli/internal/version"
|
|
8
|
-
"github.com/spf13/cobra"
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
func NewCommand(stdout io.Writer) *cobra.Command {
|
|
12
|
-
return &cobra.Command{
|
|
13
|
-
Use: "version",
|
|
14
|
-
Short: "Print pippit-cli version",
|
|
15
|
-
Args: cobra.NoArgs,
|
|
16
|
-
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
17
|
-
_, err := fmt.Fprintln(stdout, version.Current())
|
|
18
|
-
return err
|
|
19
|
-
},
|
|
20
|
-
}
|
|
21
|
-
}
|