@kitkat-vn/kitcode 0.2.0-staging.4 → 0.2.0-staging.5
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/bin/kitcode.mjs +174 -5
- package/package.json +2 -2
- package/src/api.mjs +22 -1
- package/src/diagnostics.mjs +872 -0
- package/src/hook-prompt.mjs +13 -0
- package/src/integration-spec.mjs +29 -17
- package/src/onboarding-electron.mjs +8 -0
- package/src/onboarding-window.mjs +47 -7
- package/src/runtime.mjs +11 -6
package/bin/kitcode.mjs
CHANGED
|
@@ -10,6 +10,19 @@ import process from 'node:process';
|
|
|
10
10
|
import {fileURLToPath} from 'node:url';
|
|
11
11
|
import {createServer} from '../src/api.mjs';
|
|
12
12
|
import {isLoopbackAddress} from '../src/api-access.mjs';
|
|
13
|
+
import {
|
|
14
|
+
configProblemCode,
|
|
15
|
+
describeOutcome,
|
|
16
|
+
diagnosticsEnabled,
|
|
17
|
+
diagnosticsStatus,
|
|
18
|
+
errorCodeOf,
|
|
19
|
+
errorNameOf,
|
|
20
|
+
projectRef,
|
|
21
|
+
recordEvent,
|
|
22
|
+
sendQueued,
|
|
23
|
+
startDiagnosticsLoop,
|
|
24
|
+
sweepHookMarkers,
|
|
25
|
+
} from '../src/diagnostics.mjs';
|
|
13
26
|
import {manualHookSnippet} from '../src/hook-installers.mjs';
|
|
14
27
|
import {runPromptHook} from '../src/hook-prompt.mjs';
|
|
15
28
|
import {installIntegration, integrationStatus, uninstallIntegration} from '../src/integration-installers.mjs';
|
|
@@ -236,6 +249,8 @@ Commands:
|
|
|
236
249
|
Internal prompt hook used by Codex and Claude
|
|
237
250
|
codex on|off|status Install, remove, or inspect the Codex hook and skill
|
|
238
251
|
claude on|off|status Install, remove, or inspect the Claude hook and skill
|
|
252
|
+
diagnostics status|send
|
|
253
|
+
A tester build's diagnostics: what waits, and send it now
|
|
239
254
|
|
|
240
255
|
Options:
|
|
241
256
|
--host <host> Host to bind, default ${DEFAULT_HOST}
|
|
@@ -443,7 +458,7 @@ function openElectronEntry(entryName, environment = {}) {
|
|
|
443
458
|
|
|
444
459
|
function printElectronSetupHelp() {
|
|
445
460
|
console.error('KitCode setup needs Electron for the desktop folder picker.');
|
|
446
|
-
console.error("Reinstall the tester build:
|
|
461
|
+
console.error("Reinstall the tester build with: npm install -g @kitkat-vn/kitcode@staging");
|
|
447
462
|
console.error('If install succeeded but Electron is still missing, fix npm cache permissions and reinstall.');
|
|
448
463
|
}
|
|
449
464
|
|
|
@@ -458,7 +473,7 @@ function printElectronLaunchFailure(result, label) {
|
|
|
458
473
|
|
|
459
474
|
if (result?.reason === 'electron-binary-missing') {
|
|
460
475
|
console.error(`Electron's binary is missing: Electron points to ${result.detail}, and nothing is there.`);
|
|
461
|
-
console.error("Reinstall the tester build:
|
|
476
|
+
console.error("Reinstall the tester build with: npm install -g @kitkat-vn/kitcode@staging");
|
|
462
477
|
return;
|
|
463
478
|
}
|
|
464
479
|
|
|
@@ -480,6 +495,8 @@ async function openOnboardingWindow(options = {}) {
|
|
|
480
495
|
const platform = normalizeSetupPlatform(options.setupPlatform);
|
|
481
496
|
if (!platform) {
|
|
482
497
|
console.error('Invalid --platform. Use macos, windows, or linux (aliases: darwin, win32).');
|
|
498
|
+
recordWelcomeFailed(options.welcomeCommand ?? 'setup', {reason: 'invalid-platform'});
|
|
499
|
+
await sendOnce();
|
|
483
500
|
return false;
|
|
484
501
|
}
|
|
485
502
|
environment.KITCODE_SETUP_PLATFORM = platform;
|
|
@@ -491,6 +508,8 @@ async function openOnboardingWindow(options = {}) {
|
|
|
491
508
|
}
|
|
492
509
|
|
|
493
510
|
printElectronLaunchFailure(result, 'KitCode Welcome');
|
|
511
|
+
recordWelcomeFailed(options.welcomeCommand ?? 'setup', result);
|
|
512
|
+
await sendOnce();
|
|
494
513
|
return false;
|
|
495
514
|
}
|
|
496
515
|
|
|
@@ -650,29 +669,44 @@ async function handleUninstall(options) {
|
|
|
650
669
|
printUninstallReport(report);
|
|
651
670
|
}
|
|
652
671
|
|
|
672
|
+
// What became of the hook in the agent's settings, for a tester build's diagnostics: by outcome, and a refusal by a fixed code,
|
|
673
|
+
// never the file's path or its words.
|
|
674
|
+
function recordAgentHook(source, action, hook) {
|
|
675
|
+
recordEvent('agent-hook', {agent: source, action, outcome: hook.action, problem: hook.action === 'refused' ? configProblemCode(hook.error) : 'none'}, {source: 'command'});
|
|
676
|
+
}
|
|
677
|
+
|
|
653
678
|
async function handleHookInstaller(source, action) {
|
|
654
679
|
if (action === 'on') {
|
|
655
680
|
const status = installIntegration(source);
|
|
656
681
|
printIntegrationStatus(source, status);
|
|
682
|
+
recordAgentHook(source, 'on', status.hook);
|
|
683
|
+
let failed = false;
|
|
657
684
|
if (status.hook.action === 'refused') {
|
|
658
685
|
printRefusedHook(source, status.hook);
|
|
659
686
|
process.exitCode = 1;
|
|
687
|
+
failed = true;
|
|
660
688
|
}
|
|
661
689
|
if (!onboardingPreferences().completed) {
|
|
662
|
-
if (!(await openOnboardingWindow({host: DEFAULT_HOST, port: DEFAULT_PORT, initialProject: process.cwd()}))) {
|
|
690
|
+
if (!(await openOnboardingWindow({host: DEFAULT_HOST, port: DEFAULT_PORT, initialProject: process.cwd(), welcomeCommand: `${source}-on`}))) {
|
|
663
691
|
console.error('KitCode Welcome could not open. Fix Electron install, then re-run `kitcode setup`.');
|
|
664
692
|
process.exitCode = 1;
|
|
693
|
+
failed = true;
|
|
665
694
|
}
|
|
666
695
|
}
|
|
696
|
+
if (failed) {
|
|
697
|
+
await sendOnce();
|
|
698
|
+
}
|
|
667
699
|
return;
|
|
668
700
|
}
|
|
669
701
|
|
|
670
702
|
if (action === 'off') {
|
|
671
703
|
const status = uninstallIntegration(source);
|
|
672
704
|
printIntegrationStatus(source, status);
|
|
705
|
+
recordAgentHook(source, 'off', status.hook);
|
|
673
706
|
if (status.hook.action === 'refused') {
|
|
674
707
|
printRefusedHook(source, status.hook);
|
|
675
708
|
process.exitCode = 1;
|
|
709
|
+
await sendOnce();
|
|
676
710
|
}
|
|
677
711
|
return;
|
|
678
712
|
}
|
|
@@ -690,6 +724,54 @@ async function isServerRunning(options) {
|
|
|
690
724
|
return Boolean(await trackerHealth(options));
|
|
691
725
|
}
|
|
692
726
|
|
|
727
|
+
// `kitcode diagnostics status` and `kitcode diagnostics send` (docs/way-of-working.md, "A full staging round"): what a tester
|
|
728
|
+
// build's diagnostics hold, and Send logs now from the terminal, for when the dashboard cannot reach the tracker. Any other
|
|
729
|
+
// build says it sends nothing, and sends nothing.
|
|
730
|
+
async function handleDiagnostics(action) {
|
|
731
|
+
if (action === 'status' || action === undefined) {
|
|
732
|
+
const status = diagnosticsStatus();
|
|
733
|
+
|
|
734
|
+
if (!status.enabled) {
|
|
735
|
+
console.log(describeOutcome({outcome: 'disabled'}));
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
sweepHookMarkers({source: 'command'});
|
|
740
|
+
console.log('KitCode diagnostics: this tester build sends KitCode\'s own events and errors to its staging server, never your code, files, prompts or project names.');
|
|
741
|
+
console.log(` staging server: ${status.destination}`);
|
|
742
|
+
console.log(` install id: ${status.installId ?? 'none yet'}`);
|
|
743
|
+
console.log(` invited email: ${status.invitedEmail ?? 'none given'}`);
|
|
744
|
+
console.log(` waiting to be sent: ${status.queued} event${status.queued === 1 ? '' : 's'}`);
|
|
745
|
+
|
|
746
|
+
if (status.closedUntil) {
|
|
747
|
+
console.log(` paused: the staging server is not taking diagnostics; KitCode tries again after ${status.closedUntil}`);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
console.log(` last send: ${status.lastSend ? `${status.lastSend.at}, ${describeOutcome(status.lastSend)}` : 'none yet'}`);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (action === 'send') {
|
|
755
|
+
if (!diagnosticsEnabled()) {
|
|
756
|
+
console.log(describeOutcome({outcome: 'disabled'}));
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
sweepHookMarkers({source: 'command'});
|
|
761
|
+
const result = await sendQueued({timeoutMs: 10_000, maxBatches: 5, manual: true, source: 'command'});
|
|
762
|
+
console.log(describeOutcome(result));
|
|
763
|
+
|
|
764
|
+
if (result.outcome !== 'stored' && result.outcome !== 'nothing') {
|
|
765
|
+
process.exitCode = 1;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
printHelp();
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
|
|
693
775
|
// The local API has no login. Bound to anything but loopback, it is on every network that can reach
|
|
694
776
|
// that address, so that takes --allow-non-loopback, and a warning.
|
|
695
777
|
function bindRefusal(options) {
|
|
@@ -712,6 +794,49 @@ function printTrackerNotRunning() {
|
|
|
712
794
|
console.error('KitCode tracker is not running. Run `kitcode track` first.');
|
|
713
795
|
}
|
|
714
796
|
|
|
797
|
+
// A tester build's diagnostics (src/diagnostics.mjs): a command that saw something fail records it and sends what waits,
|
|
798
|
+
// once, with a short timeout, and quietly, since no tracker may be running to send it later. In any other build each
|
|
799
|
+
// returns at once, and nothing is written or sent.
|
|
800
|
+
const COMMAND_NAMES = new Set(['add', 'remove', 'track', 'untrack', 'list', 'status', 'summary', 'awards', 'dashboard', 'setup', 'uninstall', 'claude', 'codex', 'diagnostics']);
|
|
801
|
+
|
|
802
|
+
function commandName(command) {
|
|
803
|
+
const name = command === 'award' || command === 'rewards' ? 'awards' : command;
|
|
804
|
+
return COMMAND_NAMES.has(name) ? name : 'other';
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Once a process: the first failure sends what waits, then, and what a command records after it waits for the next send.
|
|
808
|
+
let sentOnce = false;
|
|
809
|
+
|
|
810
|
+
async function sendOnce() {
|
|
811
|
+
if (!diagnosticsEnabled() || sentOnce) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
sentOnce = true;
|
|
816
|
+
|
|
817
|
+
try {
|
|
818
|
+
sweepHookMarkers({source: 'command'});
|
|
819
|
+
await sendQueued({timeoutMs: 3000, maxBatches: 1, source: 'command'});
|
|
820
|
+
} catch {
|
|
821
|
+
// Quietly: whatever sends next sends it.
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function recordCommandFailed(command, error) {
|
|
826
|
+
recordEvent('command-failed', {command: commandName(command), errorCode: errorCodeOf(error), errorName: errorNameOf(error)}, {source: 'command'});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function recordTrackerNotStarted(source, reason, error = null) {
|
|
830
|
+
recordEvent('tracker-not-started', {reason, errorCode: errorCodeOf(error), errorName: errorNameOf(error)}, {source});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const WELCOME_REASONS = new Set(['missing-electron', 'electron-download-failed', 'electron-binary-missing', 'missing-entry', 'spawn-error', 'exited', 'invalid-platform']);
|
|
834
|
+
|
|
835
|
+
function recordWelcomeFailed(command, result) {
|
|
836
|
+
const exitCode = Number.isInteger(result?.code) ? result.code : null;
|
|
837
|
+
recordEvent('welcome-failed', {command, reason: WELCOME_REASONS.has(result?.reason) ? result.reason : 'other', exitCode}, {source: 'command'});
|
|
838
|
+
}
|
|
839
|
+
|
|
715
840
|
async function runDaemon(options, status = createStatusReporter()) {
|
|
716
841
|
status.set('Starting KitCode tracker...');
|
|
717
842
|
const log = createLog(TRACKER_LOG_PATH);
|
|
@@ -729,6 +854,7 @@ async function runDaemon(options, status = createStatusReporter()) {
|
|
|
729
854
|
// keeps the last periodic save.
|
|
730
855
|
const fatal = (kind) => (error) => {
|
|
731
856
|
log.error(`${kind}: ${error?.stack ?? error}`);
|
|
857
|
+
recordEvent('tracker-crashed', {kind: kind === 'uncaughtException' ? 'uncaught-exception' : 'unhandled-rejection', errorCode: errorCodeOf(error), errorName: errorNameOf(error)}, {source: 'tracker'});
|
|
732
858
|
releaseTracker(process.pid);
|
|
733
859
|
process.exit(1);
|
|
734
860
|
};
|
|
@@ -741,6 +867,7 @@ async function runDaemon(options, status = createStatusReporter()) {
|
|
|
741
867
|
if (refusal) {
|
|
742
868
|
status.stop();
|
|
743
869
|
report('error', refusal);
|
|
870
|
+
recordTrackerNotStarted('tracker', 'bind-refused');
|
|
744
871
|
process.exit(1);
|
|
745
872
|
}
|
|
746
873
|
|
|
@@ -758,12 +885,14 @@ async function runDaemon(options, status = createStatusReporter()) {
|
|
|
758
885
|
if (existing && existing.state !== 'unresponsive') {
|
|
759
886
|
status.stop();
|
|
760
887
|
report('warn', `Not started: another KitCode tracker (process ${existing.record.pid}) is ${existing.state} at http://${existing.record.host}:${existing.record.port}.`);
|
|
888
|
+
recordTrackerNotStarted('tracker', 'already-running');
|
|
761
889
|
process.exit(1);
|
|
762
890
|
}
|
|
763
891
|
|
|
764
892
|
if (!claimTracker(record, existing?.record ?? null).claimed) {
|
|
765
893
|
status.stop();
|
|
766
894
|
report('warn', 'Not started: another KitCode tracker started at the same moment.');
|
|
895
|
+
recordTrackerNotStarted('tracker', 'claimed-elsewhere');
|
|
767
896
|
process.exit(1);
|
|
768
897
|
}
|
|
769
898
|
|
|
@@ -774,6 +903,7 @@ async function runDaemon(options, status = createStatusReporter()) {
|
|
|
774
903
|
} catch (error) {
|
|
775
904
|
releaseTracker(process.pid);
|
|
776
905
|
report('error', `Not started: ${error.message}`);
|
|
906
|
+
recordTrackerNotStarted('tracker', error instanceof StateFileError || error instanceof StateLockError ? 'state-unreadable' : 'start-failed', error);
|
|
777
907
|
throw error;
|
|
778
908
|
}
|
|
779
909
|
|
|
@@ -786,7 +916,17 @@ async function runDaemon(options, status = createStatusReporter()) {
|
|
|
786
916
|
report('warn', `KitCode could not make its install id (${error.message}); the claim bundle is not given out until it can.`);
|
|
787
917
|
}
|
|
788
918
|
|
|
789
|
-
|
|
919
|
+
// A tester build's diagnostics while the tracker runs (src/diagnostics.mjs): its events sent every minute, a heartbeat every
|
|
920
|
+
// hour, and Send logs now through the local API. In any other build it does nothing.
|
|
921
|
+
const startedMs = Date.now();
|
|
922
|
+
const uptimeSeconds = () => Math.max(0, Math.floor((Date.now() - startedMs) / 1000));
|
|
923
|
+
const diagnostics = startDiagnosticsLoop({
|
|
924
|
+
snapshot: () => {
|
|
925
|
+
const current = runtime.status?.() ?? {tracking: 'ok', problems: []};
|
|
926
|
+
return {projects: Object.values(runtime.state.projects).filter((project) => project.active).length, tracking: current.tracking, problems: current.problems.map((problem) => problem.code)};
|
|
927
|
+
},
|
|
928
|
+
});
|
|
929
|
+
const app = createServer(runtime, VERSION, {diagnostics});
|
|
790
930
|
let cleanup = async () => {};
|
|
791
931
|
let server = null;
|
|
792
932
|
|
|
@@ -802,6 +942,8 @@ async function runDaemon(options, status = createStatusReporter()) {
|
|
|
802
942
|
server.close(() => process.exit(code));
|
|
803
943
|
};
|
|
804
944
|
|
|
945
|
+
const PROBLEMS = new Set(['watch-failed', 'project-missing', 'scan-failed', 'git-locked', 'git-unfinished']);
|
|
946
|
+
|
|
805
947
|
cleanup = startWatchers(runtime, {
|
|
806
948
|
log,
|
|
807
949
|
onProgress(message) {
|
|
@@ -811,12 +953,19 @@ async function runDaemon(options, status = createStatusReporter()) {
|
|
|
811
953
|
onStatus(current) {
|
|
812
954
|
updateTrackerRecord(process.pid, {...current, heartbeatAt: new Date().toISOString()});
|
|
813
955
|
},
|
|
956
|
+
// Each project's problem, raised or cleared, by its fixed code and an opaque reference to the project.
|
|
957
|
+
onProblem({code, projectId, state, error}) {
|
|
958
|
+
if (PROBLEMS.has(code)) {
|
|
959
|
+
recordEvent('tracker-problem', {problem: code, state, project: projectRef(projectId), errorCode: errorCodeOf(error)}, {source: 'tracker'});
|
|
960
|
+
}
|
|
961
|
+
},
|
|
814
962
|
// state.json can no longer be read. Stop without writing it: it holds the user's progress.
|
|
815
963
|
onStateError(error) {
|
|
816
964
|
status.stop();
|
|
817
965
|
report('error', `Stopped: ${error.message}`);
|
|
818
966
|
cleanup({persist: false});
|
|
819
|
-
|
|
967
|
+
recordEvent('tracker-stopped', {reason: 'state-unreadable', uptimeSeconds: uptimeSeconds()}, {source: 'tracker'});
|
|
968
|
+
diagnostics.stop().finally(() => exit(1));
|
|
820
969
|
},
|
|
821
970
|
});
|
|
822
971
|
|
|
@@ -832,18 +981,23 @@ async function runDaemon(options, status = createStatusReporter()) {
|
|
|
832
981
|
}
|
|
833
982
|
|
|
834
983
|
log.info(`stopped (${signal})`);
|
|
984
|
+
recordEvent('tracker-stopped', {reason: signal === 'stop request' ? 'stop-request' : 'signal', uptimeSeconds: uptimeSeconds()}, {source: 'tracker'});
|
|
985
|
+
await diagnostics.stop();
|
|
835
986
|
exit(0);
|
|
836
987
|
};
|
|
837
988
|
|
|
838
989
|
server = app.listen(options.port, options.host, () => {
|
|
839
990
|
status.stop();
|
|
840
991
|
log.info(`started: KitCode ${VERSION}, process ${process.pid}, http://${options.host}:${options.port}, node ${process.version}`);
|
|
992
|
+
recordEvent('tracker-started', {projects: Object.values(runtime.state.projects).filter((project) => project.active).length, port: options.port}, {source: 'tracker'});
|
|
841
993
|
});
|
|
842
994
|
|
|
843
995
|
server.on('error', async (error) => {
|
|
844
996
|
status.stop();
|
|
845
997
|
await cleanup().catch(() => {});
|
|
846
998
|
releaseTracker(process.pid);
|
|
999
|
+
recordTrackerNotStarted('tracker', error.code === 'EADDRINUSE' ? 'port-in-use' : 'start-failed', error);
|
|
1000
|
+
await diagnostics.stop({finalSend: false});
|
|
847
1001
|
|
|
848
1002
|
if (error.code === 'EADDRINUSE') {
|
|
849
1003
|
report('error', `Not started: port ${options.port} is already in use. Stop the other server or pass --port <port>.`);
|
|
@@ -874,6 +1028,8 @@ async function startTracker(options) {
|
|
|
874
1028
|
|
|
875
1029
|
if (refusal) {
|
|
876
1030
|
console.error(refusal);
|
|
1031
|
+
recordTrackerNotStarted('command', 'bind-refused');
|
|
1032
|
+
await sendOnce();
|
|
877
1033
|
process.exit(1);
|
|
878
1034
|
}
|
|
879
1035
|
|
|
@@ -952,6 +1108,8 @@ async function startTracker(options) {
|
|
|
952
1108
|
}
|
|
953
1109
|
|
|
954
1110
|
console.error('KitCode tracker did not start. Try running `kitcode track` again.');
|
|
1111
|
+
recordTrackerNotStarted('command', 'did-not-answer');
|
|
1112
|
+
await sendOnce();
|
|
955
1113
|
process.exit(1);
|
|
956
1114
|
}
|
|
957
1115
|
|
|
@@ -1031,6 +1189,8 @@ try {
|
|
|
1031
1189
|
|
|
1032
1190
|
// The folder could not be listed: it is gone, not a folder, or git could not list it.
|
|
1033
1191
|
console.error(`KitCode could not add ${path.resolve(targetPath)}: ${error.message}`);
|
|
1192
|
+
recordCommandFailed('add', error);
|
|
1193
|
+
await sendOnce();
|
|
1034
1194
|
process.exit(1);
|
|
1035
1195
|
}
|
|
1036
1196
|
|
|
@@ -1080,6 +1240,8 @@ try {
|
|
|
1080
1240
|
await handleHookInstaller('codex', options.subcommand);
|
|
1081
1241
|
} else if (options.command === 'claude') {
|
|
1082
1242
|
await handleHookInstaller('claude', options.subcommand);
|
|
1243
|
+
} else if (options.command === 'diagnostics') {
|
|
1244
|
+
await handleDiagnostics(options.subcommand);
|
|
1083
1245
|
} else if (options.command === 'version') {
|
|
1084
1246
|
console.log(VERSION);
|
|
1085
1247
|
} else {
|
|
@@ -1087,6 +1249,13 @@ try {
|
|
|
1087
1249
|
process.exit(options.command === 'help' || !options.command ? 0 : 1);
|
|
1088
1250
|
}
|
|
1089
1251
|
} catch (error) {
|
|
1252
|
+
// A tester build records the failure, and sends it, once: but never from the prompt hook, which touches no network, nor
|
|
1253
|
+
// from the tracker, which recorded its own.
|
|
1254
|
+
if (options.command !== 'hook' && !(options.command === 'track' && process.env.KITCODE_DAEMON === '1')) {
|
|
1255
|
+
recordCommandFailed(options.command, error);
|
|
1256
|
+
await sendOnce();
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1090
1259
|
// A state file KitCode cannot read or lock stops the command with the reason. It is never replaced.
|
|
1091
1260
|
if (error instanceof StateFileError || error instanceof StateLockError) {
|
|
1092
1261
|
console.error(error.message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitkat-vn/kitcode",
|
|
3
|
-
"version": "0.2.0-staging.
|
|
3
|
+
"version": "0.2.0-staging.5",
|
|
4
4
|
"description": "Local aggregate activity server for KitCode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"optionalDependencies": {
|
|
38
38
|
"electron": "^43.0.0"
|
|
39
39
|
},
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "507aaa120cd48a65b06380f3eee194d799428db1"
|
|
41
41
|
}
|
package/src/api.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import {MAX_EVENT_STREAMS, requireDashboardOrigin, requireDashboardRead, require
|
|
|
3
3
|
import {claimBundleFromState} from './claim-bundle.mjs';
|
|
4
4
|
import {corsMiddleware} from './cors.mjs';
|
|
5
5
|
import {renderCompanionWindow} from './companion-window.mjs';
|
|
6
|
+
import {describeOutcome, diagnosticsStatus, sendQueued} from './diagnostics.mjs';
|
|
6
7
|
import {INSTALL_ID_DISPLAY_PATH, InstallIdError, readInstallId} from './install-id.mjs';
|
|
7
8
|
import {STATE_DISPLAY_PATH} from './integration-spec.mjs';
|
|
8
9
|
import {MilestoneRecordError} from './milestones-reached.mjs';
|
|
@@ -20,7 +21,8 @@ const redeemGone = (_req, res) => {
|
|
|
20
21
|
res.status(410).json({error: 'Break codes are no longer shown or recorded: each milestone reached shows its message on the dashboard.'});
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
// `diagnostics` is the tracker's send loop (src/diagnostics.mjs, startDiagnosticsLoop), for Send logs now.
|
|
25
|
+
export function createServer(runtime, version, {diagnostics = null} = {}) {
|
|
24
26
|
const app = express();
|
|
25
27
|
let openEventStreams = 0;
|
|
26
28
|
|
|
@@ -105,6 +107,25 @@ export function createServer(runtime, version) {
|
|
|
105
107
|
}
|
|
106
108
|
});
|
|
107
109
|
|
|
110
|
+
// A tester build's diagnostics, for the staging dashboard (docs/security.md, "Tester diagnostics"): whether this tracker
|
|
111
|
+
// sends them, its install id, which the dashboard links its own events by, what waits, and the last send. Only to an
|
|
112
|
+
// allow-listed Origin, as the claim bundle, which holds the same install id; never the events, or the invited email.
|
|
113
|
+
app.get('/api/diagnostics', requireDashboardOrigin, (_req, res) => {
|
|
114
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
115
|
+
const {invitedEmail, ...status} = diagnosticsStatus();
|
|
116
|
+
res.json(invitedEmail === undefined ? status : {...status, emailGiven: invitedEmail !== null});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Send logs now: the dashboard's button. A write, so it passes every write's guards first: a loopback Host, an allow-listed
|
|
120
|
+
// Origin and a JSON body (api-access.mjs). It sends what waits, past any backoff, and answers with exactly what the
|
|
121
|
+
// server confirmed, or why nothing was sent, in plain words; in a build that sends nothing, that it sends nothing.
|
|
122
|
+
app.post('/api/diagnostics/send', (_req, res, next) => {
|
|
123
|
+
(diagnostics ? diagnostics.sendNow() : sendQueued({timeoutMs: 10_000, maxBatches: 5, manual: true, source: 'tracker'})).then((result) => {
|
|
124
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
125
|
+
res.json({...result, words: describeOutcome(result)});
|
|
126
|
+
}, next);
|
|
127
|
+
});
|
|
128
|
+
|
|
108
129
|
app.get('/api/projects/:id/commits', projectLevelGone);
|
|
109
130
|
app.post('/api/projects/:id/start', projectLevelGone);
|
|
110
131
|
app.post('/api/projects/:id/stop', projectLevelGone);
|