@awiki/cli 0.0.1-beta.2
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/.github/workflows/release.yml +44 -0
- package/.goreleaser.yml +44 -0
- package/AGENTS.md +60 -0
- package/CLAUDE.md +192 -0
- package/README.md +2 -0
- package/docs/architecture/awiki-command-v2.md +955 -0
- package/docs/architecture/awiki-skill-architecture.md +475 -0
- package/docs/architecture/awiki-v2-architecture.md +1063 -0
- package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/cli-init.md +1008 -0
- package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/output-format.md +407 -0
- package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/overall-init.md +741 -0
- package/docs/harness/review-spec.md +474 -0
- package/docs/installation.md +372 -0
- package/docs/plan/awiki-v2-implementation-plan.md +903 -0
- package/docs/plan/phase-0/adr-index.md +56 -0
- package/docs/plan/phase-0/audit-findings.md +251 -0
- package/docs/plan/phase-0/capability-mapping.md +108 -0
- package/docs/plan/phase-0/implementation-constraints.md +363 -0
- package/docs/publish.md +169 -0
- package/go.mod +29 -0
- package/go.sum +73 -0
- package/internal/anpsdk/registry.go +63 -0
- package/internal/authsdk/session.go +351 -0
- package/internal/buildinfo/buildinfo.go +34 -0
- package/internal/cli/app.go +136 -0
- package/internal/cli/app_test.go +88 -0
- package/internal/cli/debug.go +104 -0
- package/internal/cli/group.go +263 -0
- package/internal/cli/id.go +473 -0
- package/internal/cli/init.go +134 -0
- package/internal/cli/msg.go +228 -0
- package/internal/cli/page.go +267 -0
- package/internal/cli/root.go +499 -0
- package/internal/cli/runtime.go +232 -0
- package/internal/cli/upgrade.go +60 -0
- package/internal/cmdmeta/catalog.go +203 -0
- package/internal/cmdmeta/catalog_test.go +21 -0
- package/internal/config/config.go +399 -0
- package/internal/config/config_test.go +104 -0
- package/internal/config/write.go +37 -0
- package/internal/content/service.go +314 -0
- package/internal/content/service_test.go +165 -0
- package/internal/content/types.go +44 -0
- package/internal/docs/topics.go +110 -0
- package/internal/doctor/doctor.go +306 -0
- package/internal/identity/client.go +267 -0
- package/internal/identity/did.go +85 -0
- package/internal/identity/did_test.go +50 -0
- package/internal/identity/layout.go +206 -0
- package/internal/identity/legacy.go +378 -0
- package/internal/identity/public.go +70 -0
- package/internal/identity/public_test.go +73 -0
- package/internal/identity/readiness.go +74 -0
- package/internal/identity/service.go +826 -0
- package/internal/identity/store.go +385 -0
- package/internal/identity/store_test.go +180 -0
- package/internal/identity/types.go +204 -0
- package/internal/message/auth.go +167 -0
- package/internal/message/group_service.go +838 -0
- package/internal/message/group_wire.go +350 -0
- package/internal/message/group_wire_test.go +67 -0
- package/internal/message/helpers.go +61 -0
- package/internal/message/http_client.go +334 -0
- package/internal/message/proof.go +156 -0
- package/internal/message/proof_test.go +61 -0
- package/internal/message/service.go +696 -0
- package/internal/message/service_test.go +97 -0
- package/internal/message/types.go +155 -0
- package/internal/message/wire.go +100 -0
- package/internal/message/wire_test.go +49 -0
- package/internal/message/ws_proxy_client.go +151 -0
- package/internal/output/output.go +350 -0
- package/internal/output/output_test.go +48 -0
- package/internal/runtime/config.go +117 -0
- package/internal/runtime/config_test.go +46 -0
- package/internal/runtime/listener/files.go +65 -0
- package/internal/runtime/listener/manager.go +142 -0
- package/internal/runtime/listener/server.go +983 -0
- package/internal/runtime/listener/server_test.go +319 -0
- package/internal/runtime/listener/sysproc_unix.go +17 -0
- package/internal/runtime/listener/sysproc_windows.go +13 -0
- package/internal/runtime/listener/types.go +21 -0
- package/internal/runtime/listener/wsclient.go +299 -0
- package/internal/runtime/listener/wsclient_test.go +41 -0
- package/internal/store/dao.go +632 -0
- package/internal/store/dao_test.go +87 -0
- package/internal/store/helpers.go +197 -0
- package/internal/store/import.go +499 -0
- package/internal/store/import_test.go +103 -0
- package/internal/store/open.go +71 -0
- package/internal/store/query.go +151 -0
- package/internal/store/schema.go +277 -0
- package/internal/store/schema_test.go +56 -0
- package/internal/store/types.go +177 -0
- package/internal/update/update.go +368 -0
- package/package.json +17 -0
- package/scripts/install.js +171 -0
- package/scripts/release/release-prerelease.sh +86 -0
- package/scripts/release/tag-release.sh +66 -0
- package/scripts/release/withdraw-release.sh +78 -0
- package/scripts/run.js +69 -0
- package/skills/README.md +32 -0
- package/skills/awiki-bundle/SKILL.md +76 -0
- package/skills/awiki-debug/SKILL.md +80 -0
- package/skills/awiki-group/SKILL.md +111 -0
- package/skills/awiki-id/SKILL.md +123 -0
- package/skills/awiki-msg/SKILL.md +131 -0
- package/skills/awiki-page/SKILL.md +93 -0
- package/skills/awiki-people/SKILL.md +66 -0
- package/skills/awiki-runtime/SKILL.md +137 -0
- package/skills/awiki-shared/SKILL.md +124 -0
- package/skills/awiki-workflow-discovery/SKILL.md +93 -0
- package/skills/awiki-workflow-onboarding/SKILL.md +119 -0
- package/skills/manifests/skills.yaml +260 -0
- package/skills/templates/bundle-skill-template.md +42 -0
- package/skills/templates/debug-skill-template.md +44 -0
- package/skills/templates/domain-skill-template.md +56 -0
- package/skills/templates/shared-skill-template.md +46 -0
- package/skills/templates/workflow-skill-template.md +46 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
package update
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"errors"
|
|
6
|
+
"fmt"
|
|
7
|
+
"net/http"
|
|
8
|
+
"os"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"strconv"
|
|
11
|
+
"strings"
|
|
12
|
+
"time"
|
|
13
|
+
|
|
14
|
+
"github.com/agentconnect/awiki-cli/internal/buildinfo"
|
|
15
|
+
appconfig "github.com/agentconnect/awiki-cli/internal/config"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const (
|
|
19
|
+
defaultMetadataCacheTTLSeconds = 3600
|
|
20
|
+
npmLatestURL = "https://registry.npmjs.org/@awiki%2Fcli/latest"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
// Metadata captures the remote version strategy state that we cache locally.
|
|
24
|
+
type Metadata struct {
|
|
25
|
+
LatestVersion string `json:"latest_version"`
|
|
26
|
+
MinSupportedVersion string `json:"min_supported_version"`
|
|
27
|
+
RetrievedAt time.Time `json:"retrieved_at"`
|
|
28
|
+
Source string `json:"source"` // "network" | "cache" | "cache_stale"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Decision describes how the CLI should behave given the current / remote versions.
|
|
32
|
+
type Decision struct {
|
|
33
|
+
CurrentVersion string `json:"current_version"`
|
|
34
|
+
LatestVersion string `json:"latest_version"`
|
|
35
|
+
MinSupportedVersion string `json:"min_supported_version"`
|
|
36
|
+
|
|
37
|
+
StrictDisabled bool `json:"strict_disabled"`
|
|
38
|
+
DevBuild bool `json:"dev_build"`
|
|
39
|
+
HasNewerVersion bool `json:"has_newer_version"`
|
|
40
|
+
Blocked bool `json:"blocked"`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check resolves the effective version policy (including config + env overrides),
|
|
44
|
+
// loads remote metadata with caching, and returns the decision for the current
|
|
45
|
+
// awiki-cli binary.
|
|
46
|
+
//
|
|
47
|
+
// This function is intentionally tolerant:
|
|
48
|
+
// - Network / cache errors never crash the CLI; callers can choose how hard to fail.
|
|
49
|
+
// - When metadata is missing or unparsable, the Decision falls back to "no block".
|
|
50
|
+
func Check(resolved *appconfig.Resolved) (Decision, error) {
|
|
51
|
+
current := strings.TrimSpace(buildinfo.Version)
|
|
52
|
+
if current == "" {
|
|
53
|
+
current = "dev"
|
|
54
|
+
}
|
|
55
|
+
devBuild := isDevVersion(current)
|
|
56
|
+
|
|
57
|
+
strictDisabled := resolved != nil && resolved.UpdateDisableStrictVersion
|
|
58
|
+
// AWIKI_CLI_DISABLE_STRICT_VERSION is a last-resort escape hatch for debugging.
|
|
59
|
+
if raw := strings.TrimSpace(os.Getenv("AWIKI_CLI_DISABLE_STRICT_VERSION")); raw != "" {
|
|
60
|
+
strictDisabled = parseBool(raw)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
ttlSeconds := defaultMetadataCacheTTLSeconds
|
|
64
|
+
if resolved != nil && resolved.UpdateMetadataCacheTTLSeconds > 0 {
|
|
65
|
+
ttlSeconds = resolved.UpdateMetadataCacheTTLSeconds
|
|
66
|
+
}
|
|
67
|
+
if raw := strings.TrimSpace(os.Getenv("AWIKI_CLI_UPDATE_CACHE_TTL")); raw != "" {
|
|
68
|
+
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 {
|
|
69
|
+
ttlSeconds = parsed
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
decision := Decision{
|
|
74
|
+
CurrentVersion: current,
|
|
75
|
+
StrictDisabled: strictDisabled,
|
|
76
|
+
DevBuild: devBuild,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
meta, err := loadMetadata(resolved, ttlSeconds)
|
|
80
|
+
if err != nil {
|
|
81
|
+
// Propagate the error so callers can log or surface it, but keep the
|
|
82
|
+
// decision usable (no block by default).
|
|
83
|
+
return decision, err
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
decision.LatestVersion = meta.LatestVersion
|
|
87
|
+
decision.MinSupportedVersion = meta.MinSupportedVersion
|
|
88
|
+
|
|
89
|
+
// Dev builds should never be blocked, but they can still see "newer available".
|
|
90
|
+
if devBuild {
|
|
91
|
+
if newer, ok := compareVersions(meta.LatestVersion, current); ok && newer > 0 {
|
|
92
|
+
decision.HasNewerVersion = true
|
|
93
|
+
}
|
|
94
|
+
return decision, nil
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Compute "has newer" and "blocked" flags based on semantic version ordering.
|
|
98
|
+
if newer, ok := compareVersions(meta.LatestVersion, current); ok && newer > 0 {
|
|
99
|
+
decision.HasNewerVersion = true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if !strictDisabled {
|
|
103
|
+
if cmp, ok := compareVersions(current, meta.MinSupportedVersion); ok && cmp < 0 {
|
|
104
|
+
decision.Blocked = true
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return decision, nil
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func isDevVersion(v string) bool {
|
|
112
|
+
v = strings.TrimSpace(strings.ToLower(v))
|
|
113
|
+
if v == "" || v == "dev" {
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
if strings.Contains(v, "-dev") {
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
if strings.HasPrefix(v, "0.0.0-") {
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func parseBool(raw string) bool {
|
|
126
|
+
raw = strings.TrimSpace(strings.ToLower(raw))
|
|
127
|
+
return raw == "1" || raw == "true" || raw == "yes" || raw == "on"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func cachePath(resolved *appconfig.Resolved) (string, error) {
|
|
131
|
+
if resolved == nil {
|
|
132
|
+
return "", errors.New("config is nil")
|
|
133
|
+
}
|
|
134
|
+
cacheDir := strings.TrimSpace(resolved.Paths.CacheDir)
|
|
135
|
+
if cacheDir == "" {
|
|
136
|
+
return "", errors.New("cache dir is empty")
|
|
137
|
+
}
|
|
138
|
+
return filepath.Join(cacheDir, "update", "metadata.json"), nil
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func loadMetadata(resolved *appconfig.Resolved, ttlSeconds int) (Metadata, error) {
|
|
142
|
+
var zero Metadata
|
|
143
|
+
|
|
144
|
+
var cached Metadata
|
|
145
|
+
cacheFile, cacheErr := cachePath(resolved)
|
|
146
|
+
if cacheErr == nil {
|
|
147
|
+
if m, ok, err := readCache(cacheFile, ttlSeconds); err == nil {
|
|
148
|
+
if ok {
|
|
149
|
+
return m, nil
|
|
150
|
+
}
|
|
151
|
+
// ok == false -> expired or empty cache; fall through to network,
|
|
152
|
+
// but remember the last good snapshot in case the network is down.
|
|
153
|
+
cached = m
|
|
154
|
+
} else {
|
|
155
|
+
// Any cache read error is treated as soft; we still try network.
|
|
156
|
+
cached = Metadata{}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
network, err := fetchFromRegistry()
|
|
161
|
+
if err != nil {
|
|
162
|
+
// If we had a usable cached value (even if TTL expired), fall back to it
|
|
163
|
+
// rather than failing hard.
|
|
164
|
+
if cached.LatestVersion != "" {
|
|
165
|
+
cached.Source = "cache_stale"
|
|
166
|
+
return cached, nil
|
|
167
|
+
}
|
|
168
|
+
return zero, err
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if cacheErr == nil {
|
|
172
|
+
_ = writeCache(cacheFile, network)
|
|
173
|
+
}
|
|
174
|
+
return network, nil
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func readCache(path string, ttlSeconds int) (Metadata, bool, error) {
|
|
178
|
+
var meta Metadata
|
|
179
|
+
raw, err := os.ReadFile(path)
|
|
180
|
+
if err != nil {
|
|
181
|
+
if errors.Is(err, os.ErrNotExist) {
|
|
182
|
+
return meta, false, nil
|
|
183
|
+
}
|
|
184
|
+
return meta, false, err
|
|
185
|
+
}
|
|
186
|
+
if err := json.Unmarshal(raw, &meta); err != nil {
|
|
187
|
+
return Metadata{}, false, err
|
|
188
|
+
}
|
|
189
|
+
if ttlSeconds <= 0 || meta.RetrievedAt.IsZero() {
|
|
190
|
+
return meta, true, nil
|
|
191
|
+
}
|
|
192
|
+
if time.Since(meta.RetrievedAt) > time.Duration(ttlSeconds)*time.Second {
|
|
193
|
+
return Metadata{}, false, nil
|
|
194
|
+
}
|
|
195
|
+
return meta, true, nil
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
func writeCache(path string, meta Metadata) error {
|
|
199
|
+
meta.RetrievedAt = time.Now().UTC()
|
|
200
|
+
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
201
|
+
return err
|
|
202
|
+
}
|
|
203
|
+
raw, err := json.MarshalIndent(meta, "", " ")
|
|
204
|
+
if err != nil {
|
|
205
|
+
return err
|
|
206
|
+
}
|
|
207
|
+
return os.WriteFile(path, raw, 0o600)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
func fetchFromRegistry() (Metadata, error) {
|
|
211
|
+
client := &http.Client{
|
|
212
|
+
Timeout: 3 * time.Second,
|
|
213
|
+
}
|
|
214
|
+
req, err := http.NewRequest(http.MethodGet, npmLatestURL, nil)
|
|
215
|
+
if err != nil {
|
|
216
|
+
return Metadata{}, err
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
resp, err := client.Do(req)
|
|
220
|
+
if err != nil {
|
|
221
|
+
return Metadata{}, err
|
|
222
|
+
}
|
|
223
|
+
defer resp.Body.Close()
|
|
224
|
+
|
|
225
|
+
if resp.StatusCode != http.StatusOK {
|
|
226
|
+
return Metadata{}, fmt.Errorf("npm registry responded with status %d", resp.StatusCode)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
var body struct {
|
|
230
|
+
Version string `json:"version"`
|
|
231
|
+
AwikiCli struct {
|
|
232
|
+
MinSupportedVersion string `json:"minSupportedVersion"`
|
|
233
|
+
} `json:"awikiCli"`
|
|
234
|
+
}
|
|
235
|
+
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
236
|
+
return Metadata{}, err
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
latest := strings.TrimSpace(body.Version)
|
|
240
|
+
if latest == "" {
|
|
241
|
+
return Metadata{}, errors.New("npm metadata missing version")
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
minSupported := strings.TrimSpace(body.AwikiCli.MinSupportedVersion)
|
|
245
|
+
if minSupported == "" {
|
|
246
|
+
// If the manifest does not provide an explicit floor, default to "no floor".
|
|
247
|
+
// The decision logic will treat an empty min as "no block".
|
|
248
|
+
minSupported = ""
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return Metadata{
|
|
252
|
+
LatestVersion: latest,
|
|
253
|
+
MinSupportedVersion: minSupported,
|
|
254
|
+
RetrievedAt: time.Now().UTC(),
|
|
255
|
+
Source: "network",
|
|
256
|
+
}, nil
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
type semVersion struct {
|
|
260
|
+
Major int
|
|
261
|
+
Minor int
|
|
262
|
+
Patch int
|
|
263
|
+
Pre string
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
func parseSemVersion(raw string) (semVersion, bool) {
|
|
267
|
+
raw = strings.TrimSpace(raw)
|
|
268
|
+
if raw == "" {
|
|
269
|
+
return semVersion{}, false
|
|
270
|
+
}
|
|
271
|
+
if raw[0] == 'v' || raw[0] == 'V' {
|
|
272
|
+
raw = raw[1:]
|
|
273
|
+
}
|
|
274
|
+
var pre string
|
|
275
|
+
if idx := strings.IndexByte(raw, '-'); idx >= 0 {
|
|
276
|
+
pre = raw[idx+1:]
|
|
277
|
+
raw = raw[:idx]
|
|
278
|
+
}
|
|
279
|
+
parts := strings.Split(raw, ".")
|
|
280
|
+
if len(parts) < 1 || len(parts) > 3 {
|
|
281
|
+
return semVersion{}, false
|
|
282
|
+
}
|
|
283
|
+
parsePart := func(s string) (int, bool) {
|
|
284
|
+
if s == "" {
|
|
285
|
+
return 0, true
|
|
286
|
+
}
|
|
287
|
+
n, err := strconv.Atoi(s)
|
|
288
|
+
if err != nil {
|
|
289
|
+
return 0, false
|
|
290
|
+
}
|
|
291
|
+
if n < 0 {
|
|
292
|
+
return 0, false
|
|
293
|
+
}
|
|
294
|
+
return n, true
|
|
295
|
+
}
|
|
296
|
+
major, ok := parsePart(parts[0])
|
|
297
|
+
if !ok {
|
|
298
|
+
return semVersion{}, false
|
|
299
|
+
}
|
|
300
|
+
minor := 0
|
|
301
|
+
patch := 0
|
|
302
|
+
if len(parts) >= 2 {
|
|
303
|
+
if minor, ok = parsePart(parts[1]); !ok {
|
|
304
|
+
return semVersion{}, false
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if len(parts) == 3 {
|
|
308
|
+
if patch, ok = parsePart(parts[2]); !ok {
|
|
309
|
+
return semVersion{}, false
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return semVersion{
|
|
313
|
+
Major: major,
|
|
314
|
+
Minor: minor,
|
|
315
|
+
Patch: patch,
|
|
316
|
+
Pre: pre,
|
|
317
|
+
}, true
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// compareVersions returns:
|
|
321
|
+
//
|
|
322
|
+
// >0 if a > b
|
|
323
|
+
// 0 if a == b
|
|
324
|
+
// <0 if a < b
|
|
325
|
+
//
|
|
326
|
+
// The second return value is false if either side could not be parsed.
|
|
327
|
+
func compareVersions(a, b string) (int, bool) {
|
|
328
|
+
av, okA := parseSemVersion(a)
|
|
329
|
+
bv, okB := parseSemVersion(b)
|
|
330
|
+
if !okA || !okB {
|
|
331
|
+
return 0, false
|
|
332
|
+
}
|
|
333
|
+
if av.Major != bv.Major {
|
|
334
|
+
if av.Major > bv.Major {
|
|
335
|
+
return 1, true
|
|
336
|
+
}
|
|
337
|
+
return -1, true
|
|
338
|
+
}
|
|
339
|
+
if av.Minor != bv.Minor {
|
|
340
|
+
if av.Minor > bv.Minor {
|
|
341
|
+
return 1, true
|
|
342
|
+
}
|
|
343
|
+
return -1, true
|
|
344
|
+
}
|
|
345
|
+
if av.Patch != bv.Patch {
|
|
346
|
+
if av.Patch > bv.Patch {
|
|
347
|
+
return 1, true
|
|
348
|
+
}
|
|
349
|
+
return -1, true
|
|
350
|
+
}
|
|
351
|
+
// Pre-release comparison: empty pre means stable and is considered newer than pre-release.
|
|
352
|
+
if av.Pre == bv.Pre {
|
|
353
|
+
return 0, true
|
|
354
|
+
}
|
|
355
|
+
if av.Pre == "" {
|
|
356
|
+
return 1, true
|
|
357
|
+
}
|
|
358
|
+
if bv.Pre == "" {
|
|
359
|
+
return -1, true
|
|
360
|
+
}
|
|
361
|
+
if av.Pre > bv.Pre {
|
|
362
|
+
return 1, true
|
|
363
|
+
}
|
|
364
|
+
if av.Pre < bv.Pre {
|
|
365
|
+
return -1, true
|
|
366
|
+
}
|
|
367
|
+
return 0, true
|
|
368
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@awiki/cli",
|
|
3
|
+
"version": "0.0.1-beta.2",
|
|
4
|
+
"description": "Awiki command-line interface (awiki-cli)",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=18"
|
|
7
|
+
},
|
|
8
|
+
"awikiCli": {
|
|
9
|
+
"minSupportedVersion": "0.0.1-beta.2"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"awiki-cli": "scripts/run.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"install-binary": "node scripts/install.js"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
|
|
10
|
+
function mapPlatform() {
|
|
11
|
+
const p = process.platform;
|
|
12
|
+
if (p === 'darwin') return 'darwin';
|
|
13
|
+
if (p === 'linux') return 'linux';
|
|
14
|
+
if (p === 'win32') return 'windows';
|
|
15
|
+
throw new Error(`Unsupported platform: ${p}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function mapArch() {
|
|
19
|
+
const a = process.arch;
|
|
20
|
+
if (a === 'x64') return 'amd64';
|
|
21
|
+
if (a === 'arm64') return 'arm64';
|
|
22
|
+
throw new Error(`Unsupported architecture: ${a}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getVersion(pkg) {
|
|
26
|
+
const v = pkg && typeof pkg.version === 'string' ? pkg.version.trim() : '';
|
|
27
|
+
if (!v) {
|
|
28
|
+
throw new Error('version is missing in package.json');
|
|
29
|
+
}
|
|
30
|
+
return v;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getDownloadUrl(version, osName, arch) {
|
|
34
|
+
const archiveBaseName = `awiki-cli-${version}-${osName}-${arch}`;
|
|
35
|
+
const ext = osName === 'windows' ? 'zip' : 'tar.gz';
|
|
36
|
+
const fileName = `${archiveBaseName}.${ext}`;
|
|
37
|
+
|
|
38
|
+
const mirror = (process.env.AWIKI_CLI_DOWNLOAD_MIRROR || '').trim();
|
|
39
|
+
const base = mirror || 'https://github.com/AgentConnect/awiki-cli/releases/download';
|
|
40
|
+
const baseNoSlash = base.replace(/\/+$/, '');
|
|
41
|
+
const tag = `v${version}`;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
url: `${baseNoSlash}/${tag}/${fileName}`,
|
|
45
|
+
fileName,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function download(url, destPath) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const file = fs.createWriteStream(destPath);
|
|
52
|
+
let finished = false;
|
|
53
|
+
|
|
54
|
+
const req = https.get(url, res => {
|
|
55
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
56
|
+
// handle redirect
|
|
57
|
+
res.destroy();
|
|
58
|
+
file.close(() => fs.unlink(destPath, () => {
|
|
59
|
+
download(res.headers.location, destPath).then(resolve, reject);
|
|
60
|
+
}));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (res.statusCode !== 200) {
|
|
65
|
+
res.resume();
|
|
66
|
+
file.close(() => fs.unlink(destPath, () => {
|
|
67
|
+
reject(new Error(`Download failed with status code ${res.statusCode}`));
|
|
68
|
+
}));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
res.pipe(file);
|
|
73
|
+
file.on('finish', () => {
|
|
74
|
+
finished = true;
|
|
75
|
+
file.close(resolve);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
req.on('error', err => {
|
|
80
|
+
if (!finished) {
|
|
81
|
+
file.close(() => fs.unlink(destPath, () => reject(err)));
|
|
82
|
+
} else {
|
|
83
|
+
reject(err);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ensureDir(dir) {
|
|
90
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function extractArchive(archivePath, destDir, osName) {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
let cmd;
|
|
96
|
+
let args;
|
|
97
|
+
|
|
98
|
+
if (osName === 'windows') {
|
|
99
|
+
// Use PowerShell Expand-Archive; requires PowerShell 5+ (Windows 10/11)
|
|
100
|
+
cmd = 'powershell';
|
|
101
|
+
args = [
|
|
102
|
+
'-NoProfile',
|
|
103
|
+
'-NonInteractive',
|
|
104
|
+
'-Command',
|
|
105
|
+
`Expand-Archive -LiteralPath '${archivePath}' -DestinationPath '${destDir}' -Force`,
|
|
106
|
+
];
|
|
107
|
+
} else {
|
|
108
|
+
cmd = 'tar';
|
|
109
|
+
args = ['-xzf', archivePath, '-C', destDir];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const child = spawn(cmd, args, { stdio: 'inherit' });
|
|
113
|
+
child.on('error', reject);
|
|
114
|
+
child.on('exit', code => {
|
|
115
|
+
if (code === 0) resolve();
|
|
116
|
+
else reject(new Error(`Extraction command ${cmd} exited with code ${code}`));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function main() {
|
|
122
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
123
|
+
const pkgPath = path.join(rootDir, 'package.json');
|
|
124
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
125
|
+
|
|
126
|
+
const version = getVersion(pkg);
|
|
127
|
+
const osName = mapPlatform();
|
|
128
|
+
const arch = mapArch();
|
|
129
|
+
const { url, fileName } = getDownloadUrl(version, osName, arch);
|
|
130
|
+
|
|
131
|
+
const binDir = path.join(rootDir, 'bin');
|
|
132
|
+
ensureDir(binDir);
|
|
133
|
+
|
|
134
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'awiki-cli-'));
|
|
135
|
+
const archivePath = path.join(tmpDir, fileName);
|
|
136
|
+
|
|
137
|
+
console.log(`Downloading awiki-cli ${version} for ${osName}/${arch} from ${url} ...`);
|
|
138
|
+
await download(url, archivePath);
|
|
139
|
+
|
|
140
|
+
console.log(`Extracting to ${binDir} ...`);
|
|
141
|
+
await extractArchive(archivePath, binDir, osName);
|
|
142
|
+
|
|
143
|
+
const exeName = osName === 'windows' ? 'awiki-cli.exe' : 'awiki-cli';
|
|
144
|
+
const exePath = path.join(binDir, exeName);
|
|
145
|
+
|
|
146
|
+
if (osName !== 'windows') {
|
|
147
|
+
try {
|
|
148
|
+
fs.chmodSync(exePath, 0o755);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
// best effort
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(`awiki-cli binary is installed at ${exePath}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (require.main === module) {
|
|
158
|
+
main().catch(err => {
|
|
159
|
+
console.error(`[awiki-cli] Failed to install binary: ${err.message}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
_internal: {
|
|
166
|
+
mapPlatform,
|
|
167
|
+
mapArch,
|
|
168
|
+
getVersion,
|
|
169
|
+
getDownloadUrl,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
5
|
+
cd "${ROOT_DIR}"
|
|
6
|
+
|
|
7
|
+
DIST_TAG="${1:-}"
|
|
8
|
+
|
|
9
|
+
if [ -z "${DIST_TAG}" ]; then
|
|
10
|
+
echo "Usage: scripts/release/release-prerelease.sh <dist-tag>" >&2
|
|
11
|
+
echo "Example: scripts/release/release-prerelease.sh beta" >&2
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
16
|
+
echo "Error: jq is required to run scripts/release/release-prerelease.sh" >&2
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
if [ ! -f package.json ]; then
|
|
21
|
+
echo "Error: package.json not found in ${ROOT_DIR}" >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
VERSION="$(jq -r '.version // empty' package.json)"
|
|
26
|
+
if [ -z "${VERSION}" ]; then
|
|
27
|
+
echo "Error: .version is missing or empty in package.json" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
if [[ "${VERSION}" != *-* ]]; then
|
|
32
|
+
echo "Error: pre-release version must contain a '-' suffix (e.g. 0.2.0-beta.1), got ${VERSION}" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
TAG="v${VERSION}"
|
|
37
|
+
|
|
38
|
+
if [ -n "$(git status --porcelain)" ]; then
|
|
39
|
+
echo "Error: working tree is not clean; please commit or stash changes before tagging" >&2
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
|
44
|
+
if [ "${BRANCH}" = "HEAD" ]; then
|
|
45
|
+
echo "Error: currently on a detached HEAD; please checkout a branch before tagging" >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
if ! git rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
|
|
50
|
+
echo "Error: current branch ${BRANCH} has no upstream; please set upstream and push before tagging" >&2
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [ -n "$(git cherry)" ]; then
|
|
55
|
+
echo "Error: there are local commits not pushed to origin; please push them before tagging" >&2
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
|
60
|
+
echo "Error: tag ${TAG} already exists locally" >&2
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
if git ls-remote --tags origin "refs/tags/${TAG}" | grep -q .; then
|
|
65
|
+
echo "Error: tag ${TAG} already exists on origin" >&2
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
echo "Creating pre-release tag ${TAG} (dist-tag: ${DIST_TAG}) on branch ${BRANCH}..."
|
|
70
|
+
git tag -a "${TAG}" -m "Pre-release ${TAG} (dist-tag: ${DIST_TAG})"
|
|
71
|
+
|
|
72
|
+
echo "Pushing tag ${TAG} to origin..."
|
|
73
|
+
git push origin "${TAG}"
|
|
74
|
+
|
|
75
|
+
cat <<EOF
|
|
76
|
+
|
|
77
|
+
Pre-release tag ${TAG} has been pushed.
|
|
78
|
+
|
|
79
|
+
Next steps:
|
|
80
|
+
- CI will build binaries and create a GitHub pre-release for ${TAG}.
|
|
81
|
+
- To publish the npm pre-release package with dist-tag "${DIST_TAG}", run:
|
|
82
|
+
|
|
83
|
+
NODE_AUTH_TOKEN=... npm publish --access public --tag ${DIST_TAG}
|
|
84
|
+
|
|
85
|
+
EOF
|
|
86
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
5
|
+
cd "${ROOT_DIR}"
|
|
6
|
+
|
|
7
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
8
|
+
echo "Error: jq is required to run scripts/release/tag-release.sh" >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
if [ ! -f package.json ]; then
|
|
13
|
+
echo "Error: package.json not found in ${ROOT_DIR}" >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
VERSION="$(jq -r '.version // empty' package.json)"
|
|
18
|
+
if [ -z "${VERSION}" ]; then
|
|
19
|
+
echo "Error: .version is missing or empty in package.json" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
TAG="v${VERSION}"
|
|
24
|
+
|
|
25
|
+
# Require clean working tree to避免把未提交修改发布出去
|
|
26
|
+
if [ -n "$(git status --porcelain)" ]; then
|
|
27
|
+
echo "Error: working tree is not clean; please commit or stash changes before tagging" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Ensure we are on a named branch, not detached HEAD
|
|
32
|
+
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
|
33
|
+
if [ "${BRANCH}" = "HEAD" ]; then
|
|
34
|
+
echo "Error: currently on a detached HEAD; please checkout a branch (e.g. main) before tagging" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Ensure the branch has an upstream and is fully pushed
|
|
39
|
+
if ! git rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
|
|
40
|
+
echo "Error: current branch ${BRANCH} has no upstream; please set upstream and push before tagging" >&2
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
if [ -n "$(git cherry)" ]; then
|
|
45
|
+
echo "Error: there are local commits not pushed to origin; please push them before tagging" >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Check for existing tag locally and remotely
|
|
50
|
+
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
|
51
|
+
echo "Error: tag ${TAG} already exists locally" >&2
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
if git ls-remote --tags origin "refs/tags/${TAG}" | grep -q .; then
|
|
56
|
+
echo "Error: tag ${TAG} already exists on origin" >&2
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
echo "Creating tag ${TAG} on branch ${BRANCH}..."
|
|
61
|
+
git tag -a "${TAG}" -m "Release ${TAG}"
|
|
62
|
+
|
|
63
|
+
echo "Pushing tag ${TAG} to origin..."
|
|
64
|
+
git push origin "${TAG}"
|
|
65
|
+
|
|
66
|
+
echo "Done. CI should pick up tag ${TAG} and run the release workflow."
|