@pippit-dev/cli 1.0.41 → 1.0.43

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.
Files changed (61) hide show
  1. package/README.md +42 -11
  2. package/checksums.txt +6 -6
  3. package/cmd/auth/auth.go +19 -5
  4. package/cmd/auth/auth_test.go +23 -0
  5. package/cmd/canvas/canvas.go +7 -7
  6. package/cmd/download_result.go +3 -2
  7. package/cmd/marketing.go +19 -3
  8. package/cmd/marketing_test.go +11 -5
  9. package/cmd/model.go +5 -4
  10. package/cmd/root.go +17 -5
  11. package/cmd/root_update_check_test.go +139 -0
  12. package/cmd/short_drama_test.go +2 -0
  13. package/cmd/source_test.go +27 -4
  14. package/cmd/space.go +199 -0
  15. package/cmd/space_test.go +469 -0
  16. package/cmd/upload_file_test.go +3 -1
  17. package/dist/checksums.txt +6 -6
  18. package/internal/adaptercli/commands_test.go +9 -5
  19. package/internal/auth/loopback.go +16 -11
  20. package/internal/auth/loopback_origins_test.go +152 -0
  21. package/internal/auth/space.go +26 -0
  22. package/internal/auth/space_test.go +42 -0
  23. package/internal/canvas/get.go +9 -2
  24. package/internal/commandnames/names.go +9 -4
  25. package/internal/common/client.go +23 -0
  26. package/internal/common/host_source.go +5 -0
  27. package/internal/common/host_source_test.go +9 -1
  28. package/internal/common/runner.go +5 -3
  29. package/internal/common/space.go +185 -0
  30. package/internal/common/space_test.go +132 -0
  31. package/internal/common/submit_run.go +2 -0
  32. package/internal/common/upload_file.go +2 -0
  33. package/internal/config/config.go +15 -12
  34. package/internal/generate_image/generate_image.go +3 -9
  35. package/internal/short_drama/submit_run.go +3 -9
  36. package/internal/space/session.go +153 -0
  37. package/internal/space/store.go +211 -0
  38. package/internal/space/store_test.go +246 -0
  39. package/internal/version/version.go +13 -0
  40. package/internal/version/version_test.go +38 -0
  41. package/internal/versioncheck/check.go +147 -0
  42. package/internal/versioncheck/check_test.go +204 -0
  43. package/internal/versioncheck/semver.go +84 -0
  44. package/package.json +1 -1
  45. package/scripts/canvas-command.js +47 -5
  46. package/scripts/install-wizard.js +1 -1
  47. package/scripts/run.js +15 -4
  48. package/scripts/telemetry.js +3 -1
  49. package/scripts/version-check.js +8 -1
  50. package/skills/short-drama/SKILL.md +12 -1
  51. package/skills/xyq-marketing-skill/SKILL.md +22 -3
  52. package/skills/xyq-marketing-skill/references/api.md +7 -1
  53. package/skills/xyq-marketing-skill/scripts/marketing.js +51 -19
  54. package/skills/xyq-nest-skill/SKILL.md +15 -4
  55. package/skills/xyq-nest-skill/commands/canvas-assets.md +1 -1
  56. package/skills/xyq-nest-skill/commands/canvas.md +5 -3
  57. package/skills/xyq-nest-skill/commands/get-credit-balance.md +2 -2
  58. package/skills/xyq-nest-skill/commands/model.md +1 -1
  59. package/skills/xyq-nest-skill/scripts/ensure-cli.js +8 -1
  60. package/skills/xyq-nest-skill/scripts/install.md +1 -1
  61. package/skills/xyq-nest-skill/workflows/canvas-edit.md +2 -2
