@mars-sea/dsh-commandcode-provider 0.7.0 → 0.8.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/lib/client.js CHANGED
@@ -358,6 +358,14 @@ window.__ModuleLoader__.load({
358
358
  this.failed = false;
359
359
  this.publish();
360
360
  }
361
+ /**
362
+ * Re-read the Host's credential facts without any staged edit. The browser
363
+ * login stores a key Host-side behind the page's back; the plugin entry
364
+ * calls this when a login lands so the configured/writable badges follow.
365
+ */
366
+ refreshCredentials() {
367
+ this.describeAll();
368
+ }
361
369
  /** Write every staged edit, then re-read the Host's accepted state. */
362
370
  async save() {
363
371
  const plan = this.plan();
@@ -749,36 +757,256 @@ window.__ModuleLoader__.load({
749
757
  return new Date(ms).toLocaleString();
750
758
  }
751
759
  //#endregion
760
+ //#region src/client/login.ts
761
+ /** How often a live attempt is polled. */
762
+ const POLL_INTERVAL_MS = 1e3;
763
+ /**
764
+ * The login panel's fetch/poll lifecycle. One poll loop at a time; a fresh
765
+ * `begin()` supersedes any previous loop via a generation token.
766
+ */
767
+ var CommandCodeLoginController = class {
768
+ remote;
769
+ listeners = /* @__PURE__ */ new Set();
770
+ pollMs;
771
+ /** Monotonic token; only the latest loop may publish polling results. */
772
+ generation = 0;
773
+ disposed = false;
774
+ phase = "idle";
775
+ authUrl;
776
+ userName;
777
+ keyName;
778
+ reason;
779
+ message;
780
+ constructor(remote, pollMs = POLL_INTERVAL_MS) {
781
+ this.remote = remote;
782
+ this.pollMs = pollMs;
783
+ }
784
+ /** Subscribe to state projections. @returns the disposer. */
785
+ subscribe(listener) {
786
+ this.listeners.add(listener);
787
+ return () => this.listeners.delete(listener);
788
+ }
789
+ /** Build the current panel state face. */
790
+ state() {
791
+ return {
792
+ phase: this.phase,
793
+ authUrl: this.authUrl,
794
+ userName: this.userName,
795
+ keyName: this.keyName,
796
+ reason: this.reason,
797
+ message: this.message
798
+ };
799
+ }
800
+ /** Start (or rejoin) a login attempt and begin polling its status. */
801
+ async begin() {
802
+ if (this.disposed || this.phase === "starting" || this.phase === "waiting") return;
803
+ const generation = ++this.generation;
804
+ this.set({
805
+ phase: "starting",
806
+ authUrl: void 0,
807
+ userName: void 0,
808
+ keyName: void 0,
809
+ reason: void 0,
810
+ message: void 0
811
+ });
812
+ const remote = this.remote();
813
+ if (remote === void 0) {
814
+ this.set({
815
+ phase: "unavailable",
816
+ authUrl: void 0,
817
+ userName: void 0,
818
+ keyName: void 0,
819
+ reason: void 0,
820
+ message: "login remote is not mounted"
821
+ });
822
+ return;
823
+ }
824
+ let result;
825
+ try {
826
+ result = await remote.loginBegin();
827
+ } catch (error) {
828
+ result = {
829
+ ok: false,
830
+ error: { message: error instanceof Error ? error.message : String(error) }
831
+ };
832
+ }
833
+ if (this.superseded(generation)) return;
834
+ if (!result.ok) {
835
+ this.set({
836
+ phase: "unavailable",
837
+ authUrl: void 0,
838
+ userName: void 0,
839
+ keyName: void 0,
840
+ reason: void 0,
841
+ message: result.error.message
842
+ });
843
+ return;
844
+ }
845
+ this.apply(result.value);
846
+ if (this.currentPhase === "waiting") this.poll(generation);
847
+ }
848
+ /** Cancel a waiting attempt. */
849
+ async cancel() {
850
+ if (this.disposed || this.phase !== "starting" && this.phase !== "waiting") return;
851
+ const generation = ++this.generation;
852
+ const remote = this.remote();
853
+ if (remote === void 0) return;
854
+ let result;
855
+ try {
856
+ result = await remote.loginCancel();
857
+ } catch {
858
+ this.set({
859
+ phase: "failed",
860
+ authUrl: void 0,
861
+ userName: void 0,
862
+ keyName: void 0,
863
+ reason: "cancelled",
864
+ message: void 0
865
+ });
866
+ return;
867
+ }
868
+ if (this.superseded(generation)) return;
869
+ if (result.ok) this.apply(result.value);
870
+ else this.set({
871
+ phase: "failed",
872
+ authUrl: void 0,
873
+ userName: void 0,
874
+ keyName: void 0,
875
+ reason: "cancelled",
876
+ message: void 0
877
+ });
878
+ }
879
+ /** Stop polling and release listeners. Idempotent. */
880
+ dispose() {
881
+ if (this.disposed) return;
882
+ this.disposed = true;
883
+ this.generation += 1;
884
+ this.listeners.clear();
885
+ }
886
+ /** Poll until the attempt leaves `waiting` or a newer loop supersedes us. */
887
+ async poll(generation) {
888
+ while (!this.disposed && !this.superseded(generation) && this.currentPhase === "waiting") {
889
+ await sleep(this.pollMs);
890
+ if (this.disposed || this.superseded(generation) || this.currentPhase !== "waiting") return;
891
+ const remote = this.remote();
892
+ if (remote === void 0) {
893
+ this.set({
894
+ phase: "unavailable",
895
+ authUrl: void 0,
896
+ userName: void 0,
897
+ keyName: void 0,
898
+ reason: void 0,
899
+ message: "login remote is not mounted"
900
+ });
901
+ return;
902
+ }
903
+ let result;
904
+ try {
905
+ result = await remote.loginStatus();
906
+ } catch {
907
+ continue;
908
+ }
909
+ if (this.superseded(generation) || this.currentPhase !== "waiting") return;
910
+ if (result.ok) this.apply(result.value);
911
+ }
912
+ }
913
+ get currentPhase() {
914
+ return this.phase;
915
+ }
916
+ /** Project one Host status onto the panel face. */
917
+ apply(status) {
918
+ const base = {
919
+ authUrl: void 0,
920
+ userName: void 0,
921
+ keyName: void 0,
922
+ reason: void 0,
923
+ message: void 0
924
+ };
925
+ if (status.state === "waiting") {
926
+ this.set({
927
+ ...base,
928
+ phase: "waiting",
929
+ authUrl: status.authUrl
930
+ });
931
+ return;
932
+ }
933
+ if (status.state === "success") {
934
+ this.set({
935
+ ...base,
936
+ phase: "success",
937
+ userName: status.userName,
938
+ keyName: status.keyName
939
+ });
940
+ return;
941
+ }
942
+ if (status.state === "failed") {
943
+ this.set({
944
+ ...base,
945
+ phase: "failed",
946
+ reason: status.reason,
947
+ message: status.message
948
+ });
949
+ return;
950
+ }
951
+ this.set({
952
+ ...base,
953
+ phase: "failed",
954
+ reason: "cancelled",
955
+ message: "the login attempt is no longer active"
956
+ });
957
+ }
958
+ /** Replace the whole state face and notify. Explicit over partial patches. */
959
+ set(state) {
960
+ this.phase = state.phase;
961
+ this.authUrl = state.authUrl;
962
+ this.userName = state.userName;
963
+ this.keyName = state.keyName;
964
+ this.reason = state.reason;
965
+ this.message = state.message;
966
+ this.publish();
967
+ }
968
+ superseded(generation) {
969
+ return this.disposed || generation !== this.generation;
970
+ }
971
+ publish() {
972
+ if (this.disposed) return;
973
+ for (const listener of [...this.listeners]) listener();
974
+ }
975
+ };
976
+ function sleep(ms) {
977
+ return new Promise((resolve) => setTimeout(resolve, ms));
978
+ }
979
+ //#endregion
752
980
  //#region src/usage-wire.ts
753
981
  /** The npm package identity both contribution registrations claim. */
754
982
  const USAGE_REMOTE_PACKAGE = "@mars-sea/dsh-commandcode-provider";
755
983
  /** Canonical `<namespace>/<method>` endpoint of the usage report Remote. */
756
984
  const USAGE_REPORT_ENDPOINT = "commandcode/report";
757
985
  /** Reject one boundary value with a field-naming error. */
758
- function reject(field) {
986
+ function reject$1(field) {
759
987
  throw new TypeError(`commandcode/report result: invalid ${field}`);
760
988
  }
761
989
  /** Read one required finite number field (`field` is the dotted error label). */
762
990
  function numberField(source, key, field) {
763
991
  const value = source[key];
764
- if (typeof value !== "number" || !Number.isFinite(value)) reject(field);
992
+ if (typeof value !== "number" || !Number.isFinite(value)) reject$1(field);
765
993
  return value;
766
994
  }
767
995
  /** Read one required string field (`field` is the dotted error label). */
768
996
  function stringField(source, key, field) {
769
997
  const value = source[key];
770
- if (typeof value !== "string") reject(field);
998
+ if (typeof value !== "string") reject$1(field);
771
999
  return value;
772
1000
  }
773
1001
  /** Read one required boolean field (`field` is the dotted error label). */
774
1002
  function booleanField(source, key, field) {
775
1003
  const value = source[key];
776
- if (typeof value !== "boolean") reject(field);
1004
+ if (typeof value !== "boolean") reject$1(field);
777
1005
  return value;
778
1006
  }
779
1007
  /** Narrow an unknown value to a plain record, or reject. */
780
1008
  function record(value, field) {
781
- if (typeof value !== "object" || value === null || Array.isArray(value)) reject(field);
1009
+ if (typeof value !== "object" || value === null || Array.isArray(value)) reject$1(field);
782
1010
  return value;
783
1011
  }
784
1012
  /** Validate one window-limit block (`fiveHour` / `weekly`). */
@@ -799,11 +1027,11 @@ window.__ModuleLoader__.load({
799
1027
  function parseUsageReport(value) {
800
1028
  const source = record(value, "report");
801
1029
  const failures = source.failures;
802
- if (!Array.isArray(failures) || failures.some((entry) => typeof entry !== "string")) reject("failures");
1030
+ if (!Array.isArray(failures) || failures.some((entry) => typeof entry !== "string")) reject$1("failures");
803
1031
  const report = { failures };
804
1032
  if (source.blocked !== void 0) {
805
1033
  const blocked = source.blocked;
806
- if (blocked !== "invalid-key" && blocked !== "service-unavailable" && blocked !== "network") reject("blocked");
1034
+ if (blocked !== "invalid-key" && blocked !== "service-unavailable" && blocked !== "network") reject$1("blocked");
807
1035
  report.blocked = blocked;
808
1036
  }
809
1037
  if (source.account !== void 0) {
@@ -841,7 +1069,7 @@ window.__ModuleLoader__.load({
841
1069
  if (source.plan !== void 0) {
842
1070
  const plan = record(source.plan, "plan");
843
1071
  const monthly = plan.monthlyCredits;
844
- if (monthly !== null && (typeof monthly !== "number" || !Number.isFinite(monthly))) reject("plan.monthlyCredits");
1072
+ if (monthly !== null && (typeof monthly !== "number" || !Number.isFinite(monthly))) reject$1("plan.monthlyCredits");
845
1073
  report.plan = {
846
1074
  planId: stringField(plan, "planId", "plan.planId"),
847
1075
  name: stringField(plan, "name", "plan.name"),
@@ -868,7 +1096,7 @@ window.__ModuleLoader__.load({
868
1096
  /** Parse the wire result into a {@link CommandCodeAccountsReport}. */
869
1097
  function parseAccountsReport(value) {
870
1098
  const accounts = record(value, "result").accounts;
871
- if (!Array.isArray(accounts)) reject("accounts");
1099
+ if (!Array.isArray(accounts)) reject$1("accounts");
872
1100
  return { accounts: accounts.map(parseAccountUsage) };
873
1101
  }
874
1102
  /** The Client-face contribution mounted on `ctx.remote`. */
@@ -889,6 +1117,92 @@ window.__ModuleLoader__.load({
889
1117
  }]
890
1118
  };
891
1119
  //#endregion
1120
+ //#region src/login-wire.ts
1121
+ /** The canonical endpoint paths of the three login Remotes. */
1122
+ const LOGIN_BEGIN_ENDPOINT = "commandcode/loginBegin";
1123
+ const LOGIN_STATUS_ENDPOINT = "commandcode/loginStatus";
1124
+ const LOGIN_CANCEL_ENDPOINT = "commandcode/loginCancel";
1125
+ const REASONS = [
1126
+ "denied",
1127
+ "timeout",
1128
+ "invalid-key",
1129
+ "network",
1130
+ "unavailable",
1131
+ "cancelled",
1132
+ "error"
1133
+ ];
1134
+ /** Reject one boundary value with a field-naming error. */
1135
+ function reject(field) {
1136
+ throw new TypeError(`commandcode/login result: invalid ${field}`);
1137
+ }
1138
+ /**
1139
+ * Parse one untrusted boundary value into a {@link CommandCodeLoginStatus}.
1140
+ * Every field is shape-checked so a malformed frame fails the boundary
1141
+ * instead of leaking into the page.
1142
+ */
1143
+ function parseLoginStatus(value) {
1144
+ if (typeof value !== "object" || value === null || Array.isArray(value)) reject("status");
1145
+ const source = value;
1146
+ const state = source.state;
1147
+ if (state !== "idle" && state !== "waiting" && state !== "success" && state !== "failed") reject("state");
1148
+ const status = { state };
1149
+ if (source.authUrl !== void 0) {
1150
+ if (typeof source.authUrl !== "string") reject("authUrl");
1151
+ status.authUrl = source.authUrl;
1152
+ }
1153
+ if (source.userName !== void 0) {
1154
+ if (typeof source.userName !== "string") reject("userName");
1155
+ status.userName = source.userName;
1156
+ }
1157
+ if (source.keyName !== void 0) {
1158
+ if (typeof source.keyName !== "string") reject("keyName");
1159
+ status.keyName = source.keyName;
1160
+ }
1161
+ if (source.reason !== void 0) {
1162
+ if (!REASONS.includes(source.reason)) reject("reason");
1163
+ status.reason = source.reason;
1164
+ }
1165
+ if (source.message !== void 0) {
1166
+ if (typeof source.message !== "string") reject("message");
1167
+ status.message = source.message;
1168
+ }
1169
+ return status;
1170
+ }
1171
+ /** The strict result codec shared by all three login endpoints. */
1172
+ const loginStatusSchema = { parse: parseLoginStatus };
1173
+ /** Build one login invocation descriptor (uniform result, no parameters). */
1174
+ function loginDescriptor(endpoint, method) {
1175
+ return {
1176
+ id: `${USAGE_REMOTE_PACKAGE}#${endpoint}`,
1177
+ service: "commandcodeUsage",
1178
+ namespace: "commandcode",
1179
+ method,
1180
+ invocation: { kind: "direct" },
1181
+ parameters: [],
1182
+ result: {
1183
+ mode: "strict",
1184
+ typeSymbol: `${USAGE_REMOTE_PACKAGE}#CommandCodeLoginStatus`,
1185
+ schema: loginStatusSchema
1186
+ }
1187
+ };
1188
+ }
1189
+ /** The Client-face contribution fragment mounted on `ctx.remote`. */
1190
+ const LOGIN_REMOTE_CONTRIBUTION = {
1191
+ package: USAGE_REMOTE_PACKAGE,
1192
+ descriptors: [
1193
+ loginDescriptor(LOGIN_BEGIN_ENDPOINT, "loginBegin"),
1194
+ loginDescriptor(LOGIN_STATUS_ENDPOINT, "loginStatus"),
1195
+ loginDescriptor(LOGIN_CANCEL_ENDPOINT, "loginCancel")
1196
+ ]
1197
+ };
1198
+ //#endregion
1199
+ //#region package.json
1200
+ var version = "0.8.0";
1201
+ var repository = {
1202
+ "type": "git",
1203
+ "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
1204
+ };
1205
+ //#endregion
892
1206
  //#region src/client/version.ts
893
1207
  /**
894
1208
  * The plugin's own version, read from package.json at build time.
@@ -902,7 +1216,154 @@ window.__ModuleLoader__.load({
902
1216
  * @module dsh-commandcode-provider/client/version
903
1217
  */
904
1218
  /** The published package version (e.g. `'0.6.0'`). */
905
- const PLUGIN_VERSION = "0.7.0";
1219
+ const PLUGIN_VERSION = version;
1220
+ /**
1221
+ * This package's GitHub releases page, derived from the repository field so
1222
+ * the update hint's link target can never drift from the published home.
1223
+ */
1224
+ const PLUGIN_RELEASES_URL = `${repository.url.replace(/^git\+/, "").replace(/\.git$/, "")}/releases`;
1225
+ /** Abort a hung registry request rather than keep the footer waiting. */
1226
+ const FETCH_TIMEOUT_MS = 5e3;
1227
+ /**
1228
+ * The npm registry document for this package's `latest` dist-tag. The scoped
1229
+ * name is path-escaped (`%2F`) so no client normalizes the slash away.
1230
+ */
1231
+ const NPM_LATEST_URL = "https://registry.npmjs.org/@mars-sea%2Fdsh-commandcode-provider/latest";
1232
+ /**
1233
+ * Compare two version strings (`major.minor.patch[-pre]`). Returns a negative
1234
+ * number when `a` sorts before `b`, positive when after, zero when equal.
1235
+ *
1236
+ * Tolerant by design: a leading `v` is stripped, unparsable numeric parts
1237
+ * count as `0`, and semver prerelease rules apply (release > prerelease;
1238
+ * numeric identifiers compare numerically, everything else lexically, a
1239
+ * shorter identifier list sorts first). Enough for release tags; not a full
1240
+ * semver validator.
1241
+ */
1242
+ function compareVersions(a, b) {
1243
+ const left = splitVersion(a);
1244
+ const right = splitVersion(b);
1245
+ const depth = Math.max(left.core.length, right.core.length);
1246
+ for (let index = 0; index < depth; index += 1) {
1247
+ const delta = (left.core[index] ?? 0) - (right.core[index] ?? 0);
1248
+ if (delta !== 0) return Math.sign(delta);
1249
+ }
1250
+ if (left.pre.length === 0 && right.pre.length === 0) return 0;
1251
+ if (left.pre.length === 0) return 1;
1252
+ if (right.pre.length === 0) return -1;
1253
+ const width = Math.max(left.pre.length, right.pre.length);
1254
+ for (let index = 0; index < width; index += 1) {
1255
+ const l = left.pre[index];
1256
+ const r = right.pre[index];
1257
+ if (l === void 0) return -1;
1258
+ if (r === void 0) return 1;
1259
+ const lNumeric = /^\d+$/.test(l);
1260
+ const rNumeric = /^\d+$/.test(r);
1261
+ let delta;
1262
+ if (lNumeric && rNumeric) delta = Number(l) - Number(r);
1263
+ else if (lNumeric) delta = -1;
1264
+ else if (rNumeric) delta = 1;
1265
+ else delta = l < r ? -1 : l > r ? 1 : 0;
1266
+ if (delta !== 0) return Math.sign(delta);
1267
+ }
1268
+ return 0;
1269
+ }
1270
+ /** True when `candidate` is strictly newer than `current`. */
1271
+ function isNewerVersion(candidate, current) {
1272
+ return compareVersions(candidate, current) > 0;
1273
+ }
1274
+ /** Split a tolerant version string into numeric core + prerelease ids. */
1275
+ function splitVersion(value) {
1276
+ const [coreText = "", preText] = value.trim().replace(/^v/i, "").split("-");
1277
+ return {
1278
+ core: coreText === "" ? [0] : coreText.split(".").map((part) => {
1279
+ const parsed = Number.parseInt(part, 10);
1280
+ return Number.isFinite(parsed) ? parsed : 0;
1281
+ }),
1282
+ pre: preText === void 0 ? [] : preText.split(".")
1283
+ };
1284
+ }
1285
+ /**
1286
+ * Extract the published version from the registry's `/latest` manifest
1287
+ * (`{ name, version, … }`). Throws on anything unexpected so callers treat a
1288
+ * shape change as a failed attempt, never as bogus data.
1289
+ */
1290
+ function parseLatestVersion(payload) {
1291
+ if (typeof payload !== "object" || payload === null) throw new Error("npm latest payload is not an object");
1292
+ const version = payload.version;
1293
+ if (typeof version !== "string" || !/^\d+\.\d+\./.test(version)) throw new Error("npm latest payload has no usable version");
1294
+ return version;
1295
+ }
1296
+ /** Fetch and parse the published `latest` version. Rejects on any failure. */
1297
+ async function fetchLatestVersion(fetchImpl = fetch) {
1298
+ const controller = new AbortController();
1299
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1300
+ try {
1301
+ const response = await fetchImpl(NPM_LATEST_URL, { signal: controller.signal });
1302
+ if (!response.ok) throw new Error(`registry responded ${response.status}`);
1303
+ return parseLatestVersion(await response.json());
1304
+ } finally {
1305
+ clearTimeout(timer);
1306
+ }
1307
+ }
1308
+ /** The `localStorage` key holding {@link UpdateCheckRecord}. */
1309
+ const UPDATE_CHECK_CACHE_KEY = "@mars-sea/dsh-commandcode-provider/update-check";
1310
+ /**
1311
+ * A {@link UpdateCheckStore} backed by `localStorage`. Tolerates a missing or
1312
+ * throwing storage (SSR-ish contexts, private modes): reads yield `undefined`,
1313
+ * writes are dropped.
1314
+ */
1315
+ function localStorageUpdateStore(storage = typeof localStorage === "undefined" ? void 0 : localStorage) {
1316
+ return {
1317
+ read() {
1318
+ if (storage === void 0) return void 0;
1319
+ try {
1320
+ const raw = storage.getItem(UPDATE_CHECK_CACHE_KEY);
1321
+ if (raw === null) return void 0;
1322
+ const parsed = JSON.parse(raw);
1323
+ if (typeof parsed !== "object" || parsed === null) return void 0;
1324
+ const at = parsed.at;
1325
+ if (typeof at !== "number" || !Number.isFinite(at)) return void 0;
1326
+ const version = parsed.version;
1327
+ return {
1328
+ at,
1329
+ version: typeof version === "string" && version !== "" ? version : void 0
1330
+ };
1331
+ } catch {
1332
+ return;
1333
+ }
1334
+ },
1335
+ write(record) {
1336
+ if (storage === void 0) return;
1337
+ try {
1338
+ storage.setItem(UPDATE_CHECK_CACHE_KEY, JSON.stringify(record));
1339
+ } catch {}
1340
+ }
1341
+ };
1342
+ }
1343
+ /**
1344
+ * Run one throttled update check. Resolves with the newest published version
1345
+ * when it is newer than `currentVersion`, otherwise `undefined`.
1346
+ *
1347
+ * Within the throttle window (or on failure) the cached version answers, so
1348
+ * the hint keeps working offline; past the window the registry is consulted
1349
+ * again and the attempt time is refreshed either way.
1350
+ */
1351
+ async function checkForUpdate(options) {
1352
+ const { currentVersion, now, store } = options;
1353
+ const hintOf = (version) => version !== void 0 && isNewerVersion(version, currentVersion) ? version : void 0;
1354
+ const cache = store.read();
1355
+ if (cache !== void 0 && now - cache.at < 864e5) return hintOf(cache.version);
1356
+ let learned;
1357
+ try {
1358
+ learned = await fetchLatestVersion(options.fetchImpl);
1359
+ } catch {}
1360
+ const version = learned ?? cache?.version;
1361
+ store.write({
1362
+ at: now,
1363
+ version
1364
+ });
1365
+ return hintOf(version);
1366
+ }
906
1367
  //#endregion
907
1368
  //#region src/client/section.tsx
908
1369
  /**
@@ -1074,6 +1535,82 @@ window.__ModuleLoader__.load({
1074
1535
  ]
1075
1536
  });
1076
1537
  }
1538
+ /** The per-reason copy for a failed login attempt. */
1539
+ function loginFailureCopy(reason, t) {
1540
+ if (reason === "denied") return t("loginDenied");
1541
+ if (reason === "timeout") return t("loginTimeout");
1542
+ if (reason === "invalid-key") return t("loginInvalidKey");
1543
+ if (reason === "network") return t("loginNetwork");
1544
+ if (reason === "unavailable") return t("loginStoreFailed");
1545
+ if (reason === "cancelled") return t("loginCancelled");
1546
+ return t("loginFailedGeneric");
1547
+ }
1548
+ /**
1549
+ * The sign-in alternative to pasting a key: one field row that starts the
1550
+ * Host-side browser login, links to the Studio authorization page while the
1551
+ * attempt is live, and reports the outcome. The key itself never crosses to
1552
+ * the browser — only "who signed in" does.
1553
+ */
1554
+ function LoginPanel({ state, disabled, t, onBegin, onCancel }) {
1555
+ const busy = state.phase === "starting" || state.phase === "waiting";
1556
+ let hint = t("loginHintIdle");
1557
+ let hintTitle;
1558
+ let hintClass = "cc-hint";
1559
+ if (state.phase === "starting" || state.phase === "waiting") hint = t(state.phase === "starting" ? "loginStarting" : "loginWaiting");
1560
+ else if (state.phase === "success") {
1561
+ const keyName = state.keyName !== void 0 && state.keyName !== "" ? ` · ${state.keyName}` : "";
1562
+ hint = `${t("loginSuccess")} ${state.userName ?? ""}${keyName}`.trim();
1563
+ hintClass = "cc-loginDone";
1564
+ } else if (state.phase === "failed") {
1565
+ hint = loginFailureCopy(state.reason, t);
1566
+ hintClass = "cc-loginError";
1567
+ if (state.message !== void 0) hintTitle = state.message;
1568
+ } else if (state.phase === "unavailable") {
1569
+ hint = `${t("loginUnavailable")} ${state.message ?? ""}`.trim();
1570
+ hintClass = "cc-loginError";
1571
+ }
1572
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1573
+ className: "cc-field",
1574
+ children: [
1575
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1576
+ className: "cc-fieldHead",
1577
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1578
+ className: "cc-label",
1579
+ children: t("loginTitle")
1580
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1581
+ className: "cc-badges",
1582
+ children: busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1583
+ type: "button",
1584
+ className: "cc-reset",
1585
+ onClick: onCancel,
1586
+ children: t("loginCancel")
1587
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1588
+ type: "button",
1589
+ className: "cc-reset",
1590
+ disabled,
1591
+ onClick: onBegin,
1592
+ children: t("loginButton")
1593
+ })
1594
+ })]
1595
+ }),
1596
+ state.authUrl !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1597
+ className: "cc-hint",
1598
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1599
+ className: "cc-loginLink",
1600
+ href: state.authUrl,
1601
+ target: "_blank",
1602
+ rel: "noreferrer",
1603
+ children: t("loginOpenLink")
1604
+ })
1605
+ }) : null,
1606
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1607
+ className: hintClass,
1608
+ title: hintTitle,
1609
+ children: hint
1610
+ })
1611
+ ]
1612
+ });
1613
+ }
1077
1614
  /** One stat tile in the account card's summary grid. */
