happyskills 0.53.0 → 1.0.0
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 +30 -0
- package/package.json +1 -1
- package/src/api/auth.js +18 -2
- package/src/api/client.js +29 -3
- package/src/api/feedback.js +14 -5
- package/src/api/repos.js +28 -10
- package/src/api/translate.js +90 -0
- package/src/commands/delete.js +15 -1
- package/src/commands/feedback.js +2 -2
- package/src/commands/init.js +5 -1
- package/src/commands/install.js +58 -32
- package/src/commands/postlex.js +53 -35
- package/src/commands/postlex.test.js +48 -18
- package/src/commands/pull.js +5 -1
- package/src/commands/reconcile.js +52 -4
- package/src/commands/release.js +45 -15
- package/src/commands/schema.js +179 -0
- package/src/commands/search.js +34 -22
- package/src/commands/search.test.js +59 -33
- package/src/commands/uninstall.js +20 -11
- package/src/commands/validate.js +33 -11
- package/src/constants/error_codes.js +197 -0
- package/src/constants/exit_codes.js +54 -0
- package/src/constants/next_step_actions.js +133 -0
- package/src/constants/next_step_by_error_code.js +249 -0
- package/src/constants.js +2 -1
- package/src/index.js +51 -7
- package/src/integration/api_envelope.test.js +499 -0
- package/src/integration/bump.test.js +13 -4
- package/src/integration/cli.test.js +169 -147
- package/src/integration/drift.test.js +16 -4
- package/src/integration/install_fresh.test.js +37 -29
- package/src/integration/reconcile.test.js +77 -56
- package/src/integration/release.test.js +48 -31
- package/src/integration/schema.test.js +167 -0
- package/src/schema/envelope.schema.json +73 -0
- package/src/schema/envelope_test_helpers.js +94 -0
- package/src/schema/envelope_validator.js +239 -0
- package/src/schema/envelope_validator.test.js +333 -0
- package/src/ui/envelope.js +171 -0
- package/src/ui/output.js +66 -2
- package/src/utils/errors.js +116 -47
- package/src/utils/intent.js +22 -1
package/src/utils/errors.js
CHANGED
|
@@ -1,44 +1,85 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict'
|
|
2
|
+
// Error classes + central exit_with_error marshaller. Spec
|
|
3
|
+
// 260525-cli-default-json § 4 + § 5. Every error class carries a closed
|
|
4
|
+
// error.code that emit_envelope writes into the `error` slot. Domain
|
|
5
|
+
// errors not modelled by a dedicated class can be raised as CliError(msg,
|
|
6
|
+
// { code, exit_code, details, context, next_step }).
|
|
7
|
+
//
|
|
8
|
+
// exit_with_error builds the envelope, emits to stdout (json mode) or
|
|
9
|
+
// stderr (text mode), and exits with the mapped exit code.
|
|
10
|
+
|
|
11
|
+
const { ERROR_CODES, ERROR_CODE_SET } = require('../constants/error_codes')
|
|
12
|
+
const { exit_code_for_error } = require('../constants/exit_codes')
|
|
13
|
+
const { next_step_for_error } = require('../constants/next_step_by_error_code')
|
|
2
14
|
|
|
3
15
|
class CliError extends Error {
|
|
4
|
-
|
|
16
|
+
// Accepts the legacy positional shape `new CliError(msg, exit_code)` and
|
|
17
|
+
// the new options shape `new CliError(msg, { code, exit_code, details,
|
|
18
|
+
// context, next_step })`. The legacy form maps to code 'INTERNAL_ERROR'.
|
|
19
|
+
constructor(message, opts_or_exit_code) {
|
|
5
20
|
super(message)
|
|
6
21
|
this.name = 'CliError'
|
|
7
|
-
|
|
22
|
+
if (typeof opts_or_exit_code === 'object' && opts_or_exit_code !== null) {
|
|
23
|
+
const { code, exit_code, details, context, next_step } = opts_or_exit_code
|
|
24
|
+
this.code = code || ERROR_CODES.INTERNAL_ERROR
|
|
25
|
+
this.exit_code = exit_code != null ? exit_code : exit_code_for_error(this.code)
|
|
26
|
+
if (details) this.details = details
|
|
27
|
+
if (context) this.context = context
|
|
28
|
+
if (next_step) this.next_step = next_step
|
|
29
|
+
} else {
|
|
30
|
+
this.code = ERROR_CODES.INTERNAL_ERROR
|
|
31
|
+
this.exit_code = typeof opts_or_exit_code === 'number' ? opts_or_exit_code : 1
|
|
32
|
+
}
|
|
8
33
|
}
|
|
9
34
|
}
|
|
10
35
|
|
|
11
36
|
class UsageError extends CliError {
|
|
12
|
-
constructor(message) {
|
|
13
|
-
super(message,
|
|
37
|
+
constructor(message, opts = {}) {
|
|
38
|
+
super(message, { code: ERROR_CODES.USAGE_ERROR, exit_code: 2, ...opts })
|
|
14
39
|
this.name = 'UsageError'
|
|
15
40
|
}
|
|
16
41
|
}
|
|
17
42
|
|
|
18
43
|
class AuthError extends CliError {
|
|
19
|
-
constructor(message = 'Authentication required. Run `happyskills login` first.') {
|
|
20
|
-
super(message,
|
|
44
|
+
constructor(message = 'Authentication required. Run `happyskills login` first.', opts = {}) {
|
|
45
|
+
super(message, { code: ERROR_CODES.AUTH_REQUIRED, exit_code: 3, ...opts })
|
|
21
46
|
this.name = 'AuthError'
|
|
22
47
|
}
|
|
23
48
|
}
|
|
24
49
|
|
|
25
50
|
class NetworkError extends CliError {
|
|
26
|
-
constructor(message = 'Network error. Check your connection and try again.') {
|
|
27
|
-
super(message,
|
|
51
|
+
constructor(message = 'Network error. Check your connection and try again.', opts = {}) {
|
|
52
|
+
super(message, { code: ERROR_CODES.NETWORK_ERROR, exit_code: 4, ...opts })
|
|
28
53
|
this.name = 'NetworkError'
|
|
29
54
|
}
|
|
30
55
|
}
|
|
31
56
|
|
|
32
57
|
class ApiError extends CliError {
|
|
33
|
-
constructor(message, status_code, error_code) {
|
|
34
|
-
|
|
58
|
+
constructor(message, status_code, error_code, opts = {}) {
|
|
59
|
+
const code = (() => {
|
|
60
|
+
if (error_code && ERROR_CODE_SET.has(error_code)) return error_code
|
|
61
|
+
if (status_code === 401) return ERROR_CODES.AUTH_REQUIRED
|
|
62
|
+
if (status_code === 404) return ERROR_CODES.NOT_FOUND
|
|
63
|
+
if (status_code === 429) return ERROR_CODES.RATE_LIMITED
|
|
64
|
+
if (error_code) return ERROR_CODES.UNKNOWN_CODE
|
|
65
|
+
return ERROR_CODES.INTERNAL_ERROR
|
|
66
|
+
})()
|
|
67
|
+
// Status-code-driven override: 401 always exits 3 regardless of code.
|
|
68
|
+
const exit_code = opts.exit_code != null
|
|
69
|
+
? opts.exit_code
|
|
70
|
+
: (status_code === 401 ? 3 : exit_code_for_error(code))
|
|
71
|
+
super(message, { code, exit_code, ...opts })
|
|
35
72
|
this.name = 'ApiError'
|
|
36
73
|
this.status_code = status_code
|
|
37
|
-
this.error_code
|
|
38
|
-
if (
|
|
74
|
+
this.error_code = error_code
|
|
75
|
+
if (error_code && !ERROR_CODE_SET.has(error_code)) {
|
|
76
|
+
this.details = { ...(this.details || {}), original_code: error_code }
|
|
77
|
+
}
|
|
39
78
|
}
|
|
40
79
|
}
|
|
41
80
|
|
|
81
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
42
83
|
const print_log_hint = (log_path) => {
|
|
43
84
|
if (log_path) console.error(` Log: ${log_path}`)
|
|
44
85
|
}
|
|
@@ -51,13 +92,8 @@ const is_usage_error = (err) => {
|
|
|
51
92
|
|
|
52
93
|
const extract_root_cause = (err) => {
|
|
53
94
|
if (!Array.isArray(err)) return err?.message || 'An unexpected error occurred'
|
|
54
|
-
// Walk the error array to find the deepest, most specific message.
|
|
55
|
-
// catch_errors wraps errors outermost-first: [outermost, ..., root_cause].
|
|
56
|
-
// We prefer the last error (root cause) for the user-facing message,
|
|
57
|
-
// but fall back to the first CliError if one exists.
|
|
58
95
|
const messages = err.map(x => x?.message).filter(Boolean)
|
|
59
96
|
if (messages.length === 0) return 'An unexpected error occurred'
|
|
60
|
-
// The last message is typically the root cause (deepest in the chain)
|
|
61
97
|
return messages[messages.length - 1]
|
|
62
98
|
}
|
|
63
99
|
|
|
@@ -66,54 +102,87 @@ const format_error_chain = (err) => {
|
|
|
66
102
|
return err.map(x => x?.stack || x?.message || String(x)).filter(Boolean)
|
|
67
103
|
}
|
|
68
104
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
105
|
+
const get_error_target = (err) => {
|
|
106
|
+
if (!Array.isArray(err)) return err
|
|
107
|
+
// Prefer the deepest CliError; that is where the structured code lives.
|
|
108
|
+
for (let i = err.length - 1; i >= 0; i--) {
|
|
109
|
+
if (err[i] instanceof CliError) return err[i]
|
|
73
110
|
}
|
|
111
|
+
return err[err.length - 1] || err[0]
|
|
112
|
+
}
|
|
74
113
|
|
|
75
|
-
|
|
76
|
-
|
|
114
|
+
const get_error_info = (err) => {
|
|
115
|
+
const target = get_error_target(err)
|
|
77
116
|
const message = extract_root_cause(err)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
117
|
+
let code = ERROR_CODES.INTERNAL_ERROR
|
|
118
|
+
let exit_code = 1
|
|
119
|
+
let details = null
|
|
120
|
+
let context = null
|
|
121
|
+
let next_step = null
|
|
122
|
+
|
|
123
|
+
if (target instanceof CliError) {
|
|
124
|
+
code = target.code || code
|
|
125
|
+
exit_code = target.exit_code != null ? target.exit_code : exit_code_for_error(code)
|
|
126
|
+
if (target.details) details = target.details
|
|
127
|
+
if (target.context) context = target.context
|
|
128
|
+
if (target.next_step) next_step = target.next_step
|
|
129
|
+
} else {
|
|
130
|
+
exit_code = exit_code_for_error(code)
|
|
90
131
|
}
|
|
91
132
|
|
|
92
|
-
return { code, message, exit_code }
|
|
133
|
+
return { code, message, exit_code, details, context, next_step }
|
|
93
134
|
}
|
|
94
135
|
|
|
136
|
+
// ── Central marshaller ────────────────────────────────────────────────────
|
|
137
|
+
|
|
95
138
|
const exit_with_error = (err) => {
|
|
96
139
|
const { is_json_mode } = require('../state')
|
|
140
|
+
const { code, message, exit_code, details, context, next_step: explicit_next_step } = get_error_info(err)
|
|
97
141
|
|
|
98
142
|
if (is_json_mode()) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
143
|
+
// Build envelope. Default next_step from the mapping table, unless
|
|
144
|
+
// the throwing site provided one explicitly.
|
|
145
|
+
const { emit_and_exit } = require('../ui/envelope')
|
|
146
|
+
const error_payload = { code, message }
|
|
147
|
+
if (details) error_payload.details = details
|
|
148
|
+
// Domain-specific extras land alongside code/message (spec § 4.4).
|
|
149
|
+
if (context) {
|
|
150
|
+
for (const k of Object.keys(context)) {
|
|
151
|
+
if (k === 'commands' || k === 'options' || k === 'suggestion' || k === 'got'
|
|
152
|
+
|| k === 'available' || k === 'requested' || k === 'modified_files'
|
|
153
|
+
|| k === 'candidates' || k === 'dependencies' || k === 'would_remove'
|
|
154
|
+
|| k === 'min_cli_version' || k === 'current_cli_version'
|
|
155
|
+
|| k === 'retry_after_seconds' || k === 'target_version' || k === 'current_top_entry'
|
|
156
|
+
|| k === 'disk_version' || k === 'requested_bump' || k === 'lock_version'
|
|
157
|
+
|| k === 'bump' || k === 'current_version' || k === 'skill') {
|
|
158
|
+
// These belong on next_step.context, not on error.
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
if (!(k in error_payload)) error_payload[k] = context[k]
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const next_step = explicit_next_step || next_step_for_error(code, message, context || {}) || {}
|
|
165
|
+
emit_and_exit({
|
|
166
|
+
data: {},
|
|
167
|
+
error: error_payload,
|
|
168
|
+
next_step,
|
|
169
|
+
meta_overrides: { exit_code },
|
|
170
|
+
})
|
|
105
171
|
return
|
|
106
172
|
}
|
|
107
173
|
|
|
108
174
|
const { print_error } = require('../ui/output')
|
|
109
175
|
const { write_error_log } = require('./logger')
|
|
110
|
-
|
|
111
176
|
const log_path = is_usage_error(err) ? null : write_error_log(err)
|
|
112
|
-
const { message, exit_code } = get_error_info(err)
|
|
113
|
-
|
|
114
177
|
print_error(message)
|
|
115
178
|
print_log_hint(log_path)
|
|
116
179
|
process.exit(exit_code)
|
|
117
180
|
}
|
|
118
181
|
|
|
119
|
-
module.exports = {
|
|
182
|
+
module.exports = {
|
|
183
|
+
CliError, UsageError, AuthError, NetworkError, ApiError,
|
|
184
|
+
exit_with_error,
|
|
185
|
+
get_error_info,
|
|
186
|
+
extract_root_cause,
|
|
187
|
+
format_error_chain,
|
|
188
|
+
}
|
package/src/utils/intent.js
CHANGED
|
@@ -39,4 +39,25 @@ const clear_envelope = () => {
|
|
|
39
39
|
delete process.env[ENV_VAR]
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
// Spec 260521-03 addendum § 4 — Extract the intent_id UUID from the active
|
|
43
|
+
// envelope so the CLI can attach it to /telemetry/discovery beacon payloads.
|
|
44
|
+
// The envelope is `<base64url-payload>.<base64url-hmac>`; the CLI doesn't
|
|
45
|
+
// have the HMAC secret and doesn't need to verify — it just reads the
|
|
46
|
+
// payload portion. Returns null when no envelope is active or the payload
|
|
47
|
+
// can't be decoded.
|
|
48
|
+
const get_intent_id = () => {
|
|
49
|
+
const env = get_envelope()
|
|
50
|
+
if (!env) return null
|
|
51
|
+
try {
|
|
52
|
+
const dot = env.indexOf('.')
|
|
53
|
+
if (dot <= 0) return null
|
|
54
|
+
const body = env.slice(0, dot)
|
|
55
|
+
const json = Buffer.from(body, 'base64url').toString('utf-8')
|
|
56
|
+
const payload = JSON.parse(json)
|
|
57
|
+
return payload.intent_id || null
|
|
58
|
+
} catch {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { init_from_env, init_from_flag, get_envelope, set_envelope, clear_envelope, get_intent_id, ENV_VAR }
|