cli-jaw 2.2.17 → 2.2.18
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 +4 -0
- package/dist/bin/cli-jaw.js +9 -1
- package/dist/bin/cli-jaw.js.map +1 -1
- package/dist/bin/commands/ask.js +245 -0
- package/dist/bin/commands/ask.js.map +1 -0
- package/dist/bin/commands/chat.js +41 -3
- package/dist/bin/commands/chat.js.map +1 -1
- package/dist/bin/commands/dispatch.js +210 -167
- package/dist/bin/commands/dispatch.js.map +1 -1
- package/dist/bin/commands/init.js +33 -4
- package/dist/bin/commands/init.js.map +1 -1
- package/dist/bin/commands/slack.js +73 -3
- package/dist/bin/commands/slack.js.map +1 -1
- package/dist/bin/commands/tui/raw-pipe-mode.js +90 -0
- package/dist/bin/commands/tui/raw-pipe-mode.js.map +1 -0
- package/dist/bin/commands/tui/simple-mode.js +11 -2
- package/dist/bin/commands/tui/simple-mode.js.map +1 -1
- package/dist/bin/postinstall.js +54 -30
- package/dist/bin/postinstall.js.map +1 -1
- package/dist/server.js +10 -0
- package/dist/server.js.map +1 -1
- package/dist/src/agent/pi-runtime.js +10 -2
- package/dist/src/agent/pi-runtime.js.map +1 -1
- package/dist/src/agent/spawn/queue.js +21 -0
- package/dist/src/agent/spawn/queue.js.map +1 -1
- package/dist/src/agent/spawn.js +19 -6
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/cli/cli-status.js +65 -5
- package/dist/src/cli/cli-status.js.map +1 -1
- package/dist/src/cli/handlers.js +9 -1
- package/dist/src/cli/handlers.js.map +1 -1
- package/dist/src/cli/tui/jawcode-render.js +18 -5
- package/dist/src/cli/tui/jawcode-render.js.map +1 -1
- package/dist/src/core/config.js +85 -0
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/exec-name.js +55 -0
- package/dist/src/core/exec-name.js.map +1 -0
- package/dist/src/core/runtime-settings.js +9 -1
- package/dist/src/core/runtime-settings.js.map +1 -1
- package/dist/src/core/settings-watch.js +10 -1
- package/dist/src/core/settings-watch.js.map +1 -1
- package/dist/src/lib/tui/bun-shim.mjs +75 -0
- package/dist/src/memory/bootstrap.js +8 -1
- package/dist/src/memory/bootstrap.js.map +1 -1
- package/dist/src/messaging/channel-validate.js +11 -1
- package/dist/src/messaging/channel-validate.js.map +1 -1
- package/dist/src/orchestrator/gateway.js +23 -2
- package/dist/src/orchestrator/gateway.js.map +1 -1
- package/dist/src/orchestrator/pipeline.js +6 -0
- package/dist/src/orchestrator/pipeline.js.map +1 -1
- package/dist/src/orchestrator/request-registry.js +116 -0
- package/dist/src/orchestrator/request-registry.js.map +1 -0
- package/dist/src/prompt/templates/a1-system.md +5 -5
- package/dist/src/routes/messaging.js +94 -1
- package/dist/src/routes/messaging.js.map +1 -1
- package/dist/src/routes/settings.js +61 -4
- package/dist/src/routes/settings.js.map +1 -1
- package/dist/src/routes/system.js +23 -3
- package/dist/src/routes/system.js.map +1 -1
- package/dist/src/slack/bot.js +36 -1
- package/dist/src/slack/bot.js.map +1 -1
- package/dist/src/slack/commands.js +29 -1
- package/dist/src/slack/commands.js.map +1 -1
- package/dist/src/slack/events.js +4 -1
- package/dist/src/slack/events.js.map +1 -1
- package/dist/src/slack/history.js +10 -3
- package/dist/src/slack/history.js.map +1 -1
- package/dist/src/slack/identity.js +553 -0
- package/dist/src/slack/identity.js.map +1 -0
- package/dist/src/slack/ingress.js +42 -0
- package/dist/src/slack/ingress.js.map +1 -1
- package/dist/src/slack/manifest.js +95 -71
- package/dist/src/slack/manifest.js.map +1 -1
- package/dist/src/slack/roster.js +180 -0
- package/dist/src/slack/roster.js.map +1 -0
- package/package.json +4 -3
- package/public/css/sidebar.css +29 -51
- package/public/dist/assets/AdvancedExport-c41zCkwB.js +1 -0
- package/public/dist/assets/{Agent-CNNQ7KkJ.js → Agent-nMPZsMfN.js} +1 -1
- package/public/dist/assets/ChannelsSlack-CVNKtoo4.js +1 -0
- package/public/dist/assets/{app-Dsgp4xpU.js → app-DqSJmDwP.js} +4 -4
- package/public/dist/assets/{app-DjgGeXhJ.css → app-DuyLYRC6.css} +1 -1
- package/public/dist/assets/bgtask-badge-CjWz0F6U.js +1 -0
- package/public/dist/assets/{bgtask-badge-CpavV0Jc.js → bgtask-badge-D6XYRFI5.js} +1 -1
- package/public/dist/assets/{employees-BJyEgim7.js → employees-B_wKPfTO.js} +1 -1
- package/public/dist/assets/{iframe-renderer-B8k3AvTv.js → iframe-renderer-D1-zDg8V.js} +2 -2
- package/public/dist/assets/iframe-renderer-qRVLHPnT.js +1 -0
- package/public/dist/assets/{manager-kXYQyMhR.js → manager-Bjuh1jpq.js} +1 -1
- package/public/dist/assets/{memory-D2RbBVO-.js → memory-CNUpXWEC.js} +1 -1
- package/public/dist/assets/memory-CcWGiPy_.js +1 -0
- package/public/dist/assets/message-history-BamMPUnL.js +1 -0
- package/public/dist/assets/{message-history-Bw2E_b8L.js → message-history-DB1rynP-.js} +1 -1
- package/public/dist/assets/{render-BkGcpH7z.js → render-DXwQzjsJ.js} +1 -1
- package/public/dist/assets/{settings-B1FPORO2.js → settings-CXndjCad.js} +14 -14
- package/public/dist/assets/settings-RGpifFHy.js +1 -0
- package/public/dist/assets/settings-core-X_5UHfr3.js +1 -0
- package/public/dist/assets/settings-core-t5yYVRrv.js +162 -0
- package/public/dist/assets/{sidebar-DW78DANx.js → sidebar-DgIj-LuT.js} +3 -3
- package/public/dist/assets/skills-CbwIimit.js +1 -0
- package/public/dist/assets/{skills-lWi0ZFiV.js → skills-QqxjqDHg.js} +1 -1
- package/public/dist/assets/{slash-commands-DYOI6nKj.js → slash-commands-B2V_jngP.js} +1 -1
- package/public/dist/assets/slash-commands-tCNRlNTC.js +1 -0
- package/public/dist/assets/{trace-drawer-DBX9xzmt.js → trace-drawer-CBe9nRjS.js} +1 -1
- package/public/dist/assets/{ui-Bx7eqnXt.js → ui-CDozpEQt.js} +1 -1
- package/public/dist/assets/ui-o-VpvrSv.js +1 -0
- package/public/dist/index.html +15 -34
- package/public/dist/manager/index.html +1 -1
- package/public/index.html +13 -32
- package/public/js/features/channel-onboarding-flow.ts +25 -2
- package/public/js/features/channel-onboarding.ts +111 -8
- package/public/js/features/settings-cli-status.ts +18 -4
- package/public/js/features/settings-slack.ts +127 -29
- package/public/js/features/settings-types.ts +11 -2
- package/public/locales/en.json +17 -8
- package/public/locales/ja.json +17 -8
- package/public/locales/ko.json +17 -8
- package/public/locales/zh.json +17 -8
- package/public/manager/src/settings/pages/AdvancedExport.tsx +1 -0
- package/public/manager/src/settings/pages/Agent.tsx +10 -1
- package/public/manager/src/settings/pages/ChannelsSlack.tsx +13 -1
- package/scripts/atomic-build.sh +10 -3
- package/scripts/release-preview.sh +16 -1
- package/scripts/verify-dist-assets.sh +53 -0
- package/public/dist/assets/AdvancedExport-DYGcCAUq.js +0 -1
- package/public/dist/assets/ChannelsSlack-ybvupgdK.js +0 -1
- package/public/dist/assets/bgtask-badge-C6h5C-o4.js +0 -1
- package/public/dist/assets/iframe-renderer-BO_GgCTR.js +0 -1
- package/public/dist/assets/memory-amsvg4Ey.js +0 -1
- package/public/dist/assets/message-history-BGHZxvgV.js +0 -1
- package/public/dist/assets/settings-CMN-zN4Q.js +0 -1
- package/public/dist/assets/settings-core-B_OjGYVN.js +0 -1
- package/public/dist/assets/settings-core-CxRwqx86.js +0 -144
- package/public/dist/assets/skills-Dz-9Yrxr.js +0 -1
- package/public/dist/assets/slash-commands-xJFCdTTM.js +0 -1
- package/public/dist/assets/ui-JOcrYeG-.js +0 -1
package/public/dist/index.html
CHANGED
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Outfit:wght@400;500;600;700&display=swap"
|
|
32
32
|
rel="stylesheet">
|
|
33
33
|
<!-- Vite handles module bundling in dev (HMR) and production (build) -->
|
|
34
|
-
<script type="module" crossorigin src="/dist/assets/app-
|
|
34
|
+
<script type="module" crossorigin src="/dist/assets/app-DqSJmDwP.js"></script>
|
|
35
35
|
<link rel="stylesheet" crossorigin href="/dist/assets/vendor-render-Bjnw0wQ6.css">
|
|
36
|
-
<link rel="stylesheet" crossorigin href="/dist/assets/app-
|
|
36
|
+
<link rel="stylesheet" crossorigin href="/dist/assets/app-DuyLYRC6.css">
|
|
37
37
|
</head>
|
|
38
38
|
|
|
39
39
|
<body>
|
|
@@ -556,12 +556,15 @@
|
|
|
556
556
|
<h4 class="section-title-row">
|
|
557
557
|
<span><span data-provider="slack"></span> Slack</span>
|
|
558
558
|
<span class="section-title-actions">
|
|
559
|
-
<button class="perm-btn onboard-trigger" type="button" data-onboard-channel="slack"
|
|
559
|
+
<button id="slack-onboarding-trigger" class="perm-btn onboard-trigger" type="button" data-onboard-channel="slack"
|
|
560
560
|
data-i18n="onboarding.open">설정 마법사</button>
|
|
561
|
+
<button id="slack-reset-connection" class="perm-btn onboard-trigger" type="button"
|
|
562
|
+
data-i18n="settings.slack.resetConnection">연결 초기화</button>
|
|
561
563
|
<button class="help-trigger" type="button" data-help-topic="slack"
|
|
562
564
|
data-i18n-aria="help.slack.aria" aria-label="Slack 도움말">?</button>
|
|
563
565
|
</span>
|
|
564
566
|
</h4>
|
|
567
|
+
<div id="slack-environment-managed" class="settings-managed-notice" role="status" style="display:none"></div>
|
|
565
568
|
<div class="settings-row">
|
|
566
569
|
<label data-i18n="label.enabled">활성화</label>
|
|
567
570
|
<div>
|
|
@@ -569,37 +572,15 @@
|
|
|
569
572
|
<button id="slOn" class="perm-btn" data-i18n="btn.on">켬</button>
|
|
570
573
|
</div>
|
|
571
574
|
</div>
|
|
572
|
-
<div class="
|
|
573
|
-
<
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
</div>
|
|
582
|
-
</div>
|
|
583
|
-
</div>
|
|
584
|
-
<div class="slack-setup-step">
|
|
585
|
-
<span class="slack-step-num">2</span>
|
|
586
|
-
<div class="slack-step-body">
|
|
587
|
-
<div class="slack-step-title" data-i18n="settings.slack.guide.step2Title">봇 토큰 붙여넣기</div>
|
|
588
|
-
<div class="slack-step-desc" data-i18n="settings.slack.guide.step2Desc">앱 설정 → OAuth & Permissions → files:read 확인 → Reinstall to Workspace → Bot User OAuth Token 복사</div>
|
|
589
|
-
<input type="password" id="slBotToken" placeholder="xoxb-..." class="input-sm">
|
|
590
|
-
<div class="slack-field-error" id="slack-bot-token-error" style="display:none" data-i18n="settings.slack.guide.botTokenError">xoxb-로 시작하는 봇 토큰이어야 합니다</div>
|
|
591
|
-
</div>
|
|
592
|
-
</div>
|
|
593
|
-
<div class="slack-setup-step">
|
|
594
|
-
<span class="slack-step-num">3</span>
|
|
595
|
-
<div class="slack-step-body">
|
|
596
|
-
<div class="slack-step-title" data-i18n="settings.slack.guide.step3Title">수신용 앱 토큰 (선택)</div>
|
|
597
|
-
<div class="slack-step-desc" data-i18n="settings.slack.guide.step3Location">앱 설정 → Basic Information → App-Level Tokens → Generate Token (connections:write)</div>
|
|
598
|
-
<input type="password" id="slAppToken" placeholder="xapp-... (Socket Mode)" class="input-sm">
|
|
599
|
-
<div class="slack-field-error" id="slack-app-token-error" style="display:none" data-i18n="settings.slack.guide.appTokenError">xapp-로 시작하는 앱 레벨 토큰이어야 합니다</div>
|
|
600
|
-
<div class="slack-step-desc" data-i18n="settings.slack.guide.step3Desc">비워두면 전송 전용(outbound-only)으로 동작합니다</div>
|
|
601
|
-
</div>
|
|
602
|
-
</div>
|
|
575
|
+
<div class="settings-row sub-row">
|
|
576
|
+
<label for="slBotToken" data-i18n="label.botToken">봇 토큰</label>
|
|
577
|
+
<input type="password" id="slBotToken" placeholder="xoxb-..." class="input-sm">
|
|
578
|
+
<div class="slack-field-error" id="slack-bot-token-error" style="display:none" data-i18n="settings.slack.guide.botTokenError">xoxb-로 시작하는 봇 토큰이어야 합니다</div>
|
|
579
|
+
</div>
|
|
580
|
+
<div class="settings-row sub-row">
|
|
581
|
+
<label for="slAppToken" data-i18n="settings.slack.guide.step3Title">수신용 앱 토큰 (선택)</label>
|
|
582
|
+
<input type="password" id="slAppToken" placeholder="xapp-... (Socket Mode)" class="input-sm">
|
|
583
|
+
<div class="slack-field-error" id="slack-app-token-error" style="display:none" data-i18n="settings.slack.guide.appTokenError">xapp-로 시작하는 앱 레벨 토큰이어야 합니다</div>
|
|
603
584
|
</div>
|
|
604
585
|
<details class="slack-advanced">
|
|
605
586
|
<summary data-i18n="settings.slack.advanced">고급 설정</summary>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<link rel="icon" type="image/png" href="/dist/assets/icon-192-CwXPIYF5.png">
|
|
7
7
|
<title>Jaw Manager</title>
|
|
8
8
|
<script src="/manager/theme-boot.js"></script>
|
|
9
|
-
<script type="module" crossorigin src="/dist/assets/manager-
|
|
9
|
+
<script type="module" crossorigin src="/dist/assets/manager-Bjuh1jpq.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/dist/assets/vendor-render-Bjnw0wQ6.css">
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/dist/assets/manager-LkGs6qEJ.css">
|
|
12
12
|
</head>
|
package/public/index.html
CHANGED
|
@@ -566,12 +566,15 @@
|
|
|
566
566
|
<h4 class="section-title-row">
|
|
567
567
|
<span><span data-provider="slack"></span> Slack</span>
|
|
568
568
|
<span class="section-title-actions">
|
|
569
|
-
<button class="perm-btn onboard-trigger" type="button" data-onboard-channel="slack"
|
|
569
|
+
<button id="slack-onboarding-trigger" class="perm-btn onboard-trigger" type="button" data-onboard-channel="slack"
|
|
570
570
|
data-i18n="onboarding.open">설정 마법사</button>
|
|
571
|
+
<button id="slack-reset-connection" class="perm-btn onboard-trigger" type="button"
|
|
572
|
+
data-i18n="settings.slack.resetConnection">연결 초기화</button>
|
|
571
573
|
<button class="help-trigger" type="button" data-help-topic="slack"
|
|
572
574
|
data-i18n-aria="help.slack.aria" aria-label="Slack 도움말">?</button>
|
|
573
575
|
</span>
|
|
574
576
|
</h4>
|
|
577
|
+
<div id="slack-environment-managed" class="settings-managed-notice" role="status" style="display:none"></div>
|
|
575
578
|
<div class="settings-row">
|
|
576
579
|
<label data-i18n="label.enabled">활성화</label>
|
|
577
580
|
<div>
|
|
@@ -579,37 +582,15 @@
|
|
|
579
582
|
<button id="slOn" class="perm-btn" data-i18n="btn.on">켬</button>
|
|
580
583
|
</div>
|
|
581
584
|
</div>
|
|
582
|
-
<div class="
|
|
583
|
-
<
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
</div>
|
|
592
|
-
</div>
|
|
593
|
-
</div>
|
|
594
|
-
<div class="slack-setup-step">
|
|
595
|
-
<span class="slack-step-num">2</span>
|
|
596
|
-
<div class="slack-step-body">
|
|
597
|
-
<div class="slack-step-title" data-i18n="settings.slack.guide.step2Title">봇 토큰 붙여넣기</div>
|
|
598
|
-
<div class="slack-step-desc" data-i18n="settings.slack.guide.step2Desc">앱 설정 → OAuth & Permissions → files:read 확인 → Reinstall to Workspace → Bot User OAuth Token 복사</div>
|
|
599
|
-
<input type="password" id="slBotToken" placeholder="xoxb-..." class="input-sm">
|
|
600
|
-
<div class="slack-field-error" id="slack-bot-token-error" style="display:none" data-i18n="settings.slack.guide.botTokenError">xoxb-로 시작하는 봇 토큰이어야 합니다</div>
|
|
601
|
-
</div>
|
|
602
|
-
</div>
|
|
603
|
-
<div class="slack-setup-step">
|
|
604
|
-
<span class="slack-step-num">3</span>
|
|
605
|
-
<div class="slack-step-body">
|
|
606
|
-
<div class="slack-step-title" data-i18n="settings.slack.guide.step3Title">수신용 앱 토큰 (선택)</div>
|
|
607
|
-
<div class="slack-step-desc" data-i18n="settings.slack.guide.step3Location">앱 설정 → Basic Information → App-Level Tokens → Generate Token (connections:write)</div>
|
|
608
|
-
<input type="password" id="slAppToken" placeholder="xapp-... (Socket Mode)" class="input-sm">
|
|
609
|
-
<div class="slack-field-error" id="slack-app-token-error" style="display:none" data-i18n="settings.slack.guide.appTokenError">xapp-로 시작하는 앱 레벨 토큰이어야 합니다</div>
|
|
610
|
-
<div class="slack-step-desc" data-i18n="settings.slack.guide.step3Desc">비워두면 전송 전용(outbound-only)으로 동작합니다</div>
|
|
611
|
-
</div>
|
|
612
|
-
</div>
|
|
585
|
+
<div class="settings-row sub-row">
|
|
586
|
+
<label for="slBotToken" data-i18n="label.botToken">봇 토큰</label>
|
|
587
|
+
<input type="password" id="slBotToken" placeholder="xoxb-..." class="input-sm">
|
|
588
|
+
<div class="slack-field-error" id="slack-bot-token-error" style="display:none" data-i18n="settings.slack.guide.botTokenError">xoxb-로 시작하는 봇 토큰이어야 합니다</div>
|
|
589
|
+
</div>
|
|
590
|
+
<div class="settings-row sub-row">
|
|
591
|
+
<label for="slAppToken" data-i18n="settings.slack.guide.step3Title">수신용 앱 토큰 (선택)</label>
|
|
592
|
+
<input type="password" id="slAppToken" placeholder="xapp-... (Socket Mode)" class="input-sm">
|
|
593
|
+
<div class="slack-field-error" id="slack-app-token-error" style="display:none" data-i18n="settings.slack.guide.appTokenError">xapp-로 시작하는 앱 레벨 토큰이어야 합니다</div>
|
|
613
594
|
</div>
|
|
614
595
|
<details class="slack-advanced">
|
|
615
596
|
<summary data-i18n="settings.slack.advanced">고급 설정</summary>
|
|
@@ -40,12 +40,13 @@ export const CHANNEL_FIELDS: Record<OnboardChannel, readonly FieldDef[]> = {
|
|
|
40
40
|
export const ISSUER_URLS: Record<OnboardChannel, string> = {
|
|
41
41
|
telegram: 'https://t.me/BotFather',
|
|
42
42
|
discord: 'https://discord.com/developers/applications',
|
|
43
|
-
slack: 'https://api.slack.com/apps',
|
|
43
|
+
slack: 'https://api.slack.com/apps?new_app=1',
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
export const TOTAL_STEPS = 4;
|
|
47
47
|
/** 1 = 안내, 2 = 자격 증명 입력, 3 = 연결 검증, 4 = 저장 완료. */
|
|
48
48
|
export type StepIndex = 1 | 2 | 3 | 4;
|
|
49
|
+
export type SlackSetupStage = 'manifest' | 'issuer' | 'ready';
|
|
49
50
|
|
|
50
51
|
export type Draft = Record<string, string>;
|
|
51
52
|
|
|
@@ -53,6 +54,8 @@ export type FlowState = {
|
|
|
53
54
|
channel: OnboardChannel;
|
|
54
55
|
step: StepIndex;
|
|
55
56
|
draft: Draft;
|
|
57
|
+
/** Step-1 handoff: copy manifest, open Slack, then permit token entry. */
|
|
58
|
+
slackSetupStage: SlackSetupStage;
|
|
56
59
|
/** Set once step 3 passes; cleared whenever a credential changes. */
|
|
57
60
|
validatedIdentity: string | null;
|
|
58
61
|
validatedTeamId: string;
|
|
@@ -70,6 +73,7 @@ export function createFlow(channel: OnboardChannel, draft: Draft = {}): FlowStat
|
|
|
70
73
|
channel,
|
|
71
74
|
step: 1,
|
|
72
75
|
draft: { ...draft },
|
|
76
|
+
slackSetupStage: 'manifest',
|
|
73
77
|
validatedIdentity: null,
|
|
74
78
|
validatedTeamId: '',
|
|
75
79
|
error: null,
|
|
@@ -104,7 +108,11 @@ export function checkCredentials(channel: OnboardChannel, draft: Draft): string
|
|
|
104
108
|
|
|
105
109
|
/** Why the given step cannot be left yet, or null when it may advance. */
|
|
106
110
|
export function blockerForStep(state: FlowState): string | null {
|
|
107
|
-
if (state.step === 1)
|
|
111
|
+
if (state.step === 1) {
|
|
112
|
+
return state.channel === 'slack' && state.slackSetupStage !== 'ready'
|
|
113
|
+
? 'slack_setup_required'
|
|
114
|
+
: null;
|
|
115
|
+
}
|
|
108
116
|
if (state.step === 2) return checkCredentials(state.channel, state.draft);
|
|
109
117
|
if (state.step === 3) return state.validatedIdentity ? null : 'validation_required';
|
|
110
118
|
return null;
|
|
@@ -128,6 +136,21 @@ export function goBack(state: FlowState): FlowState {
|
|
|
128
136
|
return { ...state, step: (state.step - 1) as StepIndex, error: null };
|
|
129
137
|
}
|
|
130
138
|
|
|
139
|
+
export function markSlackManifestGenerated(state: FlowState): FlowState {
|
|
140
|
+
if (state.channel !== 'slack') return state;
|
|
141
|
+
return { ...state, slackSetupStage: 'issuer', error: null };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function markSlackIssuerOpened(state: FlowState): FlowState {
|
|
145
|
+
if (state.channel !== 'slack' || state.slackSetupStage === 'manifest') return state;
|
|
146
|
+
return { ...state, slackSetupStage: 'ready', error: null };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function resetSlackSetup(state: FlowState): FlowState {
|
|
150
|
+
if (state.channel !== 'slack') return state;
|
|
151
|
+
return { ...state, slackSetupStage: 'manifest', error: null };
|
|
152
|
+
}
|
|
153
|
+
|
|
131
154
|
/**
|
|
132
155
|
* Record a credential edit. Any change invalidates a previous validation:
|
|
133
156
|
* otherwise a user could validate a good token, paste a bad one, and still
|
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
createFlow,
|
|
18
18
|
fieldsFor,
|
|
19
19
|
goBack,
|
|
20
|
+
markSlackIssuerOpened,
|
|
21
|
+
markSlackManifestGenerated,
|
|
20
22
|
markSaved,
|
|
23
|
+
resetSlackSetup,
|
|
21
24
|
setField,
|
|
22
25
|
settingsPatch,
|
|
23
26
|
validationPayload,
|
|
@@ -25,6 +28,7 @@ import {
|
|
|
25
28
|
type OnboardChannel,
|
|
26
29
|
} from './channel-onboarding-flow.js';
|
|
27
30
|
import { maybeRequestNotificationPermission } from './notifications.js';
|
|
31
|
+
import { copyText } from './copy-text.js';
|
|
28
32
|
|
|
29
33
|
export type { OnboardChannel };
|
|
30
34
|
|
|
@@ -32,6 +36,7 @@ let overlay: HTMLDivElement | null = null;
|
|
|
32
36
|
let flow: FlowState | null = null;
|
|
33
37
|
let validating = false;
|
|
34
38
|
let saving = false;
|
|
39
|
+
let slackAppName = 'cli-jaw';
|
|
35
40
|
/**
|
|
36
41
|
* Bumped whenever the wizard is (re)opened. An in-flight validation compares
|
|
37
42
|
* it on return: switching to another channel — or reopening the same one —
|
|
@@ -85,6 +90,7 @@ export function openChannelOnboarding(channel: OnboardChannel): void {
|
|
|
85
90
|
draft[field.key] = readSettingsInput(field.settingsId);
|
|
86
91
|
}
|
|
87
92
|
flow = createFlow(channel, draft);
|
|
93
|
+
if (channel === 'slack') slackAppName = 'cli-jaw';
|
|
88
94
|
validating = false;
|
|
89
95
|
saving = false;
|
|
90
96
|
flowGeneration += 1;
|
|
@@ -117,11 +123,30 @@ function ensureOverlay(): void {
|
|
|
117
123
|
|
|
118
124
|
function stepBody(state: FlowState): string {
|
|
119
125
|
if (state.step === 1) {
|
|
126
|
+
const slackStage = state.slackSetupStage;
|
|
127
|
+
const slackManifestBuilder = state.channel === 'slack' ? `
|
|
128
|
+
<div class="onboarding-field">
|
|
129
|
+
<label for="onboard-slack-app-name">${escapeHtml(t('onboarding.slackAppName'))}</label>
|
|
130
|
+
<input id="onboard-slack-app-name" data-onboard-app-name="1" class="input-sm"
|
|
131
|
+
type="text" maxlength="35" pattern="[a-z0-9._-]+" autocomplete="off"
|
|
132
|
+
autocapitalize="none" spellcheck="false"
|
|
133
|
+
aria-describedby="onboard-slack-app-name-hint onboard-slack-manifest-status"
|
|
134
|
+
value="${escapeHtml(slackAppName)}">
|
|
135
|
+
<span class="onboarding-hint" id="onboard-slack-app-name-hint">${escapeHtml(t('onboarding.slackAppNameHint'))}</span>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="onboarding-actions">
|
|
138
|
+
<button type="button" class="perm-btn ${slackStage === 'manifest' ? 'active' : ''}" data-onboard-generate-manifest="1">
|
|
139
|
+
${escapeHtml(t('onboarding.slackGenerateManifest'))}
|
|
140
|
+
</button>
|
|
141
|
+
<span class="onboarding-manifest-status" id="onboard-slack-manifest-status"
|
|
142
|
+
data-onboard-manifest-status="1" role="status" aria-live="polite"></span>
|
|
143
|
+
</div>` : '';
|
|
120
144
|
return `
|
|
145
|
+
${slackManifestBuilder}
|
|
121
146
|
<p class="onboarding-guide">${escapeHtml(t(`onboarding.guide.${state.channel}`))}</p>
|
|
122
|
-
${state.channel === 'slack' ? `<p class="onboarding-guide">${escapeHtml(t('onboarding.slackExistingApp'))}</p>` : ''}
|
|
123
147
|
<div class="onboarding-actions">
|
|
124
|
-
<button type="button" class="perm-btn
|
|
148
|
+
<button type="button" class="perm-btn ${state.channel === 'slack' && slackStage === 'issuer' ? 'active' : ''}"
|
|
149
|
+
data-onboard-issuer="1" ${state.channel === 'slack' && slackStage === 'manifest' ? 'disabled' : ''}>
|
|
125
150
|
${escapeHtml(t('onboarding.openIssuer'))}
|
|
126
151
|
</button>
|
|
127
152
|
</div>`;
|
|
@@ -140,9 +165,7 @@ function stepBody(state: FlowState): string {
|
|
|
140
165
|
<span class="onboarding-hint">${escapeHtml(t(`onboarding.hint.${state.channel}.${field.key}`))}</span>
|
|
141
166
|
</div>`;
|
|
142
167
|
}).join('');
|
|
143
|
-
return
|
|
144
|
-
? `<p class="onboarding-guide">${escapeHtml(t('onboarding.slackExistingApp'))}</p>`
|
|
145
|
-
: ''}`;
|
|
168
|
+
return fields;
|
|
146
169
|
}
|
|
147
170
|
if (state.step === 3) {
|
|
148
171
|
const status = validating
|
|
@@ -171,7 +194,8 @@ function footer(state: FlowState): string {
|
|
|
171
194
|
: '';
|
|
172
195
|
const primary = state.step === TOTAL_STEPS
|
|
173
196
|
? `<button type="button" class="perm-btn active" data-onboard-save="1">${escapeHtml(t('onboarding.save'))}</button>`
|
|
174
|
-
: `<button type="button" class="perm-btn ${canAdvance(state) ? 'active' : ''}" data-onboard-next="1"
|
|
197
|
+
: `<button type="button" class="perm-btn ${canAdvance(state) ? 'active' : ''}" data-onboard-next="1"
|
|
198
|
+
${state.channel === 'slack' && state.step === 1 && !canAdvance(state) ? 'disabled' : ''}>${escapeHtml(t('onboarding.next'))}</button>`;
|
|
175
199
|
return `${back}${primary}`;
|
|
176
200
|
}
|
|
177
201
|
|
|
@@ -210,7 +234,29 @@ function render(): void {
|
|
|
210
234
|
|
|
211
235
|
overlay.querySelectorAll('[data-onboard-close]').forEach(el => el.addEventListener('click', close));
|
|
212
236
|
overlay.querySelector('[data-onboard-issuer]')?.addEventListener('click', () => {
|
|
213
|
-
if (flow)
|
|
237
|
+
if (!flow) return;
|
|
238
|
+
window.open(ISSUER_URLS[flow.channel], '_blank', 'noopener');
|
|
239
|
+
if (flow.channel === 'slack') {
|
|
240
|
+
flow = markSlackIssuerOpened(flow);
|
|
241
|
+
syncSlackStepActions();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
overlay.querySelector('[data-onboard-generate-manifest]')?.addEventListener('click', () => {
|
|
245
|
+
void runSlackManifestGeneration();
|
|
246
|
+
});
|
|
247
|
+
overlay.querySelector('[data-onboard-app-name]')?.addEventListener('input', (ev) => {
|
|
248
|
+
slackAppName = (ev.currentTarget as HTMLInputElement).value;
|
|
249
|
+
if (flow?.channel === 'slack') flow = resetSlackSetup(flow);
|
|
250
|
+
const status = overlay?.querySelector<HTMLElement>('[data-onboard-manifest-status]');
|
|
251
|
+
status?.classList.remove('is-error');
|
|
252
|
+
if (status) status.textContent = '';
|
|
253
|
+
syncSlackStepActions();
|
|
254
|
+
});
|
|
255
|
+
overlay.querySelector('[data-onboard-app-name]')?.addEventListener('keydown', (ev) => {
|
|
256
|
+
const keyEvent = ev as KeyboardEvent;
|
|
257
|
+
if (keyEvent.key !== 'Enter' || keyEvent.shiftKey || keyEvent.isComposing) return;
|
|
258
|
+
keyEvent.preventDefault();
|
|
259
|
+
void runSlackManifestGeneration();
|
|
214
260
|
});
|
|
215
261
|
overlay.querySelector('[data-onboard-back]')?.addEventListener('click', () => {
|
|
216
262
|
if (!flow) return;
|
|
@@ -246,6 +292,20 @@ function render(): void {
|
|
|
246
292
|
focusFirstEmptyField();
|
|
247
293
|
}
|
|
248
294
|
|
|
295
|
+
function syncSlackStepActions(): void {
|
|
296
|
+
if (!overlay || flow?.channel !== 'slack' || flow.step !== 1) return;
|
|
297
|
+
const stage = flow.slackSetupStage;
|
|
298
|
+
const manifest = overlay.querySelector<HTMLButtonElement>('[data-onboard-generate-manifest]');
|
|
299
|
+
const issuer = overlay.querySelector<HTMLButtonElement>('[data-onboard-issuer]');
|
|
300
|
+
const next = overlay.querySelector<HTMLButtonElement>('[data-onboard-next]');
|
|
301
|
+
|
|
302
|
+
manifest?.classList.toggle('active', stage === 'manifest');
|
|
303
|
+
issuer?.classList.toggle('active', stage === 'issuer');
|
|
304
|
+
if (issuer) issuer.disabled = stage === 'manifest';
|
|
305
|
+
next?.classList.toggle('active', stage === 'ready');
|
|
306
|
+
if (next) next.disabled = stage !== 'ready';
|
|
307
|
+
}
|
|
308
|
+
|
|
249
309
|
/** Whatever the current step's primary button does — Enter mirrors it. */
|
|
250
310
|
function primaryAction(): void {
|
|
251
311
|
if (!flow) return;
|
|
@@ -262,7 +322,7 @@ function primaryAction(): void {
|
|
|
262
322
|
* is also what a freshly opened step-2 should focus.
|
|
263
323
|
*/
|
|
264
324
|
function focusFirstEmptyField(): void {
|
|
265
|
-
const inputs = [...(overlay?.querySelectorAll('[data-onboard-field]') ?? [])] as HTMLInputElement[];
|
|
325
|
+
const inputs = [...(overlay?.querySelectorAll('[data-onboard-app-name], [data-onboard-field]') ?? [])] as HTMLInputElement[];
|
|
266
326
|
if (!inputs.length) return;
|
|
267
327
|
const target = inputs.find(input => !input.value.trim()) ?? inputs[0];
|
|
268
328
|
target?.focus();
|
|
@@ -271,6 +331,49 @@ function focusFirstEmptyField(): void {
|
|
|
271
331
|
target?.setSelectionRange?.(end, end);
|
|
272
332
|
}
|
|
273
333
|
|
|
334
|
+
async function runSlackManifestGeneration(): Promise<void> {
|
|
335
|
+
if (!overlay || flow?.channel !== 'slack') return;
|
|
336
|
+
const generation = flowGeneration;
|
|
337
|
+
const input = overlay.querySelector<HTMLInputElement>('[data-onboard-app-name]');
|
|
338
|
+
const button = overlay.querySelector<HTMLButtonElement>('[data-onboard-generate-manifest]');
|
|
339
|
+
const status = overlay.querySelector<HTMLElement>('[data-onboard-manifest-status]');
|
|
340
|
+
if (!input || !button || !status) return;
|
|
341
|
+
|
|
342
|
+
const appName = input.value.trim();
|
|
343
|
+
slackAppName = appName;
|
|
344
|
+
if (!appName || Array.from(appName).length > 35 || !/^[a-z0-9._-]+$/.test(appName)) {
|
|
345
|
+
status.classList.add('is-error');
|
|
346
|
+
status.textContent = t('onboarding.slackAppNameError');
|
|
347
|
+
input.focus();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
status.classList.remove('is-error');
|
|
352
|
+
status.textContent = '';
|
|
353
|
+
button.disabled = true;
|
|
354
|
+
button.textContent = t('onboarding.slackManifestGenerating');
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const data = await api<{ json?: string }>(`/api/slack/manifest?name=${encodeURIComponent(appName)}`);
|
|
358
|
+
if (generation !== flowGeneration || flow?.channel !== 'slack' || slackAppName.trim() !== appName) return;
|
|
359
|
+
const json = data?.json || '';
|
|
360
|
+
if (!json) throw new Error('empty manifest');
|
|
361
|
+
const copied = await copyText(json);
|
|
362
|
+
if (!copied.ok) throw new Error(copied.error || 'copy failed');
|
|
363
|
+
if (generation !== flowGeneration || flow?.channel !== 'slack' || slackAppName.trim() !== appName) return;
|
|
364
|
+
flow = markSlackManifestGenerated(flow);
|
|
365
|
+
status.textContent = t('onboarding.slackManifestReady');
|
|
366
|
+
syncSlackStepActions();
|
|
367
|
+
} catch {
|
|
368
|
+
status.classList.add('is-error');
|
|
369
|
+
status.textContent = t('onboarding.slackManifestError');
|
|
370
|
+
} finally {
|
|
371
|
+
button.disabled = false;
|
|
372
|
+
button.textContent = t('onboarding.slackGenerateManifest');
|
|
373
|
+
syncSlackStepActions();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
274
377
|
function captureInputs(): void {
|
|
275
378
|
if (!flow) return;
|
|
276
379
|
for (const el of overlay?.querySelectorAll('[data-onboard-field]') ?? []) {
|
|
@@ -308,11 +308,16 @@ function renderCliStatus(data: { cliStatus: Record<string, CliStatusInfo> | null
|
|
|
308
308
|
if (SIDEBAR_HIDDEN_CLIS.has(name)) continue;
|
|
309
309
|
const probeDescription = describeCliProbe(info);
|
|
310
310
|
const checking = probeDescription === 'checking';
|
|
311
|
+
const probeFailing = probeDescription === 'probe-failing';
|
|
311
312
|
const capabilityFailed = probeDescription === 'capability-failed';
|
|
312
|
-
const q = checking ? undefined : quota?.[name];
|
|
313
|
+
const q = checking || probeFailing ? undefined : quota?.[name];
|
|
313
314
|
let dotClass: string;
|
|
314
315
|
if (checking) {
|
|
315
316
|
dotClass = 'checking';
|
|
317
|
+
} else if (probeFailing) {
|
|
318
|
+
// A preserved snapshot would otherwise render green while every
|
|
319
|
+
// probe is erroring — the dashboard lie #277 reports.
|
|
320
|
+
dotClass = 'warn';
|
|
316
321
|
} else if (!info.available || capabilityFailed) {
|
|
317
322
|
dotClass = 'missing';
|
|
318
323
|
} else if (!q || q.error) {
|
|
@@ -335,7 +340,10 @@ function renderCliStatus(data: { cliStatus: Record<string, CliStatusInfo> | null
|
|
|
335
340
|
}
|
|
336
341
|
|
|
337
342
|
let authHint = '';
|
|
338
|
-
|
|
343
|
+
// A failing probe means we know nothing current about this runtime, so
|
|
344
|
+
// offering install/auth remediation would send the user to fix the
|
|
345
|
+
// wrong thing entirely (#277).
|
|
346
|
+
if (!checking && !probeFailing && (!info.available || dotClass === 'warn')) {
|
|
339
347
|
const hint = AUTH_HINTS[name];
|
|
340
348
|
if (hint) {
|
|
341
349
|
const isNotInstalled = !info.available;
|
|
@@ -405,7 +413,9 @@ function renderCliStatus(data: { cliStatus: Record<string, CliStatusInfo> | null
|
|
|
405
413
|
|
|
406
414
|
const probeLine = checking
|
|
407
415
|
? '<div role="status" aria-live="polite" style="font-size:10px;color:var(--text-dim);margin:2px 0 0 16px">상태 확인 중</div>'
|
|
408
|
-
:
|
|
416
|
+
: probeFailing
|
|
417
|
+
? `<div role="alert" style="font-size:10px;color:var(--warning);margin:2px 0 0 16px">상태 확인 실패 · 재시도 중${info.probeError ? ` · ${escapeHtml(info.probeError)}` : ''}</div>`
|
|
418
|
+
: capabilityFailed
|
|
409
419
|
? `<div role="status" style="font-size:10px;color:var(--error);margin:2px 0 0 16px">설치됨, app-server 사용 불가${info.reason ? ` · ${escapeHtml(info.reason)}` : ''}</div>`
|
|
410
420
|
: probeDescription === 'stale'
|
|
411
421
|
? '<div role="status" style="font-size:10px;color:var(--text-dim);margin:2px 0 0 16px">이전 상태 표시 중 · 새로 확인 중</div>'
|
|
@@ -429,12 +439,16 @@ function renderCliStatus(data: { cliStatus: Record<string, CliStatusInfo> | null
|
|
|
429
439
|
if (el) el.innerHTML = html;
|
|
430
440
|
|
|
431
441
|
const allEntries = Object.entries(cliStatus);
|
|
442
|
+
// While probes are failing we cannot tell ready from unready, so the
|
|
443
|
+
// "no ready CLI" alarm would be exactly the false alarm #277 reports —
|
|
444
|
+
// the runtime worked the whole time.
|
|
445
|
+
const anyProbeFailing = allEntries.some(([, info]) => describeCliProbe(info) === 'probe-failing');
|
|
432
446
|
const hasReadyCli = allEntries.some(([name, info]) => {
|
|
433
447
|
if (describeCliProbe(info) !== 'ready') return false;
|
|
434
448
|
const q = quota?.[name];
|
|
435
449
|
return !q || q.authenticated !== false;
|
|
436
450
|
});
|
|
437
|
-
if (!hasReadyCli && allEntries.length > 0 && el) {
|
|
451
|
+
if (!hasReadyCli && !anyProbeFailing && allEntries.length > 0 && el) {
|
|
438
452
|
el.insertAdjacentHTML('afterbegin',
|
|
439
453
|
`<div style="padding:8px 10px;margin-bottom:8px;background:var(--warning-dim);border:1px solid var(--warning);border-radius:6px;font-size:11px;color:var(--warning)">
|
|
440
454
|
${ICONS.warning} ${t('cli.noReadyCli')}
|