@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.
Files changed (119) hide show
  1. package/.github/workflows/release.yml +44 -0
  2. package/.goreleaser.yml +44 -0
  3. package/AGENTS.md +60 -0
  4. package/CLAUDE.md +192 -0
  5. package/README.md +2 -0
  6. package/docs/architecture/awiki-command-v2.md +955 -0
  7. package/docs/architecture/awiki-skill-architecture.md +475 -0
  8. package/docs/architecture/awiki-v2-architecture.md +1063 -0
  9. package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/cli-init.md +1008 -0
  10. package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/output-format.md +407 -0
  11. package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/overall-init.md +741 -0
  12. package/docs/harness/review-spec.md +474 -0
  13. package/docs/installation.md +372 -0
  14. package/docs/plan/awiki-v2-implementation-plan.md +903 -0
  15. package/docs/plan/phase-0/adr-index.md +56 -0
  16. package/docs/plan/phase-0/audit-findings.md +251 -0
  17. package/docs/plan/phase-0/capability-mapping.md +108 -0
  18. package/docs/plan/phase-0/implementation-constraints.md +363 -0
  19. package/docs/publish.md +169 -0
  20. package/go.mod +29 -0
  21. package/go.sum +73 -0
  22. package/internal/anpsdk/registry.go +63 -0
  23. package/internal/authsdk/session.go +351 -0
  24. package/internal/buildinfo/buildinfo.go +34 -0
  25. package/internal/cli/app.go +136 -0
  26. package/internal/cli/app_test.go +88 -0
  27. package/internal/cli/debug.go +104 -0
  28. package/internal/cli/group.go +263 -0
  29. package/internal/cli/id.go +473 -0
  30. package/internal/cli/init.go +134 -0
  31. package/internal/cli/msg.go +228 -0
  32. package/internal/cli/page.go +267 -0
  33. package/internal/cli/root.go +499 -0
  34. package/internal/cli/runtime.go +232 -0
  35. package/internal/cli/upgrade.go +60 -0
  36. package/internal/cmdmeta/catalog.go +203 -0
  37. package/internal/cmdmeta/catalog_test.go +21 -0
  38. package/internal/config/config.go +399 -0
  39. package/internal/config/config_test.go +104 -0
  40. package/internal/config/write.go +37 -0
  41. package/internal/content/service.go +314 -0
  42. package/internal/content/service_test.go +165 -0
  43. package/internal/content/types.go +44 -0
  44. package/internal/docs/topics.go +110 -0
  45. package/internal/doctor/doctor.go +306 -0
  46. package/internal/identity/client.go +267 -0
  47. package/internal/identity/did.go +85 -0
  48. package/internal/identity/did_test.go +50 -0
  49. package/internal/identity/layout.go +206 -0
  50. package/internal/identity/legacy.go +378 -0
  51. package/internal/identity/public.go +70 -0
  52. package/internal/identity/public_test.go +73 -0
  53. package/internal/identity/readiness.go +74 -0
  54. package/internal/identity/service.go +826 -0
  55. package/internal/identity/store.go +385 -0
  56. package/internal/identity/store_test.go +180 -0
  57. package/internal/identity/types.go +204 -0
  58. package/internal/message/auth.go +167 -0
  59. package/internal/message/group_service.go +838 -0
  60. package/internal/message/group_wire.go +350 -0
  61. package/internal/message/group_wire_test.go +67 -0
  62. package/internal/message/helpers.go +61 -0
  63. package/internal/message/http_client.go +334 -0
  64. package/internal/message/proof.go +156 -0
  65. package/internal/message/proof_test.go +61 -0
  66. package/internal/message/service.go +696 -0
  67. package/internal/message/service_test.go +97 -0
  68. package/internal/message/types.go +155 -0
  69. package/internal/message/wire.go +100 -0
  70. package/internal/message/wire_test.go +49 -0
  71. package/internal/message/ws_proxy_client.go +151 -0
  72. package/internal/output/output.go +350 -0
  73. package/internal/output/output_test.go +48 -0
  74. package/internal/runtime/config.go +117 -0
  75. package/internal/runtime/config_test.go +46 -0
  76. package/internal/runtime/listener/files.go +65 -0
  77. package/internal/runtime/listener/manager.go +142 -0
  78. package/internal/runtime/listener/server.go +983 -0
  79. package/internal/runtime/listener/server_test.go +319 -0
  80. package/internal/runtime/listener/sysproc_unix.go +17 -0
  81. package/internal/runtime/listener/sysproc_windows.go +13 -0
  82. package/internal/runtime/listener/types.go +21 -0
  83. package/internal/runtime/listener/wsclient.go +299 -0
  84. package/internal/runtime/listener/wsclient_test.go +41 -0
  85. package/internal/store/dao.go +632 -0
  86. package/internal/store/dao_test.go +87 -0
  87. package/internal/store/helpers.go +197 -0
  88. package/internal/store/import.go +499 -0
  89. package/internal/store/import_test.go +103 -0
  90. package/internal/store/open.go +71 -0
  91. package/internal/store/query.go +151 -0
  92. package/internal/store/schema.go +277 -0
  93. package/internal/store/schema_test.go +56 -0
  94. package/internal/store/types.go +177 -0
  95. package/internal/update/update.go +368 -0
  96. package/package.json +17 -0
  97. package/scripts/install.js +171 -0
  98. package/scripts/release/release-prerelease.sh +86 -0
  99. package/scripts/release/tag-release.sh +66 -0
  100. package/scripts/release/withdraw-release.sh +78 -0
  101. package/scripts/run.js +69 -0
  102. package/skills/README.md +32 -0
  103. package/skills/awiki-bundle/SKILL.md +76 -0
  104. package/skills/awiki-debug/SKILL.md +80 -0
  105. package/skills/awiki-group/SKILL.md +111 -0
  106. package/skills/awiki-id/SKILL.md +123 -0
  107. package/skills/awiki-msg/SKILL.md +131 -0
  108. package/skills/awiki-page/SKILL.md +93 -0
  109. package/skills/awiki-people/SKILL.md +66 -0
  110. package/skills/awiki-runtime/SKILL.md +137 -0
  111. package/skills/awiki-shared/SKILL.md +124 -0
  112. package/skills/awiki-workflow-discovery/SKILL.md +93 -0
  113. package/skills/awiki-workflow-onboarding/SKILL.md +119 -0
  114. package/skills/manifests/skills.yaml +260 -0
  115. package/skills/templates/bundle-skill-template.md +42 -0
  116. package/skills/templates/debug-skill-template.md +44 -0
  117. package/skills/templates/domain-skill-template.md +56 -0
  118. package/skills/templates/shared-skill-template.md +46 -0
  119. 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."