1078
1615
  function UsageStat({ label, value, sub }) {
1079
1616
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1168,7 +1705,7 @@ window.__ModuleLoader__.load({
1168
1705
  * One pool account's facts (identity, totals, credits, window limits)
1169
1706
  * rendered inside the account-usage card.
1170
1707
  */
1171
- function AccountReport({ entry, t, onRemove }) {
1708
+ function AccountReport({ entry, fetchedAt, t, onRemove }) {
1172
1709
  const report = entry.report;
1173
1710
  const account = report.account;
1174
1711
  const accountName = account === void 0 ? "" : account.userName || account.name;
@@ -1176,6 +1713,8 @@ window.__ModuleLoader__.load({
1176
1713
  const plan = report.plan;
1177
1714
  const planName = plan?.name ?? "";
1178
1715
  const planStatus = plan !== void 0 && plan.status !== "" && plan.status !== "active" ? plan.status : "";
1716
+ const showPeriod = plan !== void 0 && plan.currentPeriodEnd > 0;
1717
+ const showPartial = report.failures.length > 0 && report.blocked === void 0;
1179
1718
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1180
1719
  className: "cc-accountReport",
1181
1720
  children: [
@@ -1279,31 +1818,32 @@ window.__ModuleLoader__.load({
1279
1818
  t
1280
1819
  })]
1281
1820
  }) : null,
1282
- plan !== void 0 && plan.currentPeriodEnd > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1821
+ showPeriod || showPartial || fetchedAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1283
1822
  className: "cc-usageMeta",
1284
1823
  children: [
1285
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1824
+ showPeriod ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1286
1825
  className: "cc-usageUpdated",
1287
1826
  children: [
1288
1827
  t("usagePeriodEnd"),
1289
1828
  " ",
1290
1829
  new Date(plan.currentPeriodEnd).toLocaleDateString()
1291
1830
  ]
1292
- }),
1293
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }),
1294
- report.failures.length > 0 && report.blocked === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1831
+ }) : null,
1832
+ showPartial ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1295
1833
  className: "cc-usagePartial",
