storymaps 1.1.2 → 1.2.2
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 +9 -0
- package/bin/storymaps.js +7 -0
- package/lib/commands/log.js +42 -0
- package/lib/commands/push.js +10 -1
- package/lib/util/api.js +6 -0
- package/lib/util/summary.js +34 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -133,6 +133,15 @@ storymaps status other.yml # progress overview of a specific file
|
|
|
133
133
|
|
|
134
134
|
Progress overview: slices, status counts, and blocked stories.
|
|
135
135
|
|
|
136
|
+
### `log`
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
storymaps log # activity log for storymap.yml
|
|
140
|
+
storymaps log storymaps.io/abc123 # activity log for a specific map
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Shows timestamped activity entries with source labels (cli, mobile, web).
|
|
144
|
+
|
|
136
145
|
### `lock` / `unlock`
|
|
137
146
|
|
|
138
147
|
```bash
|
package/bin/storymaps.js
CHANGED
|
@@ -16,6 +16,7 @@ import { openCommand } from '../lib/commands/open.js';
|
|
|
16
16
|
import { lockCommand } from '../lib/commands/lock.js';
|
|
17
17
|
import { unlockCommand } from '../lib/commands/unlock.js';
|
|
18
18
|
import { statusCommand } from '../lib/commands/status.js';
|
|
19
|
+
import { logCommand } from '../lib/commands/log.js';
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
const program = new Command();
|
|
@@ -101,6 +102,12 @@ program
|
|
|
101
102
|
.option('-f, --file <path>', 'path to storymap file', 'storymap.yml')
|
|
102
103
|
.action(statusCommand);
|
|
103
104
|
|
|
105
|
+
program
|
|
106
|
+
.command('log')
|
|
107
|
+
.description('Show activity log for a map')
|
|
108
|
+
.argument('[target]', 'map ID, domain/ID, or full URL')
|
|
109
|
+
.option('-f, --file <path>', 'local file path', 'storymap.yml')
|
|
110
|
+
.action(logCommand);
|
|
104
111
|
|
|
105
112
|
program.parseAsync().catch(err => {
|
|
106
113
|
const message = err.name === 'ExitPromptError' ? 'Cancelled.'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Storymaps CLI — AGPL-3.0 — see LICENCE for details
|
|
2
|
+
// storymaps log — show activity log for a map
|
|
3
|
+
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { resolveTarget } from '../util/identity.js';
|
|
7
|
+
import { getLog } from '../util/api.js';
|
|
8
|
+
|
|
9
|
+
function formatTime(ts) {
|
|
10
|
+
const d = new Date(ts);
|
|
11
|
+
const now = new Date();
|
|
12
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
13
|
+
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
14
|
+
|
|
15
|
+
if (d.toDateString() === now.toDateString()) return time;
|
|
16
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${time}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sourceLabel(entry) {
|
|
20
|
+
if (entry.src === 'cli') return '>_';
|
|
21
|
+
if (entry.src === 'mobile') return '📱';
|
|
22
|
+
return '🌐';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function logCommand(target, opts) {
|
|
26
|
+
const filePath = resolve(opts.file || 'storymap.yml');
|
|
27
|
+
const { id, baseUrl } = await resolveTarget(target, filePath);
|
|
28
|
+
|
|
29
|
+
const entries = await getLog(baseUrl, id);
|
|
30
|
+
|
|
31
|
+
if (entries.length === 0) {
|
|
32
|
+
console.log(chalk.dim('No activity yet.'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const time = chalk.dim(formatTime(entry.ts));
|
|
38
|
+
const icon = sourceLabel(entry);
|
|
39
|
+
const who = entry.src !== 'cli' ? `${entry.name || 'guest'}: ` : '';
|
|
40
|
+
console.log(`${time} ${icon} ${who}${entry.text}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
package/lib/commands/push.js
CHANGED
|
@@ -7,7 +7,8 @@ import { resolve } from 'node:path';
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import { password } from '@inquirer/prompts';
|
|
9
9
|
import jsyaml from 'js-yaml';
|
|
10
|
-
import { importFromYaml } from '../core/yaml.js';
|
|
10
|
+
import { exportToYaml, importFromYaml, yamlObjToJson } from '../core/yaml.js';
|
|
11
|
+
import { printChangeSummary } from '../util/summary.js';
|
|
11
12
|
import { resolveTarget } from '../util/identity.js';
|
|
12
13
|
import { getMap, putMap, postMap, ApiError } from '../util/api.js';
|
|
13
14
|
import { detectFormat, readCache, writeCache } from '../util/file.js';
|
|
@@ -70,6 +71,14 @@ export async function pushCommand(target, opts) {
|
|
|
70
71
|
|
|
71
72
|
const contentType = format === 'json' ? 'application/json' : 'text/yaml';
|
|
72
73
|
|
|
74
|
+
// Show a change summary before pushing
|
|
75
|
+
try {
|
|
76
|
+
const { data: remoteData } = await getMap(baseUrl, id, 'yaml');
|
|
77
|
+
const remoteNorm = exportToYaml(yamlObjToJson(jsyaml.load(remoteData)));
|
|
78
|
+
const localNorm = exportToYaml(yamlObjToJson(jsyaml.load(content)));
|
|
79
|
+
if (remoteNorm !== localNorm) printChangeSummary(remoteNorm, localNorm);
|
|
80
|
+
} catch { /* non-fatal */ }
|
|
81
|
+
|
|
73
82
|
const cache = await readCache(id);
|
|
74
83
|
let etag = opts.force ? undefined : cache.etag;
|
|
75
84
|
let lockPasswordHash = cache.hash;
|
package/lib/util/api.js
CHANGED
|
@@ -97,4 +97,10 @@ export async function removeLock(baseUrl, id, passwordHash) {
|
|
|
97
97
|
return res.json();
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
export async function getLog(baseUrl, id) {
|
|
101
|
+
const res = await fetch(`${baseUrl}/api/maps/${id}/log`);
|
|
102
|
+
await handleResponse(res);
|
|
103
|
+
return res.json();
|
|
104
|
+
}
|
|
105
|
+
|
|
100
106
|
export { ApiError };
|
package/lib/util/summary.js
CHANGED
|
@@ -42,26 +42,43 @@ export function changeSummary(oldYaml, newYaml) {
|
|
|
42
42
|
lines.push(chalk.red(` - slice: ${s}`));
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
//
|
|
46
|
-
const
|
|
47
|
-
const
|
|
45
|
+
// Detect card-level changes per slice (name-aware, handles renames)
|
|
46
|
+
const collectCardNames = (map) => {
|
|
47
|
+
const result = {};
|
|
48
48
|
for (const slice of (map.slices || [])) {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
const names = [];
|
|
50
|
+
for (const cards of Object.values(slice.stories || {})) {
|
|
51
|
+
if (Array.isArray(cards)) names.push(...cards.map(c => c.name).filter(Boolean));
|
|
52
|
+
}
|
|
53
|
+
result[slice.name] = names;
|
|
52
54
|
}
|
|
53
|
-
return
|
|
55
|
+
return result;
|
|
54
56
|
};
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
for (const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
const oldCards = collectCardNames(old);
|
|
58
|
+
const newCards = collectCardNames(cur);
|
|
59
|
+
for (const sliceName of new Set([...Object.keys(oldCards), ...Object.keys(newCards)])) {
|
|
60
|
+
const oldNames = oldCards[sliceName] || [];
|
|
61
|
+
const newNames = newCards[sliceName] || [];
|
|
62
|
+
const oldCounts = new Map();
|
|
63
|
+
const newCounts = new Map();
|
|
64
|
+
for (const n of oldNames) oldCounts.set(n, (oldCounts.get(n) || 0) + 1);
|
|
65
|
+
for (const n of newNames) newCounts.set(n, (newCounts.get(n) || 0) + 1);
|
|
66
|
+
const allNames = new Set([...oldCounts.keys(), ...newCounts.keys()]);
|
|
67
|
+
let added = 0, removed = 0;
|
|
68
|
+
for (const n of allNames) {
|
|
69
|
+
const o = oldCounts.get(n) || 0;
|
|
70
|
+
const c = newCounts.get(n) || 0;
|
|
71
|
+
if (c > o) added += c - o;
|
|
72
|
+
if (o > c) removed += o - c;
|
|
73
|
+
}
|
|
74
|
+
if (added === 0 && removed === 0) continue;
|
|
75
|
+
const net = newNames.length - oldNames.length;
|
|
76
|
+
if (net === 0) {
|
|
77
|
+
const word = added === 1 ? 'card' : 'cards';
|
|
78
|
+
lines.push(chalk.yellow(` ~ ${sliceName}: ${added} ${word} renamed`));
|
|
79
|
+
} else {
|
|
80
|
+
if (added) lines.push(chalk.green(` + ${sliceName}: ${added} ${added === 1 ? 'card' : 'cards'}`));
|
|
81
|
+
if (removed) lines.push(chalk.red(` - ${sliceName}: ${removed} ${removed === 1 ? 'card' : 'cards'}`));
|
|
65
82
|
}
|
|
66
83
|
}
|
|
67
84
|
|