happyskills 0.6.0 → 0.7.1
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
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.1] - 2026-03-06
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Fix `uninstall` incorrectly orphaning all user-installed skills — `find_orphans` now treats `__root__` as a protected requester, so only skills that were installed solely as transitive dependencies are pruned
|
|
14
|
+
|
|
15
|
+
## [0.7.0] - 2026-03-06
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Add `--json --browser` combined flags to `login` command for single-command authentication — returns already-logged-in status or launches the browser flow and returns the result as JSON, eliminating the two-step check-then-login dance
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Change `login_device` expired-token handling from `process.exit()` to a thrown error so callers (including `--json` mode) can handle it gracefully
|
|
22
|
+
|
|
10
23
|
## [0.6.0] - 2026-03-06
|
|
11
24
|
|
|
12
25
|
### Added
|
package/package.json
CHANGED
package/src/commands/login.js
CHANGED
|
@@ -22,7 +22,8 @@ Options:
|
|
|
22
22
|
Examples:
|
|
23
23
|
happyskills login
|
|
24
24
|
happyskills login --browser
|
|
25
|
-
happyskills login --password
|
|
25
|
+
happyskills login --password
|
|
26
|
+
happyskills login --json --browser Check status or authenticate (for automation)`
|
|
26
27
|
|
|
27
28
|
const prompt = (question) => new Promise((resolve) => {
|
|
28
29
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
|
|
@@ -120,8 +121,33 @@ const run = (args) => catch_errors('Login failed', async () => {
|
|
|
120
121
|
const email = payload?.email || 'unknown'
|
|
121
122
|
const username = payload?.['cognito:username'] || payload?.sub || 'unknown'
|
|
122
123
|
print_json({ data: { status: 'already_logged_in', username, email } })
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Not logged in — check if browser flow requested
|
|
128
|
+
if (!args.flags.browser) {
|
|
129
|
+
print_json({ error: { code: 'INTERACTIVE_REQUIRED', message: 'Login requires browser interaction. Run `happyskills login --json --browser` to authenticate.', exit_code: EXIT_CODES.ERROR } })
|
|
130
|
+
process.exit(EXIT_CODES.ERROR)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --json --browser: run browser flow, return JSON result
|
|
135
|
+
const [errors] = await run_browser_flow()
|
|
136
|
+
if (errors) {
|
|
137
|
+
print_json({ error: { code: 'AUTH_FAILED', message: errors[0]?.message || 'Browser login failed', exit_code: EXIT_CODES.ERROR } })
|
|
138
|
+
process.exit(EXIT_CODES.ERROR)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Browser flow succeeded — read saved token
|
|
143
|
+
const [, new_token] = await load_token()
|
|
144
|
+
if (new_token) {
|
|
145
|
+
const payload = decode_jwt_payload(new_token.id_token)
|
|
146
|
+
const email = payload?.email || 'unknown'
|
|
147
|
+
const username = payload?.['cognito:username'] || payload?.sub || 'unknown'
|
|
148
|
+
print_json({ data: { status: 'logged_in', username, email } })
|
|
123
149
|
} else {
|
|
124
|
-
print_json({ error: { code: '
|
|
150
|
+
print_json({ error: { code: 'AUTH_FAILED', message: 'Login flow completed but token could not be loaded', exit_code: EXIT_CODES.ERROR } })
|
|
125
151
|
process.exit(EXIT_CODES.ERROR)
|
|
126
152
|
}
|
|
127
153
|
return
|
|
@@ -45,7 +45,7 @@ const run_browser_flow = () => catch_errors('Browser login failed', async () =>
|
|
|
45
45
|
const api_err = token_err.find(err => err.error_code)
|
|
46
46
|
if (api_err?.error_code === 'EXPIRED_TOKEN') {
|
|
47
47
|
spinner.fail('Authorization expired. Please run `happyskills login` again.')
|
|
48
|
-
|
|
48
|
+
throw new Error('Authorization expired. Please run `happyskills login` again.')
|
|
49
49
|
}
|
|
50
50
|
// Transient error — keep polling
|
|
51
51
|
continue
|
|
@@ -9,11 +9,8 @@ const find_orphans = (skills, removed_skill) => {
|
|
|
9
9
|
const orphans = []
|
|
10
10
|
for (const [name, data] of Object.entries(skills)) {
|
|
11
11
|
if (name === removed_skill) continue
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
continue
|
|
15
|
-
}
|
|
16
|
-
const remaining = data.requested_by.filter(r => r !== removed_skill && skills[r])
|
|
12
|
+
const requested_by = data.requested_by || []
|
|
13
|
+
const remaining = requested_by.filter(r => r === '__root__' || (r !== removed_skill && skills[r]))
|
|
17
14
|
if (remaining.length === 0) {
|
|
18
15
|
orphans.push(name)
|
|
19
16
|
}
|
|
@@ -87,12 +87,12 @@ describe('find_orphans', () => {
|
|
|
87
87
|
assert.ok(result.includes('acme/auth'))
|
|
88
88
|
})
|
|
89
89
|
|
|
90
|
-
it('
|
|
90
|
+
it('protects skills installed by the user (requested_by includes __root__)', () => {
|
|
91
91
|
const skills = {
|
|
92
92
|
'acme/deploy': { requested_by: ['__root__'] }
|
|
93
93
|
}
|
|
94
|
-
// __root__
|
|
94
|
+
// __root__ marks a user-installed (root) package — should never be orphaned
|
|
95
95
|
const result = find_orphans(skills, 'acme/other')
|
|
96
|
-
assert.ok(result.includes('acme/deploy'))
|
|
96
|
+
assert.ok(!result.includes('acme/deploy'))
|
|
97
97
|
})
|
|
98
98
|
})
|
|
@@ -340,6 +340,32 @@ describe('CLI — --json: success responses use { data } envelope', () => {
|
|
|
340
340
|
fs.rmSync(tmp_xdg, { recursive: true, force: true })
|
|
341
341
|
}
|
|
342
342
|
})
|
|
343
|
+
|
|
344
|
+
it('login --json --browser with stored credentials returns already_logged_in', () => {
|
|
345
|
+
const tmp_xdg = make_tmp()
|
|
346
|
+
try {
|
|
347
|
+
const creds_dir = path.join(tmp_xdg, 'happyskills')
|
|
348
|
+
fs.mkdirSync(creds_dir, { recursive: true })
|
|
349
|
+
const payload = { email: 'test@example.com', 'cognito:username': 'testuser', sub: 'test-sub-123' }
|
|
350
|
+
const fake_jwt = `eyJhbGciOiJSUzI1NiJ9.${Buffer.from(JSON.stringify(payload)).toString('base64url')}.fakesig`
|
|
351
|
+
fs.writeFileSync(path.join(creds_dir, 'credentials.json'), JSON.stringify({
|
|
352
|
+
id_token: fake_jwt,
|
|
353
|
+
access_token: 'fake-access',
|
|
354
|
+
refresh_token: 'fake-refresh',
|
|
355
|
+
expires_in: 3600,
|
|
356
|
+
stored_at: new Date().toISOString()
|
|
357
|
+
}, null, '\t'))
|
|
358
|
+
const { stdout, code } = run(['login', '--json', '--browser'], { XDG_CONFIG_HOME: tmp_xdg })
|
|
359
|
+
assert.strictEqual(code, 0)
|
|
360
|
+
const out = parse_json_output(stdout, 'login --json --browser with credentials')
|
|
361
|
+
assert.ok('data' in out)
|
|
362
|
+
assert.strictEqual(out.data.status, 'already_logged_in')
|
|
363
|
+
assert.strictEqual(out.data.username, 'testuser')
|
|
364
|
+
assert.strictEqual(out.data.email, 'test@example.com')
|
|
365
|
+
} finally {
|
|
366
|
+
fs.rmSync(tmp_xdg, { recursive: true, force: true })
|
|
367
|
+
}
|
|
368
|
+
})
|
|
343
369
|
})
|
|
344
370
|
|
|
345
371
|
// ─── --json flag: existing commands use { data } envelope ─────────────────────
|