@productbrain/cli 0.1.0-beta.71 → 0.1.0-beta.72

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 (80) hide show
  1. package/dist/__tests__/audit.test.js +44 -44
  2. package/dist/__tests__/capture.test.js +37 -37
  3. package/dist/__tests__/constellation.test.js +14 -14
  4. package/dist/__tests__/context-strategy.test.js +8 -8
  5. package/dist/__tests__/fields.test.js +20 -20
  6. package/dist/__tests__/ingest.test.js +28 -28
  7. package/dist/__tests__/orient.test.js +8 -8
  8. package/dist/__tests__/promote.test.js +15 -15
  9. package/dist/__tests__/proposals.test.js +18 -18
  10. package/dist/__tests__/relate.test.js +14 -14
  11. package/dist/__tests__/session-touch.test.js +11 -11
  12. package/dist/__tests__/session.test.js +2 -2
  13. package/dist/__tests__/setup.test.js +7 -7
  14. package/dist/__tests__/update.test.js +21 -21
  15. package/dist/__tests__/workspace.test.js +20 -20
  16. package/dist/commands/accept.js +4 -4
  17. package/dist/commands/admin/cockpit.d.ts +4 -2
  18. package/dist/commands/admin/cockpit.d.ts.map +1 -1
  19. package/dist/commands/admin/cockpit.js +213 -6
  20. package/dist/commands/admin/cockpit.js.map +1 -1
  21. package/dist/commands/admin/index.d.ts.map +1 -1
  22. package/dist/commands/admin/index.js +2 -0
  23. package/dist/commands/admin/index.js.map +1 -1
  24. package/dist/commands/admin/inspect.d.ts +9 -0
  25. package/dist/commands/admin/inspect.d.ts.map +1 -1
  26. package/dist/commands/admin/inspect.js +19 -0
  27. package/dist/commands/admin/inspect.js.map +1 -1
  28. package/dist/commands/admin/inspect.test.js +20 -1
  29. package/dist/commands/admin/inspect.test.js.map +1 -1
  30. package/dist/commands/admin/manage.d.ts +8 -0
  31. package/dist/commands/admin/manage.d.ts.map +1 -0
  32. package/dist/commands/admin/manage.js +76 -0
  33. package/dist/commands/admin/manage.js.map +1 -0
  34. package/dist/commands/audit.js +4 -4
  35. package/dist/commands/brief.js +4 -4
  36. package/dist/commands/capture.js +6 -6
  37. package/dist/commands/chain-walk.js +2 -2
  38. package/dist/commands/changes.js +2 -2
  39. package/dist/commands/codex-prep.js +2 -2
  40. package/dist/commands/collections.js +5 -5
  41. package/dist/commands/constellation.js +2 -2
  42. package/dist/commands/context.js +2 -2
  43. package/dist/commands/cross-cut.js +2 -2
  44. package/dist/commands/doctor.js +3 -3
  45. package/dist/commands/doctor.test.js +1 -1
  46. package/dist/commands/fields.js +2 -2
  47. package/dist/commands/get.js +3 -3
  48. package/dist/commands/handshake.js +5 -5
  49. package/dist/commands/ingest.js +6 -6
  50. package/dist/commands/init.d.ts +1 -1
  51. package/dist/commands/init.js +1 -1
  52. package/dist/commands/orient.js +2 -2
  53. package/dist/commands/promote.js +4 -4
  54. package/dist/commands/proposals.js +2 -2
  55. package/dist/commands/reject.js +2 -2
  56. package/dist/commands/relate.js +3 -3
  57. package/dist/commands/search.js +2 -2
  58. package/dist/commands/session.js +7 -7
  59. package/dist/commands/setup.js +5 -5
  60. package/dist/commands/update.js +3 -3
  61. package/dist/commands/usage.d.ts +1 -1
  62. package/dist/commands/usage.js +4 -4
  63. package/dist/commands/verify.js +2 -2
  64. package/dist/commands/workspace.js +4 -4
  65. package/dist/generators/chain-rules.d.ts +3 -3
  66. package/dist/generators/chain-rules.js +3 -3
  67. package/dist/generators/chain-rules.test.js +2 -2
  68. package/dist/generators/portable-knowledge.js +1 -1
  69. package/dist/index.js +1 -1
  70. package/dist/index.js.map +1 -1
  71. package/dist/lib/activation.js +2 -2
  72. package/dist/lib/activation.test.js +3 -3
  73. package/dist/lib/client.d.ts +4 -4
  74. package/dist/lib/client.js +8 -8
  75. package/dist/lib/client.js.map +1 -1
  76. package/dist/lib/onboarding-path-b.js +8 -8
  77. package/dist/lib/onboarding-shared.js +2 -2
  78. package/dist/lib/onboarding.js +4 -4
  79. package/dist/lib/workspace-probe.js +2 -2
  80. package/package.json +1 -1
