@pellux/goodvibes-agent 0.1.8 → 0.1.10

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 (39) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -1
  3. package/docs/getting-started.md +1 -1
  4. package/docs/release-and-publishing.md +2 -2
  5. package/package.json +4 -1
  6. package/src/cli/agent-knowledge-command.ts +43 -21
  7. package/src/cli/help.ts +17 -4
  8. package/src/cli/management-commands.ts +3 -3
  9. package/src/cli/management.ts +7 -1
  10. package/src/cli/parser.ts +3 -0
  11. package/src/cli/service-posture.ts +6 -6
  12. package/src/cli/status.ts +9 -9
  13. package/src/cli/surface-command.ts +3 -3
  14. package/src/cli/types.ts +2 -0
  15. package/src/input/commands/experience-runtime.ts +1 -1
  16. package/src/input/commands/hooks-runtime.ts +30 -2
  17. package/src/input/handler.ts +1 -0
  18. package/src/input/onboarding/onboarding-runtime-status.ts +8 -8
  19. package/src/input/onboarding/onboarding-wizard-apply.ts +13 -53
  20. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +4 -4
  21. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +1 -1
  22. package/src/input/onboarding/onboarding-wizard-constants.ts +7 -7
  23. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +4 -4
  24. package/src/input/onboarding/onboarding-wizard-steps.ts +13 -13
  25. package/src/input/settings-modal-agent-policy.ts +18 -0
  26. package/src/input/settings-modal-types.ts +17 -0
  27. package/src/input/settings-modal.ts +30 -29
  28. package/src/main.ts +3 -26
  29. package/src/renderer/process-indicator.ts +7 -6
  30. package/src/renderer/process-modal.ts +17 -8
  31. package/src/renderer/settings-modal.ts +12 -8
  32. package/src/renderer/ui-factory.ts +4 -32
  33. package/src/runtime/bootstrap-shell.ts +0 -13
  34. package/src/runtime/bootstrap.ts +0 -10
  35. package/src/runtime/onboarding/derivation.ts +6 -6
  36. package/src/verification/live-verifier.ts +148 -13
  37. package/src/version.ts +10 -3
  38. package/src/input/commands/quit-shared.ts +0 -162
  39. package/src/renderer/git-status.ts +0 -89
@@ -2,7 +2,6 @@ import { type Line, type Cell, createEmptyLine, createEmptyCell } from '../types
2
2
  import { LAYOUT } from './layout.ts';
3
3
  import { VERSION } from '../version.ts';
4
4
  import { fitDisplay, getDisplayWidth, truncateDisplay, wrapText, interpolateColor } from '../utils/terminal-width.ts';
5
- import type { GitHeaderInfo } from './git-status.ts';
6
5
  import { renderConversationFragment, renderConversationStatusLine, type ConversationStatusSegment } from './conversation-surface.ts';
7
6
  import { GLYPHS } from './ui-primitives.ts';
8
7
 
@@ -11,22 +10,6 @@ const GRADIENT_CYCLE_FRAMES = 50;
11
10
  /** Number of frames before rotating to the next thinking phrase (~30 seconds at 80ms/frame). */
12
11
  const PHRASE_ROTATION_FRAMES = 375;
13
12
 
14
- /** Build the git segment string and its display width. Single source of truth for header layout. */
15
- function buildGitSegment(gitInfo: GitHeaderInfo): { text: string; width: number } {
16
- const branch = ` git:${gitInfo.branch}`;
17
- if (gitInfo.dirty) {
18
- const text = `${branch} * `;
19
- return { text, width: getDisplayWidth(text) };
20
- }
21
- if (gitInfo.ahead > 0 || gitInfo.behind > 0) {
22
- const arrows = (gitInfo.ahead > 0 ? ` +${gitInfo.ahead}` : '') + (gitInfo.behind > 0 ? ` -${gitInfo.behind}` : '');
23
- const text = `${branch}${arrows} `;
24
- return { text, width: getDisplayWidth(text) };
25
- }
26
- const text = `${branch} `;
27
- return { text, width: getDisplayWidth(text) };
28
- }
29
-
30
13
  /** Format a number: up to 999, then 1.0k, 1.0M, 1.0B, 1.0T */
