@hystudio/dsh-quota-pill 0.2.7 → 0.2.12

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/AGENTS.md CHANGED
@@ -9,6 +9,39 @@ the human only for things only they can supply (an API key, an OAuth login).
9
9
 
10
10
  ---
11
11
 
12
+ ## Before 1: install the bundle into DSH
13
+
14
+ This guide assumes the **bundle** is installed and enabled, not just the sidecar.
15
+ For a fresh DSH Desktop:
16
+
17
+ 1. If `dsh` is not on PATH: DSH Desktop → Settings → **Manage dsh Command**.
18
+ 2. Install a **pinned** build, never the bare name:
19
+ - GUI: **Plugins → Install** → `@hystudio/dsh-quota-pill@0.2.12`
20
+ - CLI: `dsh plugin add @hystudio/dsh-quota-pill@0.2.12 --profile desktop`
21
+ - If pnpm's minimumReleaseAge rejects the fresh 0.2.12 release, use the
22
+ branch until it ages: `github:598934321/dsh-quota-pill#master`
23
+ 3. Confirm the dependency and the bundle are present:
24
+
25
+ ```bash
26
+ python3 - <<'PY'
27
+ import json, os
28
+ p = os.path.expanduser('~/.dsh/profiles/desktop/package.json')
29
+ d = json.load(open(p))
30
+ print(d.get('dependencies', {}).get('@hystudio/dsh-quota-pill'))
31
+ print('enabled:', '@hystudio/dsh-quota-pill' in d.get('dsh', {}).get('profile', {}).get('bundles', []))
32
+ PY
33
+ ```
34
+
35
+ **Why pinned:** pnpm's `minimumReleaseAge` can resolve an unpinned install to the
36
+ broken 0.2.2 (its client half fails to import and the web GUI shows "Failed to
37
+ load plugins"). 0.2.12 is the first version with all providers, Open Design
38
+ Control Keys, sidecar auto-start, and user-defined adapters.
39
+
40
+ The host half starts the sidecar when nothing is listening on :8319, so a normal
41
+ desktop install does **not** need a separate `npx` or launchd unit.
42
+
43
+ ---
44
+
12
45
  ## 0. What this plugin does
13
46
 
14
47
  It shows how much quota is **left** for the LLM subscriptions the user has, in a
@@ -33,7 +66,7 @@ There are **two credential families**, and this matters for every step:
33
66
  | DSH Platform sign-in | `deepseek` | **nothing** — works if they are signed in to DeepSeek Platform in DSH |
34
67
  | API key | `kimi`, `opencode`, `commandcode` | a key pasted into `~/.dsh/.credentials.yaml` |
35
68
  | OAuth via CLIProxyAPI | `xai`, `codex`, `agy` | **CLIProxyAPI installed and logged in once** |
36
- | Console session cookie | `opendesign` | the `__Secure-better-auth.session_token` cookie from the Open Design cloud dashboard |
69
+ | Open Design Control Key | `opendesign` | a `vela_ctrl_…` Control Key from the Open Design device sign-in, plus the workspace id (optional); a `__Secure-better-auth.session_token` console cookie is a fallback |
37
70
 
38
71
  **Check DeepSeek first.** It is the official account and needs no setup beyond a
39
72
  DSH Platform sign-in, so it is usually the one the user actually wants. If it
@@ -44,6 +77,10 @@ DSH account menu, then re-run step 5.
44
77
 
45
78
  ## 1. Verify the sidecar runs
46
79
 
80
+ The 0.2.12 host plugin starts it automatically when :8319 is free. Give DSH a few
81
+ seconds, then check. If the host half could not start it (or you opted out with
82
+ `QUOTA_PILL_NO_AUTOSTART=1`), run it manually:
83
+
47
84
  ```bash
48
85
  npx @hystudio/dsh-quota-pill
49
86
  ```
@@ -60,7 +97,7 @@ Then, in another shell:
60
97
 
61
98
  ```bash
62
99
  curl -s http://127.0.0.1:8319/health
63
- # {"ok":true,"port":8319,"providers":["kimi","opencode","commandcode","xai","codex","agy"]}
100
+ # {"ok":true,"port":8319,"providers":["deepseek","kimi","opencode","commandcode","xai","codex","agy","opendesign"]}
64
101
  ```
65
102
 
66
103
  If `CLIProxyAPI: not set up`, the three OAuth providers will report "needs
@@ -68,36 +105,114 @@ CLIProxyAPI". That is expected on a fresh machine — tell the human that
68
105
  `kimi` / `opencode` / `commandcode` work without it, and ask whether they want
69
106
  the OAuth three as well.
70
107
 
71
- **Keep it running.** Do not leave it in the foreground of a terminal the human
72
- will close. Offer a background service (launchd on macOS, systemd on Linux):
108
+ **Auto-start is the default from 0.2.12**; do not add a service unless the human
109
+ asks for one or DSH is headless. If you do add one, set
110
+ `QUOTA_PILL_NO_AUTOSTART=1` so the plugin and the service do not race, and use
111
+ the **absolute** Node path (launchd's PATH does not include nvm/homebrew):
73
112
 
74
113
  ```bash
75
114
  # macOS: ~/Library/LaunchAgents/dev.quota-pill.plist
76
- # ProgramArguments: ["/usr/bin/env","node","<abs path>/bin/quota-pill-server.js"]
115
+ # ProgramArguments: ["/absolute/path/to/node","<abs path>/bin/quota-pill-server.js"]
77
116
  # RunAtLoad: true
78
117
  # KeepAlive: true
118
+ # EnvironmentVariables: { QUOTA_PILL_NO_AUTOSTART: "1" }
79
119
  ```
80
120
 
81
121
  Ask before installing a service — that is a persistent change to their machine.
82
122
 
83
123
  ---
84
124
 
85
- ## 2. Ask which providers the user actually has
125
+ ## 2. Discover and configure automatically
126
+
127
+ **Do not ask the user which providers they have.** Enumerate DSH's catalog
128
+ yourself, build the worklist, and configure everything that is possible. The only
129
+ things to ask for are secrets/approvals that the agent cannot supply.
130
+
131
+ 1. **DSH catalog + supported adapters + required refs:**
132
+
133
+ ```bash
134
+ curl -s 'http://127.0.0.1:8319/setup?fresh=1' | python3 -m json.tool
135
+ ```
136
+
137
+ Read catalog (DSH's live session/modelCatalog), unadaptedCatalog,
138
+ userAdapters, and each entry in providers (inCatalog, requires, credential).
139
+ This is the authoritative "what can DSH route to" list.
140
+
141
+ 2. **What is already readable:**
142
+
143
+ ```bash
144
+ curl -s 'http://127.0.0.1:8319/quota/all?fresh=1' | python3 -m json.tool
145
+ ```
146
+
147
+ 3. **Credential files** (names only; never print values):
148
+ ~/.dsh/.credentials.yaml refs, and ~/.cli-proxy-api/* token files.
149
+
150
+ Then act on the classification — do not present a menu:
151
+
152
+ | Situation | Agent action |
153
+ |---|---|
154
+ | Already readable | verify it, include the number in the report |
155
+ | Supported + in catalog, missing credential | configure it; if a secret is required, ask for that exact ref only |
156
+ | Unadapted catalog id / provider "unknown" | do not fake a reading; follow §2b and add it to the final report |
157
+ | In catalog but not supported and user does not want to adapt | say so; leave it out |
158
+
159
+ Only these need the human: paste a missing API key, install/login CLIProxyAPI,
160
+ or approve the Open Design device page. DeepSeek needs nothing. Do not ask
161
+ "which provider would you like me to set up?" — set up every available one.
162
+
163
+ If the user truly has no providers at all: stop and say the plugin has nothing to
164
+ show yet. Do not install a fake provider to "make it look working".
86
165
 
87
- Do **not** guess. Ask, in one message, listing the options with what each needs:
166
+ ---
167
+
168
+ ## 2b. Adapting a provider the plugin does not know
169
+
170
+ When the Current page cannot read a model's provider, it shows a hint and a
171
+ "复制提示词 / Copy prompt" button. That button copies the full instruction below
172
+ into the clipboard; the human can paste it into a new session. As an agent, you
173
+ can also start from this section directly.
174
+
175
+ The adaptation is a **user adapter**, not a package edit:
176
+
177
+ - Directory: ~/.dsh/quota-pill/adapters (created by the sidecar)
178
+ - File: <provider>.mjs
179
+ - It is outside the npm package, so **plugin updates never overwrite it**
180
+ - The sidecar discovers it on the next request/Refresh; no restart
181
+
182
+ Steps:
183
+
184
+ 1. curl /setup?fresh=1; find the unknown id in catalog / unadaptedCatalog, and
185
+ check existing files in ~/.dsh/quota-pill/adapters (do not overwrite a working
186
+ adapter for a different provider).
187
+ 2. Find the provider's real quota API and credential. An inference key is not a
188
+ quota key; never infer a number from model prices or a guess.
189
+ 3. Write ~/.dsh/quota-pill/adapters/<provider>.mjs:
190
+
191
+ ```js
192
+ export const provider = 'acme';
193
+ export const catalogIds = ['acme-go']; // DSH catalog/model ids
194
+ export const displayName = 'Acme';
195
+ export const credential = { kind: 'api-key', refs: ['ACME_API_KEY'] };
196
+ export async function read(ctx) {
197
+ // ctx.model, ctx.readRef, ctx.readFirstRef, ctx.getJson,
198
+ // ctx.getJsonViaCurl, ctx.postJsonViaCurl, ctx.env, ctx.dshHome
199
+ const key = ctx.readFirstRef(['ACME_API_KEY']);
200
+ if (!key) return { kind: 'none', provider: 'acme', error: 'no ACME_API_KEY' };
201
+ const r = await ctx.getJson('https://api.acme.example/usage', { token: key });
202
+ if (!r.ok) return { kind: 'probe-failed', provider: 'acme', error: r.error };
203
+ return { kind: 'subscription-quota', provider: 'acme', remainingPct: 80, window: '5h' };
204
+ }
205
+ ```
88
206
 
89
- > I can read quota for six providers. Which do you have?
90
- >
91
- > - **DeepSeek** — works already if you are signed in to DeepSeek Platform in DSH
92
- > - **Kimi Coding** — needs a `KIMI_CODING_API_KEY`
93
- > - **OpenCode Go** — needs an `OPENCODE_GO_API_KEY`
94
- > - **Command Code** — needs a `COMMANDCODE_API_KEY`
95
- > - **xAI / Grok**, **Codex / ChatGPT Plus**, **Antigravity** — need CLIProxyAPI installed and logged in
96
- >
97
- > Tell me which ones, and paste the key for any of the first three.
207
+ Return the same kinds as built-ins: subscription-quota (with
208
+ remainingPct/windows), payg (with credits.remain), usage-only, none (missing
209
+ credential; explain in error) or probe-failed. Never invent a number.
210
+ 4. Verify with curl 'http://127.0.0.1:8319/quota?model=<model>&fresh=1'; if it
211
+ fails, say exactly what is missing.
212
+ 5. Run /quota/all?fresh=1 and /setup?fresh=1 and report adapted/unadapted.
98
213
 
99
- If they have none of them: stop and say the plugin has nothing to show yet.
100
- Do not install a fake provider to "make it look working".
214
+ The user adapter contract is also documented in src/user-adapters.js; read it
215
+ before writing the file. Do not put user adapters inside the installed package.
101
216
 
102
217
  ---
103
218
 
@@ -110,7 +225,7 @@ it usually already exists and contains other credentials that must not be lost.
110
225
  refs:
111
226
  KIMI_CODING_API_KEY: sk-...
112
227
  OPENCODE_GO_API_KEY: sk-...
113
- COMMANDCODE_API_KEY: sk-...
228
+ COMMANDCODE_API_KEY: sk-... # or reuse COMMANDCODE_GOAT_API_KEY
114
229
  ```
115
230
 
116
231
  Rules:
@@ -128,7 +243,7 @@ python3 - <<'PY'
128
243
  import yaml, os
129
244
  d = yaml.safe_load(open(os.path.expanduser('~/.dsh/.credentials.yaml')))
130
245
  refs = d.get('refs') or {}
131
- for k in ('KIMI_CODING_API_KEY','OPENCODE_GO_API_KEY','COMMANDCODE_API_KEY'):
246
+ for k in ('KIMI_CODING_API_KEY','OPENCODE_GO_API_KEY','COMMANDCODE_API_KEY','COMMANDCODE_GOAT_API_KEY'):
132
247
  print(f'{k}: ' + ('present' if refs.get(k) else 'MISSING'))
133
248
  PY
134
249
  ```
@@ -163,6 +278,52 @@ the OAuth flow itself.
163
278
  reports the `X-Codex-*` headers CLIProxyAPI observed. Without it running, the
164
279
  fallback hits `chatgpt.com`, which blocks non-browser TLS and returns 403.
165
280
 
281
+ ### Open Design (optional)
282
+
283
+ Open Design quota uses a **Control Key** (`vela_ctrl_…`), not the `sk-amr-…`
284
+ inference key. Run the bundled device sign-in and approve its URL in the user's
285
+ browser:
286
+
287
+ ```bash
288
+ node bin/opendesign-login.js --write
289
+ ```
290
+
291
+ That writes `OPENDESIGN_CONTROL_KEY` and `OPENDESIGN_WORKSPACE_ID` into
292
+ `~/.dsh/.credentials.yaml`. A console session cookie
293
+ (`OPENDESIGN_SESSION_COOKIE`) is still accepted as a fallback. If the user only
294
+ has an inference key, skip Open Design — an inference key returns 401 on the
295
+ control plane.
296
+
297
+ ---
298
+
299
+ ## 4b. Adding or removing a provider later
300
+
301
+ No restart/reinstall is needed for supported providers:
302
+
303
+ | Change | When it appears |
304
+ |---|---|
305
+ | New credential for a supported provider already in the DSH catalog | next `/quota/all` (All refresh); no sidecar restart |
306
+ | New provider group added to DSH's catalog | within the 2-minute catalog TTL, or immediately with `/quota/all?fresh=1` / **Refresh** / reopening All |
307
+ | Provider removed from DSH | same: next refresh removes it |
308
+ | Brand-new provider **type** (not one of the eight adapters) | needs a user adapter in ~/.dsh/quota-pill/adapters (see §2b); the catalog alone is not enough |
309
+
310
+ The All page reads fresh on open, on **Refresh**, and when DSH regains focus; it
311
+ is not a background poll. /setup?fresh=1 is the quickest way for an agent to see
312
+ the current catalog, `unadaptedCatalog`, and which supported provider it has no
313
+ credential for.
314
+
315
+ ---
316
+
317
+ ## 4c. After a plugin update
318
+
319
+ 1. **Do not delete** ~/.dsh/quota-pill/adapters; it is outside the package and
320
+ is the user's adapted providers. An update must never overwrite it.
321
+ 2. Restart DSH/sidecar so the new code loads (the host half starts the sidecar).
322
+ 3. Run /setup?fresh=1 and compare catalog with the adapted/user providers.
323
+ 4. For every id still in unadaptedCatalog, run §2b (add or update the user
324
+ adapter). Do not remove a working adapter just because the package changed.
325
+ 5. Run /quota/all?fresh=1 and report the full provider list again.
326
+
166
327
  ---
167
328
 
168
329
  ## 5. Verify end to end
@@ -195,6 +356,13 @@ plainly:
195
356
 
196
357
  - which providers are working
197
358
  - which are skipped, and the single action that would enable each
359
+ - the DSH catalog and which ids are still in `unadaptedCatalog` (if any)
360
+
361
+ **Initial-install summary must include the unknown-provider path:** if
362
+ `unadaptedCatalog` is non-empty (or a current model's provider is unknown), tell
363
+ the user: "这些 provider 还没适配;打开(或重开)当前会话的额度面板,Current 页有
364
+ 「复制提示词」按钮,复制到新会话让 agent 按 §2b 适配即可。" The button copies the
365
+ full adapter flow, so the user does not need to explain anything.
198
366
 
199
367
  Do not paste raw JSON. Do not claim a provider works if it returned `none` or
200
368
  `probe-failed`.
package/README.md CHANGED
@@ -18,11 +18,11 @@ into the live number.
18
18
  | **DeepSeek** (official) | the Platform sign-in DSH already holds — nothing to add |
19
19
  | **Kimi Coding** | `refs.KIMI_CODING_API_KEY` in `~/.dsh/.credentials.yaml` |
20
20
  | **OpenCode Go** | `refs.OPENCODE_GO_API_KEY` |
21
- | **Command Code** | `refs.COMMANDCODE_API_KEY` |
21
+ | **Command Code** | `refs.COMMANDCODE_API_KEY` or `refs.COMMANDCODE_GOAT_API_KEY` (DSH names it after its `apiKeyEnv`) |
22
22
  | **xAI (Grok)** | OAuth token from [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) |
23
23
  | **Codex (ChatGPT Plus)** | same — and CLIProxyAPI must be **running** |
24
24
  | **Antigravity** | same |
25
- | **Open Design** (optional) | `refs.OPENDESIGN_SESSION_COOKIE` — the cloud console's session cookie |
25
+ | **Open Design** (optional) | `refs.OPENDESIGN_CONTROL_KEY` (`vela_ctrl_…`) + `refs.OPENDESIGN_WORKSPACE_ID`; a console session cookie (`OPENDESIGN_SESSION_COOKIE`) also works as a fallback |
26
26
 
27
27
  **DeepSeek works with no setup at all** if you are signed in to DeepSeek Platform
28
28
  in DSH — that is the official account, and it is the one most users care about.
@@ -36,22 +36,73 @@ the pill reads DSH's `session/modelCatalog` to learn which providers the app can
36
36
  route to, so removing a provider from DSH removes it from the pill too. A provider
37
37
  that is present but has no credential is skipped rather than shown as an empty row.
38
38
 
39
+ ### What "auto-detect" means
40
+
41
+ - **A new key for a supported provider** (e.g. adding `OPENCODE_GO_API_KEY`)
42
+ appears on the next All refresh — no DSH restart, no sidecar restart.
43
+ - **A provider newly added to DSH's catalog** appears within the 2-minute catalog
44
+ TTL, or immediately when you press **Refresh** / reopen the All page (a forced
45
+ read drops the catalog cache). The All page also re-reads when DSH regains focus.
46
+ - **A brand-new provider type** that is not one of the eight adapters needs a
47
+ **user adapter** in `~/.dsh/quota-pill/adapters/` (see below); a catalog entry
48
+ alone cannot invent its quota API.
49
+
50
+ ### Unknown providers and user adapters
51
+
52
+ If the Current page cannot read the session's provider, it shows a short hint and
53
+ a **复制提示词 / Copy prompt** button. The copied prompt contains the whole
54
+ adaptation flow; paste it into a new session and an agent can add the provider
55
+ without touching package files.
56
+
57
+ User adapters live in:
58
+
59
+ ```text
60
+ ~/.dsh/quota-pill/adapters/<provider>.mjs
61
+ ```
62
+
63
+ They are outside the npm package, so **updating the plugin never overwrites
64
+ them**. The sidecar discovers a new/changed file on the next Refresh (no restart).
65
+ `/setup?fresh=1` reports `unadaptedCatalog` and `userAdapters`, and after a
66
+ plugin update the same check tells the agent which providers still need an
67
+ adapter. The module contract (exports, `read(ctx)`, standard kinds) is documented
68
+ in [`src/user-adapters.js`](src/user-adapters.js) and in AGENTS.md §2b.
69
+
39
70
  ## Install
40
71
 
41
- ```bash
42
- # 1. the DSH plugin
43
- dsh plugin add @hystudio/dsh-quota-pill
72
+ **DSH Desktop (recommended).** Open **Plugins → Install**, paste a spec, enable
73
+ the bundle. **Pin the version**: an unpinned install can resolve 0.2.2, which
74
+ breaks web boot. 0.2.12 is the first version with every provider, Open Design
75
+ Control Keys, sidecar auto-start, and **user-defined adapters** for providers the
76
+ package does not ship.
44
77
 
45
- # 2. the sidecar (reads credentials, talks to upstream APIs)
46
- npx @hystudio/dsh-quota-pill
78
+ ```text
79
+ @hystudio/dsh-quota-pill@0.2.12
80
+ ```
81
+
82
+ 0.2.12 is published on npm. If pnpm's minimumReleaseAge rejects this fresh
83
+ release, install the branch until it ages:
84
+
85
+ ```text
86
+ github:598934321/dsh-quota-pill#master
87
+ ```
88
+
89
+ **CLI.** If `dsh` is not on PATH, open DSH Desktop → Settings → **Manage dsh
90
+ Command** first. Then:
91
+
92
+ ```bash
93
+ dsh plugin add @hystudio/dsh-quota-pill@0.2.12
47
94
  ```
48
95
 
49
96
  > The npm package is `@hystudio/...` while the repo lives at `598934321/...` —
50
97
  > they are the same author under two account names.
51
98
 
52
- The pill shows **"quota service is not running"** with that exact command until
53
- the sidecar is up. Keep it running in the background — see
99
+ **The sidecar now starts automatically with the plugin** (the host half spawns it
100
+ when nothing is listening on :8319), so there is no second command for a normal
101
+ desktop install. If you prefer to manage it yourself, run
102
+ `npx @hystudio/dsh-quota-pill` or a service unit — see
54
103
  [Running it as a service](#running-it-as-a-service).
104
+ The pill says **"quota service is not running"** only if :8319 is still closed a
105
+ few seconds after DSH starts.
55
106
 
56
107
  Then add whichever keys you have:
57
108
 
@@ -78,6 +129,10 @@ Read AGENTS.md in this repo and set up dsh-quota-pill for me.
78
129
 
79
130
  ## Running it as a service
80
131
 
132
+ The host plugin already starts the sidecar; this section is only for users who
133
+ want it independent of DSH (server, headless, multiple app profiles). Set
134
+ `QUOTA_PILL_NO_AUTOSTART=1` so the plugin does not race the service.
135
+
81
136
  `npx` in a terminal dies with the terminal. Pick one:
82
137
 
83
138
  **macOS (launchd)** — `~/Library/LaunchAgents/dev.quota-pill.plist`
@@ -193,13 +248,27 @@ would flicker between models you do not even have configured.
193
248
  `monthlyCap = weekly.cap × 2`, an identity verified against two different plans
194
249
  (31.6 + 38.4 = 70 with `weekly.cap` 35; 0.225 + 9.775 = 10 with `weekly.cap` 6).
195
250
 
196
- **Open Design** keeps its allowance in the cloud console, not in the API key you
197
- use for inference. The `amr-link.open-design.ai/v1` gateway has no balance
198
- endpoint at all; the console's own `amr-api` does, and it authenticates with a
199
- `__Secure-better-auth.session_token` cookie. That cookie is HttpOnly, so grab it
200
- from devtools → Application → Cookies. The workspace id is discovered
201
- automatically. The request goes through system curl because Cloudflare rejects
202
- Node's TLS fingerprint on that host with `error code: 1010`.
251
+ **Open Design** keeps its allowance in the cloud control plane, not in the
252
+ `sk-amr-…` inference key. The preferred credential is a **Control Key**
253
+ (`vela_ctrl_…`), which is accepted by the Bearer-only
254
+ `/billing/preflight` and `/wallet/balance` endpoints alongside
255
+ `x-vela-workspace-id`. Get one with the bundled device sign-in (the same flow
256
+ as the official `vela login`):
257
+
258
+ ```bash
259
+ npx -p @hystudio/dsh-quota-pill dsh-quota-pill-opendesign-login --write
260
+ ```
261
+
262
+ It prints an activation URL, waits for you to approve it in a signed-in browser,
263
+ then writes `OPENDESIGN_CONTROL_KEY` and `OPENDESIGN_WORKSPACE_ID` into
264
+ `~/.dsh/.credentials.yaml`. The workspace id is optional when using a Control
265
+ Key — it is otherwise resolved from `/workspaces/current`.
266
+
267
+ The older console-session route (`__Secure-better-auth.session_token` cookie,
268
+ browser-session-only `/billing/coding-plan-usage`) is still supported as a
269
+ fallback; that cookie is HttpOnly, so copy it from devtools → Application →
270
+ Cookies. Both routes go through system curl because Cloudflare rejects Node's
271
+ TLS fingerprint on `amr-api` with `error code: 1010`.
203
272
 
204
273
  **DeepSeek** is a prepaid CNY wallet, so there is no percentage to show — the pill
205
274
  shows the balance and, separately, any bonus credit. Two traps worth recording:
@@ -221,7 +290,7 @@ down to wallet currency and balance. We never handle the Platform token ourselve
221
290
  ```bash
222
291
  git clone https://github.com/598934321/dsh-quota-pill
223
292
  cd dsh-quota-pill
224
- npm test # 26 tests, offline
293
+ npm test # 70 tests, offline
225
294
  npm start # sidecar on :8319
226
295
  ```
227
296
 
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Sign in to Open Design and write the Control Key the quota pill needs.
4
+ *
5
+ * node bin/opendesign-login.js # print the values and a YAML snippet
6
+ * node bin/opendesign-login.js --write # add/update the two refs in ~/.dsh/.credentials.yaml
7
+ *
8
+ * The browser approval page is the same one the Open Design CLI (`vela login`)
9
+ * and magpie use. Only the Control Key is stored by `--write`; the runtime key
10
+ * is not needed by the quota pill and is not written.
11
+ */
12
+ import fs from 'node:fs';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import { loginOpenDesign } from '../src/opendesign-auth.js';
16
+
17
+ const write = process.argv.includes('--write');
18
+ const credentialsFile = path.join(
19
+ process.env.DSH_HOME || path.join(os.homedir(), '.dsh'),
20
+ '.credentials.yaml',
21
+ );
22
+
23
+ function setRef(text, key, value) {
24
+ const lines = String(text || '').split('\n');
25
+ const lineRe = new RegExp('^(\\s*)' + key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ':');
26
+ const existing = lines.findIndex((line) => lineRe.test(line));
27
+ if (existing >= 0) {
28
+ lines[existing] = lines[existing].replace(/^(\s*).*$/, '$1' + key + ': ' + value);
29
+ return lines.join('\n');
30
+ }
31
+ const refsAt = lines.findIndex((line) => /^refs:\s*$/.test(line));
32
+ if (refsAt >= 0) {
33
+ lines.splice(refsAt + 1, 0, ' ' + key + ': ' + value);
34
+ return lines.join('\n');
35
+ }
36
+ const base = text && !text.endsWith('\n') ? text + '\n' : String(text || '');
37
+ return base + 'refs:\n ' + key + ': ' + value + '\n';
38
+ }
39
+
40
+ function mask(value) {
41
+ if (value.length <= 12) return value;
42
+ return value.slice(0, 10) + '…' + value.slice(-4);
43
+ }
44
+
45
+ const result = await loginOpenDesign({
46
+ onDevice(device) {
47
+ console.log('Open Design sign-in');
48
+ console.log(' 1. Open: ' + (device.activationUrl || '(activation URL missing)'));
49
+ console.log(' 2. Confirm code: ' + device.userCode);
50
+ console.log(' 3. Approve the request in the browser.');
51
+ console.log('Waiting for approval…');
52
+ },
53
+ onPending() {},
54
+ });
55
+
56
+ console.log('');
57
+ console.log('Signed in' + (result.email ? ' as ' + result.email : '') + '.');
58
+ console.log('Control Key: ' + (write ? mask(result.controlKey) : result.controlKey));
59
+ console.log('Workspace ID: ' + result.workspaceId);
60
+
61
+ if (!write) {
62
+ console.log('');
63
+ console.log('Add these to ' + credentialsFile + ' (keep the others already there):');
64
+ console.log('refs:');
65
+ console.log(' OPENDESIGN_CONTROL_KEY: ' + result.controlKey);
66
+ console.log(' OPENDESIGN_WORKSPACE_ID: ' + result.workspaceId);
67
+ } else {
68
+ let text = '';
69
+ try { text = fs.readFileSync(credentialsFile, 'utf8'); } catch { /* new file */ }
70
+ text = setRef(text, 'OPENDESIGN_CONTROL_KEY', result.controlKey);
71
+ text = setRef(text, 'OPENDESIGN_WORKSPACE_ID', result.workspaceId);
72
+ fs.mkdirSync(path.dirname(credentialsFile), { recursive: true });
73
+ fs.writeFileSync(credentialsFile, text, { mode: 0o600 });
74
+ try { fs.chmodSync(credentialsFile, 0o600); } catch { /* best effort */ }
75
+ console.log('');
76
+ console.log('Written to ' + credentialsFile);
77
+ }
78
+
79
+ console.log('');
80
+ console.log('Restart the quota-pill sidecar (or launch it again) to pick up the new refs.');