@@ -13,9 +13,9 @@
13
13
  import { beforeEach, describe, expect, it, vi } from 'vitest';
14
14
  import { runWorkspaceVerify, runWorkspaceRepair, runDefinitionsDiff } from '../commands/workspace.js';
15
15
  // ── Mocks ──────────────────────────────────────────────────────────────────
16
- const convexCallMock = vi.fn();
16
+ const kernelCallMock = vi.fn();
17
17
  vi.mock('../lib/client.js', () => ({
18
- convexCall: (...args) => convexCallMock(...args),
18
+ kernelCall: (...args) => kernelCallMock(...args),
19
19
  }));
20
20
  vi.mock('../lib/config.js', () => ({
21
21
  getConfigOrGuide: vi.fn(() => Promise.resolve({ apiKey: 'pb_sk_test', siteUrl: 'https://test.convex.site' })),
@@ -84,12 +84,12 @@ const missingSeedsResult = {
84
84
  describe('formatWorkspaceVerify', () => {
85
85
  beforeEach(() => vi.clearAllMocks());
86
86
  it('renders workspace name in the heading', async () => {
87
- convexCallMock.mockResolvedValue(healthyResult);
87
+ kernelCallMock.mockResolvedValue(healthyResult);
88
88
  const output = await captureStdout(() => runWorkspaceVerify());
89
89
  expect(output).toContain('Acme Workspace');
90
90
  });
91
91
  it('shows healthy status when all checks pass', async () => {
92
- convexCallMock.mockResolvedValue(healthyResult);
92
+ kernelCallMock.mockResolvedValue(healthyResult);
93
93
  const output = await captureStdout(() => runWorkspaceVerify());
94
94
  expect(output).toContain('healthy');
95
95
  // No gaps section
@@ -97,7 +97,7 @@ describe('formatWorkspaceVerify', () => {
97
97
  expect(output).not.toContain('pb workspace repair');
98
98
  });
99
99
  it('shows all four check labels', async () => {
100
- convexCallMock.mockResolvedValue(healthyResult);
100
+ kernelCallMock.mockResolvedValue(healthyResult);
101
101
  const output = await captureStdout(() => runWorkspaceVerify());
102
102
  expect(output).toContain('Collections aligned');
103
103
  expect(output).toContain('Glossary seeded');
@@ -105,7 +105,7 @@ describe('formatWorkspaceVerify', () => {
105
105
  expect(output).toContain('Default circle');
106
106
  });
107
107
  it('shows incomplete status with gaps section', async () => {
108
- convexCallMock.mockResolvedValue(incompletResult);
108
+ kernelCallMock.mockResolvedValue(incompletResult);
109
109
  const output = await captureStdout(() => runWorkspaceVerify());
110
110
  expect(output).toContain('incomplete');
111
111
  expect(output).toContain('Gaps');
@@ -113,13 +113,13 @@ describe('formatWorkspaceVerify', () => {
113
113
  expect(output).toContain('pb workspace repair');
114
114
  });
115
115
  it('shows missing-seeds status', async () => {
116
- convexCallMock.mockResolvedValue(missingSeedsResult);
116
+ kernelCallMock.mockResolvedValue(missingSeedsResult);
117
117
  const output = await captureStdout(() => runWorkspaceVerify());
118
118
  expect(output).toContain('missing-seeds');
119
119
  expect(output).toContain('collections missing');
120
120
  });
121
121
  it('lists all gaps individually', async () => {
122
- convexCallMock.mockResolvedValue(incompletResult);
122
+ kernelCallMock.mockResolvedValue(incompletResult);
123
123
  const output = await captureStdout(() => runWorkspaceVerify());
124
124
  for (const gap of incompletResult.gaps) {
125
125
  expect(output).toContain(gap);
@@ -144,31 +144,31 @@ describe('formatWorkspaceRepair', () => {
144
144
  alreadyHealthy: false,
145
145
  };
146
146
  it('renders already-healthy message when alreadyHealthy is true', async () => {
147
- convexCallMock.mockResolvedValue(alreadyHealthyResult);
147
+ kernelCallMock.mockResolvedValue(alreadyHealthyResult);
148
148
  const output = await captureStdout(() => runWorkspaceRepair());
149
149
  expect(output).toContain('already healthy');
150
150
  expect(output).not.toContain('Scheduled');
151
151
  });
152
152
  it('lists scheduled jobs when seeds were missing', async () => {
153
- convexCallMock.mockResolvedValue(repairedResult);
153
+ kernelCallMock.mockResolvedValue(repairedResult);
154
154
  const output = await captureStdout(() => runWorkspaceRepair());
155
155
  expect(output).toContain('Scheduled');
156
156
  expect(output).toContain('seedSystemEntries');
157
157
  expect(output).toContain('seedSemanticTypes');
158
158
  });
159
159
  it('lists skipped jobs when already present', async () => {
160
- convexCallMock.mockResolvedValue(repairedResult);
160
+ kernelCallMock.mockResolvedValue(repairedResult);
161
161
  const output = await captureStdout(() => runWorkspaceRepair());
162
162
  expect(output).toContain('Skipped');
163
163
  expect(output).toContain('seedDefaultCircle');
164
164
  });
165
165
  it('shows async-confirmation hint when seeds were scheduled', async () => {
166
- convexCallMock.mockResolvedValue(repairedResult);
166
+ kernelCallMock.mockResolvedValue(repairedResult);
167
167
  const output = await captureStdout(() => runWorkspaceRepair());
168
168
  expect(output).toContain('pb workspace verify');
169
169
  });
170
170
  it('renders workspace name', async () => {
171
- convexCallMock.mockResolvedValue(repairedResult);
171
+ kernelCallMock.mockResolvedValue(repairedResult);
172
172
  const output = await captureStdout(() => runWorkspaceRepair());
173
173
  expect(output).toContain('Fixed Workspace');
174
174
  });
@@ -215,7 +215,7 @@ describe('formatDefinitionsDiff — via runDefinitionsDiff mock', () => {
215
215
  const dir = mkdtempSync(join(tmpdir(), 'pb-test-'));
216
216
  const file = join(dir, 'defs.json');
217
217
  writeFileSync(file, JSON.stringify([]));
218
- convexCallMock.mockResolvedValue([]);
218
+ kernelCallMock.mockResolvedValue([]);
219
219
  const output = await captureStdout(() => runDefinitionsDiff({ file }));
220
220
  expect(output).toContain('No definitions found');
221
221
  rmSync(dir, { recursive: true, force: true });
@@ -228,7 +228,7 @@ describe('formatDefinitionsDiff — via runDefinitionsDiff mock', () => {
228
228
  const file = join(dir, 'defs.json');
229
229
  const def = { slug: 'bets', name: 'Bets', description: 'Work packages' };
230
230
  writeFileSync(file, JSON.stringify([def]));
231
- convexCallMock.mockResolvedValue([def]);
231
+ kernelCallMock.mockResolvedValue([def]);
232
232
  const output = await captureStdout(() => runDefinitionsDiff({ file }));
233
233
  expect(output).toContain('Identical');
234
234
  expect(output).toContain('1');
@@ -242,7 +242,7 @@ describe('formatDefinitionsDiff — via runDefinitionsDiff mock', () => {
242
242
  const dir = mkdtempSync(join(tmpdir(), 'pb-test-'));
243
243
  const file = join(dir, 'defs.json');
244
244
  writeFileSync(file, JSON.stringify([{ slug: 'experiments', name: 'Experiments' }]));
245
- convexCallMock.mockResolvedValue([]);
245
+ kernelCallMock.mockResolvedValue([]);
246
246
  const output = await captureStdout(() => runDefinitionsDiff({ file }));
247
247
  expect(output).toContain('Only local');
248
248
  expect(output).toContain('experiments');
@@ -255,7 +255,7 @@ describe('formatDefinitionsDiff — via runDefinitionsDiff mock', () => {
255
255
  const dir = mkdtempSync(join(tmpdir(), 'pb-test-'));
256
256
  const file = join(dir, 'defs.json');
257
257
  writeFileSync(file, JSON.stringify([]));
258
- convexCallMock.mockResolvedValue([{ slug: 'tensions', name: 'Tensions' }]);
258
+ kernelCallMock.mockResolvedValue([{ slug: 'tensions', name: 'Tensions' }]);
259
259
  const output = await captureStdout(() => runDefinitionsDiff({ file }));
260
260
  expect(output).toContain('Only server');
261
261
  expect(output).toContain('tensions');
@@ -268,7 +268,7 @@ describe('formatDefinitionsDiff — via runDefinitionsDiff mock', () => {
268
268
  const dir = mkdtempSync(join(tmpdir(), 'pb-test-'));
269
269
  const file = join(dir, 'defs.json');
270
270
  writeFileSync(file, JSON.stringify([{ slug: 'bets', name: 'Old name' }]));
271
- convexCallMock.mockResolvedValue([{ slug: 'bets', name: 'New name' }]);
271
+ kernelCallMock.mockResolvedValue([{ slug: 'bets', name: 'New name' }]);
272
272
  const output = await captureStdout(() => runDefinitionsDiff({ file }));
273
273
  expect(output).toContain('Different');
274
274
  expect(output).toContain('bets');
@@ -283,7 +283,7 @@ describe('formatDefinitionsDiff — via runDefinitionsDiff mock', () => {
283
283
  const localDef = { slug: 'bets', name: 'Bets' };
284
284
  writeFileSync(file, JSON.stringify([localDef]));
285
285
  // Server returns same data but with Convex metadata — should still be identical
286
- convexCallMock.mockResolvedValue([{ ...localDef, _id: 'abc123', _creationTime: 1234567890 }]);
286
+ kernelCallMock.mockResolvedValue([{ ...localDef, _id: 'abc123', _creationTime: 1234567890 }]);
287
287
  const output = await captureStdout(() => runDefinitionsDiff({ file }));
288
288
  expect(output).toContain('Identical');
289
289
  expect(output).toContain('All definitions match');
@@ -298,7 +298,7 @@ describe('formatDefinitionsDiff — via runDefinitionsDiff mock', () => {
298
298
  const identical = { slug: 'bets', name: 'Bets' };
299
299
  const changed = { slug: 'tensions', name: 'Old tensions' };
300
300
  writeFileSync(file, JSON.stringify([identical, changed]));
301
- convexCallMock.mockResolvedValue([identical, { slug: 'tensions', name: 'New tensions' }]);
301
+ kernelCallMock.mockResolvedValue([identical, { slug: 'tensions', name: 'New tensions' }]);
302
302
  const output = await captureStdout(() => runDefinitionsDiff({ file }));
303
303
  // 1 identical / 2 total
304
304
  expect(output).toContain('1/2');
@@ -10,7 +10,7 @@
10
10
  * skipped for manual review. Reports what was approved and what was skipped.
11
11
  */
12
12
  import { getConfigOrGuide } from '../lib/config.js';
13
- import { convexCall } from '../lib/client.js';
13
+ import { kernelCall } from '../lib/client.js';
14
14
  import { runCliCommand } from '../lib/runner.js';
15
15
  import { CLIError, ErrorCode } from '../lib/errors.js';
16
16
  import { formatAcceptReceipt, formatAutoAcceptReport, } from '../formatters/proposals.js';
@@ -22,7 +22,7 @@ export async function runAccept(options) {
22
22
  if (options.auto) {
23
23
  await runCliCommand({
24
24
  fn: async () => {
25
- const proposals = await convexCall('governance.listProposals', {});
25
+ const proposals = await kernelCall('governance.listProposals', {});
26
26
  const approved = [];
27
27
  const skipped = [];
28
28
  for (const p of proposals) {
@@ -38,7 +38,7 @@ export async function runAccept(options) {
38
38
  continue;
39
39
  }
40
40
  try {
41
- await convexCall('governance.respondToProposal', {
41
+ await kernelCall('governance.respondToProposal', {
42
42
  proposalId: p._id,
43
43
  verdict: 'approve',
44
44
  });
@@ -65,7 +65,7 @@ export async function runAccept(options) {
65
65
  }
66
66
  await runCliCommand({
67
67
  fn: async () => {
68
- return await convexCall('governance.respondToProposal', {
68
+ return await kernelCall('governance.respondToProposal', {
69
69
  proposalId: options.proposalId,
70
70
  verdict: 'approve',
71
71
  });
@@ -56,7 +56,7 @@ export type ChangelogRow = {
56
56
  timestamp: number;
57
57
  definitionSlug: string;
58
58
  };
59
- export type CockpitTab = 'workspaces' | 'seeds' | 'changelog';
59
+ export type CockpitTab = 'workspaces' | 'seeds' | 'changelog' | 'manage';
60
60
  export type CockpitProps = {
61
61
  workspaces: WorkspaceRow[];
62
62
  currentKeyWorkspaceId: string | null;
@@ -70,8 +70,10 @@ export type CockpitProps = {
70
70
  seedRows: SeedRow[];
71
71
  /** Changelog rows for heatmap. WP-317 E8 / S4. */
72
72
  changelogRows: ChangelogRow[];
73
+ /** Called when ManageTab destructive flow becomes active/inactive. WP-324 S2. */
74
+ onFlowActive?: (active: boolean) => void;
73
75
  };
74
- export declare function Cockpit({ workspaces, currentKeyWorkspaceId, onExit, onDrillIn, onRunFix, siteUrl, seedRows, changelogRows }: CockpitProps): import("react/jsx-runtime").JSX.Element;
76
+ export declare function Cockpit({ workspaces, currentKeyWorkspaceId, onExit, onDrillIn, onRunFix, siteUrl, seedRows, changelogRows, onFlowActive }: CockpitProps): import("react/jsx-runtime").JSX.Element;
75
77
  export type WorkspaceHealthData = {
76
78
  readiness: unknown;
77
79
  collections: unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"cockpit.d.ts","sourceRoot":"","sources":["../../../src/commands/admin/cockpit.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cockpit.d.ts","sourceRoot":"","sources":["../../../src/commands/admin/cockpit.tsx"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
3
  * Ink TUI cockpit for pb admin (WP-314 S1, WP-317 S3).
4
4
  *
@@ -21,6 +21,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
21
21
  import { useState, useMemo, useEffect, useCallback } from 'react';
22
22
  import { Box, Text, useInput, useApp } from 'ink';
23
23
  import TextInput from 'ink-text-input';
24
+ import { adminCall } from '../../lib/client.js';
25
+ import { resolveWorkspaceArg, resolveUserArg } from './inspect.js';
24
26
  // ---------------------------------------------------------------------------
25
27
  // Env label derivation
26
28
  // ---------------------------------------------------------------------------
@@ -86,11 +88,12 @@ function padLeft(s, width) {
86
88
  // ---------------------------------------------------------------------------
87
89
  // Tab names and order
88
90
  // ---------------------------------------------------------------------------
89
- const TABS = ['workspaces', 'seeds', 'changelog'];
91
+ const TABS = ['workspaces', 'seeds', 'changelog', 'manage'];
90
92
  const TAB_LABELS = {
91
93
  workspaces: 'Workspaces',
92
94
  seeds: 'Seeds',
93
95
  changelog: 'Changelog',
96
+ manage: 'Manage',
94
97
  };
95
98
  function nextTab(current) {
96
99
  const idx = TABS.indexOf(current);
@@ -212,10 +215,208 @@ function ChangelogTab({ changelogRows, siteUrl }) {
212
215
  })] }, dow));
213
216
  }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["\u00B7 0 \u2591 1-2 \u2592 3-5 \u2588 6+ ", days.length, " days, ", changelogRows.length, " change", changelogRows.length !== 1 ? 's' : ''] }) })] }));
214
217
  }
218
+ const TEST_ONLY_WARNING = '⚠ TEST-ONLY — for onboarding reset testing only (BR-210)';
219
+ const MANAGE_OPS = [
220
+ { op: 'delete-workspace', label: 'delete-workspace', description: 'Permanently delete a workspace and all its data' },
221
+ { op: 'delete-user', label: 'delete-user', description: 'Permanently delete a user across all workspaces' },
222
+ ];
223
+ function ManageTab({ onFlowActive }) {
224
+ const [flowState, setFlowState] = useState('idle');
225
+ const [selectedOpIdx, setSelectedOpIdx] = useState(0);
226
+ const [selectedOp, setSelectedOp] = useState(null);
227
+ const [targetInput, setTargetInput] = useState('');
228
+ const [resolvedId, setResolvedId] = useState(null);
229
+ const [dryRunResult, setDryRunResult] = useState(null);
230
+ const [confirmInput, setConfirmInput] = useState('');
231
+ const [loading, setLoading] = useState(false);
232
+ const [errorMsg, setErrorMsg] = useState(null);
233
+ const [spinnerFrame, setSpinnerFrame] = useState(0);
234
+ const [doneResult, setDoneResult] = useState(null);
235
+ useEffect(() => {
236
+ const isActive = flowState !== 'idle' && flowState !== 'done';
237
+ onFlowActive?.(isActive);
238
+ }, [flowState, onFlowActive]);
239
+ useEffect(() => {
240
+ if (!loading)
241
+ return;
242
+ const timer = setInterval(() => setSpinnerFrame((f) => (f + 1) % SPINNER_FRAMES.length), 80);
243
+ return () => clearInterval(timer);
244
+ }, [loading]);
245
+ function goIdle() {
246
+ setFlowState('idle');
247
+ setSelectedOp(null);
248
+ setTargetInput('');
249
+ setResolvedId(null);
250
+ setDryRunResult(null);
251
+ setConfirmInput('');
252
+ setErrorMsg(null);
253
+ setDoneResult(null);
254
+ }
255
+ useInput((input, key) => {
256
+ if (flowState === 'idle') {
257
+ if (key.upArrow) {
258
+ setSelectedOpIdx((i) => Math.max(0, i - 1));
259
+ return;
260
+ }
261
+ if (key.downArrow) {
262
+ setSelectedOpIdx((i) => Math.min(MANAGE_OPS.length - 1, i + 1));
263
+ return;
264
+ }
265
+ if (key.return) {
266
+ const op = MANAGE_OPS[selectedOpIdx].op;
267
+ setSelectedOp(op);
268
+ setFlowState('entering-target');
269
+ return;
270
+ }
271
+ return;
272
+ }
273
+ if (flowState === 'entering-target') {
274
+ if (key.escape) {
275
+ goIdle();
276
+ return;
277
+ }
278
+ return;
279
+ }
280
+ if (flowState === 'showing-dry-run') {
281
+ if (key.escape) {
282
+ goIdle();
283
+ return;
284
+ }
285
+ if (key.return) {
286
+ setFlowState('confirming');
287
+ setConfirmInput('');
288
+ return;
289
+ }
290
+ return;
291
+ }
292
+ if (flowState === 'confirming') {
293
+ if (key.escape) {
294
+ goIdle();
295
+ return;
296
+ }
297
+ return;
298
+ }
299
+ if (flowState === 'done') {
300
+ goIdle();
301
+ return;
302
+ }
303
+ });
304
+ async function handleTargetSubmit(value) {
305
+ if (!value.trim())
306
+ return;
307
+ setLoading(true);
308
+ setErrorMsg(null);
309
+ try {
310
+ let id;
311
+ if (selectedOp === 'delete-workspace') {
312
+ id = await resolveWorkspaceArg(value.trim());
313
+ if (!id) {
314
+ setErrorMsg(`Workspace not found: "${value.trim()}"`);
315
+ setLoading(false);
316
+ return;
317
+ }
318
+ }
319
+ else {
320
+ id = await resolveUserArg(value.trim());
321
+ }
322
+ setResolvedId(id);
323
+ let result;
324
+ if (selectedOp === 'delete-workspace') {
325
+ result = await adminCall('manage:delete-workspace', { workspaceId: id, dryRun: true });
326
+ }
327
+ else {
328
+ result = await adminCall('manage:delete-user', { clerkUserId: id, dryRun: true });
329
+ }
330
+ if (result?.success === false && result?.reason === 'user_owns_workspace') {
331
+ setErrorMsg('Cannot delete: user is the sole member of a workspace. Delete the workspace first.');
332
+ setLoading(false);
333
+ return;
334
+ }
335
+ setDryRunResult(result);
336
+ setFlowState('showing-dry-run');
337
+ }
338
+ catch (err) {
339
+ setErrorMsg(err instanceof Error ? err.message : String(err));
340
+ }
341
+ finally {
342
+ setLoading(false);
343
+ }
344
+ }
345
+ async function handleConfirmSubmit(value) {
346
+ if (!resolvedId || !selectedOp)
347
+ return;
348
+ const expectedText = `DELETE ${targetInput.trim()}`;
349
+ if (value !== expectedText) {
350
+ setErrorMsg('Input did not match — try again');
351
+ setConfirmInput('');
352
+ return;
353
+ }
354
+ setLoading(true);
355
+ setErrorMsg(null);
356
+ try {
357
+ let result;
358
+ if (selectedOp === 'delete-workspace') {
359
+ result = await adminCall('manage:delete-workspace', { workspaceId: resolvedId, dryRun: false });
360
+ }
361
+ else {
362
+ result = await adminCall('manage:delete-user', { clerkUserId: resolvedId, dryRun: false });
363
+ }
364
+ if (result?.success === false && result?.reason === 'user_owns_workspace') {
365
+ setErrorMsg('Cannot delete: user is the sole member of a workspace.');
366
+ goIdle();
367
+ return;
368
+ }
369
+ setDoneResult(result);
370
+ setFlowState('done');
371
+ // done state renders success screen; user presses any key → goIdle() fires via useInput handler
372
+ }
373
+ catch (err) {
374
+ setErrorMsg(err instanceof Error ? err.message : String(err));
375
+ goIdle();
376
+ }
377
+ finally {
378
+ setLoading(false);
379
+ }
380
+ }
381
+ function renderDryRunSummary(result) {
382
+ if (!result)
383
+ return '—';
384
+ const total = result.totalDeleted ?? 0;
385
+ const tables = result.tablesDeleted ?? {};
386
+ const lines = [` Total rows to delete: ${total}`];
387
+ for (const [table, count] of Object.entries(tables)) {
388
+ if (count > 0)
389
+ lines.push(` ${table}: ${count}`);
390
+ }
391
+ return lines.join('\n');
392
+ }
393
+ if (flowState === 'idle') {
394
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "red", children: "Manage \u2014 Destructive Operations" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: TEST_ONLY_WARNING }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "gray", children: "Select an operation:" }) }), MANAGE_OPS.map((item, idx) => {
395
+ const isSelected = idx === selectedOpIdx;
396
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? 'cyan' : undefined, children: isSelected ? '▶ ' : ' ' }), _jsx(Text, { bold: isSelected, color: isSelected ? 'cyan' : 'white', children: item.label })] }), isSelected && (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, color: "gray", children: item.description }) }))] }, item.op));
397
+ }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, color: "gray", children: "\u2191\u2193 navigate \u00B7 Enter select" }) })] }));
398
+ }
399
+ if (flowState === 'entering-target') {
400
+ const prompt = selectedOp === 'delete-workspace' ? 'Enter workspace slug:' : 'Enter user email:';
401
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "red", children: ["Manage \u2014 ", selectedOp] }) }), loading ? (_jsxs(Text, { color: "cyan", children: [SPINNER_FRAMES[spinnerFrame], " Resolving\u2026"] })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsxs(Text, { color: "cyan", children: [prompt, " "] }), _jsx(TextInput, { value: targetInput, onChange: setTargetInput, onSubmit: (v) => { void handleTargetSubmit(v); } })] }), errorMsg && _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["\u2717 ", errorMsg] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, color: "gray", children: "Enter confirm \u00B7 Escape cancel" }) })] }))] }));
402
+ }
403
+ if (flowState === 'showing-dry-run') {
404
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "yellow", paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["Dry-run impact \u2014 ", selectedOp] }), _jsxs(Text, { color: "gray", children: ["Target: ", targetInput.trim()] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { color: "red", bold: true, children: TEST_ONLY_WARNING }) }), _jsx(Text, { color: "white", children: renderDryRunSummary(dryRunResult) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, color: "gray", children: "Enter to confirm \u2192 \u00B7 Escape cancel" }) })] }));
405
+ }
406
+ if (flowState === 'confirming') {
407
+ const expectedText = `DELETE ${targetInput.trim()}`;
408
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "red", paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, color: "red", children: ["\u26A0 CONFIRM DELETION \u2014 ", selectedOp] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { color: "red", bold: true, children: TEST_ONLY_WARNING }) }), loading ? (_jsxs(Text, { color: "cyan", children: [SPINNER_FRAMES[spinnerFrame], " Deleting\u2026"] })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsxs(Text, { color: "cyan", children: ["Type ", _jsx(Text, { bold: true, children: expectedText }), " to confirm: "] }), _jsx(TextInput, { value: confirmInput, onChange: setConfirmInput, onSubmit: (v) => { void handleConfirmSubmit(v); } })] }), errorMsg && _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["\u2717 ", errorMsg] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, color: "gray", children: "Enter confirm \u00B7 Escape cancel" }) })] }))] }));
409
+ }
410
+ if (flowState === 'done') {
411
+ const total = doneResult?.totalDeleted ?? 0;
412
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "green", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: "green", children: "\u2713 Deleted successfully" }), _jsxs(Text, { color: "gray", children: [total, " rows removed"] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, color: "gray", children: "Press any key to return" }) })] }));
413
+ }
414
+ return null;
415
+ }
215
416
  // ---------------------------------------------------------------------------