31
14
  function fmtNum(n: number): string {
32
15
  if (n < 1000) return String(n);
@@ -40,7 +23,7 @@ function fmtNum(n: number): string {
40
23
  * UIFactory - Generates standard UI fragments without needing Ink/React overhead.
41
24
  */
42
25
  export class UIFactory {
43
- public static createHeader(width: number, model: string, provider: string, title?: string, gitInfo?: GitHeaderInfo): Line[] {
26
+ public static createHeader(width: number, model: string, provider: string, title?: string): Line[] {
44
27
  const lines: Line[] = [];
45
28
  const CYAN = '#00ffff';
46
29
  const GREY = '244';
@@ -56,9 +39,8 @@ export class UIFactory {
56
39
  // Optional conversation title — shown after brand/ver, truncated to fit
57
40
  if (title) {
58
41
  const titleStr = `│ ${title} `;
59
- // Reserve space for git info (if present) + model/provider on the right
60
- const gitReserved = gitInfo ? buildGitSegment(gitInfo).width : 0;
61
- const rightReserved = getDisplayWidth(stats + prov) + gitReserved;
42
+ // Reserve space for model/provider on the right.
43
+ const rightReserved = getDisplayWidth(stats + prov);
62
44
  const maxTitleW = width - curX - rightReserved - 1;
63
45
  let displayTitle: string;
64
46
  if (getDisplayWidth(titleStr) <= maxTitleW) {
@@ -76,19 +58,9 @@ export class UIFactory {
76
58
  }
77
59
  for (const char of displayTitle) { if (curX < width) line[curX++] = { char, fg: TITLE_COLOR, bg: '', bold: false, dim: true, underline: false, italic: false, strikethrough: false }; }
78
60
  }
79
- // Build git info segment
80
- let gitStr = '';
81
- let gitFg = '238';
82
- if (gitInfo) {
83
- gitStr = buildGitSegment(gitInfo).text;
84
- if (gitInfo.dirty || gitInfo.ahead > 0 || gitInfo.behind > 0) {
85
- gitFg = '220'; // yellow when dirty or out-of-sync
86
- }
87
- }
88
61
  const rightSideText = stats + prov;
89
- const rightSideW = getDisplayWidth(rightSideText) + getDisplayWidth(gitStr);
62
+ const rightSideW = getDisplayWidth(rightSideText);
90
63
  let rightX = width - rightSideW;
91
- for (const char of gitStr) { if (rightX >= 0 && rightX < width) line[rightX++] = { char, fg: gitFg, bg: '', bold: false, dim: !gitInfo?.dirty && !(gitInfo?.ahead || gitInfo?.behind), underline: false, italic: false, strikethrough: false }; }
92
64
  for (const char of stats) { if (rightX < width) line[rightX++] = { char, fg: CYAN, bg: '', bold: true, dim: false, underline: false, italic: false, strikethrough: false }; }
93
65
  for (const char of prov) { if (rightX < width) line[rightX++] = { char, fg: GREY, bg: '', bold: false, dim: true, underline: false, italic: false, strikethrough: false }; }
94
66
  lines.push(line);
@@ -11,8 +11,6 @@ import type { OpsControlPlane } from '@/runtime/index.ts';
11
11
  import { CommandRegistry } from '../input/command-registry.ts';
12
12
  import { registerBuiltinCommands } from '../input/commands.ts';
13
13
  import { InputHistory } from '../input/input-history.ts';
14
- import { GitStatusProvider } from '../renderer/git-status.ts';
15
- import type { GitHeaderInfo } from '../renderer/git-status.ts';
16
14
  import type { PermissionRequestHandler } from '@pellux/goodvibes-sdk/platform/permissions';
17
15
  import { registerBuiltinPanels } from '../panels/builtin-panels.ts';
18
16
  import { SystemMessagesPanel } from '../panels/system-messages-panel.ts';
@@ -38,8 +36,6 @@ import { createKnowledgeApi } from '@pellux/goodvibes-sdk/platform/knowledge';
38
36
  export interface BootstrapShellState {
39
37
  readonly commandRegistry: CommandRegistry;
40
38
  readonly commandContext: CommandContext;
41
- readonly gitStatusProvider: GitStatusProvider;
42
- readonly lastGitInfoRef: { value: GitHeaderInfo | undefined };
43
39
  readonly inputHistory: InputHistory;
44
40
  readonly systemMessageRouter: SystemMessageRouter;
45
41
  }
@@ -260,13 +256,6 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
260
256
  });
261
257
  commandContextRef = commandContext;
262
258
 
263
- const gitStatusProvider = new GitStatusProvider(services.workingDirectory);
264
- const lastGitInfoRef = { value: undefined as GitHeaderInfo | undefined };
265
- gitStatusProvider.getStatus().then((info) => {
266
- lastGitInfoRef.value = info;
267
- requestRender();
268
- }).catch(() => { /* non-fatal */ });
269
-
270
259
  const saveHistory = configManager.get('behavior.saveHistory') as boolean;
271
260
  const inputHistory = new InputHistory({
272
261
  historyPath: services.shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'input-history.json'),
@@ -276,8 +265,6 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
276
265
  return {
277
266
  commandRegistry,
278
267
  commandContext,
279
- gitStatusProvider,
280
- lastGitInfoRef,
281
268
  inputHistory,
282
269
  systemMessageRouter,
283
270
  };
@@ -16,8 +16,6 @@ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
16
16
  import type { PermissionRequestHandler } from '@pellux/goodvibes-sdk/platform/permissions';
17
17
  import type { CommandContext } from '../input/command-registry.ts';
18
18
  import type { InputHistory } from '../input/input-history.ts';
19
- import type { GitStatusProvider } from '../renderer/git-status.ts';
20
- import type { GitHeaderInfo } from '../renderer/git-status.ts';
21
19
  import type { SelectionManager } from '../input/selection.ts';
22
20
  import type { Compositor } from '../renderer/compositor.ts';
23
21
 
@@ -81,10 +79,6 @@ export type BootstrapContext = RuntimeContext & {
81
79
  uiServices: UiRuntimeServices;
82
80
  /** Persists and navigates input history across sessions. */
83
81
  inputHistory: InputHistory;
84
- /** Provides git branch/dirty state for the header. */
85
- gitStatusProvider: GitStatusProvider;
86
- /** Mutable ref so async git refreshes propagate without closure capture issues. */
87
- lastGitInfoRef: { value: GitHeaderInfo | undefined };
88
82
  /** Unsubscribe functions owned by bootstrap (cleared on shutdown). */
89
83
  bootstrapUnsubs: Array<() => void>;
90
84
  /** Ref holding the periodic agent-status interval (use ref — not local var — to keep shutdown in sync). */
@@ -291,9 +285,7 @@ export async function bootstrapRuntime(
291
285
  systemMessageRouterRef.value = systemMessageRouter;
292
286
  const commandRegistry = shell.commandRegistry;
293
287
  const commandContext = shell.commandContext;
294
- const gitStatusProvider = shell.gitStatusProvider;
295
288
  const inputHistory = shell.inputHistory;
296
- const lastGitInfoRef = shell.lastGitInfoRef;
297
289
  const pluginCommandRegistry = {
298
290
  register(command: {
299
291
  readonly name: string;
@@ -507,8 +499,6 @@ export async function bootstrapRuntime(
507
499
  commandContext,
508
500
  uiServices,
509
501
  inputHistory,
510
- gitStatusProvider,
511
- lastGitInfoRef,
512
502
  bootstrapUnsubs,
513
503
  agentStatusIntervalRef,
514
504
  orchestratorRefs,
@@ -267,7 +267,7 @@ function describeLocalTuiOnly(snapshot: OnboardingSnapshotState): string {
267
267
  return 'Use GoodVibes Agent in this terminal while connecting only to an externally managed daemon. Agent does not enable service mode, HTTP listeners, external app surfaces, or network setup.';
268
268
  }
269
269
 
270
- return 'Keep Agent local-only by not enabling browser access, background services, HTTP listeners, external app surfaces, or network setup.';
270
+ return 'Keep Agent local-only by not requesting browser access, service posture changes, HTTP listeners, external app surfaces, or network setup from the daemon owner.';
271
271
  }
272
272
 
273
273
  function describeBrowserAccess(snapshot: OnboardingSnapshotState): string {
@@ -278,14 +278,14 @@ function describeBrowserAccess(snapshot: OnboardingSnapshotState): string {
278
278
 
279
279
  function describeRemoteDeviceAccess(snapshot: OnboardingSnapshotState): string {
280
280
  return hasRemoteDeviceAccess(snapshot)
281
- ? 'Keep enabled GoodVibes services reachable from other devices on your LAN. Local authentication is required.'
282
- : 'Make enabled GoodVibes services reachable from other devices on your LAN. Local authentication is required.';
281
+ ? 'Review external daemon surfaces reachable from other devices on your LAN. Local authentication is required.'
282
+ : 'Review the external daemon surfaces required for other-device LAN access. Local authentication is required.';
283
283
  }
284
284
 
285
285
  function describeWebhookIngress(snapshot: OnboardingSnapshotState): string {
286
286
  return hasWebhookOrEventIngress(snapshot)
287
- ? 'Keep the HTTP listener available for incoming webhooks, callbacks, and automation events.'
288
- : 'Turn on the HTTP listener for incoming webhooks, callbacks, and automation events.';
287
+ ? 'Review the external HTTP listener used for incoming webhooks, callbacks, and automation events.'
288
+ : 'Review the external HTTP listener required for incoming webhooks, callbacks, and automation events.';
289
289
  }
290
290
 
291
291
  function describeExternalIntegrations(snapshot: OnboardingSnapshotState): string {
@@ -306,7 +306,7 @@ function describeCloudflareBatch(snapshot: OnboardingSnapshotState): string {
306
306
  return 'Review Cloudflare Workers/Queues batch processing, token storage, and optional remote daemon provisioning settings.';
307
307
  }
308
308
 
309
- return 'Optionally configure Cloudflare Workers and Queues for explicit or eligible background batch jobs. Immediate local daemon behavior stays the default unless enabled.';
309
+ return 'Optionally configure Cloudflare Workers and Queues for explicit or eligible background batch jobs. The external daemon still owns execution.';
310
310
  }
311
311
 
312
312
  function getAcknowledgementAccepted(
@@ -213,6 +213,54 @@ async function fetchCheck(
213
213
  }
214
214
  }
215
215
 
216
+ async function fetchJsonCheck(
217
+ id: string,
218
+ title: string,
219
+ url: string,
220
+ token: string | undefined,
221
+ options: {
222
+ readonly method?: 'GET' | 'POST';
223
+ readonly body?: unknown;
224
+ readonly validate: (status: number, body: string) => { status: LiveVerificationStatus; summary: string; detail?: string };
225
+ },
226
+ ): Promise<LiveVerificationCheck> {
227
+ if (!token) {
228
+ return {
229
+ id,
230
+ title,
231
+ status: 'skip',
232
+ summary: 'No daemon bearer token was available.',
233
+ };
234
+ }
235
+ try {
236
+ const response = await fetch(url, {
237
+ method: options.method ?? 'GET',
238
+ headers: {
239
+ Authorization: `Bearer ${token}`,
240
+ 'Content-Type': 'application/json',
241
+ },
242
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
243
+ signal: AbortSignal.timeout(5000),
244
+ });
245
+ const body = await response.text();
246
+ const validated = options.validate(response.status, body);
247
+ return {
248
+ id,
249
+ title,
250
+ ...validated,
251
+ detail: validated.detail ?? compact(body),
252
+ };
253
+ } catch (error) {
254
+ return {
255
+ id,
256
+ title,
257
+ status: 'fail',
258
+ summary: 'Request failed.',
259
+ detail: error instanceof Error ? error.message : String(error),
260
+ };
261
+ }
262
+ }
263
+
216
264
  function countStatuses(checks: readonly LiveVerificationCheck[]): Record<LiveVerificationStatus, number> {
217
265
  return checks.reduce<Record<LiveVerificationStatus, number>>(
218
266
  (counts, check) => {
@@ -256,7 +304,7 @@ export async function buildLiveVerificationReport(options: LiveVerificationOptio
256
304
 
257
305
  checks.push({
258
306
  id: 'compiled-cli-present',
259
- title: 'Compiled GoodVibes CLI binary',
307
+ title: 'Compiled GoodVibes Agent CLI binary',
260
308
  status: existsSync(binaryPath) ? 'pass' : 'fail',
261
309
  summary: existsSync(binaryPath) ? `Found ${binaryPath}.` : `Missing ${binaryPath}.`,
262
310
  });
@@ -264,47 +312,61 @@ export async function buildLiveVerificationReport(options: LiveVerificationOptio
264
312
  if (existsSync(binaryPath)) {
265
313
  checks.push(commandCheck(
266
314
  'cli-version',
267
- 'CLI version command',
268
- await runCommand(binaryPath, ['version'], projectRoot),
269
- 'CLI version returned successfully.',
315
+ 'Agent CLI version command',
316
+ await runCommand(binaryPath, ['--version'], projectRoot),
317
+ 'Agent CLI version returned successfully.',
270
318
  ));
271
319
  checks.push(commandCheck(
272
320
  'cli-status-json',
273
- 'CLI status JSON command',
274
- await runCommand(binaryPath, ['status', '--output', 'json'], projectRoot),
275
- 'CLI status returned parseable JSON.',
321
+ 'Agent CLI status JSON command',
322
+ await runCommand(binaryPath, ['status', '--json'], projectRoot),
323
+ 'Agent CLI status returned parseable JSON.',
324
+ { parseJson: true },
325
+ ));
326
+ checks.push(commandCheck(
327
+ 'cli-compat-json',
328
+ 'Agent CLI compatibility JSON command',
329
+ await runCommand(binaryPath, ['compat', '--json'], projectRoot),
330
+ 'Agent CLI compatibility returned parseable JSON.',
276
331
  { parseJson: true },
277
332
  ));
333
+ checks.push(commandCheck(
334
+ 'cli-agent-knowledge-status',
335
+ 'Agent Knowledge CLI status command',
336
+ await runCommand(binaryPath, ['knowledge', 'status', '--json'], projectRoot),
337
+ 'Agent Knowledge status returned parseable JSON.',
338
+ { parseJson: true, warnOnNonZero: true },
339
+ ));
278
340
  checks.push(commandCheck(
279
341
  'cli-providers',
280
- 'CLI providers command',
342
+ 'Agent CLI providers command',
281
343
  await runCommand(binaryPath, ['providers'], projectRoot),
282
344
  'Provider inventory rendered successfully.',
283
345
  ));
284
346
  checks.push(commandCheck(
285
347
  'cli-control-plane-status',
286
- 'CLI control-plane status command',
348
+ 'Read-only control-plane status command',
287
349
  await runCommand(binaryPath, ['control-plane', 'status'], projectRoot),
288
350
  'Control-plane status rendered successfully.',
289
351
  { warnOnNonZero: true },
290
352
  ));
291
353
  checks.push(commandCheck(
292
354
  'cli-listener-test',
293
- 'CLI listener readiness command',
355
+ 'Read-only listener readiness command',
294
356
  await runCommand(binaryPath, ['listener', 'test'], projectRoot),
295
357
  'HTTP listener readiness rendered successfully.',
296
358
  { warnOnNonZero: true },
297
359
  ));
298
360
  checks.push(commandCheck(
299
361
  'cli-surfaces-check',
300
- 'CLI surfaces readiness command',
362
+ 'Read-only surfaces readiness command',
301
363
  await runCommand(binaryPath, ['surfaces', 'check'], projectRoot),
302
364
  'Surface readiness rendered successfully.',
303
365
  { warnOnNonZero: true },
304
366
  ));
305
367
  checks.push(commandCheck(
306
368
  'cli-service-check',
307
- 'CLI service posture command',
369
+ 'Read-only service posture command',
308
370
  await runCommand(binaryPath, ['service', 'check'], projectRoot),
309
371
  'Service posture rendered successfully.',
310
372
  { warnOnNonZero: true },
@@ -376,6 +438,79 @@ export async function buildLiveVerificationReport(options: LiveVerificationOptio
376
438
  },
377
439
  ));
378
440
 
441
+ checks.push(await fetchJsonCheck(
442
+ 'agent-knowledge-status',
443
+ 'Agent Knowledge isolated /status',
444
+ `${daemonBaseUrl}/api/goodvibes-agent/knowledge/status`,
445
+ token,
446
+ {
447
+ validate: (status, body) => {
448
+ if (status !== 200) return { status: 'fail', summary: `/api/goodvibes-agent/knowledge/status returned ${status}.` };
449
+ try {
450
+ JSON.parse(body);
451
+ return { status: 'pass', summary: 'Agent Knowledge status route returned parseable JSON.' };
452
+ } catch {
453
+ return { status: 'fail', summary: 'Agent Knowledge status was not parseable JSON.' };
454
+ }
455
+ },
456
+ },
457
+ ));
458
+
459
+ checks.push(await fetchJsonCheck(
460
+ 'agent-knowledge-ask-isolated',
461
+ 'Agent Knowledge isolated ask',
462
+ `${daemonBaseUrl}/api/goodvibes-agent/knowledge/ask`,
463
+ token,
464
+ {
465
+ method: 'POST',
466
+ body: {
467
+ query: 'What is GoodVibes Agent?',
468
+ limit: 5,
469
+ mode: 'concise',
470
+ includeSources: true,
471
+ includeConfidence: true,
472
+ includeLinkedObjects: true,
473
+ },
474
+ validate: (status, body) => {
475
+ if (status !== 200) return { status: 'fail', summary: `/api/goodvibes-agent/knowledge/ask returned ${status}.` };
476
+ try {
477
+ JSON.parse(body);
478
+ } catch {
479
+ return { status: 'fail', summary: 'Agent Knowledge ask was not parseable JSON.' };
480
+ }
481
+ const lower = body.toLowerCase();
482
+ if (lower.includes('home assistant') || lower.includes('homegraph') || lower.includes('home graph')) {
483
+ return { status: 'fail', summary: 'Agent Knowledge ask returned HomeGraph/Home Assistant contamination.' };
484
+ }
485
+ return { status: 'pass', summary: 'Agent Knowledge ask stayed on the isolated Agent route.' };
486
+ },
487
+ },
488
+ ));
489
+
490
+ checks.push(await fetchJsonCheck(
491
+ 'agent-knowledge-search-isolated',
492
+ 'Agent Knowledge isolated search',
493
+ `${daemonBaseUrl}/api/goodvibes-agent/knowledge/search`,
494
+ token,
495
+ {
496
+ method: 'POST',
497
+ body: { query: 'What is GoodVibes Agent?', limit: 5 },
498
+ validate: (status, body) => {
499
+ if (status !== 200) return { status: 'fail', summary: `/api/goodvibes-agent/knowledge/search returned ${status}.` };
500
+ try {
501
+ JSON.parse(body);
502
+ } catch {
503
+ return { status: 'fail', summary: 'Agent Knowledge search was not parseable JSON.' };
504
+ }
505
+ const lower = body.toLowerCase();
506
+ if (lower.includes('home assistant') || lower.includes('homegraph') || lower.includes('home graph')) {
507
+ return { status: 'fail', summary: 'Agent Knowledge search returned HomeGraph/Home Assistant contamination.' };
508
+ }
509
+ return { status: 'pass', summary: 'Agent Knowledge search stayed on the isolated Agent route.' };
510
+ },
511
+ },
512
+ ));
513
+
379
514
  const counts = countStatuses(checks);
380
515
  const ok = counts.fail === 0 && (!options.strict || counts.warn === 0);
381
516
  return {
@@ -392,7 +527,7 @@ export async function buildLiveVerificationReport(options: LiveVerificationOptio
392
527
 
393
528
  export function renderLiveVerificationReportMarkdown(report: LiveVerificationReport): string {
394
529
  const lines: string[] = [
395
- '# GoodVibes Live Verification',
530
+ '# GoodVibes Agent Live Verification',
396
531
  '',
397
532
  `Generated: ${report.generatedAt}`,
398
533
  `Home: \`${report.homeDir}\``,
package/src/version.ts CHANGED
@@ -6,12 +6,19 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.1.8';
9
+ let _version = '0.1.10';
10
+ let _sdkVersion = '0.33.35';
10
11
  try {
11
- const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
- _version = pkg.version ?? _version;
12
+ const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {
13
+ readonly version?: unknown;
14
+ readonly dependencies?: Record<string, unknown>;
15
+ };
16
+ _version = typeof pkg.version === 'string' ? pkg.version : _version;
17
+ const packageSdkVersion = pkg.dependencies?.['@pellux/goodvibes-sdk'];
18
+ _sdkVersion = typeof packageSdkVersion === 'string' ? packageSdkVersion : _sdkVersion;
13
19
  } catch {
14
20
  // Compiled binary or missing package.json — use fallback
15
21
  }
16
22
 
17
23
  export const VERSION = _version;
24
+ export const SDK_VERSION = _sdkVersion;
@@ -1,162 +0,0 @@
1
- import type { StatusResult } from 'simple-git';
2
- import { basename } from 'path';
3
- import type { CommandContext } from '../command-registry.ts';
4
- import { GitService } from '@pellux/goodvibes-sdk/platform/git';
5
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
6
-
7
- type GitLike = Pick<GitService, 'addAll' | 'status' | 'commit'>;
8
-
9
- export type GitChange = {
10
- action: 'add' | 'update' | 'delete' | 'rename';
11
- path: string;
12
- from?: string;
13
- to?: string;
14
- };
15
-
16
- type GitStatusLike = Pick<StatusResult, 'staged' | 'modified' | 'not_added' | 'deleted' | 'created' | 'renamed'> & {
17
- isClean?: () => boolean;
18
- };
19
-
20
- export function collectGitChanges(status: GitStatusLike): GitChange[] {
21
- const changes = new Map<string, GitChange>();
22
-
23
- for (const rename of status.renamed ?? []) {
24
- const to = rename.to || rename.from;
25
- if (!to) continue;
26
- changes.set(to, { action: 'rename', path: to, from: rename.from, to: rename.to });
27
- }
28
-
29
- for (const path of status.created ?? []) {
30
- if (!changes.has(path)) changes.set(path, { action: 'add', path });
31
- }
32
-
33
- for (const path of status.not_added ?? []) {
34
- if (!changes.has(path)) changes.set(path, { action: 'add', path });
35
- }
36
-
37
- for (const path of status.deleted ?? []) {
38
- if (!changes.has(path)) changes.set(path, { action: 'delete', path });
39
- }
40
-
41
- for (const path of status.modified ?? []) {
42
- if (!changes.has(path)) changes.set(path, { action: 'update', path });
43
- }
44
-
45
- for (const path of status.staged ?? []) {
46
- if (!changes.has(path)) changes.set(path, { action: 'update', path });
47
- }
48
-
49
- return Array.from(changes.values()).sort((a, b) => a.path.localeCompare(b.path));
50
- }
51
-
52
- function formatList(items: string[]): string {
53
- if (items.length === 0) return '';
54
- if (items.length === 1) return items[0]!;
55
- if (items.length === 2) return `${items[0]} and ${items[1]}`;
56
- return `${items.slice(0, -1).join(', ')}, and ${items[items.length - 1]}`;
57
- }
58
-
59
- function topLevelScope(path: string): string {
60
- const normalized = path.replace(/\\/g, '/').replace(/^\.\/+/, '');
61
- if (!normalized) return 'root';
62
- const [first] = normalized.split('/');
63
- return first && first.length > 0 ? first : 'root';
64
- }
65
-
66
- function summarizeScopes(changes: GitChange[]): string | null {
67
- const scopes = Array.from(new Set(changes.map((change) => topLevelScope(change.path)).filter(Boolean)));
68
- if (scopes.length === 0) return null;
69
- if (scopes.length <= 3) {
70
- return scopes.length === 1 ? `${scopes[0]} files` : `${formatList(scopes)} files`;
71
- }
72
- return `${changes.length} files`;
73
- }
74
-
75
- function shortPath(path: string): string {
76
- return path.length > 42 ? basename(path) : path;
77
- }
78
-
79
- export function buildWriteQuitCommitMessage(changes: GitChange[]): string {
80
- if (changes.length === 0) return 'Update working tree';
81
-
82
- if (changes.length === 1) {
83
- const [change] = changes;
84
- if (!change) return 'Update working tree';
85
- if (change.action === 'rename') {
86
- return `Rename ${shortPath(change.from ?? change.path)} to ${shortPath(change.to ?? change.path)}`;
87
- }
88
- const verb = change.action === 'add'
89
- ? 'Add'
90
- : change.action === 'delete'
91
- ? 'Delete'
92
- : 'Update';
93
- return `${verb} ${shortPath(change.path)}`;
94
- }
95
-
96
- const uniqueActions = Array.from(new Set(changes.map((change) => change.action)));
97
- if (uniqueActions.length === 1) {
98
- const [action] = uniqueActions;
99
- const verb = action === 'add'
100
- ? 'Add'
101
- : action === 'delete'
102
- ? 'Delete'
103
- : action === 'rename'
104
- ? 'Rename'
105
- : 'Update';
106
- const scopeLabel = summarizeScopes(changes);
107
- if (scopeLabel) return `${verb} ${scopeLabel}`;
108
- return `${verb} ${changes.length} files`;
109
- }
110
-
111
- const scopeLabel = summarizeScopes(changes);
112
- if (scopeLabel) return `Update ${scopeLabel}`;
113
- return `Update ${changes.length} files`;
114
- }
115
-
116
- export type ExecuteWriteQuitOptions = {
117
- cwd?: string;
118
- isGitRepo?: (cwd: string) => boolean;
119
- getRepoRoot?: (cwd: string) => string | null;
120
- gitFactory?: (cwd: string) => GitLike;
121
- };
122
-
123
- export async function executeWriteQuit(
124
- ctx: Pick<CommandContext, 'print' | 'exit'> & {
125
- workspace?: CommandContext['workspace'];
126
- },
127
- options: ExecuteWriteQuitOptions = {},
128
- ): Promise<void> {
129
- const cwd = options.cwd ?? ctx.workspace?.shellPaths?.workingDirectory;
130
- if (!cwd) {
131
- throw new Error('commandContext.workspace.shellPaths is required when executeWriteQuit() is called without an explicit cwd');
132
- }
133
- const isGitRepo = options.isGitRepo ?? ((dir: string) => GitService.isGitRepo(dir));
134
- if (!isGitRepo(cwd)) {
135
- ctx.exit();
136
- return;
137
- }
138
-
139
- const repoRoot = options.getRepoRoot?.(cwd) ?? GitService.getRepoRoot(cwd) ?? cwd;
140
- const git = options.gitFactory?.(repoRoot) ?? new GitService(repoRoot);
141
-
142
- try {
143
- ctx.print(`[wq] Staging changes in ${repoRoot}...`);
144
- await git.addAll();
145
- const status = await git.status();
146
- if (status.isClean()) {
147
- ctx.print('[wq] Working tree clean. Exiting without creating a commit.');
148
- ctx.exit();
149
- return;
150
- }
151
-
152
- const changes = collectGitChanges(status);
153
- const message = buildWriteQuitCommitMessage(changes);
154
- ctx.print(`[wq] Committing ${changes.length} change${changes.length === 1 ? '' : 's'}: ${message}`);
155
- const result = await git.commit(message);
156
- const shortHash = result.hash ? result.hash.slice(0, 7) : 'unknown';
157
- ctx.print(`[wq] Commit complete: ${shortHash} ${message}`);
158
- ctx.exit();
159
- } catch (error) {
160
- ctx.print(`[wq] Commit failed: ${summarizeError(error)}`);
161
- }
162
- }