@aexol/opencode-wizard 0.1.12 → 0.1.14

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/README.md CHANGED
@@ -45,7 +45,7 @@ Use `skills.urls` only for public registries that are intentionally cacheable by
45
45
 
46
46
  ## Catalog discovery and auth bootstrap
47
47
 
48
- On chat/system-context startup, the plugin attempts to load the catalog automatically with the stored plugin session at `plugin/opencode-wizard/.generated/auth-state.json`. If no valid plugin session exists, startup stays passive and reports that interactive fetch will bootstrap browser login when needed; it does not open the browser from system context.
48
+ On chat/system-context startup, the plugin attempts to load the catalog automatically with the stored plugin auth at `~/.config/opencode/opencode-wizard.json` (`auth` field). If no valid plugin session exists, startup stays passive and reports that interactive fetch will bootstrap browser login when needed; it does not open the browser from system context.
49
49
 
50
50
  Call `opencode_wizard_published_skills_fetch` without `skill` or `skills` to manually bootstrap plugin login if needed and return catalog-only discovery output for the current directory scope.
51
51
 
package/dist/server.d.ts CHANGED
@@ -25,7 +25,7 @@ export type PublishedSkillCatalogPayload = {
25
25
  repositoryUrl?: string | null;
26
26
  defaultBranch?: string | null;
27
27
  status: string;
28
- };
28
+ } | null;
29
29
  directoryPath: string;
30
30
  skills: PublishedSkillCatalogItem[];
31
31
  };
package/dist/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import http from 'node:http';
3
+ import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import crypto from 'node:crypto';
5
6
  import { execFile } from 'node:child_process';
@@ -11,7 +12,8 @@ const PACKAGE_ROOT_PATH = path.resolve(path.dirname(MODULE_FILE_PATH), '..');
11
12
  export const PLUGIN_ID = 'opencode-wizard';
12
13
  const CACHE_TTL_MS = 30_000;
13
14
  const ROOT_SKILL_SEED_PATH = '.opencode/skills';
14
- const AUTH_STATE_PATH = 'plugin/opencode-wizard/.generated/auth-state.json';
15
+ const GLOBAL_CONFIG_PATH = path.join(os.homedir(), '.config', 'opencode', 'opencode-wizard.json');
16
+ const LEGACY_AUTH_STATE_PATH = 'plugin/opencode-wizard/.generated/auth-state.json';
15
17
  const PUBLISHED_BACKEND_ORIGIN = 'https://opencode-wizard.aexol.work';
16
18
  const OIDC_ISSUER = 'https://login.microsoftonline.com/86f4caf4-0d6f-4682-9a06-ea57f3e4e76c/v2.0';
17
19
  const OIDC_CLIENT_ID = 'da963901-2375-442b-9e99-14e59f43eda2';
@@ -208,7 +210,7 @@ export const resolveConfig = async worktree => {
208
210
  actionsUrl: `${backendOrigin}/api/opencode-plugin/actions`,
209
211
  fallbackWorkspaceSlug: resolveFallbackWorkspaceSlug(worktree),
210
212
  rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
211
- authStatePath: AUTH_STATE_PATH
213
+ authStatePath: GLOBAL_CONFIG_PATH
212
214
  };
213
215
  };
214
216
  const normalizeAbsolutePath = value => path.resolve(value);
@@ -332,15 +334,45 @@ const isAuthState = value => {
332
334
  if (!isRecord(value)) return false;
333
335
  return value.pluginId === PLUGIN_ID && typeof value.sessionToken === 'string' && isValidIsoDateString(value.expiresAt) && isValidIsoDateString(value.authenticatedAt) && typeof value.userId === 'string' && typeof value.email === 'string';
334
336
  };
