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.
Files changed (43) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/package.json +1 -1
  3. package/src/api/auth.js +18 -2
  4. package/src/api/client.js +29 -3
  5. package/src/api/feedback.js +14 -5
  6. package/src/api/repos.js +28 -10
  7. package/src/api/translate.js +90 -0
  8. package/src/commands/delete.js +15 -1
  9. package/src/commands/feedback.js +2 -2
  10. package/src/commands/init.js +5 -1
  11. package/src/commands/install.js +58 -32
  12. package/src/commands/postlex.js +53 -35
  13. package/src/commands/postlex.test.js +48 -18
  14. package/src/commands/pull.js +5 -1
  15. package/src/commands/reconcile.js +52 -4
  16. package/src/commands/release.js +45 -15
  17. package/src/commands/schema.js +179 -0
  18. package/src/commands/search.js +34 -22
  19. package/src/commands/search.test.js +59 -33
  20. package/src/commands/uninstall.js +20 -11
  21. package/src/commands/validate.js +33 -11
  22. package/src/constants/error_codes.js +197 -0
  23. package/src/constants/exit_codes.js +54 -0
  24. package/src/constants/next_step_actions.js +133 -0
  25. package/src/constants/next_step_by_error_code.js +249 -0
  26. package/src/constants.js +2 -1
  27. package/src/index.js +51 -7
  28. package/src/integration/api_envelope.test.js +499 -0
  29. package/src/integration/bump.test.js +13 -4
  30. package/src/integration/cli.test.js +169 -147
  31. package/src/integration/drift.test.js +16 -4
  32. package/src/integration/install_fresh.test.js +37 -29
  33. package/src/integration/reconcile.test.js +77 -56
  34. package/src/integration/release.test.js +48 -31
  35. package/src/integration/schema.test.js +167 -0
  36. package/src/schema/envelope.schema.json +73 -0
  37. package/src/schema/envelope_test_helpers.js +94 -0
  38. package/src/schema/envelope_validator.js +239 -0
  39. package/src/schema/envelope_validator.test.js +333 -0
  40. package/src/ui/envelope.js +171 -0
  41. package/src/ui/output.js +66 -2
  42. package/src/utils/errors.js +116 -47
  43. 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
@@ -79,7 +79,8 @@ const COMMANDS = [
79
79
  'snapshot',
80
80
  'reconcile',
81
81
  'release',
82
- 'feedback'
82
+ 'feedback',
83
+ 'schema'
83
84
  ]
84
85
 
85
86
  module.exports = {
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
- if (args.flags.json) {
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 such
139
- // as the MCP server). Initializing here makes the envelope visible to
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, UsageError } = require('./utils/errors')
165
- exit_with_error(new UsageError(`Unknown command: "${command_name}"`))
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
  }