storymaps 1.3.0 → 1.3.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 CHANGED
@@ -138,9 +138,10 @@ storymaps diff file1.yml file2.yml # compare two local files
138
138
  ```bash
139
139
  storymaps status # progress overview of storymap.yml
140
140
  storymaps status other.yml # progress overview of a specific file
141
+ storymaps status storymaps.io/abc123 # progress overview of a map on the server, no file needed
141
142
  ```
142
143
 
143
- Progress overview: slices, status counts, and blocked stories.
144
+ Progress overview: slices, status counts, and blocked stories. A link to a map inside a project works too.
144
145
 
145
146
  ### `log`
146
147
 
@@ -159,17 +160,47 @@ storymaps unlock # session unlock
159
160
  storymaps unlock --remove # permanently remove the lock
160
161
  ```
161
162
 
162
- ### `backup` / `backups` / `restore`
163
+ ### `backup`
164
+
165
+ Backs up the map on the server. Add a note to tell the backup apart later.
163
166
 
164
167
  ```bash
165
- storymaps backup # back up the map on the server
168
+ storymaps backup # back up the map in storymap.yml
166
169
  storymaps backup -m "Before the workshop" # with a note
167
- storymaps backups # list the backups, newest first
168
- storymaps restore 7ed8450a830a # restore one, after backing up the map as it is now
169
- storymaps restore 7ed8450a830a --force # restore without asking first
170
+ storymaps backup storymaps.io/abc123 # back up a map you haven't pulled
171
+ ```
172
+
173
+ The backup shows in the map's activity log, and in **Menu → Backups** in the app.
174
+
175
+ ### `backups`
176
+
177
+ Lists a map's backups, newest first, with the ID you need to restore one.
178
+
179
+ ```bash
180
+ > storymaps backups
181
+
182
+ 6e40f124ae7e 13:25 63 cards Before the workshop
183
+ 998e16f31f84 11:02 58 cards (no note)
170
184
  ```
171
185
 
172
- These are the same backups as **Menu → Backups** in the app, and the server keeps the newest 8 of each map. A restore changes the map on the server, not your local file, so run `storymaps pull` afterwards. Until you do, `storymaps push` stops with a conflict, so an old local file can't undo the restore. On a locked map, restore asks for the password, or uses the one saved by `storymaps unlock`. A self-hosted server needs updating before the CLI can make backups on it.
186
+ ### `restore`
187
+
188
+ Puts the map back the way it was in one of its backups. Give it the backup ID from `storymaps backups`.
189
+
190
+ ```bash
191
+ storymaps restore 6e40f124ae7e # asks before it restores
192
+ storymaps restore 6e40f124ae7e --force # restores without asking
193
+ ```
194
+
195
+ - **It backs up first.** Before restoring, the CLI backs up the map as it is now, so a restore can be undone with another restore.
196
+ - **It changes the server, not your file.** Run `storymaps pull` afterwards to get the restored map. Until you do, `storymaps push` stops with a conflict, so an old local file can't undo the restore by accident.
197
+ - **Locked maps** use the password saved by `storymaps unlock`, or ask for it.
198
+
199
+ ### About backups
200
+
201
+ - The CLI and the app share the same backups. A backup made in the app shows in `storymaps backups`, and one made with the CLI shows in the app.
202
+ - The server keeps the newest 8 backups of each map.
203
+ - A self-hosted server needs updating before the CLI can make backups on it. An older one turns the request away, and the CLI says so.
173
204
 
174
205
  ### `validate`
175
206
 
@@ -181,7 +212,7 @@ storymaps validate storymap.yml # check YAML structure
181
212
 
182
213
  A target can be a map ID, `domain/id`, or a full URL, including a map inside a project link (`storymaps.io/p/<project>/<map>`).
183
214
 
184
- `pull`, `push`, `open`, `lock`, `unlock`, `status` and `log` take `-f, --file <path>` to use a file other than `storymap.yml`.
215
+ `pull`, `push`, `open`, `lock`, `unlock`, `status`, `log`, `backup`, `backups` and `restore` take `-f, --file <path>` to use a file other than `storymap.yml`.
185
216
 
186
217
  ### `convert`
187
218
 
package/bin/storymaps.js CHANGED
@@ -99,7 +99,7 @@ program
99
99
  program
100
100
  .command('status')
101
101
  .description('Show a progress overview of the story map')
102
- .argument('[file]', 'path to storymap file')
102
+ .argument('[target]', 'storymap file, or a map ID, domain/ID, or full URL')
103
103
  .option('-f, --file <path>', 'path to storymap file', 'storymap.yml')
104
104
  .action(statusCommand);
105
105
 
@@ -128,7 +128,7 @@ program
128
128
  program
129
129
  .command('restore')
130
130
  .description('Restore a map from one of its backups, after backing up the map as it is now')
131
- .argument('<backup>', 'backup ID, from storymaps backups')
131
+ .argument('<backup-id>', 'the ID of a backup, from storymaps backups')
132
132
  .argument('[target]', 'map ID, domain/ID, or full URL')
133
133
  .option('-f, --file <path>', 'local file path', 'storymap.yml')
134
134
  .option('--force', 'restore without asking first')
@@ -33,7 +33,7 @@ export async function backupsCommand(target, opts) {
33
33
  const imported = b.imported ? chalk.dim(' imported') : '';
34
34
  console.log(`${chalk.cyan(b.id)} ${chalk.dim(formatTime(b.timestamp))} ${cards(b.cardCount).padEnd(9)} ${note}${imported}`);
35
35
  }
36
- console.log(chalk.dim(`\nRestore one with ${chalk.cyan('storymaps restore <backup>')}`));
36
+ console.log(chalk.dim(`\nRestore one with ${chalk.cyan('storymaps restore <backup ID>')}`));
37
37
  }
38
38
 
39
39
  // The lock password for a locked map, checked with the server before anything changes.
@@ -83,7 +83,7 @@ export async function restoreCommand(backupId, target, opts) {
83
83
 
84
84
  // Like the app: a backup of the map as it is now, so the restore can be undone
85
85
  const safety = await createBackup(baseUrl, id, 'Auto: before restore');
86
- await putMap(baseUrl, id, backup.data, { contentType: 'application/json', lockPasswordHash });
86
+ await putMap(baseUrl, id, backup.data, { contentType: 'application/json', lockPasswordHash, restoredBackup: backup.id });
87
87
 
88
88
  console.log(chalk.green(`Restored map ${id} from backup ${backup.id}`));
89
89
  console.log(chalk.dim(`The map before the restore is backup ${safety.id}.`));
@@ -5,6 +5,8 @@ import { readFile } from 'node:fs/promises';
5
5
  import { resolve } from 'node:path';
6
6
  import chalk from 'chalk';
7
7
  import jsyaml from 'js-yaml';
8
+ import { resolveTarget } from '../util/identity.js';
9
+ import { getMap, ApiError } from '../util/api.js';
8
10
 
9
11
  const STATUS_ICONS = {
10
12
  'done': chalk.green('●'),
@@ -24,16 +26,34 @@ function pct(n, total) {
24
26
  return `${Math.round((n / total) * 100)}%`;
25
27
  }
26
28
 
27
- export async function statusCommand(target, opts) {
28
- const filePath = resolve(target || opts.file || 'storymap.yml');
29
-
30
- let content;
29
+ // A local file, or else the map on the server when the target is a link or map ID
30
+ async function loadContent(target, opts) {
31
+ const isUrl = target && /^https?:\/\//i.test(target);
32
+ const filePath = resolve((!isUrl && target) || opts.file || 'storymap.yml');
33
+ if (!isUrl) {
34
+ try {
35
+ return await readFile(filePath, 'utf-8');
36
+ } catch {
37
+ if (!target) {
38
+ console.error(chalk.red(`File not found: ${filePath}`));
39
+ process.exit(1);
40
+ }
41
+ }
42
+ }
43
+ const { id, baseUrl } = await resolveTarget(target, resolve(opts.file || 'storymap.yml'));
31
44
  try {
32
- content = await readFile(filePath, 'utf-8');
33
- } catch {
34
- console.error(chalk.red(`File not found: ${filePath}`));
35
- process.exit(1);
45
+ return (await getMap(baseUrl, id, 'yaml')).data;
46
+ } catch (err) {
47
+ if (err instanceof ApiError && err.status === 404 && !isUrl && !target.includes('/')) {
48
+ console.error(chalk.red(`No file or map found: ${target}`));
49
+ process.exit(1);
50
+ }
51
+ throw err;
36
52
  }
53
+ }
54
+
55
+ export async function statusCommand(target, opts) {
56
+ const content = await loadContent(target, opts);
37
57
 
38
58
  const map = jsyaml.load(content);
39
59
  if (!map || typeof map !== 'object') {
package/lib/util/api.js CHANGED
@@ -47,10 +47,12 @@ export async function getMap(baseUrl, id, format = 'yaml') {
47
47
  return { data, etag };
48
48
  }
49
49
 
50
- export async function putMap(baseUrl, id, body, { etag, contentType = 'text/yaml', lockPasswordHash } = {}) {
50
+ export async function putMap(baseUrl, id, body, { etag, contentType = 'text/yaml', lockPasswordHash, restoredBackup } = {}) {
51
51
  const headers = { 'Content-Type': contentType };
52
52
  if (etag) headers['If-Match'] = etag;
53
53
  if (lockPasswordHash) headers['X-Lock-Password'] = lockPasswordHash;
54
+ // Tells the server this push is a restore, so the activity log says so
55
+ if (restoredBackup) headers['X-Restored-Backup'] = restoredBackup;
54
56
  const res = await request(`${baseUrl}/api/maps/${id}`, {
55
57
  method: 'PUT',
56
58
  headers,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storymaps",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "CLI tool for storymaps.io - import, export, validate, and manage user story maps from the terminal",
5
5
  "type": "module",
6
6
  "bin": {