@kitkat-vn/kitcode 0.2.0-staging.3

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/bin/kitcode.mjs +1097 -0
  4. package/package.json +41 -0
  5. package/src/api-access.mjs +119 -0
  6. package/src/api.mjs +165 -0
  7. package/src/atomic-file.mjs +85 -0
  8. package/src/break-progress.mjs +87 -0
  9. package/src/claim-bundle.mjs +37 -0
  10. package/src/companion-controls.mjs +40 -0
  11. package/src/companion-electron.mjs +87 -0
  12. package/src/companion-preload.cjs +10 -0
  13. package/src/companion-window.mjs +57 -0
  14. package/src/cors.mjs +51 -0
  15. package/src/equals-ledger.mjs +166 -0
  16. package/src/focus-clock.mjs +86 -0
  17. package/src/fonts/fira-code/OFL.txt +93 -0
  18. package/src/fonts/fira-code/fira-code-latin-400-normal.woff2 +0 -0
  19. package/src/fonts/fira-code/fira-code-latin-500-normal.woff2 +0 -0
  20. package/src/fonts/fira-code/fira-code-latin-700-normal.woff2 +0 -0
  21. package/src/fonts/vt323/OFL.txt +93 -0
  22. package/src/fonts/vt323/vt323-latin-400-normal.woff2 +0 -0
  23. package/src/git.mjs +448 -0
  24. package/src/hook-installers.mjs +397 -0
  25. package/src/hook-prompt.mjs +201 -0
  26. package/src/install-id.mjs +94 -0
  27. package/src/integration-installers.mjs +28 -0
  28. package/src/integration-spec.mjs +398 -0
  29. package/src/log-file.mjs +60 -0
  30. package/src/milestones-reached.mjs +130 -0
  31. package/src/next-break-reminder.mjs +78 -0
  32. package/src/onboarding-electron.mjs +240 -0
  33. package/src/onboarding-platform.mjs +50 -0
  34. package/src/onboarding-preload.cjs +19 -0
  35. package/src/onboarding-window.mjs +874 -0
  36. package/src/open-dashboard.mjs +30 -0
  37. package/src/reward.mjs +311 -0
  38. package/src/runner-installer.mjs +153 -0
  39. package/src/runtime.mjs +1323 -0
  40. package/src/scan-worker.mjs +23 -0
  41. package/src/scanner.mjs +84 -0
  42. package/src/seen-lines.mjs +148 -0
  43. package/src/skill-installer.mjs +76 -0
  44. package/src/source-snapshot.mjs +725 -0
  45. package/src/store.mjs +595 -0
  46. package/src/tracker-instance.mjs +166 -0
  47. package/src/tracker-problems.mjs +30 -0
  48. package/src/tracker-stop.mjs +73 -0
  49. package/src/uninstall.mjs +80 -0
  50. package/src/vibe.mjs +4 -0
  51. package/src/window-guards.mjs +7 -0
  52. package/src/window-page.mjs +49 -0