216
417
  // Cockpit component
217
418
  // ---------------------------------------------------------------------------
218
- export function Cockpit({ workspaces, currentKeyWorkspaceId, onExit, onDrillIn, onRunFix, siteUrl, seedRows, changelogRows }) {
419
+ export function Cockpit({ workspaces, currentKeyWorkspaceId, onExit, onDrillIn, onRunFix, siteUrl, seedRows, changelogRows, onFlowActive }) {
219
420
  const { exit } = useApp();
220
421
  const [activeTab, setActiveTab] = useState('workspaces');
221
422
  const [filter, setFilter] = useState('');
@@ -228,6 +429,12 @@ export function Cockpit({ workspaces, currentKeyWorkspaceId, onExit, onDrillIn,
228
429
  const [spinnerFrame, setSpinnerFrame] = useState(0);
229
430
  /** Set of workspace _ids whose topIssue was cleared locally after a fix ran. */
230
431
  const [localFixes, setLocalFixes] = useState(new Set());
432
+ // ManageTab flow active — blocks Tab/Shift-Tab (WP-324 S2, done-when #4)
433
+ const [manageFlowActive, setManageFlowActive] = useState(false);
434
+ const handleManageFlowActive = useCallback((active) => {
435
+ setManageFlowActive(active);
436
+ onFlowActive?.(active);
437
+ }, [onFlowActive]);
231
438
  // Animate spinner while a fix is running
232
439
  useEffect(() => {
233
440
  if (!runningFix)
@@ -260,8 +467,8 @@ export function Cockpit({ workspaces, currentKeyWorkspaceId, onExit, onDrillIn,
260
467
  }, [scored, filter]);
261
468
  // Keyboard handling
262
469
  useInput((input, key) => {
263
- // Tab navigation — always active (unless in filter mode)
264
- if (key.tab && !isFiltering && !confirmingFix && !runningFix) {
470
+ // Tab navigation — blocked during ManageTab destructive flow (WP-324 S2)
471
+ if (key.tab && !isFiltering && !confirmingFix && !runningFix && !manageFlowActive) {
265
472
  if (key.shift) {
266
473
  setActiveTab((t) => prevTab(t));
267
474
  }
@@ -360,7 +567,7 @@ export function Cockpit({ workspaces, currentKeyWorkspaceId, onExit, onDrillIn,
360
567
  }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: ' ' + '─'.repeat(COL_WORKSPACE + COL_ENTRIES + COL_COLS + COL_PARITY + COL_HEALTH + 4) }) }), _jsx(Box, { marginTop: 0, children: isFiltering ? (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "/ " }), _jsx(TextInput, { value: filter, onChange: setFilter, onSubmit: () => setIsFiltering(false) })] })) : runningFix ? (_jsxs(Text, { color: "cyan", children: [SPINNER_FRAMES[spinnerFrame], " Running fix\u2026"] })) : (_jsxs(Text, { color: "gray", children: [filtered.length, " workspace", filtered.length !== 1 ? 's' : '', filter ? ` matching "${filter}"` : '', ' ', _jsxs(Text, { color: "gray", dimColor: true, children: ['/ filter · ↑↓ nav · Enter drill-in · q quit · ? help', (() => {
361
568
  const sel = filtered[clampedIdx];
362
569
  return sel?.ws.topIssue && !localFixes.has(sel.ws._id) ? ' · f fix' : '';
363
- })()] })] })) }), confirmingFix && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "cyan", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Run fix?" }), _jsxs(Text, { color: "white", children: [" ", confirmingFix.cmd.label] }), _jsxs(Text, { dimColor: true, color: "gray", children: [" cmd: pb admin seed run ", confirmingFix.cmd.cmd, " est. ", confirmingFix.cmd.estSeconds, "s"] }), _jsx(Text, { color: "gray", children: " " }), _jsxs(Text, { color: "gray", children: [_jsx(Text, { color: "cyan", children: "[y]" }), _jsx(Text, { color: "gray", children: " yes " }), _jsx(Text, { color: "cyan", children: "[n]" }), _jsx(Text, { color: "gray", children: " no / Escape" })] })] })), showHelp && !confirmingFix && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Keyboard shortcuts" }), _jsx(Text, { color: "gray", children: " Tab / Shift+Tab Switch tabs" }), _jsx(Text, { color: "gray", children: " / Start fuzzy filter" }), _jsx(Text, { color: "gray", children: " Escape Clear filter / exit cockpit" }), _jsx(Text, { color: "gray", children: " \u2191 / \u2193 Navigate rows" }), _jsx(Text, { color: "gray", children: " Enter Drill into selected workspace" }), _jsx(Text, { color: "gray", children: " f Run suggested fix for selected row" }), _jsx(Text, { color: "gray", children: " q Quit cockpit" }), _jsx(Text, { color: "gray", children: " ? Toggle this help panel" }), _jsx(Text, { dimColor: true, color: "gray", children: " Press ? again to close" })] }))] })), activeTab === 'seeds' && (_jsx(SeedsTab, { seedRows: seedRows })), activeTab === 'changelog' && (_jsx(ChangelogTab, { changelogRows: changelogRows, siteUrl: siteUrl }))] }));
570
+ })()] })] })) }), confirmingFix && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "cyan", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Run fix?" }), _jsxs(Text, { color: "white", children: [" ", confirmingFix.cmd.label] }), _jsxs(Text, { dimColor: true, color: "gray", children: [" cmd: pb admin seed run ", confirmingFix.cmd.cmd, " est. ", confirmingFix.cmd.estSeconds, "s"] }), _jsx(Text, { color: "gray", children: " " }), _jsxs(Text, { color: "gray", children: [_jsx(Text, { color: "cyan", children: "[y]" }), _jsx(Text, { color: "gray", children: " yes " }), _jsx(Text, { color: "cyan", children: "[n]" }), _jsx(Text, { color: "gray", children: " no / Escape" })] })] })), showHelp && !confirmingFix && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Keyboard shortcuts" }), _jsx(Text, { color: "gray", children: " Tab / Shift+Tab Switch tabs" }), _jsx(Text, { color: "gray", children: " / Start fuzzy filter" }), _jsx(Text, { color: "gray", children: " Escape Clear filter / exit cockpit" }), _jsx(Text, { color: "gray", children: " \u2191 / \u2193 Navigate rows" }), _jsx(Text, { color: "gray", children: " Enter Drill into selected workspace" }), _jsx(Text, { color: "gray", children: " f Run suggested fix for selected row" }), _jsx(Text, { color: "gray", children: " q Quit cockpit" }), _jsx(Text, { color: "gray", children: " ? Toggle this help panel" }), _jsx(Text, { dimColor: true, color: "gray", children: " Press ? again to close" })] }))] })), activeTab === 'seeds' && (_jsx(SeedsTab, { seedRows: seedRows })), activeTab === 'changelog' && (_jsx(ChangelogTab, { changelogRows: changelogRows, siteUrl: siteUrl })), activeTab === 'manage' && (_jsx(ManageTab, { onFlowActive: handleManageFlowActive }))] }));
364
571
  }
365
572
  function statusIcon(ok, warn) {
366
573
  if (ok)