incremnt 0.1.3 → 0.1.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/package.json +1 -1
- package/src/contract.js +79 -20
- package/src/format.js +4 -10
- package/src/lib.js +61 -49
- package/src/remote.js +3 -0
- package/src/sync-service.js +4 -0
package/package.json
CHANGED
package/src/contract.js
CHANGED
|
@@ -8,16 +8,85 @@ export const capabilities = {
|
|
|
8
8
|
remoteBootstrap: true
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// Single source of truth for all read commands.
|
|
12
|
+
// Drives: officialCommands, readCommands, help text, and the contract JSON schema.
|
|
13
|
+
// When adding a command: add one entry here — everything else updates automatically.
|
|
14
|
+
export const commandSchema = [
|
|
15
|
+
{
|
|
16
|
+
command: 'sessions list',
|
|
17
|
+
id: 'session-insights',
|
|
18
|
+
description: 'List recent sessions',
|
|
19
|
+
options: [
|
|
20
|
+
{ name: 'limit', type: 'number', required: false, description: 'Max sessions to return (default: 10)' }
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
command: 'sessions show',
|
|
25
|
+
id: 'session-show',
|
|
26
|
+
description: 'Show session details',
|
|
27
|
+
usage: 'sessions show --id <session-id>',
|
|
28
|
+
options: [
|
|
29
|
+
{ name: 'id', type: 'string', required: true, description: 'Session ID' }
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
command: 'sessions compare',
|
|
34
|
+
id: 'planned-vs-actual',
|
|
35
|
+
description: 'Compare planned vs actual',
|
|
36
|
+
usage: 'sessions compare --session-id <session-id>',
|
|
37
|
+
options: [
|
|
38
|
+
{ name: 'session-id', type: 'string', required: true, description: 'Session ID' }
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
command: 'sessions explain',
|
|
43
|
+
id: 'why-did-this-change',
|
|
44
|
+
description: 'Explain session context',
|
|
45
|
+
usage: 'sessions explain --session-id <session-id>',
|
|
46
|
+
options: [
|
|
47
|
+
{ name: 'session-id', type: 'string', required: true, description: 'Session ID' }
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
command: 'programs list',
|
|
52
|
+
id: 'program-list',
|
|
53
|
+
description: 'List all programs',
|
|
54
|
+
options: []
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
command: 'programs current',
|
|
58
|
+
id: 'program-summary',
|
|
59
|
+
description: 'Show active program',
|
|
60
|
+
options: []
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
command: 'programs show',
|
|
64
|
+
id: 'program-detail',
|
|
65
|
+
description: 'Show program with all exercises',
|
|
66
|
+
usage: 'programs show --id <program-id>',
|
|
67
|
+
options: [
|
|
68
|
+
{ name: 'id', type: 'string', required: false, description: 'Program ID (defaults to active program)' }
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
command: 'exercises history',
|
|
73
|
+
id: 'exercise-history',
|
|
74
|
+
description: 'Exercise history',
|
|
75
|
+
usage: 'exercises history --name <exercise-name>',
|
|
76
|
+
options: [
|
|
77
|
+
{ name: 'name', type: 'string', required: true, description: 'Exercise name' }
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
command: 'records',
|
|
82
|
+
id: 'records',
|
|
83
|
+
description: 'Personal records',
|
|
84
|
+
options: []
|
|
85
|
+
}
|
|
86
|
+
];
|
|
87
|
+
|
|
11
88
|
export const officialCommands = [
|
|
12
|
-
|
|
13
|
-
'sessions show --id <session-id>',
|
|
14
|
-
'sessions compare --session-id <session-id>',
|
|
15
|
-
'sessions explain --session-id <session-id>',
|
|
16
|
-
'programs list',
|
|
17
|
-
'programs current',
|
|
18
|
-
'programs show --id <program-id>',
|
|
19
|
-
'exercises history --name <exercise-name>',
|
|
20
|
-
'records',
|
|
89
|
+
...commandSchema.map((c) => c.usage ?? c.command),
|
|
21
90
|
'status',
|
|
22
91
|
'contract',
|
|
23
92
|
'login',
|
|
@@ -29,14 +98,4 @@ export const officialCommands = [
|
|
|
29
98
|
'logout'
|
|
30
99
|
];
|
|
31
100
|
|
|
32
|
-
export const readCommands = new Set(
|
|
33
|
-
'session-insights',
|
|
34
|
-
'session-show',
|
|
35
|
-
'exercise-history',
|
|
36
|
-
'records',
|
|
37
|
-
'program-list',
|
|
38
|
-
'program-summary',
|
|
39
|
-
'program-detail',
|
|
40
|
-
'planned-vs-actual',
|
|
41
|
-
'why-did-this-change'
|
|
42
|
-
]);
|
|
101
|
+
export const readCommands = new Set(commandSchema.map((c) => c.id));
|
package/src/format.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { commandSchema } from './contract.js';
|
|
2
3
|
|
|
3
4
|
const shortMonths = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
4
5
|
const shortDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
@@ -377,18 +378,11 @@ export function formatHelp() {
|
|
|
377
378
|
` incremnt <command> [options]`,
|
|
378
379
|
'',
|
|
379
380
|
header('COMMANDS'),
|
|
380
|
-
cmd(
|
|
381
|
-
cmd('sessions show --id <id>', 'Show session details'),
|
|
382
|
-
cmd('sessions compare --id <id>', 'Compare planned vs actual'),
|
|
383
|
-
cmd('sessions explain --id <id>', 'Explain session context'),
|
|
384
|
-
cmd('programs list', 'List all programs'),
|
|
385
|
-
cmd('programs current', 'Show active program'),
|
|
386
|
-
cmd('programs show --id <id>', 'Show program with all exercises'),
|
|
387
|
-
cmd('exercises history --name <name>', 'Exercise history'),
|
|
388
|
-
cmd('records', 'Personal records'),
|
|
381
|
+
...commandSchema.map((c) => cmd(c.usage ?? c.command, c.description)),
|
|
389
382
|
'',
|
|
390
383
|
header('AUTH'),
|
|
391
|
-
cmd('login
|
|
384
|
+
cmd('login', 'Sign in (opens browser)'),
|
|
385
|
+
cmd('login --base-url <url>', 'Sign in to a specific server'),
|
|
392
386
|
cmd('login --base-url <url> --token <t>', 'Sign in with token'),
|
|
393
387
|
cmd('login --snapshot <file>', 'Use local snapshot'),
|
|
394
388
|
cmd('login --session-file <file>', 'Import session file'),
|
package/src/lib.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import { capabilities, contractVersion, officialCommands, readCommands } from './contract.js';
|
|
3
|
+
import { capabilities, commandSchema, contractVersion, officialCommands, readCommands } from './contract.js';
|
|
4
4
|
import {
|
|
5
5
|
bootstrapSessionFromRemoteBaseUrl,
|
|
6
6
|
bootstrapSessionFromRemoteBaseUrlWithDeviceFlow,
|
|
@@ -53,14 +53,7 @@ function parseArgs(argv) {
|
|
|
53
53
|
export async function runCli(argv, stdout, stderr) {
|
|
54
54
|
const { command, options } = parseArgs(argv);
|
|
55
55
|
const normalizedCommand = ({
|
|
56
|
-
|
|
57
|
-
'sessions show': 'session-show',
|
|
58
|
-
'programs list': 'program-list',
|
|
59
|
-
'programs current': 'program-summary',
|
|
60
|
-
'programs show': 'program-detail',
|
|
61
|
-
'exercises history': 'exercise-history',
|
|
62
|
-
'sessions compare': 'planned-vs-actual',
|
|
63
|
-
'sessions explain': 'why-did-this-change',
|
|
56
|
+
...Object.fromEntries(commandSchema.map((c) => [c.command, c.id])),
|
|
64
57
|
insights: 'session-insights',
|
|
65
58
|
history: 'exercise-history',
|
|
66
59
|
prs: 'records',
|
|
@@ -115,7 +108,8 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
115
108
|
contractVersion,
|
|
116
109
|
binary: 'incremnt',
|
|
117
110
|
capabilities,
|
|
118
|
-
officialCommands
|
|
111
|
+
officialCommands,
|
|
112
|
+
schema: commandSchema
|
|
119
113
|
}, null, 2)}\n`);
|
|
120
114
|
return 0;
|
|
121
115
|
}
|
|
@@ -136,6 +130,37 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
136
130
|
}
|
|
137
131
|
|
|
138
132
|
if (normalizedCommand === 'login') {
|
|
133
|
+
if (options.snapshot) {
|
|
134
|
+
try {
|
|
135
|
+
const result = await bootstrapSessionFromSnapshot(options.snapshot);
|
|
136
|
+
stdout.write(`${JSON.stringify({
|
|
137
|
+
ok: true,
|
|
138
|
+
sessionPath: result.path,
|
|
139
|
+
account: result.session.account,
|
|
140
|
+
transport: result.session.transport
|
|
141
|
+
}, null, 2)}\n`);
|
|
142
|
+
return 0;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
stderr.write(`${error.message}\n`);
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (options['session-file']) {
|
|
150
|
+
try {
|
|
151
|
+
const result = await importSessionFile(options['session-file']);
|
|
152
|
+
stdout.write(`${JSON.stringify({
|
|
153
|
+
ok: true,
|
|
154
|
+
sessionPath: result.path,
|
|
155
|
+
account: result.session.account
|
|
156
|
+
}, null, 2)}\n`);
|
|
157
|
+
return 0;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
stderr.write(`${error.message}\n`);
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
139
164
|
const loginBaseUrl = resolveLoginBaseUrl(options, sessionState);
|
|
140
165
|
|
|
141
166
|
if (loginBaseUrl) {
|
|
@@ -229,49 +254,28 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
229
254
|
}
|
|
230
255
|
}
|
|
231
256
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
stdout.write(`${JSON.stringify({
|
|
236
|
-
ok: true,
|
|
237
|
-
sessionPath: result.path,
|
|
238
|
-
account: result.session.account,
|
|
239
|
-
transport: result.session.transport
|
|
240
|
-
}, null, 2)}\n`);
|
|
241
|
-
return 0;
|
|
242
|
-
} catch (error) {
|
|
243
|
-
stderr.write(`${error.message}\n`);
|
|
244
|
-
return 1;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (options['session-file']) {
|
|
249
|
-
try {
|
|
250
|
-
const result = await importSessionFile(options['session-file']);
|
|
251
|
-
stdout.write(`${JSON.stringify({
|
|
252
|
-
ok: true,
|
|
253
|
-
sessionPath: result.path,
|
|
254
|
-
account: result.session.account
|
|
255
|
-
}, null, 2)}\n`);
|
|
256
|
-
return 0;
|
|
257
|
-
} catch (error) {
|
|
258
|
-
stderr.write(`${error.message}\n`);
|
|
259
|
-
return 1;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
stderr.write('Pass --base-url, --snapshot, or --session-file to login, or set INCREMNT_BASE_URL.\n');
|
|
257
|
+
// loginBaseUrl always resolves (falls back to DEFAULT_BASE_URL) so this is unreachable
|
|
258
|
+
// unless INCREMNT_BASE_URL is explicitly set to empty string to disable the default
|
|
259
|
+
stderr.write('Pass --snapshot or --session-file, or run incremnt login to sign in.\n');
|
|
264
260
|
return 1;
|
|
265
261
|
}
|
|
266
262
|
|
|
263
|
+
const wantJson = options.json || !(stdout.isTTY ?? false);
|
|
264
|
+
const explicitJson = Boolean(options.json);
|
|
265
|
+
|
|
267
266
|
if (!readCommands.has(normalizedCommand)) {
|
|
268
|
-
|
|
267
|
+
const message = `Unknown command: ${command}`;
|
|
268
|
+
if (explicitJson) {
|
|
269
|
+
stdout.write(`${JSON.stringify({ error: message, code: 'UNKNOWN_COMMAND' }, null, 2)}\n`);
|
|
270
|
+
} else {
|
|
271
|
+
stderr.write(`${message}\n`);
|
|
272
|
+
}
|
|
273
|
+
|
|
269
274
|
return 1;
|
|
270
275
|
}
|
|
271
276
|
|
|
272
277
|
try {
|
|
273
278
|
const payload = await transport.executeReadCommand(normalizedCommand, options);
|
|
274
|
-
const wantJson = options.json || !(stdout.isTTY ?? false);
|
|
275
279
|
if (wantJson) {
|
|
276
280
|
stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
277
281
|
} else {
|
|
@@ -281,16 +285,24 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
281
285
|
|
|
282
286
|
return 0;
|
|
283
287
|
} catch (error) {
|
|
284
|
-
|
|
288
|
+
if (explicitJson) {
|
|
289
|
+
stdout.write(`${JSON.stringify({ error: error.message, code: error.code ?? null }, null, 2)}\n`);
|
|
290
|
+
} else {
|
|
291
|
+
stderr.write(`${error.message}\n`);
|
|
292
|
+
}
|
|
293
|
+
|
|
285
294
|
return 1;
|
|
286
295
|
}
|
|
287
296
|
}
|
|
288
297
|
|
|
298
|
+
const DEFAULT_BASE_URL = 'https://incremnt-sync.onrender.com';
|
|
299
|
+
|
|
289
300
|
function resolveLoginBaseUrl(options, sessionState) {
|
|
290
|
-
return options['base-url']
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
301
|
+
if (options['base-url']) return options['base-url'];
|
|
302
|
+
// If INCREMNT_BASE_URL is explicitly set (even to empty string), respect it — empty = no URL
|
|
303
|
+
if ('INCREMNT_BASE_URL' in process.env) return process.env.INCREMNT_BASE_URL || null;
|
|
304
|
+
if (sessionState.session?.transport?.baseUrl) return sessionState.session.transport.baseUrl;
|
|
305
|
+
return DEFAULT_BASE_URL;
|
|
294
306
|
}
|
|
295
307
|
|
|
296
308
|
async function maybeOpenBrowser(url) {
|
package/src/remote.js
CHANGED
|
@@ -27,6 +27,7 @@ const remoteCommandHandlers = {
|
|
|
27
27
|
records: executeRemoteRead,
|
|
28
28
|
'program-list': executeRemoteRead,
|
|
29
29
|
'program-summary': executeRemoteRead,
|
|
30
|
+
'program-detail': executeRemoteRead,
|
|
30
31
|
'planned-vs-actual': executeRemoteRead,
|
|
31
32
|
'why-did-this-change': executeRemoteRead
|
|
32
33
|
};
|
|
@@ -105,6 +106,8 @@ function endpointForCommand(baseUrl, normalizedCommand, options) {
|
|
|
105
106
|
return resolveServiceUrl(baseUrl, '/cli/programs');
|
|
106
107
|
case 'program-summary':
|
|
107
108
|
return resolveServiceUrl(baseUrl, '/cli/programs/current');
|
|
109
|
+
case 'program-detail':
|
|
110
|
+
return resolveServiceUrl(baseUrl, options.id ? `/cli/programs/${options.id}` : '/cli/programs/active');
|
|
108
111
|
case 'exercise-history': {
|
|
109
112
|
const historyUrl = resolveServiceUrl(baseUrl, '/cli/exercises/history');
|
|
110
113
|
historyUrl.searchParams.set('name', options.name ?? options.exercise);
|
package/src/sync-service.js
CHANGED
|
@@ -212,6 +212,10 @@ function routeRequest(url) {
|
|
|
212
212
|
return { command: 'program-summary', options: {} };
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
if (pathname === '/cli/programs/active') {
|
|
216
|
+
return { command: 'program-detail', options: {} };
|
|
217
|
+
}
|
|
218
|
+
|
|
215
219
|
const programShowMatch = pathname.match(/^\/cli\/programs\/([^/]+)$/);
|
|
216
220
|
if (programShowMatch) {
|
|
217
221
|
return { command: 'program-detail', options: { id: programShowMatch[1] } };
|