@@ -0,0 +1,139 @@
1
+ package cmd
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ "strings"
10
+ "testing"
11
+ "time"
12
+
13
+ "github.com/Pippit-dev/pippit-cli/internal/adapter"
14
+ "github.com/Pippit-dev/pippit-cli/internal/version"
15
+ "github.com/spf13/cobra"
16
+ )
17
+
18
+ type noSelfUpdateAdapter struct{ adapter.Default }
19
+
20
+ func (noSelfUpdateAdapter) CLIOptions() adapter.CLIOptions {
21
+ options := adapter.Default{}.CLIOptions()
22
+ options.SelfUpdateEnabled = false
23
+ return options
24
+ }
25
+
26
+ func setupRootUpdateCache(t *testing.T) string {
27
+ t.Helper()
28
+ home := t.TempDir()
29
+ t.Setenv("HOME", home)
30
+ t.Setenv("USERPROFILE", home)
31
+ for _, key := range []string{"CI", "PIPPIT_CLI_DISABLE_UPDATE_CHECK", "PIPPIT_CLI_UPDATE_CHECKED"} {
32
+ t.Setenv(key, "")
33
+ }
34
+ previous := version.Version
35
+ version.Version = "1.0.41"
36
+ t.Cleanup(func() { version.Version = previous })
37
+ cacheFile := filepath.Join(home, ".pippit_tool_cli", "version-check.json")
38
+ if err := os.MkdirAll(filepath.Dir(cacheFile), 0700); err != nil {
39
+ t.Fatal(err)
40
+ }
41
+ data := fmt.Sprintf(`{"latest":"1.0.42","checkedAt":%d}`, time.Now().UnixMilli())
42
+ if err := os.WriteFile(cacheFile, []byte(data), 0600); err != nil {
43
+ t.Fatal(err)
44
+ }
45
+ return cacheFile
46
+ }
47
+
48
+ func TestNativeBusinessInvocationReportsUpdateBeforeUnchangedJSON(t *testing.T) {
49
+ setupRootUpdateCache(t)
50
+ for _, commandName := range []string{"probe", "login"} {
51
+ for range 2 {
52
+ var stdout, stderr bytes.Buffer
53
+ root := NewRootCommand(&stdout, &stderr)
54
+ probe := &cobra.Command{Use: "probe"}
55
+ if commandName == "login" {
56
+ probe, _, _ = root.Find([]string{"login"})
57
+ } else {
58
+ root.AddCommand(probe)
59
+ }
60
+ probe.RunE = func(*cobra.Command, []string) error {
61
+ if !strings.Contains(stderr.String(), "1.0.41 -> 1.0.42") {
62
+ t.Fatal("business operation started before the synchronous notice")
63
+ }
64
+ _, err := fmt.Fprintln(&stdout, `{"ok":true}`)
65
+ return err
66
+ }
67
+ root.SetArgs([]string{commandName})
68
+ if err := root.Execute(); err != nil {
69
+ t.Fatal(err)
70
+ }
71
+ if !json.Valid(stdout.Bytes()) || stdout.String() != "{\"ok\":true}\n" ||
72
+ strings.Count(stderr.String(), "New version available") != 1 {
73
+ t.Fatalf("stdout/stderr contract changed: %q / %q", stdout.String(), stderr.String())
74
+ }
75
+ if (commandName == "login") != strings.Contains(stderr.String(), "authorization fails") {
76
+ t.Fatal("login-specific notice was omitted or leaked into another command")
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ func TestNativeUpdateCheckRespectsAdapterAndInvocationBoundaries(t *testing.T) {
83
+ setupRootUpdateCache(t)
84
+ for _, scenario := range []string{"help", "version", "empty", "invalid", "invalid flag", "update", "adapter", "dev", "CI", "PIPPIT_CLI_DISABLE_UPDATE_CHECK", "PIPPIT_CLI_UPDATE_CHECKED"} {
85
+ t.Run(scenario, func(t *testing.T) {
86
+ policy := adapter.Adapter(adapter.Default{})
87
+ args := []string{"probe"}
88
+ switch scenario {
89
+ case "help":
90
+ args = []string{"probe", "--help"}
91
+ case "version":
92
+ args = []string{"--version"}
93
+ case "empty":
94
+ args = []string{}
95
+ case "invalid":
96
+ args = []string{"unknown-command"}
97
+ case "invalid flag":
98
+ args = []string{"probe", "--unknown"}
99
+ case "update":
100
+ args = []string{"update"}
101
+ case "adapter":
102
+ policy = noSelfUpdateAdapter{}
103
+ case "dev":
104
+ previous := version.Version
105
+ version.Version = "dev"
106
+ t.Cleanup(func() { version.Version = previous })
107
+ default:
108
+ t.Setenv(scenario, "1")
109
+ }
110
+ var stdout, stderr bytes.Buffer
111
+ root := NewRootCommandWithAdapter(&stdout, &stderr, policy)
112
+ root.AddCommand(&cobra.Command{Use: "probe", Run: func(*cobra.Command, []string) {}})
113
+ if scenario == "update" {
114
+ update, _, _ := root.Find([]string{"update"})
115
+ update.RunE = func(*cobra.Command, []string) error { return nil }
116
+ }
117
+ root.SetArgs(args)
118
+ err := root.Execute()
119
+ if (scenario == "invalid" || scenario == "invalid flag") != (err != nil) {
120
+ t.Fatalf("unexpected command result: %v", err)
121
+ }
122
+ if strings.Contains(stderr.String(), "New version available") {
123
+ t.Fatalf("skipped invocation emitted an update notice: %s", stderr.String())
124
+ }
125
+ })
126
+ }
127
+ }
128
+
129
+ func TestConstructingRootHasNoVersionCheckSideEffects(t *testing.T) {
130
+ cacheFile := setupRootUpdateCache(t)
131
+ if err := os.Remove(cacheFile); err != nil {
132
+ t.Fatal(err)
133
+ }
134
+ var stdout, stderr bytes.Buffer
135
+ _ = NewRootCommand(&stdout, &stderr)
136
+ if _, err := os.Stat(cacheFile); !os.IsNotExist(err) || stdout.Len() != 0 || stderr.Len() != 0 {
137
+ t.Fatal("constructing the root queried versions, wrote cache or produced output")
138
+ }
139
+ }
@@ -14,6 +14,7 @@ import (
14
14
  "github.com/Pippit-dev/pippit-cli/internal/auth"
15
15
  "github.com/Pippit-dev/pippit-cli/internal/common"
16
16
  "github.com/Pippit-dev/pippit-cli/internal/config"
17
+ "github.com/Pippit-dev/pippit-cli/internal/space"
17
18
  "github.com/Pippit-dev/pippit-cli/internal/version"
18
19
  "github.com/bytedance/sonic"
19
20
  "github.com/spf13/cobra"
@@ -965,6 +966,7 @@ func newTestRootCommandWithAccessKey(t *testing.T, stdout, stderr io.Writer, bas
965
966
  client := common.NewHTTPClient(cfg.BaseURL, cfg.HTTPTimeout, common.NewAccessKeyAuthorizer(cfg.AccessKey))
966
967
  runner := common.NewRunner(cfg, client)
967
968
  runner.Auth = auth.NewManager(cfg)
969
+ runner.SpaceStore = &space.Store{Dir: t.TempDir()}
968
970
  return newRootCommand(stdout, stderr, runner)
969
971
  }
970
972
 
@@ -16,7 +16,7 @@ import (
16
16
  )
17
17
 
18
18
  func TestSkillSubmitSourcePreservesCreativeRequest(t *testing.T) {
19
- for _, key := range []string{"PIPPIT_CLI_SOURCE", "CODEX_THREAD_ID", "CODEX_SESSION_ID", "CLAUDECODE", "CURSOR_AGENT", "GEMINI_CLI"} {
19
+ for _, key := range []string{"PIPPIT_CLI_SOURCE", "CODEX_THREAD_ID", "CODEX_SESSION_ID", "CLAUDECODE", "CURSOR_AGENT", "GEMINI_CLI", "OPENCODE", "QWEN_CODE", "CLINE_ACTIVE", "COPILOT_CLI", "COPILOT_AGENT_SESSION_ID"} {
20
20
  t.Setenv(key, "")
21
21
  }
22
22
 
@@ -130,6 +130,10 @@ func TestSkillSubmitSourcePreservesCreativeRequest(t *testing.T) {
130
130
  if received["platform"] != "codex" {
131
131
  t.Errorf("runtime host not detected: %#v", received["platform"])
132
132
  }
133
+ execute("--source", "")
134
+ if received["platform"] != "codex" {
135
+ t.Error("explicit empty must allow runtime fallback")
136
+ }
133
137
  t.Setenv("PIPPIT_CLI_SOURCE", " workbuddy ")
134
138
  execute()
135
139
  if received["platform"] != "workbuddy" {
@@ -139,11 +143,30 @@ func TestSkillSubmitSourcePreservesCreativeRequest(t *testing.T) {
139
143
  if received["platform"] != "doubao_office" {
140
144
  t.Error("explicit flag must win")
141
145
  }
142
- execute("--source", "")
143
- if _, exists := received["platform"]; exists {
144
- t.Error("explicit empty must suppress attribution")
146
+ for _, flags := range [][]string{{"--source", ""}, {"--source", " \t "}, {"--source="}, {"--source=codex", "--source="}} {
147
+ execute(flags...)
148
+ if received["platform"] != "workbuddy" {
149
+ t.Errorf("blank source must allow environment fallback: %v, got %#v", flags, received["platform"])
150
+ }
145
151
  }
146
152
  })
153
+ for _, host := range []struct{ key, want string }{
154
+ {"OPENCODE", "opencode"}, {"QWEN_CODE", "qwen_code"},
155
+ {"CLINE_ACTIVE", "cline"}, {"COPILOT_CLI", "github_copilot_cli"},
156
+ {"COPILOT_AGENT_SESSION_ID", "github_copilot_cli"},
157
+ } {
158
+ t.Run(host.key, func(t *testing.T) {
159
+ t.Setenv(host.key, "session-must-not-be-reported")
160
+ execute("--source", "")
161
+ if received["platform"] != host.want {
162
+ t.Errorf("platform = %#v, want %q", received["platform"], host.want)
163
+ }
164
+ delete(received, "platform")
165
+ if !reflect.DeepEqual(received, wantBody) || query != wantQuery {
166
+ t.Error("runtime source changed creative request or ROI query")
167
+ }
168
+ })
169
+ }
147
170
  // A fresh invocation without source must not reuse previous attribution.
148
171
  execute()
149
172
  if _, exists := received["platform"]; exists {
package/cmd/space.go ADDED
@@ -0,0 +1,199 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "io"
7
+
8
+ "github.com/Pippit-dev/pippit-cli/internal/commandnames"
9
+ "github.com/Pippit-dev/pippit-cli/internal/common"
10
+ "github.com/Pippit-dev/pippit-cli/internal/config"
11
+ "github.com/Pippit-dev/pippit-cli/internal/space"
12
+ "github.com/spf13/cobra"
13
+ )
14
+
15
+ func prepareCommandSpace(cmd *cobra.Command, runner *common.Runner, stderr io.Writer) error {
16
+ if runner == nil {
17
+ return nil
18
+ }
19
+ teamFlag := cmd.Flags().Lookup("team-id")
20
+ var explicit *string
21
+ if teamFlag != nil && teamFlag.Changed {
22
+ id, err := space.ParseTeamID(teamFlag.Value.String())
23
+ if err != nil {
24
+ return err
25
+ }
26
+ explicit = &id
27
+ }
28
+ top := cmd
29
+ for top.Parent() != nil && top.Parent().Parent() != nil {
30
+ top = top.Parent()
31
+ }
32
+ switch top.Name() {
33
+ case commandnames.Space, commandnames.Login, commandnames.Logout, commandnames.Status, commandnames.Update, commandnames.Completion, commandnames.Help, commandnames.DownloadResult:
34
+ return nil
35
+ }
36
+ raw, _ := cmd.Flags().GetString("space-context")
37
+ store, err := common.SpaceStoreForRunner(cmd.Context(), runner)
38
+ if err != nil {
39
+ if explicit == nil || raw != "" {
40
+ return err
41
+ }
42
+ if stderr != nil {
43
+ fmt.Fprintln(stderr, "本地空间记录不可用,将使用显式 --team-id;本次不保存空间记录。")
44
+ }
45
+ }
46
+ if store == nil && explicit == nil && raw == "" {
47
+ return nil
48
+ }
49
+ session := &space.Session{Store: store, Explicit: explicit, Stderr: stderr, Identity: func(ctx context.Context) (string, error) { return common.SpaceAccount(ctx, runner) }}
50
+ if raw != "" {
51
+ session.Expected, err = space.DecodeBinding(raw)
52
+ if err != nil {
53
+ return err
54
+ }
55
+ }
56
+ ctx := common.WithSpace(cmd.Context(), session)
57
+ cmd.SetContext(ctx)
58
+ // Business requests resolve their space from the final request body.
59
+ return nil
60
+ }
61
+
62
+ type listedSpace struct {
63
+ space.Selection
64
+ CurrentRole *int32 `json:"current_role,omitempty"`
65
+ Current bool `json:"current"`
66
+ }
67
+
68
+ func loadSpaces(ctx context.Context, runner *common.Runner, store *space.Store) ([]listedSpace, error) {
69
+ if store != nil {
70
+ ctx = common.WithSpace(ctx, &space.Session{Store: store, Identity: func(ctx context.Context) (string, error) { return common.SpaceAccount(ctx, runner) }})
71
+ }
72
+ result := []listedSpace{{Selection: space.Personal()}}
73
+ seenTeams := map[string]bool{"0": true}
74
+ seenCursors := map[string]bool{}
75
+ cursor := "0"
76
+ for {
77
+ if seenCursors[cursor] {
78
+ return nil, fmt.Errorf("团队列表分页游标重复,未保存空间选择")
79
+ }
80
+ seenCursors[cursor] = true
81
+ var response struct {
82
+ Ret string `json:"ret"`
83
+ Errmsg string `json:"errmsg"`
84
+ LogID string `json:"log_id"`
85
+ Data *struct {
86
+ Teams []struct {
87
+ TeamID string `json:"team_id"`
88
+ Name string `json:"team_name"`
89
+ Role *int32 `json:"current_role"`
90
+ } `json:"team_info_list"`
91
+ NextCursor string `json:"next_cursor"`
92
+ HasMore bool `json:"has_more"`
93
+ } `json:"data"`
94
+ }
95
+ if err := runner.Client.SendRequest(ctx, config.GetLiteTeamListPath, map[string]any{"cursor": cursor, "count": 100}, &response); err != nil {
96
+ return nil, fmt.Errorf("获取团队列表失败,默认空间未改变: %w", err)
97
+ }
98
+ if response.Ret != "0" {
99
+ return nil, common.NewLogIDError("获取团队列表失败: "+response.Errmsg, response.LogID)
100
+ }
101
+ if response.Data == nil {
102
+ return nil, fmt.Errorf("团队列表响应缺少 data")
103
+ }
104
+ for _, team := range response.Data.Teams {
105
+ id, err := space.ParseTeamID(team.TeamID)
106
+ if err != nil || id == "0" {
107
+ return nil, fmt.Errorf("团队列表返回无效 team_id")
108
+ }
109
+ if !seenTeams[id] {
110
+ result = append(result, listedSpace{Selection: space.Selection{TeamID: id, Name: team.Name}, CurrentRole: team.Role})
111
+ seenTeams[id] = true
112
+ }
113
+ }
114
+ if !response.Data.HasMore {
115
+ return result, nil
116
+ }
117
+ if response.Data.NextCursor == "" {
118
+ return nil, fmt.Errorf("团队列表未返回下一页游标,未保存空间选择")
119
+ }
120
+ cursor = response.Data.NextCursor
121
+ }
122
+ }
123
+
124
+ func newSpaceCommand(stdout io.Writer, runner *common.Runner) *cobra.Command {
125
+ root := &cobra.Command{
126
+ Use: commandnames.Space,
127
+ Short: "查看和切换个人或团队默认空间",
128
+ Args: cobra.NoArgs,
129
+ RunE: func(cmd *cobra.Command, _ []string) error {
130
+ return cmd.Help()
131
+ },
132
+ }
133
+ list := &cobra.Command{Use: "list", Args: cobra.NoArgs, Short: "列出个人和团队空间,标记当前选择", RunE: func(cmd *cobra.Command, _ []string) error {
134
+ store, err := common.SpaceStoreForRunner(cmd.Context(), runner)
135
+ if err != nil {
136
+ return err
137
+ }
138
+ var selected *space.Selection
139
+ if store != nil {
140
+ current, err := store.Current()
141
+ if err != nil {
142
+ return err
143
+ }
144
+ selected = &current
145
+ }
146
+ spaces, err := loadSpaces(cmd.Context(), runner, store)
147
+ if err != nil {
148
+ return err
149
+ }
150
+ for i := range spaces {
151
+ spaces[i].Current = selected != nil && spaces[i].TeamID == selected.TeamID
152
+ }
153
+ return common.WriteJSON(stdout, map[string]any{"spaces": spaces, "current": selected})
154
+ }}
155
+ use := &cobra.Command{Use: "use <TEAM_ID>", Args: cobra.ExactArgs(1), Short: "保存默认空间;0 为个人,正整数为团队", RunE: func(cmd *cobra.Command, args []string) error {
156
+ id, err := space.ParseTeamID(args[0])
157
+ if err != nil {
158
+ return err
159
+ }
160
+ store, err := common.SpaceStoreForRunner(cmd.Context(), runner)
161
+ if err != nil {
162
+ return err
163
+ }
164
+ if store == nil {
165
+ return fmt.Errorf("无法识别当前账号,不能保存默认空间")
166
+ }
167
+ selected := space.Personal()
168
+ if id != "0" {
169
+ spaces, err := loadSpaces(cmd.Context(), runner, store)
170
+ if err != nil {
171
+ return err
172
+ }
173
+ found := false
174
+ for _, item := range spaces {
175
+ if item.TeamID == id {
176
+ selected = item.Selection
177
+ found = true
178
+ break
179
+ }
180
+ }
181
+ if !found {
182
+ return fmt.Errorf("当前账号的团队列表中没有 %s,默认空间未改变", id)
183
+ }
184
+ }
185
+ account, err := common.SpaceAccount(cmd.Context(), runner)
186
+ if err != nil {
187
+ return err
188
+ }
189
+ if account != store.Account {
190
+ return fmt.Errorf("账号已改变,默认空间未保存;请重新选择")
191
+ }
192
+ if err := store.SaveDefault(selected); err != nil {
193
+ return err
194
+ }
195
+ return common.WriteJSON(stdout, selected)
196
+ }}
197
+ root.AddCommand(list, use)
198
+ return root
199
+ }