getownit 0.1.2 → 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.
@@ -31,12 +31,21 @@ export async function interviewCommand(argv) {
31
31
  function exitCode(outcome) {
32
32
  return outcome && (outcome.status === 'verified' || outcome.reason === LATER) ? 0 : 1;
33
33
  }
34
+ /** A commit ID or ref, with a hint on where to find one when git does not know it. */
35
+ function commitFor(rev) {
36
+ try {
37
+ return resolveCommit(rev);
38
+ }
39
+ catch {
40
+ throw new Error(`"${rev}" is not a commit in this repo. Find commit IDs with "git log --oneline", for example --range 7a9e3f1..f3c9a12.`);
41
+ }
42
+ }
34
43
  /** A range "A..B", or a single commit, which means just that commit. */
35
44
  function parseRange(range) {
36
45
  const match = range.match(/^(.+?)\.\.(.+)$/);
37
46
  if (match)
38
- return { base: resolveCommit(match[1]), head: resolveCommit(match[2]) };
39
- const head = resolveCommit(range);
47
+ return { base: commitFor(match[1]), head: commitFor(match[2]) };
48
+ const head = commitFor(range);
40
49
  return { base: tryGit(['rev-parse', '--verify', '--quiet', `${head}^`])?.trim() || null, head };
41
50
  }
42
51
  async function debriefRange(api, prompter, repo, range) {
@@ -1,7 +1,7 @@
1
1
  import { createApi } from '../api.js';
2
2
  import { githubRepo } from '../git.js';
3
3
  import { paint } from '../ui.js';
4
- const USAGE = 'Usage: ownit settings [questions|answers|earlier] [on|off]\n';
4
+ const USAGE = 'Usage: ownit settings [questions|answers|earlier] [on|off]\nFor example: ownit settings answers on. Run "ownit help settings" for details.\n';
5
5
  /** The setting each name changes on the server. */
6
6
  const SETTING = { questions: 'show_questions', answers: 'show_answers', earlier: 'allow_backfill' };
7
7
  function describe(s) {
@@ -18,8 +18,8 @@ function note(s, changed, on) {
18
18
  return 'Debrief older commits with "ownit interview --range A..B" (or a single commit SHA). They count once verified, never as unverified.';
19
19
  if (changed === 'earlier')
20
20
  return 'New debriefs of older commits no longer count. Ones already verified stay.';
21
- if (changed && !s.show_answers)
22
- return 'Saved answers, if any, are deleted.';
21
+ if ((changed === 'answers' || changed === 'questions') && !on && !s.show_answers)
22
+ return 'Answers are hidden, and saved ones are deleted.';
23
23
  if (changed)
24
24
  return '';
25
25
  return 'Change with "ownit settings questions|answers|earlier on|off" (repo admins and maintainers).';
@@ -32,6 +32,10 @@ export async function settingsCommand(args) {
32
32
  const out = process.stderr;
33
33
  const [what, value, ...rest] = args;
34
34
  const changed = what;
35
+ if (changed !== undefined && Object.hasOwn(SETTING, changed) && value === undefined) {
36
+ out.write(`Add on or off: "ownit settings ${changed} on" or "ownit settings ${changed} off".\n`);
37
+ return 1;
38
+ }
35
39
  if (changed !== undefined && (!Object.hasOwn(SETTING, changed) || (value !== 'on' && value !== 'off') || rest.length)) {
36
40
  out.write(USAGE);
37
41
  return 1;
package/dist/debrief.js CHANGED
@@ -19,6 +19,32 @@ async function withSpinner(out, text, work) {
19
19
  spin.stop();
20
20
  }
21
21
  }
22
+ /** A provider error worth retrying: the model did not answer in time or failed on its side. */
23
+ const isUnavailable = (error) => error instanceof ApiError && error.isProviderError && error.body.reason === 'unavailable';
24
+ /**
25
+ * Runs a model request. When the model doesn't answer, it tries once more by
26
+ * itself, then asks before each further try, so a slow moment never throws
27
+ * away a debrief in progress.
28
+ */
29
+ async function withRetry(prompter, request) {
30
+ const c = paint(prompter.out);
31
+ for (let attempt = 1;; attempt++) {
32
+ try {
33
+ return await request();
34
+ }
35
+ catch (error) {
36
+ if (!isUnavailable(error))
37
+ throw error;
38
+ if (attempt === 1) {
39
+ prompter.out.write(c.dim('The model did not answer in time. Trying again…\n'));
40
+ continue;
41
+ }
42
+ const again = await prompter.ask(`${c.yellow(error.message)} Try again? [Y/n] `);
43
+ if (again === null || /^n/i.test(again.trim()))
44
+ throw error;
45
+ }
46
+ }
47
+ }
22
48
  function printQuestion(out, question, total, followUp) {
23
49
  const c = paint(out);
24
50
  const label = `Question ${question.idx + 1}/${total}`;
@@ -55,12 +81,12 @@ async function askUntilPassed(debrief, first) {
55
81
  return later(out);
56
82
  let result;
57
83
  try {
58
- result = await withSpinner(out, 'Checking your answer…', () => api.request('POST', '/api/interview/answer', {
84
+ result = await withRetry(prompter, () => withSpinner(out, 'Checking your answer…', () => api.request('POST', '/api/interview/answer', {
59
85
  session: debrief.session,
60
86
  idx: question.idx,
61
87
  answer: answer.slice(0, MAX_ANSWER_CHARS),
62
88
  ...debrief.payload,
63
- }));
89
+ })));
64
90
  }
65
91
  catch (error) {
66
92
  return failure(out, error);
@@ -92,14 +118,14 @@ export async function runDebrief(api, prompter, target) {
92
118
  };
93
119
  let start;
94
120
  try {
95
- start = await withSpinner(out, 'Nemotron is reading your push…', () => api.request('POST', '/api/interview/start', {
121
+ start = await withRetry(prompter, () => withSpinner(out, 'Nemotron is reading your push…', () => api.request('POST', '/api/interview/start', {
96
122
  repo: target.repo,
97
123
  base_sha: target.baseSha,
98
124
  head_sha: target.headSha,
99
125
  changed_lines: target.diff.changedLines,
100
126
  push_id: target.pushId,
101
127
  ...payload,
102
- }));
128
+ })));
103
129
  }
104
130
  catch (error) {
105
131
  return failure(out, error);
package/dist/index.js CHANGED
@@ -10,16 +10,36 @@ import { settingsCommand } from './commands/settings.js';
10
10
  import { closestMatch, errorMessage } from './ui.js';
11
11
  /** Every command, in the order `ownit help` lists them. */
12
12
  const COMMANDS = {
13
- login: { help: [['ownit login [--server URL]', 'Sign in with GitHub (device flow)']], run: loginCommand },
13
+ login: {
14
+ help: [['ownit login [--server URL]', 'Sign in with GitHub (device flow)']],
15
+ details: `Shows a code and opens GitHub. Approve there, and you're signed in.
16
+ Use --server URL only for a self-hosted Ownit server.`,
17
+ run: loginCommand,
18
+ },
14
19
  logout: { help: [['ownit logout', 'Sign out']], run: logoutCommand },
15
20
  whoami: { help: [['ownit whoami', 'Show server, login, and key status']], run: whoamiCommand },
16
- init: { help: [['ownit init [--repo owner/name]', 'Install the pre-push hook in this repo']], run: initCommand },
21
+ init: {
22
+ help: [['ownit init [--repo owner/name]', 'Install the pre-push hook in this repo']],
23
+ details: `Run it inside a clone of your GitHub repo. It installs the pre-push hook, registers the
24
+ repo with Ownit, and prints the README badge. The Ownit GitHub App must be installed on the
25
+ repo; if it isn't, init prints the link. Use --repo owner/name when the remote isn't GitHub.`,
26
+ run: initCommand,
27
+ },
17
28
  interview: {
18
29
  help: [
19
30
  ['ownit interview', 'List waiting pushes and debrief the one you pick'],
20
31
  ['ownit interview --all', 'Debrief every waiting push in turn'],
21
32
  ['ownit interview --range A..B', 'Debrief a commit range, or one commit (SHA)'],
22
33
  ],
34
+ details: `With no options, it lists the pushes waiting for a debrief and asks which one to do:
35
+ a number that push Enter the newest
36
+ a all, one by one q quit
37
+ During a debrief, type "s" to stop and finish later.
38
+
39
+ Older commits (count once verified, if "ownit settings earlier on"):
40
+ ownit interview --range 8d21b07 just that commit
41
+ ownit interview --range 7a9e3f1..f3c9a12 every commit after 7a9e3f1, up to f3c9a12
42
+ Find commit IDs with "git log --oneline". Keep ranges small, about the size of a normal push.`,
23
43
  run: interviewCommand,
24
44
  },
25
45
  key: {
@@ -27,9 +47,21 @@ const COMMANDS = {
27
47
  ['ownit key set [nebius|nvidia]', 'Save your model API key (stored locally)'],
28
48
  ['ownit key clear|status', 'Remove or show it'],
29
49
  ],
50
+ details: ` ownit key set asks for the provider, then the key at a hidden prompt
51
+ ownit key set nvidia an nvapi- key from build.nvidia.com
52
+ ownit key set nebius a key from tokenfactory.nebius.com
53
+ Never type the key after the command: it would end up in your shell history.
54
+ NVIDIA_API_KEY or NEBIUS_API_KEY in the environment work too.`,
30
55
  run: keyCommand,
31
56
  },
32
- badge: { help: [['ownit badge', "Print this repo's badge Markdown for your README"]], run: badgeCommand },
57
+ badge: {
58
+ help: [['ownit badge', "Print this repo's badge Markdown for your README"]],
59
+ details: `Paste the output into your README. To copy it straight away:
60
+ ownit badge | clip (Windows)
61
+ ownit badge | pbcopy (macOS)
62
+ The repo page also has a Copy button.`,
63
+ run: badgeCommand,
64
+ },
33
65
  settings: {
34
66
  help: [
35
67
  ['ownit settings', "Show this repo's settings and page link"],
@@ -37,29 +69,63 @@ const COMMANDS = {
37
69
  ['ownit settings answers on|off', 'Show passing answers on the page (admins)'],
38
70
  ['ownit settings earlier on|off', 'Count older commits once debriefed (admins)'],
39
71
  ],
72
+ details: `Anyone who can push sees the settings; admins and maintainers change them.
73
+ questions show debrief questions on the repo page (default on)
74
+ answers show passing answers there too (default off; off deletes saved ones)
75
+ earlier count commits from before Ownit once someone debriefs them (default off)
76
+ Examples:
77
+ ownit settings answers on
78
+ ownit settings earlier on
79
+ Private repos never show questions or answers.`,
40
80
  run: settingsCommand,
41
81
  },
42
82
  uninstall: { help: [['ownit uninstall', 'Remove the pre-push hook']], run: uninstallCommand },
43
- help: { help: [['ownit help', 'Show this help']], run: () => print(helpText()) },
83
+ help: { help: [['ownit help [command]', 'Show this help, or details for one command']], run: (args) => helpCommand(args) },
44
84
  // Not listed: the installed git hook calls `ownit hook pre-push`.
45
85
  hook: { run: (args) => hookCommand(args) },
46
86
  version: { run: () => print(`${version()}\n`) },
47
87
  };
48
- const ALIASES = { page: 'settings', status: 'whoami', '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version' };
88
+ const ALIASES = { interviews: 'interview', setting: 'settings', page: 'settings', status: 'whoami', '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version' };
89
+ const commandName = (name) => (Object.hasOwn(ALIASES, name) ? ALIASES[name] : name);
49
90
  function findCommand(name) {
50
- const key = Object.hasOwn(ALIASES, name) ? ALIASES[name] : name;
91
+ const key = commandName(name);
51
92
  return Object.hasOwn(COMMANDS, key) ? COMMANDS[key] : undefined;
52
93
  }
94
+ const usageLines = (lines) => lines.map(([line, description]) => ` ${line.padEnd(33)}${description}`).join('\n');
95
+ /** One command's usage lines and details. */
96
+ function commandHelp(command) {
97
+ return `Usage:\n${usageLines(command.help ?? [])}\n${command.details ? `\n${command.details}\n` : ''}`;
98
+ }
99
+ function helpCommand(args) {
100
+ const [name] = args;
101
+ if (!name)
102
+ return print(helpText());
103
+ const command = findCommand(name);
104
+ if (!command?.help)
105
+ return unknownCommand(name);
106
+ return print(commandHelp(command));
107
+ }
108
+ /** Points a typo at the closest listed command, or prints the full help. */
109
+ function unknownCommand(name) {
110
+ // Suggest only listed commands, so a typo never points at the hidden hook command.
111
+ const listed = Object.keys(COMMANDS).filter((key) => COMMANDS[key].help);
112
+ const guess = closestMatch(name, listed);
113
+ process.stderr.write(guess
114
+ ? `Unknown command "${name}". Did you mean "ownit ${guess}"?\nRun "ownit help" to see every command.\n`
115
+ : `Unknown command "${name}".\n\n${helpText()}`);
116
+ return 1;
117
+ }
53
118
  function helpText() {
54
- const usage = Object.values(COMMANDS)
55
- .flatMap((command) => command.help ?? [])
56
- .map(([line, description]) => ` ${line.padEnd(33)}${description}`)
57
- .join('\n');
119
+ const usage = usageLines(Object.values(COMMANDS).flatMap((command) => command.help ?? []));
58
120
  return `ownit: debrief the code you just pushed.
59
121
 
122
+ Get started: ownit login, then ownit key set, then ownit init in your repo.
123
+ After that, just git push: Ownit asks "Debrief now? [Y/n]".
124
+
60
125
  Usage:
61
126
  ${usage}
62
127
 
128
+ Run "ownit help <command>" (or "ownit <command> --help") for details and examples.
63
129
  Set OWNIT_SKIP=1 to push once without a debrief prompt (the push is pending).
64
130
  `;
65
131
  }
@@ -74,16 +140,21 @@ function print(text) {
74
140
  async function main(argv) {
75
141
  const [name = 'help', ...args] = argv;
76
142
  const command = findCommand(name);
77
- if (!command) {
78
- // Suggest only listed commands, so a typo never points at the hidden hook command.
79
- const listed = Object.keys(COMMANDS).filter((key) => COMMANDS[key].help);
80
- const guess = closestMatch(name, listed);
81
- process.stderr.write(guess
82
- ? `Unknown command "${name}". Did you mean "ownit ${guess}"?\nRun "ownit help" to see every command.\n`
83
- : `Unknown command "${name}".\n\n${helpText()}`);
84
- return 1;
143
+ if (!command)
144
+ return unknownCommand(name);
145
+ if (command.help && (args.includes('--help') || args.includes('-h')))
146
+ return print(commandHelp(command));
147
+ try {
148
+ return await command.run(args);
149
+ }
150
+ catch (error) {
151
+ // A mistyped option, e.g. --rnage: say where the right ones are listed.
152
+ if (error.code?.startsWith('ERR_PARSE_ARGS')) {
153
+ process.stderr.write(`Ownit: ${errorMessage(error)}\nRun "ownit help ${commandName(name)}" for the options.\n`);
154
+ return 1;
155
+ }
156
+ throw error;
85
157
  }
86
- return command.run(args);
87
158
  }
88
159
  main(process.argv.slice(2)).then((code) => {
89
160
  process.exitCode = code;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getownit",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Debrief the code you just pushed, in your terminal, with NVIDIA Nemotron.",
5
5
  "license": "MIT",
6
6
  "repository": {