faces-cli 1.8.3 → 1.8.5
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/dist/catalog.js +13 -10
- package/dist/commands/catalog/doctor.js +2 -1
- package/dist/commands/style/revert.js +9 -12
- package/dist/team-catalog.js +4 -9
- package/dist/utils.d.ts +26 -0
- package/dist/utils.js +37 -0
- package/oclif.manifest.json +331 -331
- package/package.json +1 -1
package/dist/catalog.js
CHANGED
|
@@ -2,14 +2,9 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { loadConfig } from './config.js';
|
|
5
|
+
import { MAX_FRONTMATTER_VALUE, quoteYaml, unquoteYaml } from './utils.js';
|
|
5
6
|
export const CATALOG_DIR = path.join(os.homedir(), '.faces', 'catalog');
|
|
6
7
|
export const CATALOG_INDEX = path.join(os.homedir(), '.faces', 'catalog.json');
|
|
7
|
-
function quoteYaml(value) {
|
|
8
|
-
if (/[:#\[\]{}&*!|>'"%@`\n]/.test(value) || value !== value.trim() || value === '') {
|
|
9
|
-
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
10
|
-
}
|
|
11
|
-
return value;
|
|
12
|
-
}
|
|
13
8
|
function serializeFrontmatter(fm) {
|
|
14
9
|
const lines = ['---'];
|
|
15
10
|
lines.push(`name: ${quoteYaml(fm.name)}`);
|
|
@@ -57,8 +52,7 @@ function parseFrontmatter(content) {
|
|
|
57
52
|
const colonIdx = line.indexOf(':');
|
|
58
53
|
if (colonIdx > 0) {
|
|
59
54
|
const k = line.slice(0, colonIdx).trim();
|
|
60
|
-
|
|
61
|
-
attributes[k] = v;
|
|
55
|
+
attributes[k] = unquoteYaml(line.slice(colonIdx + 1));
|
|
62
56
|
}
|
|
63
57
|
continue;
|
|
64
58
|
}
|
|
@@ -67,7 +61,7 @@ function parseFrontmatter(content) {
|
|
|
67
61
|
if (colonIdx < 0)
|
|
68
62
|
continue;
|
|
69
63
|
const key = line.slice(0, colonIdx).trim();
|
|
70
|
-
const val = line.slice(colonIdx + 1)
|
|
64
|
+
const val = unquoteYaml(line.slice(colonIdx + 1));
|
|
71
65
|
if (key === 'name')
|
|
72
66
|
fm.name = val;
|
|
73
67
|
else if (key === 'description')
|
|
@@ -137,7 +131,16 @@ export class CatalogService {
|
|
|
137
131
|
fm.description = faceData.description;
|
|
138
132
|
}
|
|
139
133
|
else if (existingDescription) {
|
|
140
|
-
|
|
134
|
+
// A description this size is corrupt, not long. Carrying it forward is
|
|
135
|
+
// what kept it alive across writes; dropping it lets the next sync
|
|
136
|
+
// replace it with the real one.
|
|
137
|
+
if (existingDescription.length > MAX_FRONTMATTER_VALUE) {
|
|
138
|
+
process.stderr.write(`warn: discarded a ${existingDescription.length}-character description on '${alias}' — ` +
|
|
139
|
+
'too large to be real. It will be restored from the server on the next sync.\n');
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
fm.description = existingDescription;
|
|
143
|
+
}
|
|
141
144
|
}
|
|
142
145
|
if (faceData.default_model)
|
|
143
146
|
fm.default_model = faceData.default_model;
|
|
@@ -4,6 +4,7 @@ import { Flags } from '@oclif/core';
|
|
|
4
4
|
import { BaseCommand } from '../../base.js';
|
|
5
5
|
import { FacesAPIError } from '../../client.js';
|
|
6
6
|
import { loadConfig } from '../../config.js';
|
|
7
|
+
import { unquoteYaml } from '../../utils.js';
|
|
7
8
|
import { CatalogService, CATALOG_DIR } from '../../catalog.js';
|
|
8
9
|
import { TeamCatalogService, TEAMS_DIR, slugifyTeamName } from '../../team-catalog.js';
|
|
9
10
|
import { resolveEndpoint, MESSAGES_ENDPOINT, RESPONSES_ENDPOINT } from '../../routing.js';
|
|
@@ -67,7 +68,7 @@ export default class CatalogDoctor extends BaseCommand {
|
|
|
67
68
|
if (ci < 0)
|
|
68
69
|
continue;
|
|
69
70
|
const k = line.slice(0, ci).trim();
|
|
70
|
-
const v = line.slice(ci + 1)
|
|
71
|
+
const v = unquoteYaml(line.slice(ci + 1));
|
|
71
72
|
if (k === 'name')
|
|
72
73
|
fm.name = v;
|
|
73
74
|
if (k === 'description')
|
|
@@ -3,9 +3,10 @@ import { BaseCommand } from '../../base.js';
|
|
|
3
3
|
import { FacesAPIError } from '../../client.js';
|
|
4
4
|
import { VERSIONS_PATH } from '../../style.js';
|
|
5
5
|
export default class StyleRevert extends BaseCommand {
|
|
6
|
-
static description = 'Go back to the style a face had before the last style:make.
|
|
7
|
-
'
|
|
8
|
-
'
|
|
6
|
+
static description = 'Go back to the style a face had before the last style:make. Two versions are kept per medium, so ' +
|
|
7
|
+
'this is a toggle: running it again returns to where you started. A face holds one style per medium, ' +
|
|
8
|
+
'so name which one with --medium when it has more than one. It changes how the face writes ' +
|
|
9
|
+
'immediately, so it asks first.';
|
|
9
10
|
static examples = [
|
|
10
11
|
'<%= config.bin %> <%= command.id %> alice --yes',
|
|
11
12
|
'<%= config.bin %> <%= command.id %> alice --medium email --yes',
|
|
@@ -50,13 +51,6 @@ export default class StyleRevert extends BaseCommand {
|
|
|
50
51
|
if (err.statusCode === 409) {
|
|
51
52
|
this.error(`Error (409): ${err.message}\nSee what is there: faces style:versions ${args.alias}`);
|
|
52
53
|
}
|
|
53
|
-
// Reverting a named medium currently fails this way even when the
|
|
54
|
-
// server itself reports can_revert: true (faces-backend-shared#590).
|
|
55
|
-
if (err.statusCode === 500) {
|
|
56
|
-
this.error(`Error (500): the server failed to revert '${args.alias}'.\n` +
|
|
57
|
-
'Reverting a per-medium style is currently broken upstream and nothing was changed. ' +
|
|
58
|
-
'Tracked as faces-backend-shared#590.');
|
|
59
|
-
}
|
|
60
54
|
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
61
55
|
}
|
|
62
56
|
throw err;
|
|
@@ -71,8 +65,11 @@ export default class StyleRevert extends BaseCommand {
|
|
|
71
65
|
else {
|
|
72
66
|
this.log(`'${args.alias}' reverted.`);
|
|
73
67
|
}
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
// Reverting again returns to where you started — it is a toggle between the
|
|
69
|
+
// two kept versions, not a walk backwards. Name the version it goes back to
|
|
70
|
+
// so the user does not have to hold it in their head.
|
|
71
|
+
const back = from === undefined ? '' : ` to return to version ${from}`;
|
|
72
|
+
this.log(`Run it again${back}. List versions: faces style:versions ${args.alias}`);
|
|
76
73
|
return data;
|
|
77
74
|
}
|
|
78
75
|
}
|
package/dist/team-catalog.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { quoteYaml, unquoteYaml } from './utils.js';
|
|
4
5
|
export const TEAMS_DIR = path.join(os.homedir(), '.faces', 'teams');
|
|
5
6
|
/**
|
|
6
7
|
* Directory name for a team.
|
|
@@ -16,12 +17,6 @@ export function slugifyTeamName(name) {
|
|
|
16
17
|
.replace(/[^a-z0-9]+/g, '-')
|
|
17
18
|
.replace(/^-|-$/g, '');
|
|
18
19
|
}
|
|
19
|
-
function quoteYaml(value) {
|
|
20
|
-
if (/[:#\[\]{}&*!|>'"%@`\n]/.test(value) || value !== value.trim() || value === '') {
|
|
21
|
-
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
22
|
-
}
|
|
23
|
-
return value;
|
|
24
|
-
}
|
|
25
20
|
function serializeFrontmatter(fm) {
|
|
26
21
|
const lines = ['---'];
|
|
27
22
|
if (fm.id)
|
|
@@ -52,13 +47,13 @@ function parseFrontmatter(content) {
|
|
|
52
47
|
const key = line.slice(0, colonIdx).trim();
|
|
53
48
|
const val = line.slice(colonIdx + 1).trim();
|
|
54
49
|
if (key === 'id') {
|
|
55
|
-
fm.id = val
|
|
50
|
+
fm.id = unquoteYaml(val);
|
|
56
51
|
}
|
|
57
52
|
else if (key === 'name') {
|
|
58
|
-
fm.name = val
|
|
53
|
+
fm.name = unquoteYaml(val);
|
|
59
54
|
}
|
|
60
55
|
else if (key === 'description') {
|
|
61
|
-
fm.description = val
|
|
56
|
+
fm.description = unquoteYaml(val);
|
|
62
57
|
}
|
|
63
58
|
else if (key === 'tags') {
|
|
64
59
|
const inner = val.replace(/^\[/, '').replace(/\]$/, '');
|
package/dist/utils.d.ts
CHANGED
|
@@ -87,3 +87,29 @@ export interface ResolvedFace {
|
|
|
87
87
|
export declare function resolveFace(client: {
|
|
88
88
|
get: (path: string) => Promise<unknown>;
|
|
89
89
|
}, spec: string, include?: string): Promise<ResolvedFace>;
|
|
90
|
+
/**
|
|
91
|
+
* A description this large is not a description.
|
|
92
|
+
*
|
|
93
|
+
* The escaping bug this bound exists to catch grew one field to 128 MiB by
|
|
94
|
+
* doubling it on every unrelated write. A ceiling turns a silent geometric
|
|
95
|
+
* corruption into one complaint on the first doubling, and stops a value that
|
|
96
|
+
* is already corrupt from being carried forward forever.
|
|
97
|
+
*/
|
|
98
|
+
export declare const MAX_FRONTMATTER_VALUE: number;
|
|
99
|
+
/**
|
|
100
|
+
* Quote a YAML scalar, escaping what would otherwise break the line.
|
|
101
|
+
*
|
|
102
|
+
* Paired with {@link unquoteYaml}. The two must stay exact inverses: if one
|
|
103
|
+
* escapes and the other does not unescape, every read-modify-write doubles the
|
|
104
|
+
* backslashes in the value, which is invisible until a catalog is measured in
|
|
105
|
+
* hundreds of megabytes.
|
|
106
|
+
*/
|
|
107
|
+
export declare function quoteYaml(value: string): string;
|
|
108
|
+
/**
|
|
109
|
+
* Reverse {@link quoteYaml}: strip the surrounding quotes, then unescape.
|
|
110
|
+
*
|
|
111
|
+
* The unescape is a single left-to-right pass, not two sequential replaces.
|
|
112
|
+
* `\\` and `\"` have to be consumed in the order they appear, or a value
|
|
113
|
+
* containing a literal backslash before a quote comes back wrong.
|
|
114
|
+
*/
|
|
115
|
+
export declare function unquoteYaml(raw: string): string;
|
package/dist/utils.js
CHANGED
|
@@ -194,3 +194,40 @@ async function findForeignFace(client, alias, owner, q) {
|
|
|
194
194
|
owner: ownedBy,
|
|
195
195
|
};
|
|
196
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* A description this large is not a description.
|
|
199
|
+
*
|
|
200
|
+
* The escaping bug this bound exists to catch grew one field to 128 MiB by
|
|
201
|
+
* doubling it on every unrelated write. A ceiling turns a silent geometric
|
|
202
|
+
* corruption into one complaint on the first doubling, and stops a value that
|
|
203
|
+
* is already corrupt from being carried forward forever.
|
|
204
|
+
*/
|
|
205
|
+
export const MAX_FRONTMATTER_VALUE = 64 * 1024;
|
|
206
|
+
/**
|
|
207
|
+
* Quote a YAML scalar, escaping what would otherwise break the line.
|
|
208
|
+
*
|
|
209
|
+
* Paired with {@link unquoteYaml}. The two must stay exact inverses: if one
|
|
210
|
+
* escapes and the other does not unescape, every read-modify-write doubles the
|
|
211
|
+
* backslashes in the value, which is invisible until a catalog is measured in
|
|
212
|
+
* hundreds of megabytes.
|
|
213
|
+
*/
|
|
214
|
+
export function quoteYaml(value) {
|
|
215
|
+
if (/[:#[\]{}&*!|>'"%@`\n]/.test(value) || value !== value.trim() || value === '') {
|
|
216
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
217
|
+
}
|
|
218
|
+
return value;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Reverse {@link quoteYaml}: strip the surrounding quotes, then unescape.
|
|
222
|
+
*
|
|
223
|
+
* The unescape is a single left-to-right pass, not two sequential replaces.
|
|
224
|
+
* `\\` and `\"` have to be consumed in the order they appear, or a value
|
|
225
|
+
* containing a literal backslash before a quote comes back wrong.
|
|
226
|
+
*/
|
|
227
|
+
export function unquoteYaml(raw) {
|
|
228
|
+
const trimmed = raw.trim();
|
|
229
|
+
const m = /^"([\S\s]*)"$/.exec(trimmed);
|
|
230
|
+
if (!m)
|
|
231
|
+
return trimmed;
|
|
232
|
+
return m[1].replace(/\\(["\\])/g, '$1');
|
|
233
|
+
}
|