1296
1834
  title: report.failures.join("; "),
1297
1835
  children: t("usagePartial")
1836
+ }) : null,
1837
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }),
1838
+ fetchedAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1839
+ className: "cc-usageUpdated",
1840
+ children: [
1841
+ t("usageUpdated"),
1842
+ " ",
1843
+ new Date(fetchedAt).toLocaleTimeString()
1844
+ ]
1298
1845
  }) : null
1299
1846
  ]
1300
- }) : report.failures.length > 0 && report.blocked === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1301
- className: "cc-usageMeta",
1302
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1303
- className: "cc-usagePartial",
1304
- title: report.failures.join("; "),
1305
- children: t("usagePartial")
1306
- })]
1307
1847
  }) : null
1308
1848
  ]
1309
1849
  });
@@ -1412,20 +1952,10 @@ window.__ModuleLoader__.load({
1412
1952
  }) : null,
1413
1953
  selected !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountReport, {
1414
1954
  entry: selected,
1955
+ fetchedAt: usage.fetchedAt,
1415
1956
  t,
1416
1957
  onRemove: removeSelected
1417
- }, selected.id) : null,
1418
- report !== void 0 && usage.fetchedAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1419
- className: "cc-usageMeta",
1420
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1421
- className: "cc-usageUpdated",
1422
- children: [
1423
- t("usageUpdated"),
1424
- " ",
1425
- new Date(usage.fetchedAt).toLocaleTimeString()
1426
- ]
1427
- })]
1428
- }) : null
1958
+ }, selected.id) : null
1429
1959
  ]
