sf-compact-cli 0.1.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 +281 -0
- package/bin/sf-compact +19 -0
- package/install.js +96 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# sf-compact
|
|
2
|
+
|
|
3
|
+
Convert Salesforce metadata XML to AI-friendly compact formats. Semantically lossless roundtrip.
|
|
4
|
+
|
|
5
|
+
Salesforce metadata XML is extremely verbose — profiles, permission sets, flows, and objects can be 20,000–50,000+ lines of XML with 70–85% structural overhead. This burns tokens and money when AI tools (Claude Code, Codex, Cursor, etc.) read or edit your metadata.
|
|
6
|
+
|
|
7
|
+
**sf-compact** converts it to compact YAML or JSON, saving 42–54% of tokens depending on format.
|
|
8
|
+
|
|
9
|
+
## Output Formats
|
|
10
|
+
|
|
11
|
+
| Format | Preserves order | Human-readable | Token savings |
|
|
12
|
+
|--------|:-:|:-:|:-:|
|
|
13
|
+
| `yaml` | No | Yes | ~49% |
|
|
14
|
+
| `yaml-ordered` | Yes | Yes | ~42% |
|
|
15
|
+
| `json` | Yes | Less | ~54% |
|
|
16
|
+
|
|
17
|
+
- **yaml** — groups repeated elements into arrays. Most compact YAML, but sibling order may change. Best for order-insensitive types (Profile, PermissionSet).
|
|
18
|
+
- **yaml-ordered** — uses `_children` sequences to preserve exact element order. Best for order-sensitive types (Flow, FlexiPage, Layout).
|
|
19
|
+
- **json** — compact single-line JSON with arrays. Preserves order, fewest tokens, less human-readable.
|
|
20
|
+
|
|
21
|
+
## Before / After
|
|
22
|
+
|
|
23
|
+
**XML (848 tokens):**
|
|
24
|
+
```xml
|
|
25
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
26
|
+
<Profile xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
27
|
+
<custom>false</custom>
|
|
28
|
+
<userLicense>Salesforce</userLicense>
|
|
29
|
+
<fieldPermissions>
|
|
30
|
+
<editable>true</editable>
|
|
31
|
+
<field>Account.AnnualRevenue</field>
|
|
32
|
+
<readable>true</readable>
|
|
33
|
+
</fieldPermissions>
|
|
34
|
+
<fieldPermissions>
|
|
35
|
+
<editable>false</editable>
|
|
36
|
+
<field>Account.BillingCity</field>
|
|
37
|
+
<readable>true</readable>
|
|
38
|
+
</fieldPermissions>
|
|
39
|
+
...
|
|
40
|
+
</Profile>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**YAML (432 tokens — 49% reduction):**
|
|
44
|
+
```yaml
|
|
45
|
+
_tag: Profile
|
|
46
|
+
_ns: http://soap.sforce.com/2006/04/metadata
|
|
47
|
+
custom: false
|
|
48
|
+
userLicense: Salesforce
|
|
49
|
+
fieldPermissions:
|
|
50
|
+
- editable: true
|
|
51
|
+
field: Account.AnnualRevenue
|
|
52
|
+
readable: true
|
|
53
|
+
- editable: false
|
|
54
|
+
field: Account.BillingCity
|
|
55
|
+
readable: true
|
|
56
|
+
...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**JSON (389 tokens — 54% reduction):**
|
|
60
|
+
```json
|
|
61
|
+
{"_tag":"Profile","_ns":"http://soap.sforce.com/2006/04/metadata","custom":"false","userLicense":"Salesforce","fieldPermissions":[{"editable":"true","field":"Account.AnnualRevenue","readable":"true"},{"editable":"false","field":"Account.BillingCity","readable":"true"}]}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
### From source (Rust required)
|
|
67
|
+
```bash
|
|
68
|
+
cargo install --path .
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### From crates.io
|
|
72
|
+
```bash
|
|
73
|
+
cargo install sf-compact
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
### Pack (XML → compact format)
|
|
79
|
+
```bash
|
|
80
|
+
sf-compact pack [source...] [-o output] [--format yaml|yaml-ordered|json] [--include pattern]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Pack entire project (default: YAML format)
|
|
85
|
+
sf-compact pack force-app -o .sf-compact
|
|
86
|
+
|
|
87
|
+
# Pack as JSON for maximum token savings
|
|
88
|
+
sf-compact pack force-app --format json
|
|
89
|
+
|
|
90
|
+
# Pack specific directories
|
|
91
|
+
sf-compact pack force-app/main/default/profiles force-app/main/default/classes
|
|
92
|
+
|
|
93
|
+
# Pack only profiles
|
|
94
|
+
sf-compact pack force-app --include "*.profile-meta.xml"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Unpack (compact format → XML)
|
|
98
|
+
```bash
|
|
99
|
+
sf-compact unpack [source...] [-o output] [--include pattern]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Auto-detects format by file extension (`.yaml` or `.json`).
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
sf-compact unpack .sf-compact -o force-app
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Stats (preview savings)
|
|
109
|
+
```bash
|
|
110
|
+
sf-compact stats [source...] [--include pattern] [--files]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Analyze metadata and preview token/byte savings without writing files.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
$ sf-compact stats force-app
|
|
117
|
+
|
|
118
|
+
Preview: what sf-compact pack would produce
|
|
119
|
+
Tokenizer: cl100k_base (GPT-4 / Claude)
|
|
120
|
+
|
|
121
|
+
XML (now) YAML (after) savings
|
|
122
|
+
--------------------------------------------------------------------------------
|
|
123
|
+
Bytes 7313 3418 53.3%
|
|
124
|
+
Tokens 1719 925 46.2%
|
|
125
|
+
|
|
126
|
+
Would save 794 tokens across 5 files
|
|
127
|
+
|
|
128
|
+
By metadata type:
|
|
129
|
+
type files now → after tokens saved
|
|
130
|
+
----------------------------------------------------------------------
|
|
131
|
+
profile 1 848 → 432 tokens 49.1%
|
|
132
|
+
flow 1 464 → 268 tokens 42.2%
|
|
133
|
+
field 1 232 → 126 tokens 45.7%
|
|
134
|
+
js 1 116 → 66 tokens 43.1%
|
|
135
|
+
cls 1 59 → 33 tokens 44.1%
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Use `--files` for per-file breakdown, `--include` to filter by glob pattern.
|
|
139
|
+
|
|
140
|
+
### Configuration
|
|
141
|
+
|
|
142
|
+
sf-compact uses a `.sfcompact.yaml` config file for per-type format control.
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Create config with smart defaults (yaml-ordered for order-sensitive types)
|
|
146
|
+
sf-compact config init
|
|
147
|
+
|
|
148
|
+
# Set format for specific types (batch — multiple types in one call)
|
|
149
|
+
sf-compact config set flow json profile yaml flexipage yaml-ordered
|
|
150
|
+
|
|
151
|
+
# Change default format for all types
|
|
152
|
+
sf-compact config set default json
|
|
153
|
+
|
|
154
|
+
# Skip a metadata type from conversion
|
|
155
|
+
sf-compact config skip customMetadata
|
|
156
|
+
|
|
157
|
+
# View current configuration
|
|
158
|
+
sf-compact config show
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Default config after `config init`:
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
default_format: yaml
|
|
165
|
+
formats:
|
|
166
|
+
Flow: yaml-ordered
|
|
167
|
+
FlexiPage: yaml-ordered
|
|
168
|
+
Layout: yaml-ordered
|
|
169
|
+
skip: []
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
When `pack` runs, it reads `.sfcompact.yaml` and applies the format per metadata type. The `--format` CLI flag overrides the config for a single run.
|
|
173
|
+
|
|
174
|
+
### Watch (auto-pack on changes)
|
|
175
|
+
```bash
|
|
176
|
+
sf-compact watch [source...] [-o output] [--format yaml|yaml-ordered|json] [--include pattern]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Watches source directories for XML changes and automatically repacks. Runs an initial pack, then monitors for file changes.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Watch default force-app directory
|
|
183
|
+
sf-compact watch
|
|
184
|
+
|
|
185
|
+
# Watch with JSON format
|
|
186
|
+
sf-compact watch force-app --format json
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Diff (detect unpacked changes)
|
|
190
|
+
```bash
|
|
191
|
+
sf-compact diff [source...] [-o packed-dir] [--include pattern]
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Compare current XML metadata against the last packed output. Shows new, modified, and deleted files.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
$ sf-compact diff
|
|
198
|
+
|
|
199
|
+
+ force-app/main/default/profiles/NewProfile.profile-meta.xml (new — not yet packed)
|
|
200
|
+
~ force-app/main/default/flows/Case_Assignment.flow-meta.xml (modified since last pack)
|
|
201
|
+
|
|
202
|
+
1 new, 1 modified, 0 deleted, 3 unchanged
|
|
203
|
+
Run `sf-compact pack` to update.
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### MCP Server
|
|
207
|
+
|
|
208
|
+
sf-compact includes a built-in [MCP](https://modelcontextprotocol.io/) server for direct AI tool integration.
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# Add to your project's .mcp.json
|
|
212
|
+
sf-compact init mcp
|
|
213
|
+
|
|
214
|
+
# Or start manually
|
|
215
|
+
sf-compact mcp-serve
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
This exposes `sf_compact_pack`, `sf_compact_unpack`, and `sf_compact_stats` as MCP tools that Claude Code, Cursor, and other MCP-compatible tools can discover and use automatically.
|
|
219
|
+
|
|
220
|
+
### AI Instructions
|
|
221
|
+
|
|
222
|
+
Generate a provider-agnostic markdown file with usage instructions for any AI tool:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
sf-compact init instructions
|
|
226
|
+
sf-compact init instructions --name SALESFORCE.md
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Manifest
|
|
230
|
+
|
|
231
|
+
Output supported metadata types in JSON (includes format support and order-sensitivity flags):
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
sf-compact manifest
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Supported Metadata Types
|
|
238
|
+
|
|
239
|
+
75 file extensions mapping to Salesforce metadata types across 9 categories:
|
|
240
|
+
|
|
241
|
+
| Category | Types |
|
|
242
|
+
|----------|-------|
|
|
243
|
+
| **Security** | Profile, PermissionSet, PermissionSetGroup, RemoteSiteSetting, CspTrustedSite, ConnectedApp, SharingRules, CustomPermission, Role, Group, AuthProvider, SamlSsoConfig, Certificate |
|
|
244
|
+
| **Schema** | CustomObject, CustomField, ValidationRule, CustomMetadata, GlobalValueSet, StandardValueSet, RecordType, MatchingRule, DuplicateRule, CustomIndex, TopicsForObjects, CustomObjectTranslation, CustomFieldTranslation, FieldSet |
|
|
245
|
+
| **Code** | ApexClass, ApexTrigger, ApexComponent, ApexPage, LightningComponentBundle (js/css/html/xml), AuraDefinitionBundle (cmp/evt), StaticResource |
|
|
246
|
+
| **Automation** | Flow*, Workflow, WorkflowRule, AssignmentRules, AutoResponseRules, EscalationRules |
|
|
247
|
+
| **UI** | Layout*, CustomLabels, CustomApplication, CustomTab, FlexiPage*, CustomSite, QuickAction, PathAssistant, ListView, CompactLayout, WebLink, HomePageLayout, AppMenu, Community, Letterhead |
|
|
248
|
+
| **Analytics** | ReportType, Report, Dashboard |
|
|
249
|
+
| **Integration** | ExternalServiceRegistration, NamedCredential, ExternalCredential, InstalledPackage |
|
|
250
|
+
| **Notifications** | CustomNotificationType, NotificationTypeConfig, LightningMessageChannel, PlatformEventChannelMember |
|
|
251
|
+
| **Content** | EmailTemplate, ManagedContentType, CleanDataService, IframeWhiteListUrlSettings, Settings |
|
|
252
|
+
|
|
253
|
+
\* Order-sensitive types — `config init` defaults these to `yaml-ordered` to preserve element order.
|
|
254
|
+
|
|
255
|
+
## Workflow
|
|
256
|
+
|
|
257
|
+
1. **Configure** (once): `sf-compact config init` — creates `.sfcompact.yaml` with smart defaults
|
|
258
|
+
2. **Pull metadata** from Salesforce (`sf project retrieve`)
|
|
259
|
+
3. **Pack**: `sf-compact pack` — creates `.sf-compact/` with compact files
|
|
260
|
+
4. **Work with compact files** — let AI tools read/edit the YAML/JSON format
|
|
261
|
+
5. **Unpack**: `sf-compact unpack` — restores XML for deployment
|
|
262
|
+
6. **Deploy** to Salesforce (`sf project deploy`)
|
|
263
|
+
|
|
264
|
+
> Use `sf-compact watch` during development to auto-pack on changes, and `sf-compact diff` to check if a repack is needed.
|
|
265
|
+
|
|
266
|
+
> Tip: Add `.sf-compact/` to `.gitignore` if you treat it as a build artifact, or commit it for AI-friendly diffs.
|
|
267
|
+
|
|
268
|
+
## How it works
|
|
269
|
+
|
|
270
|
+
- Parses Salesforce metadata XML into a tree structure
|
|
271
|
+
- Groups repeated elements (e.g., `<fieldPermissions>`) into arrays (YAML) or `_children` sequences (yaml-ordered, JSON)
|
|
272
|
+
- Coerces booleans: `"true"` → `true`, `"false"` → `false`. All other values (including numeric strings like `"59.0"`, `"0012"`) are preserved as-is
|
|
273
|
+
- Flattens simple key-value containers into inline mappings
|
|
274
|
+
- Preserves namespaces, attributes, and all structural information for semantically lossless roundtrip
|
|
275
|
+
- Order-sensitive types (Flow, FlexiPage, Layout) default to `yaml-ordered` format, which preserves exact element order via `_children` sequences
|
|
276
|
+
|
|
277
|
+
Token counting uses the `cl100k_base` tokenizer (same family used by GPT-4 and Claude).
|
|
278
|
+
|
|
279
|
+
## License
|
|
280
|
+
|
|
281
|
+
MIT
|
package/bin/sf-compact
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require("child_process");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
|
|
7
|
+
const binDir = path.join(__dirname);
|
|
8
|
+
const binary = path.join(binDir, process.platform === "win32" ? "sf-compact.exe" : "sf-compact-bin");
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(binary)) {
|
|
11
|
+
console.error("sf-compact binary not found. Try reinstalling: npm install -g sf-compact");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
execFileSync(binary, process.argv.slice(2), { stdio: "inherit" });
|
|
17
|
+
} catch (err) {
|
|
18
|
+
process.exit(err.status || 1);
|
|
19
|
+
}
|
package/install.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const https = require("https");
|
|
7
|
+
const http = require("http");
|
|
8
|
+
|
|
9
|
+
const VERSION = "0.1.1";
|
|
10
|
+
const REPO = "vradko/sf-compact";
|
|
11
|
+
|
|
12
|
+
const PLATFORMS = {
|
|
13
|
+
"darwin-arm64": `sf-compact-v${VERSION}-aarch64-apple-darwin.tar.gz`,
|
|
14
|
+
"darwin-x64": `sf-compact-v${VERSION}-x86_64-apple-darwin.tar.gz`,
|
|
15
|
+
"linux-x64": `sf-compact-v${VERSION}-x86_64-unknown-linux-musl.tar.gz`,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function getPlatformKey() {
|
|
19
|
+
return `${process.platform}-${process.arch}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function download(url) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const get = url.startsWith("https") ? https.get : http.get;
|
|
25
|
+
get(url, (res) => {
|
|
26
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
27
|
+
return download(res.headers.location).then(resolve, reject);
|
|
28
|
+
}
|
|
29
|
+
if (res.statusCode !== 200) {
|
|
30
|
+
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
31
|
+
}
|
|
32
|
+
const chunks = [];
|
|
33
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
34
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
35
|
+
res.on("error", reject);
|
|
36
|
+
}).on("error", reject);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function main() {
|
|
41
|
+
const key = getPlatformKey();
|
|
42
|
+
const filename = PLATFORMS[key];
|
|
43
|
+
|
|
44
|
+
if (!filename) {
|
|
45
|
+
console.error(`sf-compact: unsupported platform ${key}`);
|
|
46
|
+
console.error(`Supported: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
47
|
+
console.error("Install from source: cargo install sf-compact");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${filename}`;
|
|
52
|
+
const binDir = path.join(__dirname, "bin");
|
|
53
|
+
const binPath = path.join(binDir, "sf-compact-bin");
|
|
54
|
+
|
|
55
|
+
// Skip if already installed
|
|
56
|
+
if (fs.existsSync(binPath)) {
|
|
57
|
+
try {
|
|
58
|
+
const ver = execSync(`"${binPath}" --version`, { encoding: "utf8" }).trim();
|
|
59
|
+
if (ver.includes(VERSION)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`Downloading sf-compact v${VERSION} for ${key}...`);
|
|
66
|
+
|
|
67
|
+
const tarball = await download(url);
|
|
68
|
+
const tmpFile = path.join(__dirname, filename);
|
|
69
|
+
fs.writeFileSync(tmpFile, tarball);
|
|
70
|
+
|
|
71
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
72
|
+
|
|
73
|
+
const tmpDir = path.join(__dirname, "tmp-extract");
|
|
74
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
75
|
+
|
|
76
|
+
if (filename.endsWith(".tar.gz")) {
|
|
77
|
+
execSync(`tar xzf "${tmpFile}" -C "${tmpDir}"`, { stdio: "inherit" });
|
|
78
|
+
} else {
|
|
79
|
+
execSync(`unzip -o "${tmpFile}" -d "${tmpDir}"`, { stdio: "inherit" });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Rename to sf-compact-bin to avoid collision with the JS wrapper
|
|
83
|
+
const extracted = path.join(tmpDir, "sf-compact");
|
|
84
|
+
fs.renameSync(extracted, binPath);
|
|
85
|
+
fs.unlinkSync(tmpFile);
|
|
86
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
87
|
+
fs.chmodSync(binPath, 0o755);
|
|
88
|
+
|
|
89
|
+
console.log(`sf-compact v${VERSION} installed successfully.`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main().catch((err) => {
|
|
93
|
+
console.error("sf-compact install failed:", err.message);
|
|
94
|
+
console.error("Install from source: cargo install sf-compact");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sf-compact-cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Convert Salesforce metadata XML to AI-friendly compact formats. Semantically lossless roundtrip.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/vradko/sf-compact"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/vradko/sf-compact",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"salesforce",
|
|
13
|
+
"metadata",
|
|
14
|
+
"yaml",
|
|
15
|
+
"ai",
|
|
16
|
+
"token-optimization",
|
|
17
|
+
"xml",
|
|
18
|
+
"cli"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"sf-compact": "bin/sf-compact"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"postinstall": "node install.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin/",
|
|
28
|
+
"install.js",
|
|
29
|
+
"README.md"
|
|
30
|
+
]
|
|
31
|
+
}
|