@pellux/goodvibes-agent 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/release-and-publishing.md +2 -2
- package/package.json +4 -1
- package/src/cli/agent-knowledge-command.ts +46 -20
- package/src/cli/help.ts +15 -2
- package/src/cli/management-commands.ts +3 -3
- package/src/cli/management.ts +7 -1
- package/src/cli/parser.ts +3 -0
- package/src/cli/service-posture.ts +6 -6
- package/src/cli/status.ts +9 -9
- package/src/cli/surface-command.ts +3 -3
- package/src/cli/types.ts +2 -0
- package/src/input/commands/cloudflare-runtime.ts +20 -5
- package/src/input/commands/confirmation.ts +24 -0
- package/src/input/commands/discovery-runtime.ts +16 -7
- package/src/input/commands/eval.ts +27 -14
- package/src/input/commands/experience-runtime.ts +66 -27
- package/src/input/commands/health-runtime.ts +1 -1
- package/src/input/commands/hooks-runtime.ts +79 -20
- package/src/input/commands/incident-runtime.ts +17 -6
- package/src/input/commands/integration-runtime.ts +93 -50
- package/src/input/commands/knowledge.ts +38 -12
- package/src/input/commands/local-auth-runtime.ts +36 -13
- package/src/input/commands/local-provider-runtime.ts +22 -11
- package/src/input/commands/local-runtime.ts +21 -11
- package/src/input/commands/local-setup.ts +35 -16
- package/src/input/commands/managed-runtime.ts +51 -20
- package/src/input/commands/marketplace-runtime.ts +31 -16
- package/src/input/commands/mcp-runtime.ts +65 -34
- package/src/input/commands/memory-product-runtime.ts +72 -35
- package/src/input/commands/memory.ts +9 -9
- package/src/input/commands/notify-runtime.ts +27 -8
- package/src/input/commands/operator-runtime.ts +85 -17
- package/src/input/commands/planning-runtime.ts +14 -2
- package/src/input/commands/platform-access-runtime.ts +88 -45
- package/src/input/commands/platform-services-runtime.ts +51 -25
- package/src/input/commands/product-runtime.ts +54 -27
- package/src/input/commands/profile-sync-runtime.ts +17 -6
- package/src/input/commands/recall-bundle.ts +38 -17
- package/src/input/commands/recall-query.ts +15 -4
- package/src/input/commands/recall-review.ts +9 -3
- package/src/input/commands/remote-runtime-setup.ts +45 -18
- package/src/input/commands/remote-runtime.ts +25 -9
- package/src/input/commands/replay-runtime.ts +9 -2
- package/src/input/commands/services-runtime.ts +21 -10
- package/src/input/commands/session-content.ts +53 -51
- package/src/input/commands/session-workflow.ts +10 -4
- package/src/input/commands/session.ts +1 -1
- package/src/input/commands/settings-sync-runtime.ts +40 -17
- package/src/input/commands/share-runtime.ts +12 -4
- package/src/input/commands/shell-core.ts +3 -3
- package/src/input/commands/subscription-runtime.ts +35 -20
- package/src/input/commands/teleport-runtime.ts +16 -5
- package/src/input/commands/work-plan-runtime.ts +23 -12
- package/src/input/handler-content-actions.ts +11 -62
- package/src/input/handler-interactions.ts +1 -1
- package/src/input/handler-onboarding-cloudflare.ts +48 -117
- package/src/input/handler.ts +1 -0
- package/src/input/keybindings.ts +1 -1
- package/src/input/mcp-workspace.ts +25 -49
- package/src/input/onboarding/onboarding-runtime-status.ts +8 -8
- package/src/input/onboarding/onboarding-wizard-apply.ts +13 -53
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +12 -12
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +2 -7
- package/src/input/onboarding/onboarding-wizard-constants.ts +7 -7
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +4 -4
- package/src/input/onboarding/onboarding-wizard-steps.ts +13 -13
- package/src/input/profile-picker-modal.ts +13 -31
- package/src/input/session-picker-modal.ts +4 -30
- package/src/input/settings-modal-agent-policy.ts +18 -0
- package/src/input/settings-modal-subscriptions.ts +3 -3
- package/src/input/settings-modal-types.ts +17 -0
- package/src/input/settings-modal.ts +30 -29
- package/src/main.ts +3 -26
- package/src/panels/incident-review-panel.ts +1 -1
- package/src/panels/local-auth-panel.ts +4 -4
- package/src/panels/provider-account-snapshot.ts +1 -1
- package/src/panels/provider-health-domains.ts +2 -2
- package/src/panels/settings-sync-panel.ts +2 -2
- package/src/panels/subscription-panel.ts +7 -7
- package/src/renderer/block-actions.ts +1 -1
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/mcp-workspace.ts +12 -12
- package/src/renderer/process-modal.ts +17 -8
- package/src/renderer/profile-picker-modal.ts +3 -11
- package/src/renderer/session-picker-modal.ts +2 -10
- package/src/renderer/settings-modal.ts +12 -8
- package/src/renderer/ui-factory.ts +4 -32
- package/src/runtime/bootstrap-shell.ts +0 -13
- package/src/runtime/bootstrap.ts +0 -10
- package/src/runtime/onboarding/derivation.ts +6 -6
- package/src/verification/live-verifier.ts +148 -13
- package/src/version.ts +10 -3
- package/src/input/commands/quit-shared.ts +0 -162
- package/src/renderer/git-status.ts +0 -89
|
@@ -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', '--
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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.
|
|
9
|
+
let _version = '0.1.11';
|
|
10
|
+
let _sdkVersion = '0.33.35';
|
|
10
11
|
try {
|
|
11
|
-
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'))
|
|
12
|
-
|
|
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
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { GitService } from '@pellux/goodvibes-sdk/platform/git';
|
|
2
|
-
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
3
|
-
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
4
|
-
|
|
5
|
-
/** Git state shown in the header bar. */
|
|
6
|
-
export interface GitHeaderInfo {
|
|
7
|
-
branch: string;
|
|
8
|
-
dirty: boolean;
|
|
9
|
-
ahead: number;
|
|
10
|
-
behind: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const FALLBACK: GitHeaderInfo = { branch: '?', dirty: false, ahead: 0, behind: 0 };
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* GitStatusProvider — Fetches git state for the header bar.
|
|
17
|
-
*
|
|
18
|
-
* Results are cached for 2 seconds (TTL). The next call after expiry triggers
|
|
19
|
-
* a fresh fetch and returns the cached value immediately (stale-while-revalidate).
|
|
20
|
-
* Never throws — returns FALLBACK on any error.
|
|
21
|
-
*/
|
|
22
|
-
export class GitStatusProvider {
|
|
23
|
-
private cache: GitHeaderInfo = { ...FALLBACK };
|
|
24
|
-
private lastFetch = 0;
|
|
25
|
-
private readonly ttlMs = 2000;
|
|
26
|
-
private fetching = false;
|
|
27
|
-
|
|
28
|
-
constructor(private readonly workingDirectory: string) {}
|
|
29
|
-
|
|
30
|
-
/** Returns cached info immediately; refreshes in background if TTL expired. */
|
|
31
|
-
async getStatus(): Promise<GitHeaderInfo> {
|
|
32
|
-
const now = Date.now();
|
|
33
|
-
if (now - this.lastFetch < this.ttlMs) {
|
|
34
|
-
return this.cache;
|
|
35
|
-
}
|
|
36
|
-
// Fetch synchronously on first call (no cache yet), otherwise return stale
|
|
37
|
-
if (this.lastFetch === 0) {
|
|
38
|
-
await this._fetch().catch(() => {
|
|
39
|
-
// Ensure fallback is set if _fetch failed before setting lastFetch
|
|
40
|
-
if (this.lastFetch === 0) {
|
|
41
|
-
this.lastFetch = Date.now();
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
} else if (!this.fetching) {
|
|
45
|
-
this._fetch().catch(err => { logger.debug('GitStatusProvider: background refresh failed', { error: summarizeError(err) }); });
|
|
46
|
-
}
|
|
47
|
-
return this.cache;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Force a fresh fetch and update the cache. Returns updated info. */
|
|
51
|
-
async refresh(): Promise<GitHeaderInfo> {
|
|
52
|
-
await this._fetch();
|
|
53
|
-
return this.cache;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
private async _fetch(): Promise<void> {
|
|
57
|
-
if (this.fetching) return;
|
|
58
|
-
this.fetching = true;
|
|
59
|
-
try {
|
|
60
|
-
const git = new GitService(this.workingDirectory);
|
|
61
|
-
const [statusResult, branchResult] = await Promise.all([
|
|
62
|
-
git.status(),
|
|
63
|
-
git.branch(),
|
|
64
|
-
]);
|
|
65
|
-
const dirty =
|
|
66
|
-
statusResult.modified.length > 0 ||
|
|
67
|
-
statusResult.created.length > 0 ||
|
|
68
|
-
statusResult.deleted.length > 0 ||
|
|
69
|
-
statusResult.renamed.length > 0 ||
|
|
70
|
-
statusResult.conflicted.length > 0 ||
|
|
71
|
-
statusResult.not_added.length > 0;
|
|
72
|
-
this.cache = {
|
|
73
|
-
branch: branchResult.current || '?',
|
|
74
|
-
dirty,
|
|
75
|
-
ahead: statusResult.ahead ?? 0,
|
|
76
|
-
behind: statusResult.behind ?? 0,
|
|
77
|
-
};
|
|
78
|
-
this.lastFetch = Date.now();
|
|
79
|
-
} catch {
|
|
80
|
-
// Never throw — return fallback
|
|
81
|
-
if (this.lastFetch === 0) {
|
|
82
|
-
this.cache = { ...FALLBACK };
|
|
83
|
-
this.lastFetch = Date.now();
|
|
84
|
-
}
|
|
85
|
-
} finally {
|
|
86
|
-
this.fetching = false;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|