orch-code 0.1.2 → 0.1.4
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/CHANGELOG.md +6 -0
- package/README.md +13 -1
- package/cmd/doctor.go +60 -18
- package/cmd/provider_model_doctor_test.go +81 -0
- package/cmd/version.go +1 -1
- package/package.json +4 -4
- package/scripts/check-npm-published.js +33 -0
- package/scripts/postinstall.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## v0.1.4 - 2026-04-03
|
|
6
|
+
|
|
7
|
+
- feat: add live probe mode for doctor
|
|
8
|
+
## v0.1.3 - 2026-04-03
|
|
9
|
+
|
|
10
|
+
- ci: harden npm install and release flow
|
|
5
11
|
## v0.1.2 - 2026-04-03
|
|
6
12
|
|
|
7
13
|
- fix: send instructions for OpenAI account chat
|
package/README.md
CHANGED
|
@@ -247,6 +247,9 @@ go run . <command>
|
|
|
247
247
|
|
|
248
248
|
`npm i -g orch-code` becomes zero-Go for end users once GitHub Releases and npm publish are both live.
|
|
249
249
|
|
|
250
|
+
Canonical GitHub repo:
|
|
251
|
+
- `https://github.com/beydemirfurkan/orch`
|
|
252
|
+
|
|
250
253
|
Repo automation now expects:
|
|
251
254
|
- GitHub Actions secret: `NPM_TOKEN`
|
|
252
255
|
- a git tag in the form `vX.Y.Z`
|
|
@@ -276,7 +279,13 @@ The release workflow will:
|
|
|
276
279
|
- verify the tag matches package metadata
|
|
277
280
|
- build darwin/linux/windows binaries with GoReleaser
|
|
278
281
|
- publish a GitHub Release with raw binary assets
|
|
279
|
-
- publish the npm package
|
|
282
|
+
- publish the npm package if that version is not already on npm
|
|
283
|
+
- run clean install smoke tests on macOS and Linux using `npm install -g orch-code`
|
|
284
|
+
|
|
285
|
+
Recommended publish path:
|
|
286
|
+
- treat tag push as the canonical release path
|
|
287
|
+
- avoid manual `npm publish` during normal releases
|
|
288
|
+
- keep manual npm publish only as a recovery path if CI is unavailable
|
|
280
289
|
|
|
281
290
|
`release:prepare` updates these files together:
|
|
282
291
|
- `package.json`
|
|
@@ -324,8 +333,11 @@ Validate runtime readiness:
|
|
|
324
333
|
|
|
325
334
|
```bash
|
|
326
335
|
./orch doctor
|
|
336
|
+
./orch doctor --probe
|
|
327
337
|
```
|
|
328
338
|
|
|
339
|
+
`--probe` runs a small live OpenAI chat check, which is useful for validating account-mode OAuth auth beyond local token shape checks.
|
|
340
|
+
|
|
329
341
|
Generate a structured plan only:
|
|
330
342
|
|
|
331
343
|
```bash
|
package/cmd/doctor.go
CHANGED
|
@@ -9,10 +9,13 @@ import (
|
|
|
9
9
|
|
|
10
10
|
"github.com/furkanbeydemir/orch/internal/auth"
|
|
11
11
|
"github.com/furkanbeydemir/orch/internal/config"
|
|
12
|
+
"github.com/furkanbeydemir/orch/internal/providers"
|
|
12
13
|
"github.com/furkanbeydemir/orch/internal/providers/openai"
|
|
13
14
|
"github.com/spf13/cobra"
|
|
14
15
|
)
|
|
15
16
|
|
|
17
|
+
var doctorProbeFlag bool
|
|
18
|
+
|
|
16
19
|
var doctorCmd = &cobra.Command{
|
|
17
20
|
Use: "doctor",
|
|
18
21
|
Short: "Validate Orch runtime readiness",
|
|
@@ -21,6 +24,7 @@ var doctorCmd = &cobra.Command{
|
|
|
21
24
|
|
|
22
25
|
func init() {
|
|
23
26
|
rootCmd.AddCommand(doctorCmd)
|
|
27
|
+
doctorCmd.Flags().BoolVar(&doctorProbeFlag, "probe", false, "Run a live provider chat probe")
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
func runDoctor(cmd *cobra.Command, args []string) error {
|
|
@@ -85,8 +89,8 @@ func runDoctor(cmd *cobra.Command, args []string) error {
|
|
|
85
89
|
})
|
|
86
90
|
|
|
87
91
|
checks = append(checks, check{
|
|
88
|
-
name:
|
|
89
|
-
ok:
|
|
92
|
+
name: "openai.account_refresh",
|
|
93
|
+
ok: authMode != "account" || accountToken != "" || !storedAccount || storedRefresh || (storedCred != nil && storedCred.ExpiresAt.IsZero()) || time.Now().UTC().Before(storedCred.ExpiresAt),
|
|
90
94
|
detail: fmt.Sprintf("required_when_expired=%t", authMode == "account" && accountToken == ""),
|
|
91
95
|
})
|
|
92
96
|
|
|
@@ -95,30 +99,27 @@ func runDoctor(cmd *cobra.Command, args []string) error {
|
|
|
95
99
|
checks = append(checks, check{name: "openai.model.reviewer", ok: strings.TrimSpace(cfg.Provider.OpenAI.Models.Reviewer) != "", detail: cfg.Provider.OpenAI.Models.Reviewer})
|
|
96
100
|
|
|
97
101
|
if cfg.Provider.Flags.OpenAIEnabled && defaultProvider == "openai" {
|
|
98
|
-
client :=
|
|
99
|
-
client.SetTokenResolver(func(ctx context.Context) (string, error) {
|
|
100
|
-
_ = ctx
|
|
101
|
-
if authMode == "api_key" {
|
|
102
|
-
if storedCred != nil && strings.TrimSpace(storedCred.Key) != "" {
|
|
103
|
-
return strings.TrimSpace(storedCred.Key), nil
|
|
104
|
-
}
|
|
105
|
-
return "", nil
|
|
106
|
-
}
|
|
107
|
-
resolved, resolveErr := auth.ResolveAccountAccessToken(cwd, "openai")
|
|
108
|
-
if resolveErr != nil {
|
|
109
|
-
return "", resolveErr
|
|
110
|
-
}
|
|
111
|
-
return resolved, nil
|
|
112
|
-
})
|
|
102
|
+
client := newDoctorOpenAIClient(cwd, cfg.Provider.OpenAI, authMode, storedCred)
|
|
113
103
|
|
|
114
104
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
115
105
|
defer cancel()
|
|
116
106
|
validateErr := client.Validate(ctx)
|
|
117
107
|
checks = append(checks, check{
|
|
118
|
-
name: "openai.auth",
|
|
108
|
+
name: "openai.auth.local",
|
|
119
109
|
ok: validateErr == nil,
|
|
120
110
|
detail: errDetail(validateErr, "ok"),
|
|
121
111
|
})
|
|
112
|
+
|
|
113
|
+
if doctorProbeFlag {
|
|
114
|
+
probeCtx, probeCancel := context.WithTimeout(context.Background(), doctorProbeTimeout(cfg.Provider.OpenAI.TimeoutSeconds))
|
|
115
|
+
defer probeCancel()
|
|
116
|
+
probeErr := runOpenAIProbe(probeCtx, client, cfg.Provider.OpenAI.Models.Coder)
|
|
117
|
+
checks = append(checks, check{
|
|
118
|
+
name: "openai.auth.probe",
|
|
119
|
+
ok: probeErr == nil,
|
|
120
|
+
detail: errDetail(probeErr, "ok"),
|
|
121
|
+
})
|
|
122
|
+
}
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
failed := 0
|
|
@@ -147,3 +148,44 @@ func errDetail(err error, fallback string) string {
|
|
|
147
148
|
}
|
|
148
149
|
return err.Error()
|
|
149
150
|
}
|
|
151
|
+
|
|
152
|
+
func newDoctorOpenAIClient(cwd string, cfg config.OpenAIProviderConfig, authMode string, storedCred *auth.Credential) *openai.Client {
|
|
153
|
+
client := openai.New(cfg)
|
|
154
|
+
client.SetTokenResolver(func(ctx context.Context) (string, error) {
|
|
155
|
+
_ = ctx
|
|
156
|
+
if authMode == "api_key" {
|
|
157
|
+
if storedCred != nil && strings.TrimSpace(storedCred.Key) != "" {
|
|
158
|
+
return strings.TrimSpace(storedCred.Key), nil
|
|
159
|
+
}
|
|
160
|
+
return "", nil
|
|
161
|
+
}
|
|
162
|
+
resolved, resolveErr := auth.ResolveAccountAccessToken(cwd, "openai")
|
|
163
|
+
if resolveErr != nil {
|
|
164
|
+
return "", resolveErr
|
|
165
|
+
}
|
|
166
|
+
return resolved, nil
|
|
167
|
+
})
|
|
168
|
+
return client
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
func runOpenAIProbe(ctx context.Context, client *openai.Client, model string) error {
|
|
172
|
+
_, err := client.Chat(ctx, providers.ChatRequest{
|
|
173
|
+
Role: providers.RoleCoder,
|
|
174
|
+
Model: strings.TrimSpace(model),
|
|
175
|
+
SystemPrompt: "Reply with OK only.",
|
|
176
|
+
UserPrompt: "ping",
|
|
177
|
+
ReasoningEffort: "low",
|
|
178
|
+
})
|
|
179
|
+
return err
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
func doctorProbeTimeout(timeoutSeconds int) time.Duration {
|
|
183
|
+
if timeoutSeconds <= 0 {
|
|
184
|
+
return 20 * time.Second
|
|
185
|
+
}
|
|
186
|
+
timeout := time.Duration(timeoutSeconds) * time.Second
|
|
187
|
+
if timeout > 20*time.Second {
|
|
188
|
+
return 20 * time.Second
|
|
189
|
+
}
|
|
190
|
+
return timeout
|
|
191
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
package cmd
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"encoding/base64"
|
|
4
5
|
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"net/http"
|
|
8
|
+
"net/http/httptest"
|
|
5
9
|
"strings"
|
|
6
10
|
"testing"
|
|
7
11
|
|
|
@@ -56,6 +60,76 @@ func TestDoctorFailsWithoutAPIKey(t *testing.T) {
|
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
func TestDoctorProbeAccountModeSucceeds(t *testing.T) {
|
|
64
|
+
repoRoot := t.TempDir()
|
|
65
|
+
t.Chdir(repoRoot)
|
|
66
|
+
|
|
67
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
68
|
+
if r.URL.Path != "/codex/responses" {
|
|
69
|
+
t.Fatalf("unexpected path: %s", r.URL.Path)
|
|
70
|
+
}
|
|
71
|
+
if got := r.Header.Get("ChatGPT-Account-Id"); got != "acc-123" {
|
|
72
|
+
t.Fatalf("unexpected account header: %s", got)
|
|
73
|
+
}
|
|
74
|
+
w.Header().Set("Content-Type", "application/json")
|
|
75
|
+
_, _ = w.Write([]byte(`{"output_text":"OK","status":"completed","usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}`))
|
|
76
|
+
}))
|
|
77
|
+
defer server.Close()
|
|
78
|
+
|
|
79
|
+
if err := config.EnsureOrchDir(repoRoot); err != nil {
|
|
80
|
+
t.Fatalf("ensure orch dir: %v", err)
|
|
81
|
+
}
|
|
82
|
+
cfg := config.DefaultConfig()
|
|
83
|
+
cfg.Provider.OpenAI.AuthMode = "account"
|
|
84
|
+
cfg.Provider.OpenAI.BaseURL = server.URL
|
|
85
|
+
cfg.Provider.OpenAI.TimeoutSeconds = 5
|
|
86
|
+
if err := config.Save(repoRoot, cfg); err != nil {
|
|
87
|
+
t.Fatalf("save config: %v", err)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
t.Setenv("OPENAI_ACCOUNT_TOKEN", testDoctorAccountToken("acc-123"))
|
|
91
|
+
doctorProbeFlag = true
|
|
92
|
+
defer func() { doctorProbeFlag = false }()
|
|
93
|
+
|
|
94
|
+
if err := runDoctor(nil, nil); err != nil {
|
|
95
|
+
t.Fatalf("expected doctor probe to succeed: %v", err)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func TestDoctorProbeAccountModeFailsWhenProviderRejects(t *testing.T) {
|
|
100
|
+
repoRoot := t.TempDir()
|
|
101
|
+
t.Chdir(repoRoot)
|
|
102
|
+
|
|
103
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
104
|
+
w.WriteHeader(http.StatusUnauthorized)
|
|
105
|
+
_, _ = w.Write([]byte(`{"error":"unauthorized"}`))
|
|
106
|
+
}))
|
|
107
|
+
defer server.Close()
|
|
108
|
+
|
|
109
|
+
if err := config.EnsureOrchDir(repoRoot); err != nil {
|
|
110
|
+
t.Fatalf("ensure orch dir: %v", err)
|
|
111
|
+
}
|
|
112
|
+
cfg := config.DefaultConfig()
|
|
113
|
+
cfg.Provider.OpenAI.AuthMode = "account"
|
|
114
|
+
cfg.Provider.OpenAI.BaseURL = server.URL
|
|
115
|
+
cfg.Provider.OpenAI.TimeoutSeconds = 5
|
|
116
|
+
if err := config.Save(repoRoot, cfg); err != nil {
|
|
117
|
+
t.Fatalf("save config: %v", err)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
t.Setenv("OPENAI_ACCOUNT_TOKEN", testDoctorAccountToken("acc-123"))
|
|
121
|
+
doctorProbeFlag = true
|
|
122
|
+
defer func() { doctorProbeFlag = false }()
|
|
123
|
+
|
|
124
|
+
err := runDoctor(nil, nil)
|
|
125
|
+
if err == nil {
|
|
126
|
+
t.Fatalf("expected doctor probe failure")
|
|
127
|
+
}
|
|
128
|
+
if !strings.Contains(err.Error(), "doctor failed") {
|
|
129
|
+
t.Fatalf("unexpected doctor error: %v", err)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
59
133
|
func TestProviderListJSONOutput(t *testing.T) {
|
|
60
134
|
repoRoot := t.TempDir()
|
|
61
135
|
t.Chdir(repoRoot)
|
|
@@ -89,3 +163,10 @@ func TestProviderListJSONOutput(t *testing.T) {
|
|
|
89
163
|
t.Fatalf("expected openai in all providers, got: %#v", all)
|
|
90
164
|
}
|
|
91
165
|
}
|
|
166
|
+
|
|
167
|
+
func testDoctorAccountToken(accountID string) string {
|
|
168
|
+
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none"}`))
|
|
169
|
+
payload := fmt.Sprintf(`{"https://api.openai.com/auth":{"chatgpt_account_id":"%s"}}`, accountID)
|
|
170
|
+
body := base64.RawURLEncoding.EncodeToString([]byte(payload))
|
|
171
|
+
return header + "." + body + ".sig"
|
|
172
|
+
}
|
package/cmd/version.go
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orch-code",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Local-first control plane for deterministic AI coding",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
],
|
|
30
30
|
"repository": {
|
|
31
31
|
"type": "git",
|
|
32
|
-
"url": "git+https://github.com/
|
|
32
|
+
"url": "git+https://github.com/beydemirfurkan/orch.git"
|
|
33
33
|
},
|
|
34
34
|
"bugs": {
|
|
35
|
-
"url": "https://github.com/
|
|
35
|
+
"url": "https://github.com/beydemirfurkan/orch/issues"
|
|
36
36
|
},
|
|
37
|
-
"homepage": "https://github.com/
|
|
37
|
+
"homepage": "https://github.com/beydemirfurkan/orch#readme",
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=18"
|
|
40
40
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const https = require('node:https');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
7
|
+
const pkg = require(path.join(packageRoot, 'package.json'));
|
|
8
|
+
|
|
9
|
+
const version = process.argv[2] || pkg.version;
|
|
10
|
+
const encodedName = encodeURIComponent(pkg.name);
|
|
11
|
+
const url = `https://registry.npmjs.org/${encodedName}/${encodeURIComponent(version)}`;
|
|
12
|
+
|
|
13
|
+
https
|
|
14
|
+
.get(url, (response) => {
|
|
15
|
+
response.resume();
|
|
16
|
+
|
|
17
|
+
if (response.statusCode === 200) {
|
|
18
|
+
console.log('true');
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (response.statusCode === 404) {
|
|
23
|
+
console.log('false');
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.error(`[orch] unexpected npm registry status: ${response.statusCode || 'unknown'}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
})
|
|
30
|
+
.on('error', (error) => {
|
|
31
|
+
console.error(`[orch] failed to query npm registry: ${error.message}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
package/scripts/postinstall.js
CHANGED
|
@@ -45,7 +45,7 @@ function main() {
|
|
|
45
45
|
|
|
46
46
|
async function install() {
|
|
47
47
|
const assetName = resolveAssetName();
|
|
48
|
-
const baseUrl = process.env.ORCH_BINARY_BASE_URL || `https://github.com/
|
|
48
|
+
const baseUrl = process.env.ORCH_BINARY_BASE_URL || `https://github.com/beydemirfurkan/orch/releases/download/v${pkg.version}`;
|
|
49
49
|
const assetUrl = `${baseUrl}/${assetName}`;
|
|
50
50
|
|
|
51
51
|
try {
|