@aexol/opencode-wizard 0.1.11 → 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 +6 -1
- package/dist/server.js +358 -38
- package/dist/server.js.map +1 -1
- package/dist/tui.js +4 -3
- package/dist/tui.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
|
@@ -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
|
};
|
|
@@ -226,6 +226,11 @@ export declare const resolvePluginStatusSnapshot: ({ worktree, directory, signal
|
|
|
226
226
|
directory: string;
|
|
227
227
|
signal: AbortSignal;
|
|
228
228
|
}) => Promise<PluginStatusSnapshot>;
|
|
229
|
+
export declare const resolvePluginStatusSnapshotWithAuthBootstrap: ({ worktree, directory, signal, }: {
|
|
230
|
+
worktree: string;
|
|
231
|
+
directory: string;
|
|
232
|
+
signal: AbortSignal;
|
|
233
|
+
}) => Promise<PluginStatusSnapshot>;
|
|
229
234
|
declare const _default: {
|
|
230
235
|
id: string;
|
|
231
236
|
server: OpencodePluginServer;
|
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';
|
|
@@ -43,6 +45,13 @@ const createIdleLoginBootstrapSnapshot = () => ({
|
|
|
43
45
|
email: null,
|
|
44
46
|
message: null
|
|
45
47
|
});
|
|
48
|
+
const STATUS_PATH_LOGIN_RETRY_COOLDOWN_MS = 60_000;
|
|
49
|
+
const statusPathLoginBootstrap = {
|
|
50
|
+
promise: null,
|
|
51
|
+
status: 'idle',
|
|
52
|
+
message: null,
|
|
53
|
+
failedAt: null
|
|
54
|
+
};
|
|
46
55
|
const importOpencodePluginModule = new Function('specifier', 'return import(specifier)');
|
|
47
56
|
export const AVAILABLE_PUBLISHED_SKILL_TOOLS = ['opencode_wizard_published_skills_fetch', 'opencode_wizard_status'];
|
|
48
57
|
export const NATIVE_SKILLS_URL_COMPATIBILITY = {
|
|
@@ -201,7 +210,7 @@ export const resolveConfig = async worktree => {
|
|
|
201
210
|
actionsUrl: `${backendOrigin}/api/opencode-plugin/actions`,
|
|
202
211
|
fallbackWorkspaceSlug: resolveFallbackWorkspaceSlug(worktree),
|
|
203
212
|
rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
|
|
204
|
-
authStatePath:
|
|
213
|
+
authStatePath: GLOBAL_CONFIG_PATH
|
|
205
214
|
};
|
|
206
215
|
};
|
|
207
216
|
const normalizeAbsolutePath = value => path.resolve(value);
|
|
@@ -325,15 +334,45 @@ const isAuthState = value => {
|
|
|
325
334
|
if (!isRecord(value)) return false;
|
|
326
335
|
return value.pluginId === PLUGIN_ID && typeof value.sessionToken === 'string' && isValidIsoDateString(value.expiresAt) && isValidIsoDateString(value.authenticatedAt) && typeof value.userId === 'string' && typeof value.email === 'string';
|
|
327
336
|
};
|
|
328
|
-
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 => {
|
|
329
357
|
const storedAuthState = await readJsonFile(authStateFile);
|
|
330
358
|
if (storedAuthState === null) return null;
|
|
331
359
|
if (isAuthState(storedAuthState)) return storedAuthState;
|
|
332
360
|
await deleteFileIfExists(authStateFile);
|
|
333
361
|
return null;
|
|
334
362
|
};
|
|
335
|
-
const writeAuthState = async (
|
|
336
|
-
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
|
+
});
|
|
337
376
|
};
|
|
338
377
|
const toAuthState = session => ({
|
|
339
378
|
pluginId: PLUGIN_ID,
|
|
@@ -344,16 +383,24 @@ const toAuthState = session => ({
|
|
|
344
383
|
email: session.user.email
|
|
345
384
|
});
|
|
346
385
|
const resolveStoredAuthState = async (worktree, config) => {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
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);
|
|
350
392
|
return null;
|
|
351
393
|
}
|
|
352
|
-
|
|
353
|
-
|
|
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;
|
|
354
400
|
}
|
|
355
|
-
await
|
|
356
|
-
|
|
401
|
+
await writeAuthState(config.authStatePath, legacyAuthState);
|
|
402
|
+
await deleteFileIfExists(legacyAuthStateFile);
|
|
403
|
+
return legacyAuthState;
|
|
357
404
|
};
|
|
358
405
|
export const buildSkillMarkdown = item => {
|
|
359
406
|
const artifactBody = item.publishedArtifact.markdownBody.trim();
|
|
@@ -480,6 +527,10 @@ export const toPublishedSkillCatalog = payload => ({
|
|
|
480
527
|
facets: getPublishedSkillFacets(payload.skills),
|
|
481
528
|
skills: payload.skills.map(toPublishedSkillSummary)
|
|
482
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
|
+
};
|
|
483
534
|
const normalizeSkillIdentifier = value => value.trim().toLowerCase();
|
|
484
535
|
const parseSkillIdentifiers = value => {
|
|
485
536
|
const seen = new Set();
|
|
@@ -561,7 +612,7 @@ const buildSystemNote = (result, config, details) => {
|
|
|
561
612
|
const projectSkills = catalog.skills.filter(skill => skill.contextKind === 'project').slice(0, 5).map(buildSkillCatalogLine);
|
|
562
613
|
const detailLines = details.slice(0, SYSTEM_NOTE_DETAIL_LIMIT).map(buildSkillDetailSnippetLine);
|
|
563
614
|
const detailBlock = detailLines.length > 0 ? ` Loaded body snippets (capped):\n${truncateText(detailLines.join('\n'), SYSTEM_NOTE_DETAIL_CHAR_LIMIT)}` : '';
|
|
564
|
-
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(' ');
|
|
565
616
|
};
|
|
566
617
|
const toWorkspaceResolutionOutput = resolution => ({
|
|
567
618
|
requestedDirectory: resolution.requestedDirectory,
|
|
@@ -619,7 +670,8 @@ const formatStatusOutput = async (worktree, config, publishedSkillsResult, login
|
|
|
619
670
|
}
|
|
620
671
|
return JSON.stringify({
|
|
621
672
|
...base,
|
|
622
|
-
...toPublishedSkillCatalog(publishedSkillsResult.fetchResult.payload)
|
|
673
|
+
...toPublishedSkillCatalog(publishedSkillsResult.fetchResult.payload),
|
|
674
|
+
message: getWorkspaceUnavailableMessage(publishedSkillsResult.fetchResult.payload)
|
|
623
675
|
}, null, 2);
|
|
624
676
|
};
|
|
625
677
|
export const toPluginAuthStateSummary = authState => {
|
|
@@ -668,10 +720,80 @@ export const resolvePluginStatusSnapshot = async ({
|
|
|
668
720
|
fetchedAt: fetchResult.fetchedAt,
|
|
669
721
|
source: fetchResult.source,
|
|
670
722
|
availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
|
|
671
|
-
message: fetchResult.ok ?
|
|
723
|
+
message: fetchResult.ok ? getWorkspaceUnavailableMessage(fetchResult.payload) : fetchResult.message,
|
|
672
724
|
catalog: fetchResult.ok ? toPublishedSkillCatalog(fetchResult.payload) : null
|
|
673
725
|
};
|
|
674
726
|
};
|
|
727
|
+
const withStatusMessage = (snapshot, message) => ({
|
|
728
|
+
...snapshot,
|
|
729
|
+
message
|
|
730
|
+
});
|
|
731
|
+
const startStatusPathLoginBootstrap = (worktree, config) => {
|
|
732
|
+
if (statusPathLoginBootstrap.promise) return;
|
|
733
|
+
if (statusPathLoginBootstrap.status === 'failed' && statusPathLoginBootstrap.failedAt && Date.now() - statusPathLoginBootstrap.failedAt < STATUS_PATH_LOGIN_RETRY_COOLDOWN_MS) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
statusPathLoginBootstrap.status = 'pending';
|
|
737
|
+
statusPathLoginBootstrap.message = 'Browser login started automatically from the TUI/status path.';
|
|
738
|
+
statusPathLoginBootstrap.failedAt = null;
|
|
739
|
+
statusPathLoginBootstrap.promise = (async () => {
|
|
740
|
+
const loginSignal = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
|
|
741
|
+
const loginStart = await startLoginFlow(loginSignal);
|
|
742
|
+
const browserOpenError = await openBrowser(loginStart.browserUrl);
|
|
743
|
+
if (browserOpenError) {
|
|
744
|
+
statusPathLoginBootstrap.message = `Automatic browser open failed. Open ${loginStart.browserUrl} manually.`;
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
const callbackPayload = await loginStart.callbackPromise;
|
|
748
|
+
if (callbackPayload.status === 'error') {
|
|
749
|
+
throw new Error(callbackPayload.message);
|
|
750
|
+
}
|
|
751
|
+
if (callbackPayload.state !== loginStart.expectedState) {
|
|
752
|
+
throw new Error('OAuth callback state did not match the original login request.');
|
|
753
|
+
}
|
|
754
|
+
const pluginSession = await createPluginSession({
|
|
755
|
+
code: callbackPayload.code,
|
|
756
|
+
codeVerifier: loginStart.codeVerifier,
|
|
757
|
+
redirectUri: OIDC_CALLBACK_URL,
|
|
758
|
+
config,
|
|
759
|
+
signal: loginSignal
|
|
760
|
+
});
|
|
761
|
+
const authState = toAuthState(pluginSession);
|
|
762
|
+
await writeAuthState(config.authStatePath, authState);
|
|
763
|
+
statusPathLoginBootstrap.status = 'authenticated';
|
|
764
|
+
statusPathLoginBootstrap.message = `Browser login completed successfully for ${authState.email}.`;
|
|
765
|
+
return authState;
|
|
766
|
+
} finally {
|
|
767
|
+
await loginStart.closeCallbackServer().catch(() => undefined);
|
|
768
|
+
}
|
|
769
|
+
})().catch(error => {
|
|
770
|
+
statusPathLoginBootstrap.status = 'failed';
|
|
771
|
+
statusPathLoginBootstrap.failedAt = Date.now();
|
|
772
|
+
statusPathLoginBootstrap.message = error instanceof Error ? error.message : 'Browser login failed.';
|
|
773
|
+
throw error;
|
|
774
|
+
}).finally(() => {
|
|
775
|
+
statusPathLoginBootstrap.promise = null;
|
|
776
|
+
});
|
|
777
|
+
statusPathLoginBootstrap.promise.catch(() => undefined);
|
|
778
|
+
};
|
|
779
|
+
export const resolvePluginStatusSnapshotWithAuthBootstrap = async ({
|
|
780
|
+
worktree,
|
|
781
|
+
directory,
|
|
782
|
+
signal
|
|
783
|
+
}) => {
|
|
784
|
+
const snapshot = await resolvePluginStatusSnapshot({
|
|
785
|
+
worktree,
|
|
786
|
+
directory,
|
|
787
|
+
signal
|
|
788
|
+
});
|
|
789
|
+
if (snapshot.status !== 'missing_auth') return snapshot;
|
|
790
|
+
const config = await resolveConfig(worktree);
|
|
791
|
+
startStatusPathLoginBootstrap(worktree, config);
|
|
792
|
+
if (statusPathLoginBootstrap.message) {
|
|
793
|
+
return withStatusMessage(snapshot, statusPathLoginBootstrap.message);
|
|
794
|
+
}
|
|
795
|
+
return withStatusMessage(snapshot, 'Browser login is pending from the TUI/status path.');
|
|
796
|
+
};
|
|
675
797
|
const toPluginStatusMetadata = snapshot => ({
|
|
676
798
|
backendOrigin: snapshot.backendOrigin,
|
|
677
799
|
graphqlUrl: snapshot.graphqlUrl,
|
|
@@ -714,12 +836,204 @@ const fetchOidcDiscoveryDocument = async signal => {
|
|
|
714
836
|
}
|
|
715
837
|
return await response.json();
|
|
716
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
|
+
};
|
|
717
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"/>';
|
|
718
875
|
response.writeHead(statusCode, {
|
|
719
876
|
'content-type': 'text/html; charset=utf-8',
|
|
720
877
|
'cache-control': 'no-store'
|
|
721
878
|
});
|
|
722
|
-
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>`);
|
|
723
1037
|
};
|
|
724
1038
|
const startLocalCallbackServer = async ({
|
|
725
1039
|
expectedState,
|
|
@@ -784,9 +1098,6 @@ const startLocalCallbackServer = async ({
|
|
|
784
1098
|
state
|
|
785
1099
|
});
|
|
786
1100
|
});
|
|
787
|
-
server.on('error', error => {
|
|
788
|
-
fail(error instanceof Error ? error : new Error('Failed to start local OAuth callback server.'));
|
|
789
|
-
});
|
|
790
1101
|
const close = async () => {
|
|
791
1102
|
await new Promise((resolve, reject) => {
|
|
792
1103
|
server.close(error => {
|
|
@@ -799,8 +1110,17 @@ const startLocalCallbackServer = async ({
|
|
|
799
1110
|
});
|
|
800
1111
|
};
|
|
801
1112
|
await new Promise((resolve, reject) => {
|
|
802
|
-
|
|
803
|
-
|
|
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
|
+
});
|
|
804
1124
|
});
|
|
805
1125
|
signal.addEventListener('abort', () => {
|
|
806
1126
|
fail(signal.reason instanceof Error ? signal.reason : new Error('OAuth login aborted.'));
|
|
@@ -864,7 +1184,7 @@ const fetchPublishedSkillsGraphQl = async ({
|
|
|
864
1184
|
};
|
|
865
1185
|
}
|
|
866
1186
|
if (response.status === 401 || response.status === 403) {
|
|
867
|
-
await
|
|
1187
|
+
await clearAuthState(config.authStatePath);
|
|
868
1188
|
onAuthStateChanged?.();
|
|
869
1189
|
return {
|
|
870
1190
|
ok: false,
|
|
@@ -910,7 +1230,7 @@ const fetchPublishedSkillsGraphQl = async ({
|
|
|
910
1230
|
if (body.errors?.length) {
|
|
911
1231
|
const message = body.errors.map(error => error.message).join('; ');
|
|
912
1232
|
if (body.errors.some(error => isUnauthorizedGraphQlMessage(error.message))) {
|
|
913
|
-
await
|
|
1233
|
+
await clearAuthState(config.authStatePath);
|
|
914
1234
|
onAuthStateChanged?.();
|
|
915
1235
|
return {
|
|
916
1236
|
ok: false,
|
|
@@ -1199,7 +1519,6 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1199
1519
|
const catalogInflight = new Map();
|
|
1200
1520
|
const detailCache = new Map();
|
|
1201
1521
|
const detailInflight = new Map();
|
|
1202
|
-
const authStateFile = path.resolve(input.worktree, config.authStatePath);
|
|
1203
1522
|
const initialAuthState = await resolveStoredAuthState(input.worktree, config);
|
|
1204
1523
|
const loginBootstrap = {
|
|
1205
1524
|
promise: null,
|
|
@@ -1298,7 +1617,7 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1298
1617
|
};
|
|
1299
1618
|
const persistAuthState = async session => {
|
|
1300
1619
|
const authState = toAuthState(session);
|
|
1301
|
-
await writeAuthState(
|
|
1620
|
+
await writeAuthState(config.authStatePath, authState);
|
|
1302
1621
|
clearPublishedSkillState();
|
|
1303
1622
|
return authState;
|
|
1304
1623
|
};
|
|
@@ -1319,19 +1638,20 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1319
1638
|
};
|
|
1320
1639
|
const loginPromise = (async () => {
|
|
1321
1640
|
const loginSignal = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
|
|
1322
|
-
|
|
1323
|
-
const browserOpenError = await openBrowser(loginStart.browserUrl);
|
|
1324
|
-
loginBootstrap.snapshot = {
|
|
1325
|
-
status: 'pending',
|
|
1326
|
-
trigger,
|
|
1327
|
-
startedAt,
|
|
1328
|
-
expiresAt: loginStart.expiresAt,
|
|
1329
|
-
browserUrl: loginStart.browserUrl,
|
|
1330
|
-
browserOpenError,
|
|
1331
|
-
email: null,
|
|
1332
|
-
message: browserOpenError ? `Automatic browser open failed. Open ${loginStart.browserUrl} manually.` : `Browser login started for published skill ${trigger}.`
|
|
1333
|
-
};
|
|
1641
|
+
let loginStart = null;
|
|
1334
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
|
+
};
|
|
1335
1655
|
const callbackPayload = await loginStart.callbackPromise;
|
|
1336
1656
|
if (callbackPayload.status === 'error') {
|
|
1337
1657
|
throw new Error(callbackPayload.message);
|
|
@@ -1390,7 +1710,7 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1390
1710
|
};
|
|
1391
1711
|
throw error;
|
|
1392
1712
|
} finally {
|
|
1393
|
-
await loginStart
|
|
1713
|
+
await loginStart?.closeCallbackServer().catch(() => undefined);
|
|
1394
1714
|
loginBootstrap.promise = null;
|
|
1395
1715
|
}
|
|
1396
1716
|
})();
|