@kitkat-vn/kitcode 0.2.0-staging.9 → 0.3.0-staging.16
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/README.md +50 -28
- package/bin/kitcode.mjs +65 -5
- package/package.json +2 -2
- package/src/api.mjs +105 -3
- package/src/break-progress.mjs +94 -9
- package/src/claim-bundle.mjs +18 -5
- package/src/companion-window.mjs +9 -3
- package/src/cors.mjs +3 -1
- package/src/cycle-receipt.mjs +97 -0
- package/src/fonts/jetbrains-mono/OFL.txt +93 -0
- package/src/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/src/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/src/fonts/jetbrains-mono/jetbrains-mono-latin-700-normal.woff2 +0 -0
- package/src/fonts/jetbrains-mono/jetbrains-mono-vietnamese-400-normal.woff2 +0 -0
- package/src/fonts/jetbrains-mono/jetbrains-mono-vietnamese-500-normal.woff2 +0 -0
- package/src/fonts/jetbrains-mono/jetbrains-mono-vietnamese-700-normal.woff2 +0 -0
- package/src/hook-prompt.mjs +24 -3
- package/src/integration-spec.mjs +175 -48
- package/src/milestone-record.mjs +214 -0
- package/src/milestones-reached.mjs +168 -64
- package/src/next-break-reminder.mjs +4 -3
- package/src/onboarding-electron.mjs +12 -11
- package/src/onboarding-platform.mjs +5 -2
- package/src/onboarding-window.mjs +94 -63
- package/src/reward.mjs +247 -68
- package/src/runtime.mjs +48 -4
- package/src/tracking-config.mjs +501 -0
- package/src/welcome-words.mjs +115 -0
- package/src/window-page.mjs +16 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {equalsLedgerFrom} from './equals-ledger.mjs';
|
|
2
2
|
import {focusTimeFrom} from './focus-clock.mjs';
|
|
3
|
-
import {
|
|
3
|
+
import {readMilestoneRecord} from './milestone-record.mjs';
|
|
4
|
+
import {decideMilestones, MILESTONE_PERCENTS, milestonesOf, REWARD_TIERS} from './reward.mjs';
|
|
4
5
|
|
|
5
6
|
// When each milestone first became ready (docs/reward-rules.md §6.1), for the claim bundle. Kept in
|
|
6
7
|
// state.json, where the tracker writes the progress it belongs to, as `milestonesReached`:
|
|
@@ -17,40 +18,38 @@ import {buildRewardSummary, REWARD_MILESTONES} from './reward.mjs';
|
|
|
17
18
|
// 0.1.39 drops it when it rewrites the file (install-id.mjs says how). The next write of this version then
|
|
18
19
|
// begins a new record, and a milestone reached by then is recorded as reached before recording began:
|
|
19
20
|
// the time it was reached is lost, and none is made up.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
function
|
|
41
|
-
return
|
|
21
|
+
//
|
|
22
|
+
// Since 2026-10-08 the milestones are 30, 50 and 100 % (integration-spec.mjs): the entries an earlier version wrote for 10
|
|
23
|
+
// and 20 % stay in the record, never changed or removed, as every entry does, and nothing reads them now.
|
|
24
|
+
//
|
|
25
|
+
// Since 2026-10-20 (rewards/2-no-stack) the record decides the milestones: each counts from the moment the one before it
|
|
26
|
+
// was reached (reward.mjs decideMilestones), so an entry also holds the totals at that moment, written with it, once:
|
|
27
|
+
//
|
|
28
|
+
// "50": {"reachedAt": "2026-10-20T11:03:27.918Z", "focusSeconds": 4200, "equals": 21}
|
|
29
|
+
//
|
|
30
|
+
// and, the first time this KitCode writes a record that holds a milestone reached without them, by an earlier KitCode or
|
|
31
|
+
// before the record began, `freshSince` records the totals then: the milestone after it counts from there (the plan's
|
|
32
|
+
// decision 2). An earlier entry is never given totals it did not hold, and none is made up (milestone-record.mjs).
|
|
33
|
+
//
|
|
34
|
+
// Since 2026-10-22 (rewards/3-repeat-vouchers, phase 2) the record counts in cycles (milestone-record.mjs): a milestone newly
|
|
35
|
+
// reached is recorded in the cycle it is in, and a new cycle begins only by beginNextCycle, on a receipt the claim backend
|
|
36
|
+
// signed for the voucher of the cycle before, which the tracker has checked (api.mjs). Nothing else begins one: no read, no
|
|
37
|
+
// failure and no prompt resets anyone's progress.
|
|
38
|
+
|
|
39
|
+
export {MILESTONE_PERCENTS};
|
|
40
|
+
|
|
41
|
+
function totalsIn(stored, focusSeconds) {
|
|
42
|
+
return {
|
|
43
|
+
seconds: Math.floor(Math.max(0, Number(focusSeconds ?? focusTimeFrom(stored).activeSeconds) || 0)),
|
|
44
|
+
equals: Math.max(0, Number(equalsLedgerFrom(stored).total_equals) || 0),
|
|
45
|
+
};
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const ledger = equalsLedgerFrom(state);
|
|
50
|
-
const reward = buildRewardSummary({earnedSeconds: focusTimeFrom(state).activeSeconds, totalEquals: ledger.total_equals, ledger});
|
|
51
|
-
const highest = Math.max(0, ...reward.milestones.filter((milestone) => milestone.status !== 'locked').map((milestone) => milestone.percent));
|
|
52
|
-
|
|
53
|
-
return MILESTONE_PERCENTS.filter((percent) => percent <= highest);
|
|
48
|
+
function redeemedIn(stored) {
|
|
49
|
+
const tiers = new Set(REWARD_TIERS.map((tier) => tier.percent));
|
|
50
|
+
return new Set(equalsLedgerFrom(stored).earned_tiers
|
|
51
|
+
.filter((entry) => entry !== null && typeof entry === 'object' && tiers.has(Number(entry.percent)) && typeof entry.redeemed_at === 'string')
|
|
52
|
+
.map((entry) => Number(entry.percent)));
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
// Records, in `stored`, each milestone first found reached: `stored` is the file's own object, inside
|
|
@@ -59,72 +58,177 @@ function reachedNow(state) {
|
|
|
59
58
|
// before recording began, with no time, since none is known. From then on, a milestone newly reached is
|
|
60
59
|
// recorded at `now`. A record, or an entry, that is not what KitCode writes is left exactly as it is:
|
|
61
60
|
// never repaired, never written over. `now` is the tracker's clock, which a test can move.
|
|
62
|
-
|
|
61
|
+
//
|
|
62
|
+
// Since 2026-10-20 a milestone newly reached is recorded with the totals at `now`, its focus seconds, floored, and its =;
|
|
63
|
+
// `focusSeconds` is the tracker's own clock, when it folds a scan between two saves, ahead of the file's by up to one
|
|
64
|
+
// save. One write reaches one milestone at most: the next counts from that write's totals, so nothing it credited counts
|
|
65
|
+
// toward the next. A milestone reached with no totals known, and freshSince not yet written, has it written now.
|
|
66
|
+
//
|
|
67
|
+
// Since 2026-10-23 (tracking/2-cli) a milestone is found reached against the targets in force (reward.mjs milestonesOf;
|
|
68
|
+
// tracking-config.mjs), or `config`'s, a tracking config, when one is given: the record keeps every milestone it holds whatever
|
|
69
|
+
// the targets do, so a raise takes nothing reached away, and a target lowered under what the milestone in progress earned is
|
|
70
|
+
// recorded reached by this write.
|
|
71
|
+
export function noteMilestonesReached(stored, now = new Date(Date.now()), {focusSeconds, config} = {}) {
|
|
63
72
|
const record = stored.milestonesReached;
|
|
73
|
+
const read = readMilestoneRecord(record, MILESTONE_PERCENTS);
|
|
64
74
|
|
|
65
|
-
if (
|
|
75
|
+
if (read.state === 'damaged') {
|
|
66
76
|
return;
|
|
67
77
|
}
|
|
68
78
|
|
|
69
|
-
const
|
|
79
|
+
const totals = totalsIn(stored, focusSeconds);
|
|
80
|
+
const decided = decideMilestones({totals, record, redeemed: redeemedIn(stored), ...(config === undefined ? {} : {milestones: milestonesOf(config)})});
|
|
70
81
|
const at = now.toISOString();
|
|
82
|
+
const atTotals = {at, focusSeconds: totals.seconds, equals: totals.equals};
|
|
71
83
|
|
|
72
|
-
if (
|
|
84
|
+
if (read.state === 'none') {
|
|
85
|
+
const before = decided.entries.filter((entry) => entry.reached);
|
|
73
86
|
stored.milestonesReached = {
|
|
74
87
|
recordingSince: at,
|
|
75
|
-
milestones: Object.fromEntries(
|
|
88
|
+
milestones: Object.fromEntries(before.map((entry) => [String(entry.percent), {reachedBeforeRecording: true}])),
|
|
89
|
+
...(before.length > 0 ? {freshSince: atTotals} : {}),
|
|
76
90
|
};
|
|
77
91
|
return;
|
|
78
92
|
}
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
// The milestones of the cycle counted in: cycle 1's are the record's own; a later cycle's, its entry's, the last of `cycles`
|
|
95
|
+
// (milestone-record.mjs), whose numbers can skip since 2026-10-22's phase 3.
|
|
96
|
+
const cycle = read.current.cycle > 1 ? record.cycles[record.cycles.length - 1] : null;
|
|
97
|
+
const milestones = cycle === null ? record.milestones : cycle.milestones;
|
|
98
|
+
|
|
99
|
+
for (const entry of decided.entries) {
|
|
100
|
+
if (!entry.reached || entry.recorded || Object.hasOwn(milestones, String(entry.percent))) {
|
|
101
|
+
continue;
|
|
83
102
|
}
|
|
103
|
+
|
|
104
|
+
// Found reached now, at the frontier, with the totals of this write; or below one that is, its totals not known, which a
|
|
105
|
+
// later cycle never has: it counts from a start it knows.
|
|
106
|
+
milestones[String(entry.percent)] = entry.end === null
|
|
107
|
+
? {reachedAt: at}
|
|
108
|
+
: {reachedAt: at, focusSeconds: entry.end.seconds, equals: entry.end.equals};
|
|
84
109
|
}
|
|
85
|
-
}
|
|
86
110
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
111
|
+
const unknown = MILESTONE_PERCENTS.some((percent) => {
|
|
112
|
+
const entry = record.milestones[String(percent)];
|
|
113
|
+
return entry !== undefined && (entry.reachedBeforeRecording === true || entry.focusSeconds === undefined);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (unknown && record.freshSince === undefined) {
|
|
117
|
+
record.freshSince = atTotals;
|
|
92
118
|
}
|
|
93
119
|
}
|
|
94
120
|
|
|
95
|
-
|
|
96
|
-
|
|
121
|
+
// Begins the next cycle, in `stored`, the file's own object inside updateState, on `receipt`, a receipt the tracker read and
|
|
122
|
+
// found signed with the claim backend's key and for this install (cycle-receipt.mjs, api.mjs): {installId, cycle, claimId,
|
|
123
|
+
// issuedAt}. The plan of 2026-10-21's decision 1: after a claim, all progress resets. The receipt of the cycle the record is in
|
|
124
|
+
// begins the next, once: the record holds which receipt began each cycle, so the same receipt again, or one of a cycle past,
|
|
125
|
+
// changes nothing. Since 2026-10-22's phase 3 (the coordinator's ruling: the server is the authority on cycles) one of a cycle
|
|
126
|
+
// ahead, as a KitCode installed afresh after its person's earlier vouchers is given, begins the cycle after the receipt's, as the
|
|
127
|
+
// claim backend counts them; until then it changed nothing, and such an install stayed in its cycle for good. Either way a
|
|
128
|
+
// receipt only ever resets progress, and never adds any. The new cycle starts at the totals at `now`, `focusSeconds` the
|
|
129
|
+
// tracker's own clock as a save writes it (noteMilestonesReached); the totals themselves go on counting, and nothing earlier is
|
|
130
|
+
// changed or removed: the record is additive only (hard rule 4). First, as any write that makes progress does, it records what
|
|
131
|
+
// is reached by now in the cycle it closes, beginning the record if none was.
|
|
132
|
+
//
|
|
133
|
+
// Returns {outcome: 'started', cycle, startedAt, start}; {outcome: 'used', cycle, startedAt}, the cycle this receipt began
|
|
134
|
+
// already; {outcome: 'cycle', current}, the receipt of a cycle before the record's, `current`; or {outcome: 'damaged', problem},
|
|
135
|
+
// a record not what KitCode writes, left as it is.
|
|
136
|
+
export function beginNextCycle(stored, receipt, now = new Date(Date.now()), {focusSeconds} = {}) {
|
|
137
|
+
noteMilestonesReached(stored, now, {focusSeconds});
|
|
138
|
+
const read = readMilestoneRecord(stored.milestonesReached, MILESTONE_PERCENTS);
|
|
139
|
+
|
|
140
|
+
if (read.state === 'damaged') {
|
|
141
|
+
return {outcome: 'damaged', problem: read.problem};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const used = read.cycles.find((cycle) => cycle.receipt.claimId === receipt.claimId);
|
|
97
145
|
|
|
98
|
-
if (
|
|
99
|
-
return {
|
|
146
|
+
if (used !== undefined) {
|
|
147
|
+
return {outcome: 'used', cycle: used.cycle, startedAt: used.startedAt};
|
|
100
148
|
}
|
|
101
149
|
|
|
102
|
-
|
|
150
|
+
if (receipt.cycle < read.current.cycle) {
|
|
151
|
+
return {outcome: 'cycle', current: read.current.cycle};
|
|
152
|
+
}
|
|
103
153
|
|
|
104
|
-
|
|
105
|
-
|
|
154
|
+
const totals = totalsIn(stored, focusSeconds);
|
|
155
|
+
const startedAt = now.toISOString();
|
|
156
|
+
const record = stored.milestonesReached;
|
|
157
|
+
const next = {
|
|
158
|
+
cycle: receipt.cycle + 1,
|
|
159
|
+
startedAt,
|
|
160
|
+
focusSeconds: totals.seconds,
|
|
161
|
+
equals: totals.equals,
|
|
162
|
+
receipt: {claimId: receipt.claimId, issuedAt: receipt.issuedAt},
|
|
163
|
+
milestones: {},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (Array.isArray(record.cycles)) {
|
|
167
|
+
record.cycles.push(next);
|
|
168
|
+
} else {
|
|
169
|
+
record.cycles = [next];
|
|
106
170
|
}
|
|
107
171
|
|
|
108
|
-
|
|
109
|
-
|
|
172
|
+
return {outcome: 'started', cycle: next.cycle, startedAt, start: {focusSeconds: totals.seconds, equals: totals.equals}};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// A milestone record that is not what KitCode writes. The claim bundle does not guess at it.
|
|
176
|
+
export class MilestoneRecordError extends Error {
|
|
177
|
+
constructor(problem) {
|
|
178
|
+
super(`The record of when each milestone was reached, in state.json, ${problem}. KitCode has left it as it is.`);
|
|
179
|
+
this.name = 'MilestoneRecordError';
|
|
110
180
|
}
|
|
181
|
+
}
|
|
111
182
|
|
|
112
|
-
|
|
183
|
+
function totalsField(totals) {
|
|
184
|
+
return {focusSeconds: totals.seconds, equals: totals.equals};
|
|
113
185
|
}
|
|
114
186
|
|
|
115
187
|
// The record as the claim bundle reports it: when it began, or null when it has not, and each
|
|
116
|
-
// milestone's status and time, from
|
|
188
|
+
// milestone's status and time, from 30 to 100 %. Reads only; a damaged record throws a
|
|
117
189
|
// MilestoneRecordError.
|
|
190
|
+
//
|
|
191
|
+
// Since 2026-10-20 each milestone also carries `start` and `end`, the totals it counted from and those it was reached at,
|
|
192
|
+
// {focusSeconds, equals}, when its entry holds them; null otherwise: not reached, or reached with no totals known.
|
|
193
|
+
//
|
|
194
|
+
// Since 2026-10-22 the record reported is that of the cycle counted in, `cycle`, and `cycleStart` says where it began:
|
|
195
|
+
// {at: null, focusSeconds: 0, equals: 0} for cycle 1, install; the moment a later one began, and the totals then, which its
|
|
196
|
+
// 30 % counts from.
|
|
197
|
+
const FIRST_CYCLE_START = Object.freeze({at: null, focusSeconds: 0, equals: 0});
|
|
198
|
+
|
|
118
199
|
export function milestonesReachedFrom(state) {
|
|
119
|
-
const
|
|
200
|
+
const read = readMilestoneRecord(state.milestonesReached, MILESTONE_PERCENTS);
|
|
120
201
|
|
|
121
|
-
if (
|
|
122
|
-
return {recordingSince: null, milestones: MILESTONE_PERCENTS.map((percent) => ({percent, status: 'not-reached', reachedAt: null}))};
|
|
202
|
+
if (read.state === 'none') {
|
|
203
|
+
return {recordingSince: null, cycle: 1, cycleStart: {...FIRST_CYCLE_START}, milestones: MILESTONE_PERCENTS.map((percent) => ({percent, status: 'not-reached', reachedAt: null, start: null, end: null}))};
|
|
123
204
|
}
|
|
124
205
|
|
|
125
|
-
if (
|
|
126
|
-
throw new MilestoneRecordError(
|
|
206
|
+
if (read.state === 'damaged') {
|
|
207
|
+
throw new MilestoneRecordError(read.problem);
|
|
127
208
|
}
|
|
128
209
|
|
|
129
|
-
|
|
210
|
+
const {current} = read;
|
|
211
|
+
let previousEnd = {...current.start};
|
|
212
|
+
|
|
213
|
+
const milestones = MILESTONE_PERCENTS.map((percent) => {
|
|
214
|
+
const entry = current.entries.get(percent);
|
|
215
|
+
const start = previousEnd ?? current.freshSince;
|
|
216
|
+
previousEnd = entry?.end ?? null;
|
|
217
|
+
|
|
218
|
+
if (entry === undefined) {
|
|
219
|
+
return {percent, status: 'not-reached', reachedAt: null, start: null, end: null};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const segment = entry.end === null ? {start: null, end: null} : {start: totalsField(start), end: totalsField(entry.end)};
|
|
223
|
+
return entry.kind === 'before-recording'
|
|
224
|
+
? {percent, status: 'reached-before-recording', reachedAt: null, ...segment}
|
|
225
|
+
: {percent, status: 'reached', reachedAt: entry.reachedAt, ...segment};
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
recordingSince: read.recordingSince,
|
|
230
|
+
cycle: current.cycle,
|
|
231
|
+
cycleStart: current.cycle > 1 ? {at: current.startedAt, focusSeconds: current.start.seconds, equals: current.start.equals} : {...FIRST_CYCLE_START},
|
|
232
|
+
milestones,
|
|
233
|
+
};
|
|
130
234
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {milestoneProgressPercent} from './break-progress.mjs';
|
|
1
|
+
import {earnedToward, milestoneProgressPercent} from './break-progress.mjs';
|
|
2
2
|
|
|
3
3
|
export function formatBreakDuration(seconds) {
|
|
4
4
|
const totalSeconds = Math.max(0, Math.floor(Number(seconds) || 0));
|
|
@@ -48,8 +48,9 @@ export function nextBreakReminder(reward) {
|
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// What counts toward it: since 2026-10-20 what was earned since it began, the moment the milestone before it was
|
|
52
|
+
// reached (break-progress.mjs earnedToward); from a tracker before then, the totals.
|
|
53
|
+
const {seconds: earnedSeconds, equals: totalEquals} = earnedToward(reward, milestone);
|
|
53
54
|
const requiredSeconds = Math.max(0, Number(milestone.requiredSeconds) || 0);
|
|
54
55
|
const requiredEquals = Math.max(0, Number(milestone.requiredEquals) || 0);
|
|
55
56
|
// Floored, so it reads 100 only once the milestone is unlocked (docs/reward-rules.md §10).
|
|
@@ -10,6 +10,7 @@ import {DASHBOARD_URL} from './integration-spec.mjs';
|
|
|
10
10
|
import {onboardingPreferences, saveOnboardingPreferences, STORE_DIR} from './store.mjs';
|
|
11
11
|
import {resolveSetupPlatform} from './onboarding-platform.mjs';
|
|
12
12
|
import {renderOnboardingWindow} from './onboarding-window.mjs';
|
|
13
|
+
import {WELCOME_ERRORS, WELCOME_TESTER_ERRORS, WELCOME_WORDS} from './welcome-words.mjs';
|
|
13
14
|
import {keepWindowOnItsPage} from './window-guards.mjs';
|
|
14
15
|
|
|
15
16
|
const sourceDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -132,7 +133,7 @@ function setupWindowOptions(platform) {
|
|
|
132
133
|
transparent: true,
|
|
133
134
|
hasShadow: false,
|
|
134
135
|
backgroundColor: '#00000000',
|
|
135
|
-
title:
|
|
136
|
+
title: WELCOME_WORDS.windowTitle,
|
|
136
137
|
webPreferences: {preload: preloadPath, contextIsolation: true, nodeIntegration: false, sandbox: true},
|
|
137
138
|
};
|
|
138
139
|
}
|
|
@@ -154,7 +155,7 @@ app.whenReady().then(() => {
|
|
|
154
155
|
keepWindowOnItsPage(window);
|
|
155
156
|
// Project records carry absolute paths: only Welcome's own page gets them.
|
|
156
157
|
ipcMain.handle('kitcode:onboarding-initial-state', (event) => {
|
|
157
|
-
if (event.sender !== window.webContents) return {ok: false, error:
|
|
158
|
+
if (event.sender !== window.webContents) return {ok: false, error: WELCOME_ERRORS.invalidRequest, projects: [], suggestedProjects: []};
|
|
158
159
|
return {
|
|
159
160
|
...onboardingPreferences(),
|
|
160
161
|
projects: listProjectRecords(),
|
|
@@ -170,21 +171,21 @@ app.whenReady().then(() => {
|
|
|
170
171
|
return {canceled: result.canceled, paths, projects: describeProjects(paths)};
|
|
171
172
|
});
|
|
172
173
|
ipcMain.handle('kitcode:onboarding-submit', async (event, input) => {
|
|
173
|
-
if (event.sender !== window.webContents) return {ok: false, error:
|
|
174
|
+
if (event.sender !== window.webContents) return {ok: false, error: WELCOME_ERRORS.invalidRequest};
|
|
174
175
|
// A tester build's invited email, kept outside state.json, before anything else is saved; one that is not an address is
|
|
175
176
|
// asked for again, and nothing is saved.
|
|
176
177
|
if (diagnosticsEnabled() && typeof input?.invitedEmail === 'string' && !setInvitedEmail(input.invitedEmail).ok) {
|
|
177
|
-
return {ok: false, error:
|
|
178
|
+
return {ok: false, error: WELCOME_TESTER_ERRORS.invalidEmail};
|
|
178
179
|
}
|
|
179
180
|
const folders = validFolders(input?.folders);
|
|
180
|
-
if (!folders.length && !listProjectRecords().length) return {ok: false, error:
|
|
181
|
-
try { registerNewProjects(folders); } catch { return {ok: false, error:
|
|
181
|
+
if (!folders.length && !listProjectRecords().length) return {ok: false, error: WELCOME_ERRORS.noProjects};
|
|
182
|
+
try { registerNewProjects(folders); } catch { return {ok: false, error: WELCOME_ERRORS.registerFailed}; }
|
|
182
183
|
const selection = {autoTrack: input?.autoTrack === true, companionView: 'mini'};
|
|
183
184
|
if (selection.autoTrack && !(await runCli(['track', '--host', host, '--port', port]))) {
|
|
184
185
|
saveOnboardingPreferences({...selection, completed: false});
|
|
185
186
|
return {
|
|
186
187
|
ok: false,
|
|
187
|
-
error:
|
|
188
|
+
error: WELCOME_ERRORS.trackerFailed,
|
|
188
189
|
projects: listProjectRecords(),
|
|
189
190
|
};
|
|
190
191
|
}
|
|
@@ -197,14 +198,14 @@ app.whenReady().then(() => {
|
|
|
197
198
|
return {ok: true, projects};
|
|
198
199
|
});
|
|
199
200
|
ipcMain.handle('kitcode:onboarding-remove-project', async (event, projectId) => {
|
|
200
|
-
if (event.sender !== window.webContents) return {ok: false, error:
|
|
201
|
-
if (!projectId) return {ok: false, error:
|
|
201
|
+
if (event.sender !== window.webContents) return {ok: false, error: WELCOME_ERRORS.invalidRequest};
|
|
202
|
+
if (!projectId) return {ok: false, error: WELCOME_ERRORS.projectNotFound};
|
|
202
203
|
try {
|
|
203
204
|
const removed = removeProject(projectId);
|
|
204
|
-
if (!removed) return {ok: false, error:
|
|
205
|
+
if (!removed) return {ok: false, error: WELCOME_ERRORS.projectNotFound};
|
|
205
206
|
return {ok: true, projects: listProjectRecords()};
|
|
206
207
|
} catch {
|
|
207
|
-
return {ok: false, error:
|
|
208
|
+
return {ok: false, error: WELCOME_ERRORS.removeFailed};
|
|
208
209
|
}
|
|
209
210
|
});
|
|
210
211
|
ipcMain.handle('kitcode:onboarding-close', (event) => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {WELCOME_WORDS} from './welcome-words.mjs';
|
|
2
|
+
|
|
1
3
|
const PLATFORM_CHROME = {
|
|
2
4
|
darwin: {
|
|
3
5
|
id: 'darwin',
|
|
@@ -41,9 +43,10 @@ export function setupPlatformTheme(platform) {
|
|
|
41
43
|
return PLATFORM_CHROME[resolveSetupPlatform(platform)];
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
// Welcome's title bar, in Welcome's words (src/welcome-words.mjs); the Mini has its own (src/companion-controls.mjs).
|
|
44
47
|
export function renderPlatformChrome(theme = setupPlatformTheme()) {
|
|
45
|
-
return `<div class="${theme.chromeClass}" title="
|
|
46
|
-
<button class="dot filled dot-close" id="closeButton" type="button" aria-label="
|
|
48
|
+
return `<div class="${theme.chromeClass}" title="${WELCOME_WORDS.chrome.drag}">
|
|
49
|
+
<button class="dot filled dot-close" id="closeButton" type="button" aria-label="${WELCOME_WORDS.chrome.closeName}" title="${WELCOME_WORDS.chrome.close}"></button>
|
|
47
50
|
<span class="dot" aria-hidden="true"></span>
|
|
48
51
|
<span class="dot" aria-hidden="true"></span>
|
|
49
52
|
</div>`;
|