incremnt 0.1.0

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/src/lib.js ADDED
@@ -0,0 +1,341 @@
1
+ import fs from 'node:fs/promises';
2
+ import { spawn } from 'node:child_process';
3
+ import { capabilities, contractVersion, officialCommands, readCommands } from './contract.js';
4
+ import {
5
+ bootstrapSessionFromRemoteBaseUrl,
6
+ bootstrapSessionFromRemoteBaseUrlWithDeviceFlow,
7
+ bootstrapSessionFromRemoteBaseUrlWithEmail,
8
+ bootstrapSessionFromSnapshot,
9
+ fetchRemoteAuthConfig,
10
+ importSessionFile
11
+ } from './auth.js';
12
+ import { clearSessionState, isSessionExpired, readSessionState, resolveConfigDir } from './state.js';
13
+ import { createTransport } from './transport.js';
14
+ import { formatPretty } from './format.js';
15
+
16
+ function parseArgs(argv) {
17
+ const commandTokens = [];
18
+ const rest = [];
19
+ let parsingOptions = false;
20
+
21
+ for (const token of argv) {
22
+ if (!parsingOptions && !token.startsWith('--')) {
23
+ commandTokens.push(token);
24
+ continue;
25
+ }
26
+
27
+ parsingOptions = true;
28
+ rest.push(token);
29
+ }
30
+
31
+ const command = commandTokens.join(' ');
32
+ const options = {};
33
+
34
+ for (let index = 0; index < rest.length; index += 1) {
35
+ const token = rest[index];
36
+ if (!token.startsWith('--')) {
37
+ continue;
38
+ }
39
+
40
+ const key = token.slice(2);
41
+ const next = rest[index + 1];
42
+ if (next === undefined || next.startsWith('--')) {
43
+ options[key] = true;
44
+ } else {
45
+ options[key] = next;
46
+ index += 1;
47
+ }
48
+ }
49
+
50
+ return { command, options };
51
+ }
52
+
53
+ export async function runCli(argv, stdout, stderr) {
54
+ const { command, options } = parseArgs(argv);
55
+ const normalizedCommand = ({
56
+ 'sessions list': 'session-insights',
57
+ 'sessions show': 'session-show',
58
+ 'programs list': 'program-list',
59
+ 'programs current': 'program-summary',
60
+ 'exercises history': 'exercise-history',
61
+ 'sessions compare': 'planned-vs-actual',
62
+ 'sessions explain': 'why-did-this-change',
63
+ insights: 'session-insights',
64
+ history: 'exercise-history',
65
+ prs: 'records',
66
+ program: 'program-summary',
67
+ compare: 'planned-vs-actual',
68
+ explain: 'why-did-this-change'
69
+ })[command] ?? command;
70
+
71
+ if (!command) {
72
+ stderr.write('Usage: incremnt <sessions list|sessions show|sessions compare|sessions explain|programs list|programs current|exercises history|records|status|contract|login|logout> [options]\n');
73
+ return 1;
74
+ }
75
+
76
+ const sessionState = await readSessionState();
77
+ const transport = await createTransport(options, sessionState);
78
+
79
+ if (normalizedCommand === 'status') {
80
+ stdout.write(`${JSON.stringify({
81
+ contractVersion,
82
+ capabilities,
83
+ mode: transport.kind,
84
+ config: {
85
+ dir: resolveConfigDir(),
86
+ sessionPath: sessionState.path
87
+ },
88
+ auth: {
89
+ loggedIn: Boolean(sessionState.session),
90
+ sessionExists: sessionState.exists,
91
+ sessionSchemaVersion: sessionState.session?.version ?? null,
92
+ expired: isSessionExpired(sessionState.session),
93
+ error: sessionState.error,
94
+ account: sessionState.session?.account ?? null
95
+ },
96
+ remote: {
97
+ bootstrap: transport.kind === 'remote' ? transport.bootstrap ?? false : false,
98
+ source: transport.kind === 'remote' ? transport.source ?? null : null,
99
+ baseUrl: transport.kind === 'remote' ? transport.baseUrl ?? null : null,
100
+ contractVersion: transport.kind === 'remote' ? transport.contractVersion ?? null : null,
101
+ capabilities: transport.kind === 'remote' ? transport.capabilities ?? null : null,
102
+ fixturePath: transport.kind === 'remote' ? transport.fixturePath ?? null : null
103
+ },
104
+ snapshot: {
105
+ source: transport.snapshotSource?.source ?? null,
106
+ path: transport.snapshotSource?.path ?? null
107
+ }
108
+ }, null, 2)}\n`);
109
+ return 0;
110
+ }
111
+
112
+ if (normalizedCommand === 'contract') {
113
+ stdout.write(`${JSON.stringify({
114
+ contractVersion,
115
+ binary: 'incremnt',
116
+ capabilities,
117
+ officialCommands
118
+ }, null, 2)}\n`);
119
+ return 0;
120
+ }
121
+
122
+ if (normalizedCommand === 'logout') {
123
+ try {
124
+ const result = await clearSessionState();
125
+ stdout.write(`${JSON.stringify({
126
+ ok: true,
127
+ cleared: result.cleared,
128
+ sessionPath: result.path
129
+ }, null, 2)}\n`);
130
+ return 0;
131
+ } catch (error) {
132
+ stderr.write(`${error.message}\n`);
133
+ return 1;
134
+ }
135
+ }
136
+
137
+ if (normalizedCommand === 'login') {
138
+ const loginBaseUrl = resolveLoginBaseUrl(options, sessionState);
139
+
140
+ if (loginBaseUrl) {
141
+ if (options.token && options.email) {
142
+ stderr.write('Pass either --token or --email with --base-url, not both\n');
143
+ return 1;
144
+ }
145
+
146
+ if (!options.token && !options.email) {
147
+ try {
148
+ const authConfig = await fetchRemoteAuthConfig(loginBaseUrl);
149
+ const result = await bootstrapSessionFromRemoteBaseUrlWithDeviceFlow(loginBaseUrl, {
150
+ authConfig,
151
+ onChallenge: async (challenge, auth) => {
152
+ const baseUrlNormalized = loginBaseUrl.replace(/\/$/, '');
153
+ const userCodeParam = encodeURIComponent(challenge.userCode);
154
+ const providers = configuredAuthProviders(auth);
155
+
156
+ // Skip the approval form and go straight to the provider when there's exactly one
157
+ let verificationUrl;
158
+ if (providers.length === 1 && auth?.providers?.apple?.configured) {
159
+ verificationUrl = `${baseUrlNormalized}/auth/apple/start?userCode=${userCodeParam}`;
160
+ } else if (providers.length === 1 && auth?.providers?.google?.configured) {
161
+ verificationUrl = `${baseUrlNormalized}/auth/google/start?userCode=${userCodeParam}`;
162
+ } else {
163
+ verificationUrl = `${baseUrlNormalized}${challenge.verificationUri ?? '/auth/device/approve'}?userCode=${userCodeParam}`;
164
+ }
165
+
166
+ stderr.write('Signing in...\n');
167
+ const opened = await maybeOpenBrowser(verificationUrl);
168
+ if (opened) {
169
+ stderr.write('Opened your browser to sign in.\n');
170
+ } else {
171
+ stderr.write(`Open this URL to sign in: ${verificationUrl}\n`);
172
+ }
173
+ if (providers.length > 1) {
174
+ stderr.write(`Continue with one of: ${providers.join(', ')}.\n`);
175
+ } else if (providers.length === 0) {
176
+ stderr.write('If you are using the local dev sync service, run its approve-device helper with that code.\n');
177
+ }
178
+ }
179
+ });
180
+ stdout.write(`${JSON.stringify({
181
+ ok: true,
182
+ sessionPath: result.path,
183
+ account: result.session.account,
184
+ remoteContractVersion: result.session.transport?.contractVersion ?? null,
185
+ transport: result.session.transport
186
+ }, null, 2)}\n`);
187
+ return 0;
188
+ } catch (error) {
189
+ stderr.write(`${error.message}\n`);
190
+ return 1;
191
+ }
192
+ }
193
+
194
+ if (options.email) {
195
+ try {
196
+ const result = await bootstrapSessionFromRemoteBaseUrlWithEmail(loginBaseUrl, options.email, options['user-id']);
197
+ stdout.write(`${JSON.stringify({
198
+ ok: true,
199
+ sessionPath: result.path,
200
+ account: result.session.account,
201
+ remoteContractVersion: result.session.transport?.contractVersion ?? null,
202
+ transport: result.session.transport
203
+ }, null, 2)}\n`);
204
+ return 0;
205
+ } catch (error) {
206
+ stderr.write(`${error.message}\n`);
207
+ return 1;
208
+ }
209
+ }
210
+
211
+ try {
212
+ const result = await bootstrapSessionFromRemoteBaseUrl(loginBaseUrl, options.token);
213
+ stdout.write(`${JSON.stringify({
214
+ ok: true,
215
+ sessionPath: result.path,
216
+ account: result.session.account,
217
+ remoteContractVersion: result.session.transport?.contractVersion ?? null,
218
+ transport: result.session.transport
219
+ }, null, 2)}\n`);
220
+ return 0;
221
+ } catch (error) {
222
+ stderr.write(`${error.message}\n`);
223
+ return 1;
224
+ }
225
+ }
226
+
227
+ if (options.snapshot) {
228
+ try {
229
+ const result = await bootstrapSessionFromSnapshot(options.snapshot);
230
+ stdout.write(`${JSON.stringify({
231
+ ok: true,
232
+ sessionPath: result.path,
233
+ account: result.session.account,
234
+ transport: result.session.transport
235
+ }, null, 2)}\n`);
236
+ return 0;
237
+ } catch (error) {
238
+ stderr.write(`${error.message}\n`);
239
+ return 1;
240
+ }
241
+ }
242
+
243
+ if (options['session-file']) {
244
+ try {
245
+ const result = await importSessionFile(options['session-file']);
246
+ stdout.write(`${JSON.stringify({
247
+ ok: true,
248
+ sessionPath: result.path,
249
+ account: result.session.account
250
+ }, null, 2)}\n`);
251
+ return 0;
252
+ } catch (error) {
253
+ stderr.write(`${error.message}\n`);
254
+ return 1;
255
+ }
256
+ }
257
+
258
+ stderr.write('Pass --base-url, --snapshot, or --session-file to login, or set INCREMNT_BASE_URL.\n');
259
+ return 1;
260
+ }
261
+
262
+ if (!readCommands.has(normalizedCommand)) {
263
+ stderr.write(`Unknown command: ${command}\n`);
264
+ return 1;
265
+ }
266
+
267
+ try {
268
+ const payload = await transport.executeReadCommand(normalizedCommand, options);
269
+ const pretty = options.pretty ? formatPretty(normalizedCommand, payload) : null;
270
+ stdout.write(pretty ? `${pretty}\n` : `${JSON.stringify(payload, null, 2)}\n`);
271
+ return 0;
272
+ } catch (error) {
273
+ stderr.write(`${error.message}\n`);
274
+ return 1;
275
+ }
276
+ }
277
+
278
+ function resolveLoginBaseUrl(options, sessionState) {
279
+ return options['base-url']
280
+ ?? process.env.INCREMNT_BASE_URL
281
+ ?? sessionState.session?.transport?.baseUrl
282
+ ?? null;
283
+ }
284
+
285
+ async function maybeOpenBrowser(url) {
286
+ if (process.env.INCREMNT_DISABLE_BROWSER === '1') {
287
+ return false;
288
+ }
289
+
290
+ if (process.env.INCREMNT_BROWSER_CAPTURE_FILE) {
291
+ await fs.writeFile(process.env.INCREMNT_BROWSER_CAPTURE_FILE, url);
292
+ return true;
293
+ }
294
+
295
+ const command = browserCommandForPlatform();
296
+ if (!command) {
297
+ return false;
298
+ }
299
+
300
+ try {
301
+ const child = spawn(command.bin, [...command.args, url], {
302
+ stdio: 'ignore',
303
+ detached: true
304
+ });
305
+ child.unref();
306
+ return true;
307
+ } catch {
308
+ return false;
309
+ }
310
+ }
311
+
312
+ function browserCommandForPlatform() {
313
+ if (process.platform === 'darwin') {
314
+ return { bin: 'open', args: [] };
315
+ }
316
+
317
+ if (process.platform === 'linux') {
318
+ return { bin: 'xdg-open', args: [] };
319
+ }
320
+
321
+ if (process.platform === 'win32') {
322
+ return { bin: 'cmd', args: ['/c', 'start', ''] };
323
+ }
324
+
325
+ return null;
326
+ }
327
+
328
+ function configuredAuthProviders(auth) {
329
+ const providers = auth?.providers ?? {};
330
+ const labels = [];
331
+
332
+ if (providers.apple?.configured) {
333
+ labels.push('Apple');
334
+ }
335
+
336
+ if (providers.google?.configured) {
337
+ labels.push('Google');
338
+ }
339
+
340
+ return labels;
341
+ }
package/src/local.js ADDED
@@ -0,0 +1,59 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ export async function readSnapshot(inputPath) {
6
+ const raw = await fs.readFile(inputPath, 'utf8');
7
+ return JSON.parse(raw);
8
+ }
9
+
10
+ async function mostRecentDownloadSnapshot() {
11
+ const downloadsDir = path.join(os.homedir(), 'Downloads');
12
+
13
+ try {
14
+ const entries = await fs.readdir(downloadsDir, { withFileTypes: true });
15
+ const matchingFiles = entries
16
+ .filter((entry) => entry.isFile() && /^onemore-developer-snapshot\.onemore(?: \d+)?\.json$/i.test(entry.name))
17
+ .map((entry) => path.join(downloadsDir, entry.name));
18
+
19
+ if (matchingFiles.length === 0) {
20
+ return null;
21
+ }
22
+
23
+ const withStats = await Promise.all(
24
+ matchingFiles.map(async (candidate) => ({
25
+ candidate,
26
+ stat: await fs.stat(candidate)
27
+ }))
28
+ );
29
+
30
+ return withStats
31
+ .sort((lhs, rhs) => rhs.stat.mtimeMs - lhs.stat.mtimeMs)[0]
32
+ .candidate;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ export async function resolveSnapshotSource(explicitPath) {
39
+ const downloadSnapshot = await mostRecentDownloadSnapshot();
40
+ const candidates = [
41
+ explicitPath ? { path: explicitPath, source: 'input' } : null,
42
+ process.env.INCREMNT_SNAPSHOT ? { path: process.env.INCREMNT_SNAPSHOT, source: 'env:INCREMNT_SNAPSHOT' } : null,
43
+ process.env.ONEMORE_SNAPSHOT ? { path: process.env.ONEMORE_SNAPSHOT, source: 'env:ONEMORE_SNAPSHOT' } : null,
44
+ { path: path.join(process.cwd(), 'onemore-developer-snapshot.onemore.json'), source: 'cwd' },
45
+ { path: path.join(os.homedir(), 'Library', 'Application Support', 'OneMore', 'onemore-developer-snapshot.onemore.json'), source: 'app-support' },
46
+ downloadSnapshot ? { path: downloadSnapshot, source: 'downloads' } : null
47
+ ].filter(Boolean);
48
+
49
+ for (const candidate of candidates) {
50
+ try {
51
+ await fs.access(candidate.path);
52
+ return candidate;
53
+ } catch {
54
+ // Try next candidate.
55
+ }
56
+ }
57
+
58
+ return null;
59
+ }