@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 +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +282 -39
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
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
|
|
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:
|
|
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
|
|
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 (
|
|
343
|
-
await
|
|
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
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
|
363
|
-
|
|
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}
|
|
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 ?
|
|
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(
|
|
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
|
+
'&': '&',
|
|
854
|
+
'<': '<',
|
|
855
|
+
'>': '>',
|
|
856
|
+
"'": ''',
|
|
857
|
+
'"': '"'
|
|
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
|
|
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
|
-
|
|
880
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
1713
|
+
await loginStart?.closeCallbackServer().catch(() => undefined);
|
|
1471
1714
|
loginBootstrap.promise = null;
|
|
1472
1715
|
}
|
|
1473
1716
|
})();
|