@@ -0,0 +1,78 @@
1
+ import {milestoneProgressPercent} from './break-progress.mjs';
2
+
3
+ export function formatBreakDuration(seconds) {
4
+ const totalSeconds = Math.max(0, Math.floor(Number(seconds) || 0));
5
+
6
+ if (totalSeconds < 60) {
7
+ return `${totalSeconds}s`;
8
+ }
9
+
10
+ const hours = Math.floor(totalSeconds / 3600);
11
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
12
+
13
+ if (hours > 0) {
14
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
15
+ }
16
+
17
+ return `${minutes}m`;
18
+ }
19
+
20
+ export function equalsToBreakCopy(equalsLeft) {
21
+ const remaining = Math.max(0, Math.floor(Number(equalsLeft) || 0));
22
+ return remaining === 0 ? '= done' : `${remaining} = to break`;
23
+ }
24
+
25
+ export function timeToBreakCopy(secondsLeft, durationLeft = formatBreakDuration(secondsLeft)) {
26
+ const remaining = Math.max(0, Math.floor(Number(secondsLeft) || 0));
27
+ return remaining === 0 ? 'time done' : `${durationLeft} to break`;
28
+ }
29
+
30
+ export function findNextBreakMilestone(milestones = []) {
31
+ const list = Array.isArray(milestones) ? milestones : [];
32
+
33
+ return list.find((milestone) => (
34
+ !milestone.redeemed &&
35
+ milestone.status !== 'ready' &&
36
+ milestone.status !== 'redeemed' &&
37
+ !milestone.unlocked
38
+ )) ?? list.find((milestone) => !milestone.redeemed && milestone.status === 'locked') ?? null;
39
+ }
40
+
41
+ export function nextBreakReminder(reward) {
42
+ if (!reward || typeof reward !== 'object') {
43
+ return null;
44
+ }
45
+
46
+ const milestone = findNextBreakMilestone(reward.milestones);
47
+ if (!milestone) {
48
+ return null;
49
+ }
50
+
51
+ const earnedSeconds = Math.max(0, Number(reward.earnedSeconds) || 0);
52
+ const totalEquals = Math.max(0, Number(reward.totalEquals) || 0);
53
+ const requiredSeconds = Math.max(0, Number(milestone.requiredSeconds) || 0);
54
+ const requiredEquals = Math.max(0, Number(milestone.requiredEquals) || 0);
55
+ // Floored, so it reads 100 only once the milestone is unlocked (docs/reward-rules.md §10).
56
+ const progressPercent = milestoneProgressPercent(reward, milestone);
57
+ const equalsLeft = Math.max(0, requiredEquals - totalEquals);
58
+ const secondsLeft = Math.max(0, requiredSeconds - earnedSeconds);
59
+ const durationLeft = formatBreakDuration(secondsLeft);
60
+ const almost = progressPercent >= 80;
61
+ const equalsCopy = equalsToBreakCopy(equalsLeft);
62
+ const timeCopy = timeToBreakCopy(secondsLeft, durationLeft);
63
+ const shortLine = `${equalsCopy} · ${timeCopy}`;
64
+
65
+ return {
66
+ percent: milestone.percent,
67
+ equalsLeft,
68
+ secondsLeft,
69
+ progressPercent,
70
+ almost,
71
+ durationLeft,
72
+ equalsCopy,
73
+ timeCopy,
74
+ shortLine,
75
+ mentionLine: `Break next: ${shortLine} (${milestone.percent}%).`,
76
+ metaLine: shortLine,
77
+ };
78
+ }
@@ -0,0 +1,240 @@
1
+ import fs from 'node:fs';
2
+ import {spawn} from 'node:child_process';
3
+ import {app, BrowserWindow, dialog, ipcMain, nativeTheme} from 'electron';
4
+ import path from 'node:path';
5
+ import {fileURLToPath} from 'node:url';
6
+ import {describeProjects, listProjectRecords, registerNewProjects, removeProject, resolveInitialProjectSuggestion} from './runtime.mjs';
7
+ import {DASHBOARD_URL} from './integration-spec.mjs';
8
+ import {onboardingPreferences, saveOnboardingPreferences, STORE_DIR} from './store.mjs';
9
+ import {resolveSetupPlatform} from './onboarding-platform.mjs';
10
+ import {renderOnboardingWindow} from './onboarding-window.mjs';
11
+ import {keepWindowOnItsPage} from './window-guards.mjs';
12
+
13
+ const sourceDirectory = path.dirname(fileURLToPath(import.meta.url));
14
+ const preloadPath = path.join(sourceDirectory, 'onboarding-preload.cjs');
15
+ const cliEntry = process.env.KITCODE_CLI_ENTRY;
16
+ const companionEntry = path.join(sourceDirectory, 'companion-electron.mjs');
17
+ const host = process.env.KITCODE_HOST ?? '127.0.0.1';
18
+ const port = process.env.KITCODE_PORT ?? '4747';
19
+ let window;
20
+ let pendingFocus = false;
21
+ const gotSingleInstanceLock = app.requestSingleInstanceLock();
22
+
23
+ function focusSetupWindow(platform) {
24
+ if (!window || window.isDestroyed()) {
25
+ return;
26
+ }
27
+ if (window.isMinimized()) {
28
+ window.restore();
29
+ }
30
+ if (platform === 'darwin') {
31
+ app.dock?.show();
32
+ }
33
+ window.show();
34
+ window.focus();
35
+ }
36
+
37
+ if (!gotSingleInstanceLock) {
38
+ app.quit();
39
+ process.exit(0);
40
+ } else {
41
+ app.on('second-instance', () => {
42
+ if (window && !window.isDestroyed()) {
43
+ focusSetupWindow(resolveSetupPlatform());
44
+ } else {
45
+ pendingFocus = true;
46
+ }
47
+ });
48
+ }
49
+
50
+ function validFolders(folders) {
51
+ return [...new Set((Array.isArray(folders) ? folders : []).map((folder) => path.resolve(String(folder))))]
52
+ .filter((folder) => {
53
+ try { return fs.statSync(folder).isDirectory(); } catch { return false; }
54
+ });
55
+ }
56
+
57
+ function runCli(args) {
58
+ return new Promise((resolve) => {
59
+ if (!cliEntry) return resolve(false);
60
+ const child = spawn(process.env.KITCODE_NODE_PATH ?? process.execPath, [cliEntry, ...args], {
61
+ detached: true,
62
+ env: {
63
+ ...process.env,
64
+ KITCODE_NO_OPEN: '1',
65
+ NO_COLOR: process.env.NO_COLOR ?? '1',
66
+ },
67
+ stdio: 'ignore',
68
+ windowsHide: true,
69
+ });
70
+ child.once('error', () => resolve(false));
71
+ child.once('exit', (code) => resolve(code === 0));
72
+ child.unref();
73
+ });
74
+ }
75
+
76
+ function openCompanion() {
77
+ const child = spawn(process.execPath, [companionEntry], {
78
+ detached: true, stdio: 'ignore',
79
+ env: {
80
+ ...process.env,
81
+ KITCODE_HOST: host,
82
+ KITCODE_PORT: port,
83
+ KITCODE_DASHBOARD_URL: DASHBOARD_URL,
84
+ KITCODE_NO_OPEN: '1',
85
+ },
86
+ windowsHide: true,
87
+ });
88
+ child.unref();
89
+ }
90
+
91
+ function onboardingHtmlPath(platform) {
92
+ const cacheDir = path.join(STORE_DIR, 'cache');
93
+ fs.mkdirSync(cacheDir, {recursive: true});
94
+ const htmlPath = path.join(cacheDir, 'welcome.html');
95
+ fs.writeFileSync(htmlPath, renderOnboardingWindow(platform), 'utf8');
96
+ return htmlPath;
97
+ }
98
+
99
+ function loadOnboardingContent(window, platform) {
100
+ try {
101
+ window.loadFile(onboardingHtmlPath(platform));
102
+ return;
103
+ } catch {
104
+ // Fall back when local cache is unavailable.
105
+ }
106
+
107
+ window.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(renderOnboardingWindow(platform))}`);
108
+ }
109
+
110
+ function initialSuggestedProjects() {
111
+ const raw = process.env.KITCODE_INITIAL_PROJECT?.trim();
112
+ if (!raw) {
113
+ return [];
114
+ }
115
+
116
+ const project = resolveInitialProjectSuggestion(raw);
117
+ return project ? [project] : [];
118
+ }
119
+
120
+ function setupWindowOptions(platform) {
121
+ return {
122
+ width: 960,
123
+ height: 640,
124
+ resizable: false,
125
+ maximizable: false,
126
+ movable: true,
127
+ center: true,
128
+ show: false,
129
+ frame: false,
130
+ transparent: true,
131
+ hasShadow: false,
132
+ backgroundColor: '#00000000',
133
+ title: 'Welcome to KitCode',
134
+ webPreferences: {preload: preloadPath, contextIsolation: true, nodeIntegration: false, sandbox: true},
135
+ };
136
+ }
137
+
138
+ app.whenReady().then(() => {
139
+ if (!gotSingleInstanceLock) {
140
+ return;
141
+ }
142
+
143
+ const platform = resolveSetupPlatform(process.env.KITCODE_SETUP_PLATFORM || process.platform);
144
+ nativeTheme.themeSource = 'dark';
145
+ if (platform === 'darwin') {
146
+ app.dock?.hide();
147
+ }
148
+ window = new BrowserWindow(setupWindowOptions(platform));
149
+ keepWindowOnItsPage(window);
150
+ // Project records carry absolute paths: only Welcome's own page gets them.
151
+ ipcMain.handle('kitcode:onboarding-initial-state', (event) => {
152
+ if (event.sender !== window.webContents) return {ok: false, error: 'Invalid setup request.', projects: [], suggestedProjects: []};
153
+ return {
154
+ ...onboardingPreferences(),
155
+ projects: listProjectRecords(),
156
+ suggestedProjects: initialSuggestedProjects(),
157
+ };
158
+ });
159
+ ipcMain.handle('kitcode:onboarding-select-folders', async (event) => {
160
+ if (event.sender !== window.webContents) return {canceled: true, paths: []};
161
+ const result = await dialog.showOpenDialog({properties: ['openDirectory', 'multiSelections', 'createDirectory']});
162
+ const paths = validFolders(result.filePaths);
163
+ return {canceled: result.canceled, paths, projects: describeProjects(paths)};
164
+ });
165
+ ipcMain.handle('kitcode:onboarding-submit', async (event, input) => {
166
+ if (event.sender !== window.webContents) return {ok: false, error: 'Invalid setup request.'};
167
+ const folders = validFolders(input?.folders);
168
+ if (!folders.length && !listProjectRecords().length) return {ok: false, error: 'Add at least one readable project folder.'};
169
+ try { registerNewProjects(folders); } catch { return {ok: false, error: 'KitCode could not register one of those folders.'}; }
170
+ const selection = {autoTrack: input?.autoTrack === true, companionView: 'mini'};
171
+ if (selection.autoTrack && !(await runCli(['track', '--host', host, '--port', port]))) {
172
+ saveOnboardingPreferences({...selection, completed: false});
173
+ return {
174
+ ok: false,
175
+ error: 'Projects were saved, but the tracker could not start. Free the local port and retry.',
176
+ projects: listProjectRecords(),
177
+ };
178
+ }
179
+ const preferences = saveOnboardingPreferences(selection);
180
+ const projects = listProjectRecords();
181
+ setTimeout(() => {
182
+ openCompanion();
183
+ window?.close();
184
+ }, 450);
185
+ return {ok: true, projects};
186
+ });
187
+ ipcMain.handle('kitcode:onboarding-remove-project', async (event, projectId) => {
188
+ if (event.sender !== window.webContents) return {ok: false, error: 'Invalid setup request.'};
189
+ if (!projectId) return {ok: false, error: 'Project not found.'};
190
+ try {
191
+ const removed = removeProject(projectId);
192
+ if (!removed) return {ok: false, error: 'Project not found.'};
193
+ return {ok: true, projects: listProjectRecords()};
194
+ } catch {
195
+ return {ok: false, error: 'KitCode could not remove that project.'};
196
+ }
197
+ });
198
+ ipcMain.handle('kitcode:onboarding-close', (event) => {
199
+ if (event.sender === window.webContents) window.close();
200
+ return {closed: true};
201
+ });
202
+
203
+ let revealed = false;
204
+ let readyToShow = false;
205
+ let contentLoaded = false;
206
+
207
+ function revealSetupWindow() {
208
+ if (revealed || window.isDestroyed() || !readyToShow || !contentLoaded) {
209
+ return;
210
+ }
211
+ revealed = true;
212
+ focusSetupWindow(platform);
213
+ pendingFocus = false;
214
+ }
215
+
216
+ window.webContents.on('preload-error', (_event, preloadScript, error) => {
217
+ console.error(`KitCode Welcome preload failed (${preloadScript}):`, error?.message ?? error);
218
+ });
219
+ window.webContents.on('did-fail-load', (_event, code, description, url) => {
220
+ console.error(`KitCode Welcome failed to load (${code}):`, description, url);
221
+ });
222
+ window.once('ready-to-show', () => {
223
+ readyToShow = true;
224
+ revealSetupWindow();
225
+ });
226
+ window.webContents.once('did-finish-load', () => {
227
+ contentLoaded = true;
228
+ revealSetupWindow();
229
+ });
230
+ loadOnboardingContent(window, platform);
231
+ });
232
+
233
+ app.on('window-all-closed', () => {
234
+ ipcMain.removeHandler('kitcode:onboarding-initial-state');
235
+ ipcMain.removeHandler('kitcode:onboarding-select-folders');
236
+ ipcMain.removeHandler('kitcode:onboarding-submit');
237
+ ipcMain.removeHandler('kitcode:onboarding-remove-project');
238
+ ipcMain.removeHandler('kitcode:onboarding-close');
239
+ app.quit();
240
+ });
@@ -0,0 +1,50 @@
1
+ const PLATFORM_CHROME = {
2
+ darwin: {
3
+ id: 'darwin',
4
+ marker: 'macos',
5
+ chromeClass: 'title-bar',
6
+ },
7
+ win32: {
8
+ id: 'win32',
9
+ marker: 'windows',
10
+ chromeClass: 'title-bar',
11
+ },
12
+ linux: {
13
+ id: 'linux',
14
+ marker: 'linux',
15
+ chromeClass: 'title-bar',
16
+ },
17
+ };
18
+
19
+ const PLATFORM_ALIASES = {
20
+ darwin: 'darwin',
21
+ macos: 'darwin',
22
+ mac: 'darwin',
23
+ win32: 'win32',
24
+ windows: 'win32',
25
+ win: 'win32',
26
+ linux: 'linux',
27
+ };
28
+
29
+ export function normalizeSetupPlatform(value) {
30
+ if (value == null || value === '') {
31
+ return null;
32
+ }
33
+ return PLATFORM_ALIASES[String(value).trim().toLowerCase()] ?? null;
34
+ }
35
+
36
+ export function resolveSetupPlatform(platform = process.platform) {
37
+ return normalizeSetupPlatform(platform) ?? 'linux';
38
+ }
39
+
40
+ export function setupPlatformTheme(platform) {
41
+ return PLATFORM_CHROME[resolveSetupPlatform(platform)];
42
+ }
43
+
44
+ export function renderPlatformChrome(theme = setupPlatformTheme()) {
45
+ return `<div class="${theme.chromeClass}" title="Drag window">
46
+ <button class="dot filled dot-close" id="closeButton" type="button" aria-label="Close setup" title="Close"></button>
47
+ <span class="dot" aria-hidden="true"></span>
48
+ <span class="dot" aria-hidden="true"></span>
49
+ </div>`;
50
+ }
@@ -0,0 +1,19 @@
1
+ const {contextBridge, ipcRenderer} = require('electron');
2
+
3
+ contextBridge.exposeInMainWorld('kitcodeOnboarding', {
4
+ selectFolders() {
5
+ return ipcRenderer.invoke('kitcode:onboarding-select-folders');
6
+ },
7
+ submit(input) {
8
+ return ipcRenderer.invoke('kitcode:onboarding-submit', input);
9
+ },
10
+ removeProject(projectId) {
11
+ return ipcRenderer.invoke('kitcode:onboarding-remove-project', projectId);
12
+ },
13
+ initialState() {
14
+ return ipcRenderer.invoke('kitcode:onboarding-initial-state');
15
+ },
16
+ close() {
17
+ return ipcRenderer.invoke('kitcode:onboarding-close');
18
+ },
19
+ });