1430
1960
  });
1431
1961
  }
@@ -1626,14 +2156,39 @@ window.__ModuleLoader__.load({
1626
2156
  }, [tick]);
1627
2157
  return visible;
1628
2158
  }
2159
+ /**
2160
+ * The update hint: one throttled npm-registry check per page open (the
2161
+ * throttle and all failure handling live in ./update.ts). Resolves to the
2162
+ * newest published version when it is newer than this build, else undefined —
2163
+ * every failure mode degrades to no hint at all.
2164
+ */
2165
+ function usePluginUpdate() {
2166
+ const [available, setAvailable] = (0, react.useState)(void 0);
2167
+ (0, react.useEffect)(() => {
2168
+ let cancelled = false;
2169
+ checkForUpdate({
2170
+ currentVersion: PLUGIN_VERSION,
2171
+ now: Date.now(),
2172
+ store: localStorageUpdateStore()
2173
+ }).then((version) => {
2174
+ if (!cancelled) setAvailable(version);
2175
+ }, () => {});
2176
+ return () => {
2177
+ cancelled = true;
2178
+ };
2179
+ }, []);
2180
+ return available;
2181
+ }
1629
2182
  /** The settings page body: connection facts for the Command Code provider. */
1630
2183
  function CommandCodeSettingsPage(props) {
1631
2184
  const { t } = props;
1632
2185
  const state = props.useCommandCodeSettings((snapshot) => snapshot);
1633
2186
  const usage = props.useCommandCodeUsage((snapshot) => snapshot);
2187
+ const login = props.useCommandCodeLogin((snapshot) => snapshot);
1634
2188
  const disabled = !state.writable;
1635
2189
  const keyLocked = !state.apiKeyWritable;
1636
2190
  const savedVisible = useSavedFlash(state.savedCount);
2191
+ const updateVersion = usePluginUpdate();
1637
2192
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1638
2193
  className: "cc-section",
1639
2194
  "aria-label": t("title"),
@@ -1693,6 +2248,13 @@ window.__ModuleLoader__.load({
1693
2248
  onEdit: (text) => props.edit("apiKey", text),
1694
2249
  onToggleClear: () => props.toggleKeyClear("default")
1695
2250
  }),
2251
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoginPanel, {
2252
+ state: login,
2253
+ disabled: disabled || keyLocked,
2254
+ t,
2255
+ onBegin: props.beginLogin,
2256
+ onCancel: props.cancelLogin
2257
+ }),
1696
2258
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1697
2259
  id: "cc-api-base",
