storymaps 1.3.4 → 1.4.1
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 +105 -1
- package/bin/storymaps.js +89 -26
- package/lib/commands/backup.js +37 -35
- package/lib/commands/diff.js +58 -38
- package/lib/commands/init.js +12 -3
- package/lib/commands/lock.js +42 -20
- package/lib/commands/open.js +21 -3
- package/lib/commands/project-sync.js +509 -0
- package/lib/commands/project.js +291 -0
- package/lib/commands/pull.js +59 -19
- package/lib/commands/push.js +34 -24
- package/lib/commands/status.js +40 -3
- package/lib/commands/unlock.js +34 -19
- package/lib/commands/validate.js +29 -0
- package/lib/core/yaml.js +23 -0
- package/lib/util/api.js +78 -6
- package/lib/util/file.js +15 -0
- package/lib/util/full-diff.js +42 -0
- package/lib/util/identity.js +34 -13
- package/lib/util/passwords.js +101 -0
- package/lib/util/project.js +215 -0
- package/lib/util/prompt.js +73 -0
- package/lib/util/summary.js +4 -3
- package/package.json +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Storymaps CLI — AGPL-3.0 — see LICENCE for details
|
|
2
|
+
// Questions, and the passwords for locked maps and projects
|
|
3
|
+
//
|
|
4
|
+
// A question is only asked when the CLI runs in a terminal. Run from a script, CI or most agent
|
|
5
|
+
// tools there's no terminal, so:
|
|
6
|
+
// - a question that only picks how much to do takes its default, and the command says so
|
|
7
|
+
// - a question that guards against overwriting or removing something stops, naming --force
|
|
8
|
+
// -y, --yes takes the defaults in a terminal too, for agent tools that do run in one.
|
|
9
|
+
// Passwords are never flags. They're typed, remembered after an unlock, or taken from the
|
|
10
|
+
// STORYMAPS_PASSWORD environment variable, for scripts and CI.
|
|
11
|
+
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
import { confirm, password, select } from '@inquirer/prompts';
|
|
14
|
+
|
|
15
|
+
const THEME = { prefix: '>' };
|
|
16
|
+
|
|
17
|
+
export const PASSWORD_ENV = 'STORYMAPS_PASSWORD';
|
|
18
|
+
|
|
19
|
+
export const isInteractive = () => !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
20
|
+
|
|
21
|
+
export const hashPassword = (pw) => createHash('sha256').update(pw).digest('hex');
|
|
22
|
+
|
|
23
|
+
// The password from STORYMAPS_PASSWORD, hashed, or null when it isn't set
|
|
24
|
+
export const envPasswordHash = () => {
|
|
25
|
+
const pw = process.env[PASSWORD_ENV];
|
|
26
|
+
return pw ? hashPassword(pw) : null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Stops a command with a message, without the "Error:" a crash would print
|
|
30
|
+
export class StopError extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'StopError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A question about how much to do. Returns the default with --yes or no terminal.
|
|
38
|
+
export async function choose({ message, choices, defaultValue, yes }) {
|
|
39
|
+
if (yes || !isInteractive()) return defaultValue;
|
|
40
|
+
return select({ message, choices, default: defaultValue, theme: THEME });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A yes or no question about how much to do, with the same rules as choose
|
|
44
|
+
export async function ask({ message, defaultValue = false, yes }) {
|
|
45
|
+
if (yes || !isInteractive()) return defaultValue;
|
|
46
|
+
return confirm({ message, default: defaultValue, theme: THEME });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A question that guards against overwriting or removing something. --force answers yes; with
|
|
50
|
+
// no terminal and no --force, the command stops and says to use it.
|
|
51
|
+
export async function guard({ message, force, stopMessage }) {
|
|
52
|
+
if (force) return true;
|
|
53
|
+
if (!isInteractive()) throw new StopError(`${stopMessage || 'Nothing was changed.'} Run again with --force to go ahead without being asked.`);
|
|
54
|
+
return confirm({ message, default: true, theme: THEME });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A password, hashed: from STORYMAPS_PASSWORD when allowed and set, or typed in a terminal.
|
|
58
|
+
// confirmTwice asks again, for setting a new password. Throws a StopError when neither is possible.
|
|
59
|
+
export async function askPassword({ allowEnv = true, confirmTwice = false, noPasswordMessage } = {}) {
|
|
60
|
+
if (allowEnv) {
|
|
61
|
+
const fromEnv = envPasswordHash();
|
|
62
|
+
if (fromEnv) return fromEnv;
|
|
63
|
+
}
|
|
64
|
+
if (!isInteractive()) {
|
|
65
|
+
throw new StopError(noPasswordMessage || `This needs a password, and there's no terminal to type it in. Set ${PASSWORD_ENV} for this command.`);
|
|
66
|
+
}
|
|
67
|
+
const pw = await password({ message: 'Password:', theme: THEME });
|
|
68
|
+
if (confirmTwice) {
|
|
69
|
+
const again = await password({ message: 'Confirm password:', theme: THEME });
|
|
70
|
+
if (pw !== again) throw new StopError('Passwords do not match.');
|
|
71
|
+
}
|
|
72
|
+
return hashPassword(pw);
|
|
73
|
+
}
|
package/lib/util/summary.js
CHANGED
|
@@ -11,7 +11,8 @@ const FIELDS = ['body', 'color', 'url', 'status', 'points', 'tags'];
|
|
|
11
11
|
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
12
12
|
|
|
13
13
|
// Slice names can span lines (e.g. "V1:\nBasic House"); keep each summary entry on one line
|
|
14
|
-
|
|
14
|
+
// A name on one line, or (no name) for an unnamed step or slice, which would otherwise print blank
|
|
15
|
+
const oneLine = (s) => String(s ?? '').replace(/\s*\n\s*/g, ' ').trim() || '(no name)';
|
|
15
16
|
|
|
16
17
|
const changedFields = (a, b) =>
|
|
17
18
|
FIELDS.filter(f => JSON.stringify(a?.[f] ?? null) !== JSON.stringify(b?.[f] ?? null));
|
|
@@ -139,8 +140,8 @@ export function changeSummary(oldYaml, newYaml) {
|
|
|
139
140
|
const total = newSteps.length - oldSteps.length;
|
|
140
141
|
const delta = total > 0 ? chalk.green(`+${total}`) : total < 0 ? chalk.red(`${total}`) : chalk.dim('±0');
|
|
141
142
|
lines.push(` steps: ${delta}`);
|
|
142
|
-
for (const s of addedSteps) lines.push(chalk.green(` + ${s}`));
|
|
143
|
-
for (const s of removedSteps) lines.push(chalk.red(` - ${s}`));
|
|
143
|
+
for (const s of addedSteps) lines.push(chalk.green(` + ${oneLine(s)}`));
|
|
144
|
+
for (const s of removedSteps) lines.push(chalk.red(` - ${oneLine(s)}`));
|
|
144
145
|
}
|
|
145
146
|
if (reordered(oldSteps, newSteps)) lines.push(chalk.yellow(' ~ steps reordered'));
|
|
146
147
|
const stepFields = new Set();
|