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.
Files changed (135) hide show
  1. package/README.md +4 -0
  2. package/dist/bin/cli-jaw.js +9 -1
  3. package/dist/bin/cli-jaw.js.map +1 -1
  4. package/dist/bin/commands/ask.js +245 -0
  5. package/dist/bin/commands/ask.js.map +1 -0
  6. package/dist/bin/commands/chat.js +41 -3
  7. package/dist/bin/commands/chat.js.map +1 -1
  8. package/dist/bin/commands/dispatch.js +210 -167
  9. package/dist/bin/commands/dispatch.js.map +1 -1
  10. package/dist/bin/commands/init.js +33 -4
  11. package/dist/bin/commands/init.js.map +1 -1
  12. package/dist/bin/commands/slack.js +73 -3
  13. package/dist/bin/commands/slack.js.map +1 -1
  14. package/dist/bin/commands/tui/raw-pipe-mode.js +90 -0
  15. package/dist/bin/commands/tui/raw-pipe-mode.js.map +1 -0
  16. package/dist/bin/commands/tui/simple-mode.js +11 -2
  17. package/dist/bin/commands/tui/simple-mode.js.map +1 -1
  18. package/dist/bin/postinstall.js +54 -30
  19. package/dist/bin/postinstall.js.map +1 -1
  20. package/dist/server.js +10 -0
  21. package/dist/server.js.map +1 -1
  22. package/dist/src/agent/pi-runtime.js +10 -2
  23. package/dist/src/agent/pi-runtime.js.map +1 -1
  24. package/dist/src/agent/spawn/queue.js +21 -0
  25. package/dist/src/agent/spawn/queue.js.map +1 -1
  26. package/dist/src/agent/spawn.js +19 -6
  27. package/dist/src/agent/spawn.js.map +1 -1
  28. package/dist/src/cli/cli-status.js +65 -5
  29. package/dist/src/cli/cli-status.js.map +1 -1
  30. package/dist/src/cli/handlers.js +9 -1
  31. package/dist/src/cli/handlers.js.map +1 -1
  32. package/dist/src/cli/tui/jawcode-render.js +18 -5
  33. package/dist/src/cli/tui/jawcode-render.js.map +1 -1
  34. package/dist/src/core/config.js +85 -0
  35. package/dist/src/core/config.js.map +1 -1
  36. package/dist/src/core/exec-name.js +55 -0
  37. package/dist/src/core/exec-name.js.map +1 -0
  38. package/dist/src/core/runtime-settings.js +9 -1
  39. package/dist/src/core/runtime-settings.js.map +1 -1
  40. package/dist/src/core/settings-watch.js +10 -1
  41. package/dist/src/core/settings-watch.js.map +1 -1
  42. package/dist/src/lib/tui/bun-shim.mjs +75 -0
  43. package/dist/src/memory/bootstrap.js +8 -1
  44. package/dist/src/memory/bootstrap.js.map +1 -1
  45. package/dist/src/messaging/channel-validate.js +11 -1
  46. package/dist/src/messaging/channel-validate.js.map +1 -1
  47. package/dist/src/orchestrator/gateway.js +23 -2
  48. package/dist/src/orchestrator/gateway.js.map +1 -1
  49. package/dist/src/orchestrator/pipeline.js +6 -0
  50. package/dist/src/orchestrator/pipeline.js.map +1 -1
  51. package/dist/src/orchestrator/request-registry.js +116 -0
  52. package/dist/src/orchestrator/request-registry.js.map +1 -0
  53. package/dist/src/prompt/templates/a1-system.md +5 -5
  54. package/dist/src/routes/messaging.js +94 -1
  55. package/dist/src/routes/messaging.js.map +1 -1
  56. package/dist/src/routes/settings.js +61 -4
  57. package/dist/src/routes/settings.js.map +1 -1
  58. package/dist/src/routes/system.js +23 -3
  59. package/dist/src/routes/system.js.map +1 -1
  60. package/dist/src/slack/bot.js +36 -1
  61. package/dist/src/slack/bot.js.map +1 -1
  62. package/dist/src/slack/commands.js +29 -1
  63. package/dist/src/slack/commands.js.map +1 -1
  64. package/dist/src/slack/events.js +4 -1
  65. package/dist/src/slack/events.js.map +1 -1
  66. package/dist/src/slack/history.js +10 -3
  67. package/dist/src/slack/history.js.map +1 -1
  68. package/dist/src/slack/identity.js +553 -0
  69. package/dist/src/slack/identity.js.map +1 -0
  70. package/dist/src/slack/ingress.js +42 -0
  71. package/dist/src/slack/ingress.js.map +1 -1
  72. package/dist/src/slack/manifest.js +95 -71
  73. package/dist/src/slack/manifest.js.map +1 -1
  74. package/dist/src/slack/roster.js +180 -0
  75. package/dist/src/slack/roster.js.map +1 -0
  76. package/package.json +4 -3
  77. package/public/css/sidebar.css +29 -51
  78. package/public/dist/assets/AdvancedExport-c41zCkwB.js +1 -0
  79. package/public/dist/assets/{Agent-CNNQ7KkJ.js → Agent-nMPZsMfN.js} +1 -1
  80. package/public/dist/assets/ChannelsSlack-CVNKtoo4.js +1 -0
  81. package/public/dist/assets/{app-Dsgp4xpU.js → app-DqSJmDwP.js} +4 -4
  82. package/public/dist/assets/{app-DjgGeXhJ.css → app-DuyLYRC6.css} +1 -1
  83. package/public/dist/assets/bgtask-badge-CjWz0F6U.js +1 -0
  84. package/public/dist/assets/{bgtask-badge-CpavV0Jc.js → bgtask-badge-D6XYRFI5.js} +1 -1
  85. package/public/dist/assets/{employees-BJyEgim7.js → employees-B_wKPfTO.js} +1 -1
  86. package/public/dist/assets/{iframe-renderer-B8k3AvTv.js → iframe-renderer-D1-zDg8V.js} +2 -2
  87. package/public/dist/assets/iframe-renderer-qRVLHPnT.js +1 -0
  88. package/public/dist/assets/{manager-kXYQyMhR.js → manager-Bjuh1jpq.js} +1 -1
  89. package/public/dist/assets/{memory-D2RbBVO-.js → memory-CNUpXWEC.js} +1 -1
  90. package/public/dist/assets/memory-CcWGiPy_.js +1 -0
  91. package/public/dist/assets/message-history-BamMPUnL.js +1 -0
  92. package/public/dist/assets/{message-history-Bw2E_b8L.js → message-history-DB1rynP-.js} +1 -1
  93. package/public/dist/assets/{render-BkGcpH7z.js → render-DXwQzjsJ.js} +1 -1
  94. package/public/dist/assets/{settings-B1FPORO2.js → settings-CXndjCad.js} +14 -14
  95. package/public/dist/assets/settings-RGpifFHy.js +1 -0
  96. package/public/dist/assets/settings-core-X_5UHfr3.js +1 -0
  97. package/public/dist/assets/settings-core-t5yYVRrv.js +162 -0
  98. package/public/dist/assets/{sidebar-DW78DANx.js → sidebar-DgIj-LuT.js} +3 -3
  99. package/public/dist/assets/skills-CbwIimit.js +1 -0
  100. package/public/dist/assets/{skills-lWi0ZFiV.js → skills-QqxjqDHg.js} +1 -1
  101. package/public/dist/assets/{slash-commands-DYOI6nKj.js → slash-commands-B2V_jngP.js} +1 -1
  102. package/public/dist/assets/slash-commands-tCNRlNTC.js +1 -0
  103. package/public/dist/assets/{trace-drawer-DBX9xzmt.js → trace-drawer-CBe9nRjS.js} +1 -1
  104. package/public/dist/assets/{ui-Bx7eqnXt.js → ui-CDozpEQt.js} +1 -1
  105. package/public/dist/assets/ui-o-VpvrSv.js +1 -0
  106. package/public/dist/index.html +15 -34
  107. package/public/dist/manager/index.html +1 -1
  108. package/public/index.html +13 -32
  109. package/public/js/features/channel-onboarding-flow.ts +25 -2
  110. package/public/js/features/channel-onboarding.ts +111 -8
  111. package/public/js/features/settings-cli-status.ts +18 -4
  112. package/public/js/features/settings-slack.ts +127 -29
  113. package/public/js/features/settings-types.ts +11 -2
  114. package/public/locales/en.json +17 -8
  115. package/public/locales/ja.json +17 -8
  116. package/public/locales/ko.json +17 -8
  117. package/public/locales/zh.json +17 -8
  118. package/public/manager/src/settings/pages/AdvancedExport.tsx +1 -0
  119. package/public/manager/src/settings/pages/Agent.tsx +10 -1
  120. package/public/manager/src/settings/pages/ChannelsSlack.tsx +13 -1
  121. package/scripts/atomic-build.sh +10 -3
  122. package/scripts/release-preview.sh +16 -1
  123. package/scripts/verify-dist-assets.sh +53 -0
  124. package/public/dist/assets/AdvancedExport-DYGcCAUq.js +0 -1
  125. package/public/dist/assets/ChannelsSlack-ybvupgdK.js +0 -1
  126. package/public/dist/assets/bgtask-badge-C6h5C-o4.js +0 -1
  127. package/public/dist/assets/iframe-renderer-BO_GgCTR.js +0 -1
  128. package/public/dist/assets/memory-amsvg4Ey.js +0 -1
  129. package/public/dist/assets/message-history-BGHZxvgV.js +0 -1
  130. package/public/dist/assets/settings-CMN-zN4Q.js +0 -1
  131. package/public/dist/assets/settings-core-B_OjGYVN.js +0 -1
  132. package/public/dist/assets/settings-core-CxRwqx86.js +0 -144
  133. package/public/dist/assets/skills-Dz-9Yrxr.js +0 -1
  134. package/public/dist/assets/slash-commands-xJFCdTTM.js +0 -1
  135. package/public/dist/assets/ui-JOcrYeG-.js +0 -1
