027f25e pinned deno to 2.7.9 in mise.toml and ci.yaml, but the two workflows
that actually build and ship artifacts were left on 'v2.x': release.yml runs
'deno compile' for every target triple, and publish.yaml runs codegen before
'jsr publish'. So the binaries users install were produced by whatever v2.x
resolved to at build time, not the toolchain the project pins and tests on.
build-setup.yml is the dist 'github-build-setup' hook that generates the deno
step in release.yml, so it and the generated line are updated together to keep
'dist generate' a no-op.
setup-deno@v2 already runs an exact 2.7.9 in both ci.yaml jobs, so this is the
version resolution those jobs have been proving all along.
isKeyringAvailable() only probed on Linux and returned true unconditionally
everywhere else, so on macOS the keyring round-trip always attempted a real
write. Under ssh or an agent shell the login keychain has no UI session to
unlock it, so 'security add-generic-password' fails with exit 36
(errSecInteractionNotAllowed) and 'deno task test' fails locally even though
nothing is wrong with the code.
Probe with 'security show-keychain-info', which is read-only and returns 36 in
exactly that state. A read-only probe is not enough on its own: reads still
succeed while locked (find-generic-password returns 44), so only a
write-capable check detects it. The guard keys on 36 specifically rather than
any nonzero exit, so a genuine keyring bug still fails the test loudly.
CI is unaffected: its macOS keychain is unlocked, and the keyring job now sets
LINEAR_KEYRING_INTEGRATION=1 to force the test to run, so a probe that ever
wrongly reports 'unavailable' cannot quietly turn that job into a no-op.
The skill docs check was failing because `Deno.consoleSize()` returns
different values depending on the runner's pseudo-terminal dimensions
(CI is wide, local terminals vary). Since the docs are for LLM
consumption and not correctness, dropping the `git diff` check and just
verifying generation doesn't crash.
Co-authored-by: Alex Broekhof <alex@quilt.com>
Co-authored-by: Peter Schilling <code@schpet.com>
Move API key storage from plaintext TOML to OS-native keyrings (macOS
Keychain, Linux `libsecret`, Windows Credential Manager). The
credentials file retains only workspace metadata. Keys are loaded into
an in-memory cache at startup so all downstream reads remain synchronous
— no changes needed to any command files.
## Changes
### Keyring (`src/keyring/`)
- Platform-detecting wrapper with `getPassword`, `setPassword`,
`deletePassword` exports
- macOS: `/usr/bin/security` (exit 44 = not found)
- Linux: `secret-tool` via stdin for writes (exit 1 = not found)
- Windows: `Deno.dlopen("advapi32.dll")` FFI calling
`CredReadW`/`CredWriteW`/`CredDeleteW` directly
- `_setBackend()` test seam for injecting an in-memory `Map` backend
### Windows Credential Manager via FFI
The Windows backend calls `advapi32.dll` directly via Deno's FFI
(`Deno.dlopen`) rather than shelling out to PowerShell. This matches the
standard approach taken by every comparable credential tool:
-
[`danieljoos/wincred`](https://github.com/danieljoos/wincred/blob/623325312d3224d48d131159187b93e906216563/sys.go)
— Go library calling `advapi32.dll` via `windows.NewLazySystemDLL`, used
by:
-
[`docker-credential-helpers`](https://github.com/docker/docker-credential-helpers/blob/2b4e08bca3dbdb8e6c6e28790042742d0c0fc48f/wincred/wincred.go)
- [`gh`
CLI](https://github.com/cli/cli/blob/2c54a0d36a2f3c9c1f1b869a64120837c3a1e6f5/internal/keyring/keyring.go)
(via
[`zalando/go-keyring`](https://github.com/zalando/go-keyring/blob/5c6f7e0ba54d20daa8ea4e03f7ce0a27c075bfb6/keyring_windows.go))
-
[`aws-vault`](https://github.com/99designs/aws-vault/blob/70522e8f0b8f9c5b4e2e4e1e1e1cc4e3e5c3f04c/go.mod)
(via `99designs/keyring`)
-
[`node-keytar`](https://github.com/atom/node-keytar/blob/deae59a488789f2cd4a8dba6c7e58665795804fe/src/keytar_win.cc)
— C++ N-API addon, `#include <wincred.h>`
-
[`jaraco/keyring`](https://github.com/jaraco/keyring/blob/38c040133559682902f25fe96496756ee6849820/keyring/backends/Windows.py)
— Python, `win32cred` (pywin32-ctypes wrapping advapi32 via ctypes)
The implementation packs the 80-byte `CREDENTIALW` struct manually via
`DataView`, encodes strings as UTF-16LE for the `W`-suffix APIs, and
uses `GetLastError` from `kernel32.dll` to distinguish "not found"
(`ERROR_NOT_FOUND` = 1168) from real failures. DLLs are lazy-loaded so
the module import doesn't fail on macOS/Linux.
### Credentials (`src/credentials.ts`)
- `Credentials` interface changed from index signature to `{ default?:
string; workspaces: string[] }`
- `apiKeyCache` `Map` populated at startup, keeping
`getCredentialApiKey()` sync
- `addCredential`/`removeCredential` write to keyring first, only mutate
local state on success
- `parseInlineCredentials` / `parseKeyringCredentials` /
`populateKeyringCache` extracted from `loadCredentials`
- Parallel keyring lookups via `Promise.all`
- Malformed TOML parse errors caught with recovery guidance
- Warnings for: missing keyring entries, dangling default workspace,
inline format detected
### Backward Compatibility
- Inline-format TOML files (keys stored as `workspace = "lin_api_..."`)
are detected by `hasInlineKeys` and served from the file directly
- `addCredential` on an inline-format installation rewrites the file to
keyring format
### Auth List (`src/commands/auth/auth-list.ts`)
- Replaces removed `getAllCredentials()` with `getApiKeyForWorkspace()`
- Distinguishes auth errors (401/403) from network/other failures
instead of labeling everything "invalid credentials"
### CI
- Added `keyring-integration` job on `macos-latest` and `windows-latest`
for real credential round-trip testing
## Testing
- Subprocess isolation via `deno eval` for credential tests (required by
top-level `await loadCredentials()`)
- Mock keyring backend injected via `_setBackend` — covers happy paths,
error propagation, and cache consistency
- Integration test (`test/keyring.integration.test.ts`) exercises the
real macOS Keychain and Windows Credential Manager lifecycle
- Edge cases covered: keyring write/delete failures leave state
unchanged, null keyring returns warn but don't crash, dangling default
dropped on load, inline→keyring format transition on `addCredential`
## References
Closes#130
---------
Co-authored-by: Peter Schilling <code@schpet.com>
- Add CI workflow step to verify generated skill docs are up-to-date
- Add development section to README with instructions for:
- Updating skill documentation with deno task generate-skill-docs
- Running code formatting with deno fmt
- Update generate-docs.ts to automatically format generated files
- Regenerate skill docs to ensure they match current CLI version
Fixes#135
- Add explicit type annotations to team-list.ts and project-list.ts to fix implicit any errors
- Add CI workflow (.github/workflows/ci.yml) that runs type check, format, lint, and tests on PRs
- Update justfile to use 'deno check src/main.ts' instead of '--all' to avoid npm dependency type errors
use case:
- you are addressing feedback on an issue ABC-123
- run `linear issue start 123` to re-start that issue
- fire up claude code
- run `! linear issue view` to drop the issue, and comments into context
- run `! linear issue commits` to dump the diff of previous work into
context
- ask claude to address the feedback (it will have a much better shot of
knowing what you are talking about)