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
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// Default next_step factories by error code. Spec 260525-cli-default-json
|
|
3
|
+
// § 5 + § 7. Commands MAY override with a more specific next_step
|
|
4
|
+
// (carrying domain context like `available[]` for pick_version), but for
|
|
5
|
+
// the common case the central marshaller fills this in automatically.
|
|
6
|
+
//
|
|
7
|
+
// Each factory takes the original error message + an optional context blob
|
|
8
|
+
// and returns the populated next_step object (or null when no default
|
|
9
|
+
// applies — agent falls back to "explain to principal").
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
RECOVERY, DECISION, CONFIRMATION,
|
|
13
|
+
LOGIN, RETRY, RECONCILE_FIRST, PULL_REBASE_FIRST, FIX_VALIDATION_ERRORS,
|
|
14
|
+
PROVIDE_CHANGELOG, SELF_UPDATE, SHOW_FORMAT,
|
|
15
|
+
RESOLVE_REGRESSION, RESOLVE_MISSING_SKILL_JSON, RESOLVE_MISSING_DIR,
|
|
16
|
+
RESOLVE_CONFLICTS, RESOLVE_PATCH_REJECTIONS, SPECIFY_WORKSPACE,
|
|
17
|
+
SPECIFY_BUMP_TYPE, RESOLVE_BUMP_DISAGREEMENT, PICK_VERSION,
|
|
18
|
+
CONFIRM_DISCARD_OR_SNAPSHOT_FIRST, CONFIRM_CASCADE, CONFIRM_DESTRUCTIVE,
|
|
19
|
+
PASS_YES_FLAG,
|
|
20
|
+
} = require('./next_step_actions')
|
|
21
|
+
|
|
22
|
+
const recovery = (action, instructions, context = {}, opts = {}) => ({
|
|
23
|
+
kind: RECOVERY,
|
|
24
|
+
action,
|
|
25
|
+
instructions,
|
|
26
|
+
context,
|
|
27
|
+
...(opts.principal_authorization_required ? { principal_authorization_required: true } : {}),
|
|
28
|
+
...(opts.route_to_skill ? { route_to_skill: opts.route_to_skill } : {}),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const decision = (action, instructions, context = {}, opts = {}) => ({
|
|
32
|
+
kind: DECISION,
|
|
33
|
+
action,
|
|
34
|
+
instructions,
|
|
35
|
+
context,
|
|
36
|
+
principal_authorization_required: opts.principal_authorization_required !== false,
|
|
37
|
+
...(opts.route_to_skill ? { route_to_skill: opts.route_to_skill } : {}),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const confirmation = (action, instructions, context = {}) => ({
|
|
41
|
+
kind: CONFIRMATION,
|
|
42
|
+
action,
|
|
43
|
+
instructions,
|
|
44
|
+
context,
|
|
45
|
+
principal_authorization_required: true,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const NEXT_STEP_BY_ERROR_CODE = Object.freeze({
|
|
49
|
+
AUTH_REQUIRED: () => recovery(
|
|
50
|
+
LOGIN,
|
|
51
|
+
'Sign in to HappySkills, then re-run the command.',
|
|
52
|
+
{ commands: ['npx happyskills login --browser --json'] }
|
|
53
|
+
),
|
|
54
|
+
EXPIRED_TOKEN: () => recovery(
|
|
55
|
+
LOGIN,
|
|
56
|
+
'Your session has expired. Sign in again, then re-run the command.',
|
|
57
|
+
{ commands: ['npx happyskills login --browser --json'] }
|
|
58
|
+
),
|
|
59
|
+
NETWORK_ERROR: () => recovery(
|
|
60
|
+
RETRY,
|
|
61
|
+
'A network error interrupted the request. Retry shortly.',
|
|
62
|
+
{ retry_after_seconds: 5, max_attempts: 3 }
|
|
63
|
+
),
|
|
64
|
+
RATE_LIMITED: (msg, ctx = {}) => recovery(
|
|
65
|
+
RETRY,
|
|
66
|
+
'Rate limit reached. Retry after the suggested interval.',
|
|
67
|
+
{ retry_after_seconds: ctx.retry_after_seconds || 30, max_attempts: 3 }
|
|
68
|
+
),
|
|
69
|
+
DB_UNAVAILABLE: () => recovery(
|
|
70
|
+
RETRY,
|
|
71
|
+
'The registry database is temporarily unavailable. Retry shortly.',
|
|
72
|
+
{ retry_after_seconds: 10, max_attempts: 3 }
|
|
73
|
+
),
|
|
74
|
+
REGISTRY_UNAVAILABLE: () => recovery(
|
|
75
|
+
RETRY,
|
|
76
|
+
'The registry is temporarily unavailable. Retry shortly.',
|
|
77
|
+
{ retry_after_seconds: 10, max_attempts: 3 }
|
|
78
|
+
),
|
|
79
|
+
GITHUB_UNAVAILABLE: () => recovery(
|
|
80
|
+
RETRY,
|
|
81
|
+
'GitHub is temporarily unavailable. Retry shortly.',
|
|
82
|
+
{ retry_after_seconds: 10, max_attempts: 3 }
|
|
83
|
+
),
|
|
84
|
+
EMBEDDING_UNAVAILABLE: () => recovery(
|
|
85
|
+
RETRY,
|
|
86
|
+
'The embedding service is temporarily unavailable. Retry shortly.',
|
|
87
|
+
{ retry_after_seconds: 10, max_attempts: 3 }
|
|
88
|
+
),
|
|
89
|
+
COMMAND_NOT_FOUND: (_msg, ctx = {}) => recovery(
|
|
90
|
+
SHOW_FORMAT,
|
|
91
|
+
'The command does not exist. Suggest the principal run `happyskills --help` to see available commands, or pick from the suggestion if it matches their intent.',
|
|
92
|
+
{
|
|
93
|
+
got: ctx.got,
|
|
94
|
+
...(ctx.suggestion ? { suggestion: ctx.suggestion } : {}),
|
|
95
|
+
commands: ['npx happyskills --help'],
|
|
96
|
+
}
|
|
97
|
+
),
|
|
98
|
+
INVALID_SLUG: () => recovery(
|
|
99
|
+
SHOW_FORMAT,
|
|
100
|
+
'Skill identifiers use the `<owner>/<name>` format. Re-run with the corrected slug.',
|
|
101
|
+
{ expected: '<owner>/<name>' }
|
|
102
|
+
),
|
|
103
|
+
INVALID_VERSION: () => recovery(
|
|
104
|
+
SHOW_FORMAT,
|
|
105
|
+
'Version values must be valid semver (e.g. 1.2.3). Re-run with a corrected version.',
|
|
106
|
+
{ expected: 'semver (MAJOR.MINOR.PATCH)' }
|
|
107
|
+
),
|
|
108
|
+
MIN_CLI_VERSION: (_msg, ctx = {}) => recovery(
|
|
109
|
+
SELF_UPDATE,
|
|
110
|
+
'This CLI is older than the API requires. Upgrade and re-run.',
|
|
111
|
+
{
|
|
112
|
+
...(ctx.min_cli_version ? { min_cli_version: ctx.min_cli_version } : {}),
|
|
113
|
+
...(ctx.current_cli_version ? { current_cli_version: ctx.current_cli_version } : {}),
|
|
114
|
+
commands: ['npm install -g happyskills@latest'],
|
|
115
|
+
},
|
|
116
|
+
{ principal_authorization_required: true }
|
|
117
|
+
),
|
|
118
|
+
DRIFT_DETECTED: (_msg, ctx = {}) => recovery(
|
|
119
|
+
RECONCILE_FIRST,
|
|
120
|
+
'Drift must be resolved before this operation. Run reconcile, follow its next_step, then retry.',
|
|
121
|
+
{
|
|
122
|
+
commands: [`npx happyskills reconcile ${ctx.skill || '<skill>'} --json`],
|
|
123
|
+
...(ctx.drift ? { drift: ctx.drift } : {}),
|
|
124
|
+
},
|
|
125
|
+
{ route_to_skill: 'happyskills-sync' }
|
|
126
|
+
),
|
|
127
|
+
DIVERGED: (_msg, ctx = {}) => recovery(
|
|
128
|
+
PULL_REBASE_FIRST,
|
|
129
|
+
'The local skill has diverged from the registry. Pull with --rebase, resolve any rejected patches, then retry.',
|
|
130
|
+
{ commands: [`npx happyskills pull ${ctx.skill || '<skill>'} --rebase --json`] },
|
|
131
|
+
{ route_to_skill: 'happyskills-sync' }
|
|
132
|
+
),
|
|
133
|
+
VALIDATION_FAILED: (_msg, ctx = {}) => recovery(
|
|
134
|
+
FIX_VALIDATION_ERRORS,
|
|
135
|
+
'Validation failed. Fix the listed errors and re-run.',
|
|
136
|
+
{
|
|
137
|
+
...(ctx.validation_errors ? { validation_errors: ctx.validation_errors } : {}),
|
|
138
|
+
...(ctx.skill ? { commands: [`npx happyskills validate ${ctx.skill} --json`] } : {}),
|
|
139
|
+
}
|
|
140
|
+
),
|
|
141
|
+
DEPENDENCY_VALIDATION_FAILED: (_msg, ctx = {}) => recovery(
|
|
142
|
+
FIX_VALIDATION_ERRORS,
|
|
143
|
+
'Dependency validation failed. Fix the listed dependency issues and re-run.',
|
|
144
|
+
{
|
|
145
|
+
...(ctx.validation_errors ? { validation_errors: ctx.validation_errors } : {}),
|
|
146
|
+
}
|
|
147
|
+
),
|
|
148
|
+
MISSING_CHANGELOG_ENTRY: (_msg, ctx = {}) => recovery(
|
|
149
|
+
PROVIDE_CHANGELOG,
|
|
150
|
+
'CHANGELOG.md is missing an entry for the target version. Add the entry and re-run.',
|
|
151
|
+
{
|
|
152
|
+
...(ctx.target_version ? { target_version: ctx.target_version } : {}),
|
|
153
|
+
...(ctx.current_top_entry ? { current_top_entry: ctx.current_top_entry } : {}),
|
|
154
|
+
},
|
|
155
|
+
{ principal_authorization_required: true }
|
|
156
|
+
),
|
|
157
|
+
CHANGELOG_SOURCE_UNREADABLE: () => recovery(
|
|
158
|
+
PROVIDE_CHANGELOG,
|
|
159
|
+
'CHANGELOG.md could not be read. Restore the file and re-run.',
|
|
160
|
+
{},
|
|
161
|
+
{ principal_authorization_required: true }
|
|
162
|
+
),
|
|
163
|
+
|
|
164
|
+
// Decision-kind defaults
|
|
165
|
+
VERSION_NOT_FOUND: (_msg, ctx = {}) => decision(
|
|
166
|
+
PICK_VERSION,
|
|
167
|
+
'The requested version does not exist. Pick from the available versions and re-run.',
|
|
168
|
+
{
|
|
169
|
+
...(ctx.requested ? { requested: ctx.requested } : {}),
|
|
170
|
+
...(ctx.available ? { available: ctx.available } : {}),
|
|
171
|
+
...(ctx.skill && ctx.available && ctx.available.length
|
|
172
|
+
? { commands: [`npx happyskills install ${ctx.skill}@${ctx.available[ctx.available.length - 1]} --json`] }
|
|
173
|
+
: {}),
|
|
174
|
+
}
|
|
175
|
+
),
|
|
176
|
+
MISSING_VERSION: (_msg, ctx = {}) => decision(
|
|
177
|
+
SPECIFY_BUMP_TYPE,
|
|
178
|
+
'No target version determined. Pick a bump type and re-run.',
|
|
179
|
+
{
|
|
180
|
+
options: ['patch', 'minor', 'major'],
|
|
181
|
+
...(ctx.current_version ? { current_version: ctx.current_version } : {}),
|
|
182
|
+
}
|
|
183
|
+
),
|
|
184
|
+
INVALID_BUMP: (_msg, ctx = {}) => decision(
|
|
185
|
+
SPECIFY_BUMP_TYPE,
|
|
186
|
+
'The provided bump value is not valid. Pick a bump type and re-run.',
|
|
187
|
+
{
|
|
188
|
+
options: ['patch', 'minor', 'major'],
|
|
189
|
+
...(ctx.bump ? { got: ctx.bump } : {}),
|
|
190
|
+
}
|
|
191
|
+
),
|
|
192
|
+
BUMP_DISAGREEMENT: (_msg, ctx = {}) => decision(
|
|
193
|
+
RESOLVE_BUMP_DISAGREEMENT,
|
|
194
|
+
'The --bump value disagrees with the disk version. Pick one and re-run.',
|
|
195
|
+
{
|
|
196
|
+
...(ctx.disk_version ? { disk_version: ctx.disk_version } : {}),
|
|
197
|
+
...(ctx.requested_bump ? { requested_bump: ctx.requested_bump } : {}),
|
|
198
|
+
...(ctx.lock_version ? { lock_version: ctx.lock_version } : {}),
|
|
199
|
+
}
|
|
200
|
+
),
|
|
201
|
+
WORKSPACE_UNRESOLVED: (_msg, ctx = {}) => decision(
|
|
202
|
+
SPECIFY_WORKSPACE,
|
|
203
|
+
'Could not resolve a workspace from the input. Pick one and re-run.',
|
|
204
|
+
{
|
|
205
|
+
...(ctx.candidates ? { candidates: ctx.candidates } : {}),
|
|
206
|
+
}
|
|
207
|
+
),
|
|
208
|
+
|
|
209
|
+
// Confirmation-kind defaults
|
|
210
|
+
LOCAL_EDITS_PRESENT: (_msg, ctx = {}) => confirmation(
|
|
211
|
+
CONFIRM_DISCARD_OR_SNAPSHOT_FIRST,
|
|
212
|
+
'Local edits exist. The principal must choose: snapshot first (safe), or discard the edits (destructive).',
|
|
213
|
+
{
|
|
214
|
+
commands: [
|
|
215
|
+
`npx happyskills snapshot create ${ctx.skill || '<skill>'} --json`,
|
|
216
|
+
`npx happyskills install ${ctx.skill || '<skill>'}${ctx.version ? '@' + ctx.version : ''} --fresh --force-discard-local --json`,
|
|
217
|
+
],
|
|
218
|
+
...(ctx.modified_files ? { modified_files: ctx.modified_files } : {}),
|
|
219
|
+
}
|
|
220
|
+
),
|
|
221
|
+
CONFIRMATION_REQUIRED: (_msg, ctx = {}) => confirmation(
|
|
222
|
+
CONFIRM_DESTRUCTIVE,
|
|
223
|
+
'This is a destructive operation. Re-run with -y to confirm.',
|
|
224
|
+
{
|
|
225
|
+
...(ctx.commands ? { commands: ctx.commands } : {}),
|
|
226
|
+
...(ctx.would_remove ? { would_remove: ctx.would_remove } : {}),
|
|
227
|
+
}
|
|
228
|
+
),
|
|
229
|
+
DEPENDENCY_CASCADE_REQUIRED: (_msg, ctx = {}) => confirmation(
|
|
230
|
+
CONFIRM_CASCADE,
|
|
231
|
+
'This operation cascades to dependencies. Re-run with --confirm to proceed.',
|
|
232
|
+
{
|
|
233
|
+
...(ctx.dependencies ? { dependencies: ctx.dependencies } : {}),
|
|
234
|
+
...(ctx.commands ? { commands: ctx.commands } : {}),
|
|
235
|
+
}
|
|
236
|
+
),
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const next_step_for_error = (code, message, context) => {
|
|
240
|
+
const factory = NEXT_STEP_BY_ERROR_CODE[code]
|
|
241
|
+
if (!factory) return null
|
|
242
|
+
try {
|
|
243
|
+
return factory(message, context || {})
|
|
244
|
+
} catch (_) {
|
|
245
|
+
return null
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = { NEXT_STEP_BY_ERROR_CODE, next_step_for_error }
|
package/src/constants.js
CHANGED
package/src/index.js
CHANGED
|
@@ -2,6 +2,18 @@ const { CLI_VERSION, EXIT_CODES, COMMAND_ALIASES, COMMANDS } = require('./consta
|
|
|
2
2
|
const { print_error, print_help } = require('./ui/output')
|
|
3
3
|
const { dim, yellow } = require('./ui/colors')
|
|
4
4
|
|
|
5
|
+
// Mode resolution — spec 260525-cli-default-json § 8.1. Explicit flags
|
|
6
|
+
// always win; CI=true forces JSON; non-TTY stdout forces JSON; interactive
|
|
7
|
+
// TTY falls back to text. Mutual exclusion of --json + --text is a
|
|
8
|
+
// USAGE_ERROR raised in run().
|
|
9
|
+
const resolve_mode = (flags, env = process.env) => {
|
|
10
|
+
if (flags.json) return 'json'
|
|
11
|
+
if (flags.text) return 'text'
|
|
12
|
+
if (env.CI === 'true' || env.CI === '1') return 'json'
|
|
13
|
+
if (!process.stdout.isTTY) return 'json'
|
|
14
|
+
return 'text'
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
const levenshtein = (a, b) => {
|
|
6
18
|
const m = a.length, n = b.length
|
|
7
19
|
const dp = Array.from({ length: m + 1 }, (_, i) =>
|
|
@@ -129,15 +141,27 @@ const run = (argv) => {
|
|
|
129
141
|
const args = parse_args(argv)
|
|
130
142
|
args.flags = normalize_flags(args.flags)
|
|
131
143
|
|
|
132
|
-
|
|
144
|
+
// § 8.1 — resolve emission mode. --json + --text together is a USAGE_ERROR.
|
|
145
|
+
if (args.flags.json && args.flags.text) {
|
|
146
|
+
const { exit_with_error, UsageError } = require('./utils/errors')
|
|
147
|
+
const { set_json_mode } = require('./state')
|
|
148
|
+
set_json_mode()
|
|
149
|
+
exit_with_error(new UsageError('--json and --text are mutually exclusive.'))
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
const mode = resolve_mode(args.flags)
|
|
153
|
+
if (mode === 'json') {
|
|
133
154
|
const { set_json_mode } = require('./state')
|
|
134
155
|
set_json_mode()
|
|
156
|
+
// Reflect the auto-flip back onto args.flags so command modules that
|
|
157
|
+
// check args.flags.json see a consistent view.
|
|
158
|
+
args.flags.json = true
|
|
135
159
|
}
|
|
136
160
|
|
|
137
161
|
// Spec 260521-03 § 8.4 — propagate intent envelope from --intent flag or
|
|
138
|
-
// the HAPPYSKILLS_INTENT_ENVELOPE env var (set by a parent process
|
|
139
|
-
//
|
|
140
|
-
// every subsequent API call within this CLI invocation.
|
|
162
|
+
// the HAPPYSKILLS_INTENT_ENVELOPE env var (set by a parent process that
|
|
163
|
+
// shells out to `happyskills`). Initializing here makes the envelope
|
|
164
|
+
// visible to every subsequent API call within this CLI invocation.
|
|
141
165
|
if (args.flags.intent || process.env.HAPPYSKILLS_INTENT_ENVELOPE) {
|
|
142
166
|
const { init_from_flag, init_from_env } = require('./utils/intent')
|
|
143
167
|
if (args.flags.intent) init_from_flag(args.flags.intent)
|
|
@@ -160,13 +184,25 @@ const run = (argv) => {
|
|
|
160
184
|
|
|
161
185
|
if (!COMMANDS.includes(resolved)) {
|
|
162
186
|
const { is_json_mode } = require('./state')
|
|
187
|
+
const suggestion = suggest_command(command_name)
|
|
163
188
|
if (is_json_mode()) {
|
|
164
|
-
const { exit_with_error,
|
|
165
|
-
|
|
189
|
+
const { exit_with_error, CliError } = require('./utils/errors')
|
|
190
|
+
const { ERROR_CODES } = require('./constants/error_codes')
|
|
191
|
+
const { set_command } = require('./ui/envelope')
|
|
192
|
+
set_command('<unknown>')
|
|
193
|
+
exit_with_error(new CliError(
|
|
194
|
+
suggestion
|
|
195
|
+
? `Unknown command: "${command_name}". Did you mean: ${suggestion}?`
|
|
196
|
+
: `Unknown command: "${command_name}"`,
|
|
197
|
+
{
|
|
198
|
+
code: ERROR_CODES.COMMAND_NOT_FOUND,
|
|
199
|
+
exit_code: 2,
|
|
200
|
+
context: { got: command_name, ...(suggestion ? { suggestion } : {}) },
|
|
201
|
+
}
|
|
202
|
+
))
|
|
166
203
|
return
|
|
167
204
|
}
|
|
168
205
|
print_error(`Unknown command: "${command_name}"`)
|
|
169
|
-
const suggestion = suggest_command(command_name)
|
|
170
206
|
if (suggestion) {
|
|
171
207
|
console.error(dim(` Did you mean: happyskills ${suggestion}?`))
|
|
172
208
|
} else {
|
|
@@ -175,6 +211,14 @@ const run = (argv) => {
|
|
|
175
211
|
return process.exit(EXIT_CODES.USAGE)
|
|
176
212
|
}
|
|
177
213
|
|
|
214
|
+
// Stamp the command name into the envelope helper's module state so every
|
|
215
|
+
// emit_envelope call within this invocation carries meta.command.
|
|
216
|
+
{
|
|
217
|
+
const { set_command, set_workspace } = require('./ui/envelope')
|
|
218
|
+
set_command(resolved)
|
|
219
|
+
if (args.flags.workspace) set_workspace(args.flags.workspace)
|
|
220
|
+
}
|
|
221
|
+
|
|
178
222
|
if (args.flags.help) {
|
|
179
223
|
args.flags._show_help = true
|
|
180
224
|
}
|