@@ -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-Dsgp4xpU.js"></script>
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-DjgGeXhJ.css">
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="slack-setup-card">
573
- <div class="slack-setup-step">
574
- <span class="slack-step-num">1</span>
575
- <div class="slack-step-body">
576
- <div class="slack-step-title" data-i18n="settings.slack.guide.step1Title">Slack 앱 만들기</div>
577
- <div class="slack-step-desc" data-i18n="settings.slack.guide.step1Desc">Create an App → From an app manifest → 워크스페이스 선택 → 붙여넣기 (기존 앱은 files:read 추가)</div>
578
- <div class="slack-step-actions">
579
- <button id="slack-copy-manifest" class="perm-btn" type="button" data-i18n="settings.slack.guide.copyManifest">매니페스트 복사</button>
580
- <button id="slack-open-apps" class="perm-btn" type="button" data-i18n="settings.slack.guide.openApps">앱 페이지 열기</button>
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 &amp; 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-kXYQyMhR.js"></script>
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="slack-setup-card">
583
- <div class="slack-setup-step">
584
- <span class="slack-step-num">1</span>
585
- <div class="slack-step-body">
586
- <div class="slack-step-title" data-i18n="settings.slack.guide.step1Title">Slack 앱 만들기</div>
587
- <div class="slack-step-desc" data-i18n="settings.slack.guide.step1Desc">Create an App → From an app manifest → 워크스페이스 선택 → 붙여넣기 (기존 앱은 files:read 추가)</div>
588
- <div class="slack-step-actions">
589
- <button id="slack-copy-manifest" class="perm-btn" type="button" data-i18n="settings.slack.guide.copyManifest">매니페스트 복사</button>
590
- <button id="slack-open-apps" class="perm-btn" type="button" data-i18n="settings.slack.guide.openApps">앱 페이지 열기</button>
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 &amp; 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) return null;
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" data-onboard-issuer="1">
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 `${fields}${state.channel === 'slack'
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">${escapeHtml(t('onboarding.next'))}</button>`;
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) window.open(ISSUER_URLS[flow.channel], '_blank', 'noopener');
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
- if (!checking && (!info.available || dotClass === 'warn')) {
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
- : capabilityFailed
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')}