incremnt 0.1.3 → 0.1.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incremnt",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Command-line tool for querying your incremnt strength training data",
5
5
  "license": "MIT",
6
6
  "type": "module",
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
- 'sessions list',
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('sessions list', 'List recent sessions'),
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 --base-url <url>', 'Sign in (device flow)'),
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
- 'sessions list': 'session-insights',
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
- if (options.snapshot) {
233
- try {
234
- const result = await bootstrapSessionFromSnapshot(options.snapshot);
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
- stderr.write(`Unknown command: ${command}\n`);
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
- stderr.write(`${error.message}\n`);
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
- ?? process.env.INCREMNT_BASE_URL
292
- ?? sessionState.session?.transport?.baseUrl
293
- ?? null;
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) {