335
- const readAuthState = async authStateFile => {
337
+ const readGlobalConfig = async configFile => {
338
+ const storedConfig = await readJsonFile(configFile);
339
+ if (isRecord(storedConfig)) return storedConfig;
340
+ return {};
341
+ };
342
+ const writeGlobalConfig = async (configFile, config) => {
343
+ await writeJsonFile(configFile, config);
344
+ };
345
+ const readGlobalAuthState = async configFile => {
346
+ const storedConfig = await readGlobalConfig(configFile);
347
+ const storedAuthState = storedConfig.auth;
348
+ if (storedAuthState === undefined || storedAuthState === null) return null;
349
+ if (isAuthState(storedAuthState)) return storedAuthState;
350
+ await writeGlobalConfig(configFile, {
351
+ ...storedConfig,
352
+ auth: null
353
+ });
354
+ return null;
355
+ };
356
+ const readLegacyAuthState = async authStateFile => {
336
357
  const storedAuthState = await readJsonFile(authStateFile);
337
358
  if (storedAuthState === null) return null;
338
359
  if (isAuthState(storedAuthState)) return storedAuthState;
339
360
  await deleteFileIfExists(authStateFile);
340
361
  return null;
341
362
  };
342
- const writeAuthState = async (authStateFile, authState) => {
343
- await writeJsonFile(authStateFile, authState);
363
+ const writeAuthState = async (configFile, authState) => {
364
+ const storedConfig = await readGlobalConfig(configFile);
365
+ await writeGlobalConfig(configFile, {
366
+ ...storedConfig,
367
+ auth: authState
368
+ });
369
+ };
370
+ const clearAuthState = async configFile => {
371
+ const storedConfig = await readGlobalConfig(configFile);
372
+ await writeGlobalConfig(configFile, {
373
+ ...storedConfig,
374
+ auth: null
375
+ });
344
376
  };
345
377
  const toAuthState = session => ({
346
378
  pluginId: PLUGIN_ID,
@@ -351,16 +383,24 @@ const toAuthState = session => ({
351
383
  email: session.user.email
352
384
  });
353
385
  const resolveStoredAuthState = async (worktree, config) => {
354
- const authStateFile = path.resolve(worktree, config.authStatePath);
355
- const authState = await readAuthState(authStateFile);
356
- if (!authState) {
386
+ const authState = await readGlobalAuthState(config.authStatePath);
387
+ if (authState && Date.parse(authState.expiresAt) > Date.now()) {
388
+ return authState;
389
+ }
390
+ if (authState) {
391
+ await clearAuthState(config.authStatePath);
357
392
  return null;
358
393
  }
359
- if (Date.parse(authState.expiresAt) > Date.now()) {
360
- return authState;
394
+ const legacyAuthStateFile = path.resolve(worktree, LEGACY_AUTH_STATE_PATH);
395
+ const legacyAuthState = await readLegacyAuthState(legacyAuthStateFile);
396
+ if (!legacyAuthState) return null;
397
+ if (Date.parse(legacyAuthState.expiresAt) <= Date.now()) {
398
+ await deleteFileIfExists(legacyAuthStateFile);
399
+ return null;
361
400
  }
362
- await deleteFileIfExists(authStateFile);
363
- return null;
401
+ await writeAuthState(config.authStatePath, legacyAuthState);
402
+ await deleteFileIfExists(legacyAuthStateFile);
403
+ return legacyAuthState;
364
404
  };
365
405
  export const buildSkillMarkdown = item => {
366
406
  const artifactBody = item.publishedArtifact.markdownBody.trim();
@@ -487,6 +527,10 @@ export const toPublishedSkillCatalog = payload => ({
487
527
  facets: getPublishedSkillFacets(payload.skills),
488
528
  skills: payload.skills.map(toPublishedSkillSummary)
489
529
  });
530
+ const getWorkspaceUnavailableMessage = payload => {
531
+ if (payload.workspace) return null;
532
+ return 'Workspace-specific skills are unavailable because the workspace was not found; global skills are still loaded.';
533
+ };
490
534
  const normalizeSkillIdentifier = value => value.trim().toLowerCase();
491
535
  const parseSkillIdentifiers = value => {
492
536
  const seen = new Set();
@@ -568,7 +612,7 @@ const buildSystemNote = (result, config, details) => {
568
612
  const projectSkills = catalog.skills.filter(skill => skill.contextKind === 'project').slice(0, 5).map(buildSkillCatalogLine);
569
613
  const detailLines = details.slice(0, SYSTEM_NOTE_DETAIL_LIMIT).map(buildSkillDetailSnippetLine);
570
614
  const detailBlock = detailLines.length > 0 ? ` Loaded body snippets (capped):\n${truncateText(detailLines.join('\n'), SYSTEM_NOTE_DETAIL_CHAR_LIMIT)}` : '';
571
- return [`opencode-wizard published skills are available from backend runtime delivery for workspace ${result.fetchResult.payload.workspace.slug}.`, `Current directory: ${result.directoryPath}.`, `Published skills for this scope: ${renderedSkillNames}${renderedCountSuffix}; counts: ${catalog.assignmentCounts.global} global, ${catalog.assignmentCounts.project} project, ${catalog.assignmentCounts.other} other.`, 'Catalog lines use short whenToUse guidance when available; fetch the full skill only when that guidance matches the task.', 'GLOBAL_CONTEXT skills are active context skills and are not project-installable; PROJECT_INSTALLABLE skills can be assigned globally or to project/workspace scopes; assignment rows decide which skills are active here.', globalSkills.length > 0 ? `Global context skills:\n${globalSkills.join('\n')}` : 'Global context skills: none.', projectSkills.length > 0 ? `Project-scoped active skills:\n${projectSkills.join('\n')}` : 'Project-scoped active skills: none.', detailBlock, 'Use opencode_wizard_published_skills_fetch for one or multiple skills.', `Root source seed path remains non-runtime input only: ${config.rootSkillSeedPath}/**.`].filter(line => line.length > 0).join(' ');
615
+ return [result.fetchResult.payload.workspace ? `opencode-wizard published skills are available from backend runtime delivery for workspace ${result.fetchResult.payload.workspace.slug}.` : 'opencode-wizard published global skills are available from backend runtime delivery; workspace-specific skills are unavailable because the workspace was not found.', `Current directory: ${result.directoryPath}.`, `Published skills for this scope: ${renderedSkillNames}${renderedCountSuffix}; counts: ${catalog.assignmentCounts.global} global, ${catalog.assignmentCounts.project} project, ${catalog.assignmentCounts.other} other.`, 'Catalog lines use short whenToUse guidance when available; fetch the full skill only when that guidance matches the task.', 'GLOBAL_CONTEXT skills are active context skills and are not project-installable; PROJECT_INSTALLABLE skills can be assigned globally or to project/workspace scopes; assignment rows decide which skills are active here.', globalSkills.length > 0 ? `Global context skills:\n${globalSkills.join('\n')}` : 'Global context skills: none.', projectSkills.length > 0 ? `Project-scoped active skills:\n${projectSkills.join('\n')}` : 'Project-scoped active skills: none.', detailBlock, 'Use opencode_wizard_published_skills_fetch for one or multiple skills.', `Root source seed path remains non-runtime input only: ${config.rootSkillSeedPath}/**.`].filter(line => line.length > 0).join(' ');
572
616
  };
573
617
  const toWorkspaceResolutionOutput = resolution => ({
574
618
  requestedDirectory: resolution.requestedDirectory,
@@ -626,7 +670,8 @@ const formatStatusOutput = async (worktree, config, publishedSkillsResult, login
626
670
  }
627
671
  return JSON.stringify({
628
672
  ...base,
629
- ...toPublishedSkillCatalog(publishedSkillsResult.fetchResult.payload)
673
+ ...toPublishedSkillCatalog(publishedSkillsResult.fetchResult.payload),
674
+ message: getWorkspaceUnavailableMessage(publishedSkillsResult.fetchResult.payload)
630
675
  }, null, 2);
631
676
  };
632
677
  export const toPluginAuthStateSummary = authState => {
@@ -675,7 +720,7 @@ export const resolvePluginStatusSnapshot = async ({
675
720
  fetchedAt: fetchResult.fetchedAt,
676
721
  source: fetchResult.source,
677
722
  availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
678
- message: fetchResult.ok ? null : fetchResult.message,
723
+ message: fetchResult.ok ? getWorkspaceUnavailableMessage(fetchResult.payload) : fetchResult.message,
679
724
  catalog: fetchResult.ok ? toPublishedSkillCatalog(fetchResult.payload) : null
680
725
  };
681
726
  };
@@ -714,7 +759,7 @@ const startStatusPathLoginBootstrap = (worktree, config) => {
714
759
  signal: loginSignal
715
760
  });
716
761
  const authState = toAuthState(pluginSession);
717
- await writeAuthState(path.resolve(worktree, config.authStatePath), authState);
762
+ await writeAuthState(config.authStatePath, authState);
718
763
  statusPathLoginBootstrap.status = 'authenticated';
719
764
  statusPathLoginBootstrap.message = `Browser login completed successfully for ${authState.email}.`;
720
765
  return authState;
@@ -791,12 +836,204 @@ const fetchOidcDiscoveryDocument = async signal => {
791
836
  }
792
837
  return await response.json();
793
838
  };
839
+ const isCallbackPortInUseError = error => {
840
+ if (!error || typeof error !== 'object') return false;
841
+ if (!('code' in error)) return false;
842
+ return error.code === 'EADDRINUSE';
843
+ };
844
+ const toCallbackServerStartError = error => {
845
+ if (!isCallbackPortInUseError(error)) {
846
+ return error instanceof Error ? error : new Error('Failed to start local OAuth callback server.');
847
+ }
848
+ return new Error('OAuth login cannot start because localhost:24953 is already in use. Another OpenCode login is likely in progress; finish it or close the other instance, then retry.');
849
+ };
850
+ const escapeHtml = value => {
851
+ return value.replace(/[&<>'"]/g, character => {
852
+ const replacements = {
853
+ '&': '&amp;',
854
+ '<': '&lt;',
855
+ '>': '&gt;',
856
+ "'": '&#39;',
857
+ '"': '&quot;'
858
+ };
859
+ return replacements[character] ?? character;
860
+ });
861
+ };
794
862
  const sendHtmlResponse = (response, statusCode, title, message) => {
863
+ const escapedTitle = escapeHtml(title);
864
+ const escapedMessage = escapeHtml(message);
865
+ const isSuccess = statusCode >= 200 && statusCode < 300;
866
+ const pageState = isSuccess ? 'success' : statusCode === 404 ? 'not-found' : 'error';
867
+ const cardTitle = isSuccess ? 'Authorization successful' : statusCode === 404 ? 'Callback not found' : 'Authorization failed';
868
+ const escapedCardTitle = escapeHtml(cardTitle);
869
+ const eyebrow = isSuccess ? 'Authorization complete' : statusCode === 404 ? 'Callback route not found' : 'Authorization needs attention';
870
+ const actionText = isSuccess ? 'This window will close automatically in a moment. You can also close it now and return to OpenCode.' : 'You can close this window and return to OpenCode to try again.';
871
+ const autoCloseScript = isSuccess ? `<script>
872
+ window.setTimeout(() => window.close(), 2000);
873
+ </script>` : '';
874
+ const stateIcon = isSuccess ? '<path d="M7 12.5l3.1 3.1L17.5 8" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>' : statusCode === 404 ? '<path d="M10.5 17a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z" stroke="currentColor" stroke-width="2.2"/><path d="m15.5 15.5 4 4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>' : '<path d="M12 7v6" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><path d="M12 17.2v.1" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"/>';
795
875
  response.writeHead(statusCode, {
796
876
  'content-type': 'text/html; charset=utf-8',
797
877
  'cache-control': 'no-store'
798
878
  });
799
- response.end(`<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body><h1>${title}</h1><p>${message}</p><p>You can close this window and return to OpenCode.</p></body></html>`);
879
+ response.end(`<!doctype html>
880
+ <html lang="en">
881
+ <head>
882
+ <meta charset="utf-8">
883
+ <meta name="viewport" content="width=device-width, initial-scale=1">
884
+ <meta name="color-scheme" content="light dark">
885
+ <title>${escapedTitle}</title>
886
+ <style>
887
+ :root {
888
+ color-scheme: light dark;
889
+ --page-bg: #f2efe7;
890
+ --page-ink: #211d18;
891
+ --muted: #6c6258;
892
+ --panel: rgba(255, 252, 245, 0.82);
893
+ --panel-border: rgba(78, 66, 52, 0.16);
894
+ --success: #167848;
895
+ --error: #ba3329;
896
+ --not-found: #986614;
897
+ --glow: rgba(22, 120, 72, 0.18);
898
+ }
899
+
900
+ @media (prefers-color-scheme: dark) {
901
+ :root {
902
+ --page-bg: #12100d;
903
+ --page-ink: #f7efe2;
904
+ --muted: #b8aa98;
905
+ --panel: rgba(30, 26, 22, 0.78);
906
+ --panel-border: rgba(255, 244, 224, 0.14);
907
+ --success: #71e0a6;
908
+ --error: #ff897e;
909
+ --not-found: #f7c96f;
910
+ --glow: rgba(113, 224, 166, 0.2);
911
+ }
912
+ }
913
+
914
+ * {
915
+ box-sizing: border-box;
916
+ }
917
+
918
+ body {
919
+ min-height: 100vh;
920
+ margin: 0;
921
+ display: grid;
922
+ place-items: center;
923
+ padding: 24px;
924
+ overflow: hidden;
925
+ background:
926
+ radial-gradient(circle at 18% 18%, var(--glow), transparent 34rem),
927
+ radial-gradient(circle at 82% 12%, rgba(209, 142, 72, 0.18), transparent 30rem),
928
+ linear-gradient(135deg, var(--page-bg), color-mix(in srgb, var(--page-bg) 76%, #000 24%));
929
+ color: var(--page-ink);
930
+ font-family: ui-rounded, "SF Pro Rounded", "Segoe UI", system-ui, sans-serif;
931
+ }
932
+
933
+ body::before {
934
+ content: "";
935
+ position: fixed;
936
+ inset: -20%;
937
+ pointer-events: none;
938
+ background-image:
939
+ linear-gradient(rgba(128, 104, 74, 0.08) 1px, transparent 1px),
940
+ linear-gradient(90deg, rgba(128, 104, 74, 0.08) 1px, transparent 1px);
941
+ background-size: 42px 42px;
942
+ mask-image: radial-gradient(circle at center, black, transparent 68%);
943
+ }
944
+
945
+ main {
946
+ position: relative;
947
+ width: min(100%, 560px);
948
+ padding: clamp(28px, 7vw, 56px);
949
+ border: 1px solid var(--panel-border);
950
+ border-radius: 32px;
951
+ background: var(--panel);
952
+ box-shadow: 0 24px 90px rgba(0, 0, 0, 0.24);
953
+ text-align: center;
954
+ backdrop-filter: blur(18px) saturate(1.2);
955
+ }
956
+
957
+ .mark {
958
+ width: 72px;
959
+ height: 72px;
960
+ margin: 0 auto 24px;
961
+ display: grid;
962
+ place-items: center;
963
+ border-radius: 24px;
964
+ color: var(--state-color);
965
+ background: color-mix(in srgb, var(--state-color) 16%, transparent);
966
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--state-color) 28%, transparent);
967
+ }
968
+
969
+ [data-state="success"] { --state-color: var(--success); }
970
+ [data-state="error"] { --state-color: var(--error); }
971
+ [data-state="not-found"] { --state-color: var(--not-found); }
972
+
973
+ .eyebrow {
974
+ margin: 0 0 10px;
975
+ color: var(--state-color);
976
+ font-size: 0.78rem;
977
+ font-weight: 800;
978
+ letter-spacing: 0.14em;
979
+ text-transform: uppercase;
980
+ }
981
+
982
+ h1 {
983
+ margin: 0;
984
+ font-size: clamp(2rem, 7vw, 3.35rem);
985
+ line-height: 0.95;
986
+ letter-spacing: -0.06em;
987
+ }
988
+
989
+ .message {
990
+ margin: 22px auto 0;
991
+ max-width: 38rem;
992
+ color: var(--muted);
993
+ font-size: clamp(1rem, 2.5vw, 1.1rem);
994
+ line-height: 1.65;
995
+ }
996
+
997
+ .next-step {
998
+ margin: 26px 0 0;
999
+ padding: 14px 18px;
1000
+ border-radius: 999px;
1001
+ background: color-mix(in srgb, var(--state-color) 12%, transparent);
1002
+ color: var(--page-ink);
1003
+ font-size: 0.94rem;
1004
+ line-height: 1.5;
1005
+ }
1006
+
1007
+ @media (max-width: 520px) {
1008
+ body {
1009
+ padding: 16px;
1010
+ }
1011
+
1012
+ main {
1013
+ border-radius: 24px;
1014
+ }
1015
+
1016
+ .next-step {
1017
+ border-radius: 18px;
1018
+ }
1019
+ }
1020
+ </style>
1021
+ </head>
1022
+ <body>
1023
+ <main data-state="${pageState}" aria-labelledby="callback-title">
1024
+ <div class="mark" aria-hidden="true">
1025
+ <svg width="34" height="34" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1026
+ ${stateIcon}
1027
+ </svg>
1028
+ </div>
1029
+ <p class="eyebrow">${eyebrow}</p>
1030
+ <h1 id="callback-title">${escapedCardTitle}</h1>
1031
+ <p class="message">${escapedMessage}</p>
1032
+ <p class="next-step">${actionText}</p>
1033
+ </main>
1034
+ ${autoCloseScript}
1035
+ </body>
1036
+ </html>`);
800
1037
  };
801
1038
  const startLocalCallbackServer = async ({
802
1039
  expectedState,
@@ -861,9 +1098,6 @@ const startLocalCallbackServer = async ({
861
1098
  state
862
1099
  });
863
1100
  });
864
- server.on('error', error => {
865
- fail(error instanceof Error ? error : new Error('Failed to start local OAuth callback server.'));
866
- });
867
1101
  const close = async () => {
868
1102
  await new Promise((resolve, reject) => {
869
1103
  server.close(error => {
@@ -876,8 +1110,17 @@ const startLocalCallbackServer = async ({
876
1110
  });
877
1111
  };
878
1112
  await new Promise((resolve, reject) => {
879
- server.listen(24953, 'localhost', () => resolve());
880
- server.once('error', reject);
1113
+ const rejectStart = error => {
1114
+ reject(toCallbackServerStartError(error));
1115
+ };
1116
+ server.once('error', rejectStart);
1117
+ server.listen(24953, 'localhost', () => {
1118
+ server.off('error', rejectStart);
1119
+ server.on('error', error => {
1120
+ fail(error instanceof Error ? error : new Error('Local OAuth callback server failed.'));
1121
+ });
1122
+ resolve();
1123
+ });
881
1124
  });
882
1125
  signal.addEventListener('abort', () => {
883
1126
  fail(signal.reason instanceof Error ? signal.reason : new Error('OAuth login aborted.'));
@@ -941,7 +1184,7 @@ const fetchPublishedSkillsGraphQl = async ({
941
1184
  };
942
1185
  }
943
1186
  if (response.status === 401 || response.status === 403) {
944
- await deleteFileIfExists(path.resolve(worktree, config.authStatePath));
1187
+ await clearAuthState(config.authStatePath);
945
1188
  onAuthStateChanged?.();
946
1189
  return {
947
1190
  ok: false,
@@ -987,7 +1230,7 @@ const fetchPublishedSkillsGraphQl = async ({
987
1230
  if (body.errors?.length) {
988
1231
  const message = body.errors.map(error => error.message).join('; ');
989
1232
  if (body.errors.some(error => isUnauthorizedGraphQlMessage(error.message))) {
990
- await deleteFileIfExists(path.resolve(worktree, config.authStatePath));
1233
+ await clearAuthState(config.authStatePath);
991
1234
  onAuthStateChanged?.();
992
1235
  return {
993
1236
  ok: false,
@@ -1276,7 +1519,6 @@ const OpencodeWizardSkillsPlugin = async input => {
1276
1519
  const catalogInflight = new Map();
1277
1520
  const detailCache = new Map();
1278
1521
  const detailInflight = new Map();
1279
- const authStateFile = path.resolve(input.worktree, config.authStatePath);
1280
1522
  const initialAuthState = await resolveStoredAuthState(input.worktree, config);
1281
1523
  const loginBootstrap = {
1282
1524
  promise: null,
@@ -1375,7 +1617,7 @@ const OpencodeWizardSkillsPlugin = async input => {
1375
1617
  };
1376
1618
  const persistAuthState = async session => {
1377
1619
  const authState = toAuthState(session);
1378
- await writeAuthState(authStateFile, authState);
1620
+ await writeAuthState(config.authStatePath, authState);
1379
1621
  clearPublishedSkillState();
1380
1622
  return authState;
1381
1623
  };
@@ -1396,19 +1638,20 @@ const OpencodeWizardSkillsPlugin = async input => {
1396
1638
  };
1397
1639
  const loginPromise = (async () => {
1398
1640
  const loginSignal = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
1399
- const loginStart = await startLoginFlow(loginSignal);
1400
- const browserOpenError = await openBrowser(loginStart.browserUrl);
1401
- loginBootstrap.snapshot = {
1402
- status: 'pending',
1403
- trigger,
1404
- startedAt,
1405
- expiresAt: loginStart.expiresAt,
1406
- browserUrl: loginStart.browserUrl,
1407
- browserOpenError,
1408
- email: null,
1409
- message: browserOpenError ? `Automatic browser open failed. Open ${loginStart.browserUrl} manually.` : `Browser login started for published skill ${trigger}.`
1410
- };
1641
+ let loginStart = null;
1411
1642
  try {
1643
+ loginStart = await startLoginFlow(loginSignal);
1644
+ const browserOpenError = await openBrowser(loginStart.browserUrl);
1645
+ loginBootstrap.snapshot = {
1646
+ status: 'pending',
1647
+ trigger,
1648
+ startedAt,
1649
+ expiresAt: loginStart.expiresAt,
1650
+ browserUrl: loginStart.browserUrl,
1651
+ browserOpenError,
1652
+ email: null,
1653
+ message: browserOpenError ? `Automatic browser open failed. Open ${loginStart.browserUrl} manually.` : `Browser login started for published skill ${trigger}.`
1654
+ };
1412
1655
  const callbackPayload = await loginStart.callbackPromise;
1413
1656
  if (callbackPayload.status === 'error') {
1414
1657
  throw new Error(callbackPayload.message);
@@ -1467,7 +1710,7 @@ const OpencodeWizardSkillsPlugin = async input => {
1467
1710
  };
1468
1711
  throw error;
1469
1712
  } finally {
1470
- await loginStart.closeCallbackServer().catch(() => undefined);
1713
+ await loginStart?.closeCallbackServer().catch(() => undefined);
1471
1714
  loginBootstrap.promise = null;
1472
1715
  }
1473
1716
  })();