1698
2260
  label: t("apiBase"),
@@ -1780,7 +2342,23 @@ window.__ModuleLoader__.load({
1780
2342
  }),
1781
2343
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1782
2344
  className: "cc-version",
1783
- children: ["Command Code Provider v", PLUGIN_VERSION]
2345
+ children: [
2346
+ "Command Code Provider v",
2347
+ PLUGIN_VERSION,
2348
+ updateVersion !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
2349
+ className: "cc-versionLink",
2350
+ href: PLUGIN_RELEASES_URL,
2351
+ target: "_blank",
2352
+ rel: "noreferrer",
2353
+ title: t("updateHint"),
2354
+ children: [
2355
+ "v",
2356
+ updateVersion,
2357
+ " ",
2358
+ t("updateAvailable")
2359
+ ]
2360
+ })] }) : null
2361
+ ]
1784
2362
  })
1785
2363
  ]
1786
2364
  });
@@ -1867,7 +2445,25 @@ window.__ModuleLoader__.load({
1867
2445
  usageActive: "当前使用",
1868
2446
  usageCooldown: "限额冷却中",
1869
2447
  usageInvalidKey: "密钥无效",
1870
- usageUnconfigured: "该账户尚未配置 API 密钥。"
2448
+ usageUnconfigured: "该账户尚未配置 API 密钥。",
2449
+ updateAvailable: "可更新",
2450
+ updateHint: "已发布新版本,点击查看发布说明;更新插件后刷新本页,提示会自动消失。",
2451
+ loginTitle: "通过官方登录获取密钥",
2452
+ loginHintIdle: "不想手动创建密钥?点击登录后浏览器会打开 commandcode.ai 授权页,完成后密钥自动写入本机凭据服务,下次请求即生效。",
2453
+ loginButton: "登录 Command Code",
2454
+ loginStarting: "正在启动本地回调服务…",
2455
+ loginWaiting: "等待在浏览器中完成授权…",
2456
+ loginOpenLink: "打开授权页面 ↗",
2457
+ loginCancel: "取消登录",
2458
+ loginSuccess: "已登录为",
2459
+ loginUnavailable: "此环境暂不支持登录流程,请手动粘贴密钥。",
2460
+ loginDenied: "授权被拒绝。可重试,或手动粘贴密钥。",
2461
+ loginTimeout: "等待超时:未在窗口期内收到授权回调,请重试。",
2462
+ loginInvalidKey: "获取到的密钥未通过校验(401),请重试或手动粘贴。",
2463
+ loginNetwork: "无法连接 Command Code 服务校验密钥,请检查网络后重试。",
2464
+ loginStoreFailed: "密钥无法写入本机凭据服务,请手动粘贴。",
2465
+ loginCancelled: "登录已取消。",
2466
+ loginFailedGeneric: "登录失败,请重试或手动粘贴密钥。"
1871
2467
  };
1872
2468
  const en = {
1873
2469
  nav: "Command Code",
@@ -1949,7 +2545,25 @@ window.__ModuleLoader__.load({
1949
2545
  usageActive: "Active",
1950
2546
  usageCooldown: "Cooling down",
1951
2547
  usageInvalidKey: "Invalid key",
1952
- usageUnconfigured: "No API key configured for this account yet."
2548
+ usageUnconfigured: "No API key configured for this account yet.",
2549
+ updateAvailable: "update available",
2550
+ updateHint: "A newer version has been published; click for release notes. The notice disappears once the plugin is updated.",
2551
+ loginTitle: "Sign in to fetch a key",
2552
+ loginHintIdle: "Rather not create a key by hand? Sign in and your browser opens the commandcode.ai authorization page; the approved key is stored in the local credential service and applies to the next request.",
2553
+ loginButton: "Sign in to Command Code",
2554
+ loginStarting: "Starting the local callback server…",
2555
+ loginWaiting: "Waiting for authorization in your browser…",
2556
+ loginOpenLink: "Open the authorization page ↗",
2557
+ loginCancel: "Cancel sign-in",
2558
+ loginSuccess: "Signed in as",
2559
+ loginUnavailable: "Sign-in is unavailable in this environment; paste the API key instead.",
2560
+ loginDenied: "Authorization was denied. Try again or paste the key manually.",
2561
+ loginTimeout: "Timed out waiting for the authorization callback; try again.",
2562
+ loginInvalidKey: "The delivered key failed validation (401). Try again or paste it manually.",
2563
+ loginNetwork: "Could not reach the Command Code service to validate the key; check your network and retry.",
2564
+ loginStoreFailed: "The key could not be stored in the local credential service; paste it manually.",
2565
+ loginCancelled: "Sign-in cancelled.",
2566
+ loginFailedGeneric: "Sign-in failed; try again or paste the key manually."
1953
2567
  };
1954
2568
  //#endregion
1955
2569
  //#region src/client/index.ts
@@ -1973,6 +2587,13 @@ window.__ModuleLoader__.load({
1973
2587
  .cc-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}
1974
2588
  .cc-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
1975
2589
  .cc-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
2590
+ /* Selects need their own treatment to sit flush with the text inputs:
2591
+ * the UA stylesheet renders <select> border-box (34px total vs the inputs'
2592
+ * 36px) and forces its own menulist text metrics, so drop the native
2593
+ * chrome entirely (appearance:none), restore content-box so the outer box
2594
+ * matches the inputs again, and draw the chevron ourselves. Longhand
2595
+ * background-* only — the shorthand would reset .cc-input's background. */
2596
+ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box-sizing:content-box;padding-right:32px;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='7' viewBox='0 0 12 7'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23888f98' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 11px center}
1976
2597
  .cc-inputInvalid{border-color:var(--dsw-alias-label-error)}
1977
2598
  .cc-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
1978
2599
  .cc-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
@@ -2027,6 +2648,17 @@ window.__ModuleLoader__.load({
2027
2648
  .cc-usageBlockedTitle{color:var(--dsw-alias-label-error);margin:0;font-size:13px;font-weight:600;line-height:1.5}
2028
2649
  .cc-usageBlockedHint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:1.5}
2029
2650
  .cc-version{margin:4px 0 0;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
2651
+ /* The update hint rides the footer version line: warning-tinted (with a
2652
+ * muted fallback for themes without the alias), quiet until hovered. */
2653
+ .cc-versionLink{color:var(--dsw-alias-state-warning-primary,var(--dsw-alias-label-secondary));text-decoration:none}
2654
+ .cc-versionLink:hover{color:var(--dsw-alias-label-primary);text-decoration:underline;text-underline-position:under}
2655
+ /* The login panel rides the connection card as one more field row; the
2656
+ * authorization link is the only branded element on it. */
2657
+ .cc-loginLink{color:var(--dsw-alias-brand-primary);text-decoration:none;font-size:12px;line-height:1.5}
2658
+ .cc-loginLink:hover{text-decoration:underline;text-underline-position:under}
2659
+ .cc-loginBusy{color:var(--dsw-alias-label-tertiary)}
2660
+ .cc-loginDone{color:var(--dsw-alias-state-success-primary,var(--dsw-alias-label-secondary))}
2661
+ .cc-loginError{color:var(--dsw-alias-label-error)}
2030
2662
  .cc-saved{color:var(--dsw-alias-state-success-primary,var(--dsw-alias-label-secondary));margin:0;font-size:12px;font-weight:500;line-height:1.5}
2031
2663
  .cc-badgeWarn{background:var(--dsw-alias-state-warning-secondary,var(--dsw-alias-bg-module-platform));color:var(--dsw-alias-state-warning-primary,var(--dsw-alias-label-secondary))}
2032
2664
  `;
@@ -2063,10 +2695,14 @@ window.__ModuleLoader__.load({
2063
2695
  controller.subscribe(() => store.set(controller.state()));
2064
2696
  let usageNamespace;
2065
2697
  let usageMountError;
2698
+ const contribution = {
2699
+ package: USAGE_REMOTE_CONTRIBUTION.package,
2700
+ descriptors: [...USAGE_REMOTE_CONTRIBUTION.descriptors, ...LOGIN_REMOTE_CONTRIBUTION.descriptors]
2701
+ };
2066
2702
  ctx.effect(() => {
2067
2703
  let cancelled = false;
2068
2704
  let unmount;
2069
- ctx.remote.$mount(USAGE_REMOTE_CONTRIBUTION).then((dispose) => {
2705
+ ctx.remote.$mount(contribution).then((dispose) => {
2070
2706
  if (cancelled) {
2071
2707
  dispose();
2072
2708
  return;
@@ -2098,10 +2734,47 @@ window.__ModuleLoader__.load({
2098
2734
  ctx.effect(() => () => usageController.dispose(), "dsh-commandcode-provider: usage controller");
2099
2735
  const usageStore = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(usageController.state());
2100
2736
  usageController.subscribe(() => usageStore.set(usageController.state()));
2737
+ const loginRemote = {
2738
+ loginBegin: async () => {
2739
+ if (usageNamespace === void 0) return {
2740
+ ok: false,
2741
+ error: { message: usageMountError ?? "commandcode remote is not mounted" }
2742
+ };
2743
+ return usageNamespace.loginBegin();
2744
+ },
2745
+ loginStatus: async () => {
2746
+ if (usageNamespace === void 0) return {
2747
+ ok: false,
2748
+ error: { message: usageMountError ?? "commandcode remote is not mounted" }
2749
+ };
2750
+ return usageNamespace.loginStatus();
2751
+ },
2752
+ loginCancel: async () => {
2753
+ if (usageNamespace === void 0) return {
2754
+ ok: false,
2755
+ error: { message: usageMountError ?? "commandcode remote is not mounted" }
2756
+ };
2757
+ return usageNamespace.loginCancel();
2758
+ }
2759
+ };
2760
+ const loginController = new CommandCodeLoginController(() => loginRemote);
2761
+ ctx.effect(() => () => loginController.dispose(), "dsh-commandcode-provider: login controller");
2762
+ const loginStore = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(loginController.state());
2763
+ let lastLoginPhase = loginController.state().phase;
2764
+ loginController.subscribe(() => {
2765
+ const phase = loginController.state().phase;
2766
+ if (phase === "success" && lastLoginPhase !== "success") {
2767
+ controller.refreshCredentials();
2768
+ usageController.refresh();
2769
+ }
2770
+ lastLoginPhase = phase;
2771
+ loginStore.set(loginController.state());
2772
+ });
2101
2773
  const injected = () => ({
2102
2774
  hooks: {
2103
2775
  commandCodeSettings: store,
2104
- commandCodeUsage: usageStore
2776
+ commandCodeUsage: usageStore,
2777
+ commandCodeLogin: loginStore
2105
2778
  },
2106
2779
  edit: (field, text) => controller.edit(field, text),
2107
2780
  resetField: (field) => controller.resetField(field),
@@ -2111,6 +2784,8 @@ window.__ModuleLoader__.load({
2111
2784
  }),
2112
2785
  discard: () => controller.discard(),
2113
2786
  refreshUsage: () => void usageController.refresh(),
2787
+ beginLogin: () => void loginController.begin(),
2788
+ cancelLogin: () => void loginController.cancel(),
2114
2789
  addAccount: () => controller.addAccount(),
2115
2790
  removeAccount: (id) => controller.removeAccount(id),
2116
2791
  editAccountLabel: (id, text) => controller.editAccountLabel(id, text),