@wafflebase/cli 0.4.8 → 0.5.0

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.
Files changed (83) hide show
  1. package/dist/bin.js +99693 -11
  2. package/package.json +7 -6
  3. package/dist/bin.d.ts +0 -2
  4. package/dist/client/dry-run.d.ts +0 -5
  5. package/dist/client/dry-run.js +0 -16
  6. package/dist/client/http-client.d.ts +0 -59
  7. package/dist/client/http-client.js +0 -178
  8. package/dist/commands/api-keys.d.ts +0 -2
  9. package/dist/commands/api-keys.js +0 -54
  10. package/dist/commands/cells.d.ts +0 -2
  11. package/dist/commands/cells.js +0 -108
  12. package/dist/commands/ctx.d.ts +0 -5
  13. package/dist/commands/ctx.js +0 -56
  14. package/dist/commands/docs.d.ts +0 -3
  15. package/dist/commands/docs.js +0 -320
  16. package/dist/commands/login.d.ts +0 -2
  17. package/dist/commands/login.js +0 -220
  18. package/dist/commands/logout.d.ts +0 -2
  19. package/dist/commands/logout.js +0 -10
  20. package/dist/commands/root.d.ts +0 -18
  21. package/dist/commands/root.js +0 -37
  22. package/dist/commands/schema.d.ts +0 -2
  23. package/dist/commands/schema.js +0 -28
  24. package/dist/commands/sheets-export.d.ts +0 -2
  25. package/dist/commands/sheets-export.js +0 -52
  26. package/dist/commands/sheets-import.d.ts +0 -2
  27. package/dist/commands/sheets-import.js +0 -88
  28. package/dist/commands/sheets.d.ts +0 -2
  29. package/dist/commands/sheets.js +0 -17
  30. package/dist/commands/slides.d.ts +0 -3
  31. package/dist/commands/slides.js +0 -227
  32. package/dist/commands/status.d.ts +0 -2
  33. package/dist/commands/status.js +0 -22
  34. package/dist/commands/tabs.d.ts +0 -2
  35. package/dist/commands/tabs.js +0 -20
  36. package/dist/config/config.d.ts +0 -38
  37. package/dist/config/config.js +0 -135
  38. package/dist/config/session.d.ts +0 -49
  39. package/dist/config/session.js +0 -74
  40. package/dist/docs/content.d.ts +0 -37
  41. package/dist/docs/content.js +0 -90
  42. package/dist/docs/docx-export.d.ts +0 -23
  43. package/dist/docs/docx-export.js +0 -14
  44. package/dist/docs/docx-import.d.ts +0 -36
  45. package/dist/docs/docx-import.js +0 -70
  46. package/dist/docs/dom-polyfill.d.ts +0 -1
  47. package/dist/docs/dom-polyfill.js +0 -29
  48. package/dist/docs/fontkit-measurer.d.ts +0 -41
  49. package/dist/docs/fontkit-measurer.js +0 -64
  50. package/dist/docs/image-fetcher.d.ts +0 -37
  51. package/dist/docs/image-fetcher.js +0 -38
  52. package/dist/docs/import.d.ts +0 -72
  53. package/dist/docs/import.js +0 -146
  54. package/dist/docs/page-range.d.ts +0 -29
  55. package/dist/docs/page-range.js +0 -74
  56. package/dist/docs/page-slice.d.ts +0 -45
  57. package/dist/docs/page-slice.js +0 -61
  58. package/dist/docs/paginate.d.ts +0 -13
  59. package/dist/docs/paginate.js +0 -19
  60. package/dist/docs/pdf-export.d.ts +0 -47
  61. package/dist/docs/pdf-export.js +0 -95
  62. package/dist/output/binary.d.ts +0 -37
  63. package/dist/output/binary.js +0 -47
  64. package/dist/output/csv.d.ts +0 -4
  65. package/dist/output/csv.js +0 -25
  66. package/dist/output/formatter.d.ts +0 -4
  67. package/dist/output/formatter.js +0 -41
  68. package/dist/output/json.d.ts +0 -1
  69. package/dist/output/json.js +0 -3
  70. package/dist/output/table.d.ts +0 -4
  71. package/dist/output/table.js +0 -14
  72. package/dist/schema/registry.d.ts +0 -42
  73. package/dist/schema/registry.js +0 -375
  74. package/dist/slides/content.d.ts +0 -41
  75. package/dist/slides/content.js +0 -122
  76. package/dist/slides/import.d.ts +0 -82
  77. package/dist/slides/import.js +0 -170
  78. package/dist/slides/pptx-export.d.ts +0 -6
  79. package/dist/slides/pptx-export.js +0 -10
  80. package/dist/slides/pptx-import.d.ts +0 -35
  81. package/dist/slides/pptx-import.js +0 -49
  82. package/dist/util/csv-parse.d.ts +0 -21
  83. package/dist/util/csv-parse.js +0 -99
@@ -1,82 +0,0 @@
1
- import type { SlidesDocument, UploadImage } from '@wafflebase/slides/node';
2
- import { type CliPptxImportOptions } from './pptx-import.js';
3
- import type { ImportReport } from '@wafflebase/slides/node';
4
- /**
5
- * Parsing surface — split out so tests can inject a stub that returns
6
- * a synthetic `SlidesDocument` without building a real `.pptx` byte
7
- * buffer. Production callers use the default (`importPptx` from
8
- * `./pptx-import.js`).
9
- */
10
- export type SlidesImportParser = (buf: Uint8Array, opts?: CliPptxImportOptions) => Promise<{
11
- document: SlidesDocument;
12
- report: ImportReport;
13
- }>;
14
- /**
15
- * Minimal HTTP surface `runSlidesImport` needs from the CLI's
16
- * `HttpClient`. Spelled out so tests can pass a stub without depending
17
- * on the full client. Mirrors `ImportClient` in `../docs/import.ts`.
18
- */
19
- export interface SlidesImportClient {
20
- createDocument: (title: string, type?: 'doc' | 'sheet' | 'slides') => Promise<{
21
- ok: boolean;
22
- status: number;
23
- data: unknown;
24
- }>;
25
- putSlidesContent: (docId: string, deck: SlidesDocument) => Promise<{
26
- ok: boolean;
27
- status: number;
28
- data: unknown;
29
- }>;
30
- }
31
- export interface SlidesImportIO {
32
- /** Final structured payload — written to stdout as JSON. */
33
- stdout: (line: string) => void;
34
- stderr: (line: string) => void;
35
- /**
36
- * Read the pptx bytes for `path`. `'-'` reads from stdin.
37
- *
38
- * Split out so tests can inject in-memory buffers without juggling
39
- * tmp files; the default reads `process.stdin` (or the file
40
- * synchronously) at the action site.
41
- */
42
- readBytes: (path: string) => Promise<Uint8Array>;
43
- /**
44
- * Prompt the user for a yes/no answer when running interactively.
45
- * Returns `true` for affirmative responses (case-insensitive `y` /
46
- * `yes`); anything else (including end-of-stream) → `false`.
47
- */
48
- confirm: (prompt: string) => Promise<boolean>;
49
- /** Whether the current process is interactive (a TTY on stdin). */
50
- isTTY: boolean;
51
- }
52
- export declare const defaultSlidesImportIO: SlidesImportIO;
53
- export interface RunSlidesImportArgs {
54
- /** Source path. `'-'` reads from stdin. */
55
- file: string;
56
- /** Override the deck title. Falls back to the file basename. */
57
- title?: string;
58
- /** Existing document ID to overwrite. Pairs with `--yes`. */
59
- replace?: string;
60
- /** Skip the interactive `--replace` confirmation. */
61
- yes?: boolean;
62
- /** Override the default base64 image uploader. Tests pass a no-op. */
63
- uploadImage?: UploadImage;
64
- /** Suppress informational stderr output (errors still reported). */
65
- quiet?: boolean;
66
- /** Print the request that *would* fire instead of issuing it. */
67
- dryRun?: boolean;
68
- /** Override the .pptx parser — primarily for tests. */
69
- parser?: SlidesImportParser;
70
- }
71
- export interface RunSlidesImportResult {
72
- /** Process exit code. The action sets `process.exitCode` accordingly. */
73
- exitCode: number;
74
- }
75
- /**
76
- * Pure orchestration for `slides import`. Reads the source `.pptx`,
77
- * parses it into a `SlidesDocument`, then either creates a new deck
78
- * (default) or overwrites an existing one (`--replace`). Mirrors
79
- * `runDocsImport` — see that file for the rationale behind the split
80
- * between this function and the Commander action.
81
- */
82
- export declare function runSlidesImport(args: RunSlidesImportArgs, client: SlidesImportClient, io?: SlidesImportIO): Promise<RunSlidesImportResult>;
@@ -1,170 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { basename, extname } from 'node:path';
3
- import { createInterface } from 'node:readline';
4
- import { importPptx as defaultImportPptx, InvalidPptxError, } from './pptx-import.js';
5
- export const defaultSlidesImportIO = {
6
- stdout: (line) => {
7
- process.stdout.write(line);
8
- if (!line.endsWith('\n'))
9
- process.stdout.write('\n');
10
- },
11
- stderr: (line) => {
12
- console.error(line);
13
- },
14
- readBytes: async (path) => {
15
- if (path === '-') {
16
- return new Promise((resolve, reject) => {
17
- const chunks = [];
18
- process.stdin.on('data', (c) => chunks.push(c));
19
- process.stdin.on('end', () => resolve(new Uint8Array(Buffer.concat(chunks))));
20
- process.stdin.on('error', reject);
21
- });
22
- }
23
- return new Uint8Array(readFileSync(path));
24
- },
25
- confirm: (prompt) => {
26
- return new Promise((resolve) => {
27
- const rl = createInterface({ input: process.stdin, output: process.stderr });
28
- rl.question(prompt, (answer) => {
29
- rl.close();
30
- const a = answer.trim().toLowerCase();
31
- resolve(a === 'y' || a === 'yes');
32
- });
33
- });
34
- },
35
- isTTY: process.stdin.isTTY ?? false,
36
- };
37
- /**
38
- * Pure orchestration for `slides import`. Reads the source `.pptx`,
39
- * parses it into a `SlidesDocument`, then either creates a new deck
40
- * (default) or overwrites an existing one (`--replace`). Mirrors
41
- * `runDocsImport` — see that file for the rationale behind the split
42
- * between this function and the Commander action.
43
- */
44
- export async function runSlidesImport(args, client, io = defaultSlidesImportIO) {
45
- const { file, replace, yes = false, uploadImage, quiet = false, dryRun = false, parser = defaultImportPptx, } = args;
46
- const inferredTitle = args.title ?? defaultTitleFor(file);
47
- if (replace) {
48
- // `--replace` is destructive: confirm interactively unless `--yes`.
49
- if (!yes) {
50
- if (!io.isTTY) {
51
- io.stderr(JSON.stringify({
52
- error: {
53
- code: 'CONFIRMATION_REQ',
54
- message: `Pass --yes to confirm replacing deck "${replace}".`,
55
- },
56
- }, null, 2));
57
- return { exitCode: 1 };
58
- }
59
- const ok = await io.confirm(`This will replace content of ${replace}. Continue? [y/N] `);
60
- if (!ok) {
61
- if (!quiet)
62
- io.stderr('Aborted.');
63
- return { exitCode: 0 };
64
- }
65
- }
66
- const buf = await io.readBytes(file);
67
- const parsed = await safeImportPptx(buf, uploadImage, io, parser);
68
- if (parsed === null)
69
- return { exitCode: 1 };
70
- const { document: deck, report } = parsed;
71
- if (dryRun) {
72
- io.stdout(JSON.stringify({
73
- method: 'PUT',
74
- path: `/documents/${replace}/content`,
75
- body: deck,
76
- report: summariseReport(report),
77
- }, null, 2));
78
- return { exitCode: 0 };
79
- }
80
- const res = await client.putSlidesContent(replace, deck);
81
- if (!res.ok) {
82
- io.stderr(JSON.stringify(res.data ?? { error: { code: 'HTTP_ERROR' } }, null, 2));
83
- return { exitCode: 1 };
84
- }
85
- io.stdout(JSON.stringify({ id: replace, replaced: true, report: summariseReport(report) }, null, 2));
86
- return { exitCode: 0 };
87
- }
88
- // Default flow: POST + PUT.
89
- const buf = await io.readBytes(file);
90
- const parsedNew = await safeImportPptx(buf, uploadImage, io, parser);
91
- if (parsedNew === null)
92
- return { exitCode: 1 };
93
- const { document: deck, report } = parsedNew;
94
- // The parser fills `meta.title` with whatever string the .pptx carries
95
- // (often "PowerPoint Presentation" or "Imported deck"). The user's
96
- // `--title` flag is the source of truth for the deck name — keep the
97
- // Yorkie root's title in lock-step with the Document row so the deck
98
- // list, editor header, and GET /content all agree.
99
- deck.meta.title = inferredTitle;
100
- if (dryRun) {
101
- io.stdout(JSON.stringify({
102
- method: 'POST',
103
- path: '/documents',
104
- body: { title: inferredTitle, type: 'slides' },
105
- followUp: { method: 'PUT', path: '/documents/<new-id>/content' },
106
- report: summariseReport(report),
107
- }, null, 2));
108
- return { exitCode: 0 };
109
- }
110
- const created = await client.createDocument(inferredTitle, 'slides');
111
- if (!created.ok) {
112
- io.stderr(JSON.stringify(created.data ?? { error: { code: 'HTTP_ERROR' } }, null, 2));
113
- return { exitCode: 1 };
114
- }
115
- const newId = created.data?.id;
116
- if (!newId) {
117
- io.stderr(JSON.stringify({ error: { code: 'INVALID_RESPONSE', message: 'Server did not return an id' } }, null, 2));
118
- return { exitCode: 1 };
119
- }
120
- const put = await client.putSlidesContent(newId, deck);
121
- if (!put.ok) {
122
- io.stderr(JSON.stringify(put.data ?? { error: { code: 'HTTP_ERROR' } }, null, 2));
123
- return { exitCode: 1 };
124
- }
125
- io.stdout(JSON.stringify({ id: newId, title: inferredTitle, report: summariseReport(report) }, null, 2));
126
- return { exitCode: 0 };
127
- }
128
- function defaultTitleFor(file) {
129
- if (file === '-')
130
- return 'Untitled';
131
- return basename(file, extname(file)) || 'Untitled';
132
- }
133
- /**
134
- * Collect the parser's `ImportReport` counters into a plain object so
135
- * the CLI can print them as JSON. We surface every counter rather than
136
- * the `.summary()` string so agentic callers can parse without
137
- * regex-matching prose.
138
- */
139
- function summariseReport(report) {
140
- // `tablesFlattened` / `tableMergesIgnored` / `tableBordersApproximated`
141
- // were retired when the structured-TableElement importer landed
142
- // (slides-tables P2). Tables now round-trip without lossy fallbacks,
143
- // so there's no per-table counter to surface — a clean import shows
144
- // empty parts and the standard "no fallbacks" line.
145
- return {
146
- groupsFlattened: report.groupsFlattened,
147
- textBoxesPreScaled: report.textBoxesPreScaled,
148
- unknownShapes: report.unknownShapes,
149
- unknownLayoutTypes: report.unknownLayoutTypes,
150
- skippedImages: report.skippedImages,
151
- };
152
- }
153
- /**
154
- * Run `importPptx` and turn `InvalidPptxError` into a structured stderr
155
- * body + `null` return so callers can short-circuit with `exitCode: 1`
156
- * while keeping the rest of the error envelope (`HTTP_ERROR`,
157
- * `INVALID_RESPONSE`, etc.) consistent.
158
- */
159
- async function safeImportPptx(buf, uploadImage, io, parser) {
160
- try {
161
- return await parser(buf, { uploadImage });
162
- }
163
- catch (e) {
164
- if (e instanceof InvalidPptxError) {
165
- io.stderr(JSON.stringify({ error: { code: e.code, message: e.message } }, null, 2));
166
- return null;
167
- }
168
- throw e;
169
- }
170
- }
@@ -1,6 +0,0 @@
1
- import { type SlidesDocument } from '@wafflebase/slides/node';
2
- export interface CliPptxExportOptions {
3
- /** Blob-returning fetcher (the CLI's shared image fetcher). */
4
- imageFetcher?: (url: string) => Promise<Blob>;
5
- }
6
- export declare function exportPptxCli(deck: SlidesDocument, opts?: CliPptxExportOptions): Promise<Uint8Array>;
@@ -1,10 +0,0 @@
1
- import { exportPptx } from '@wafflebase/slides/node';
2
- export async function exportPptxCli(deck, opts = {}) {
3
- const fetchImage = opts.imageFetcher
4
- ? async (src) => {
5
- const blob = await opts.imageFetcher(src);
6
- return { bytes: new Uint8Array(await blob.arrayBuffer()), mime: blob.type || 'image/png' };
7
- }
8
- : undefined;
9
- return exportPptx(deck, { fetchImage });
10
- }
@@ -1,35 +0,0 @@
1
- import '../docs/dom-polyfill.js';
2
- import { type ImportPptxResult, type UploadImage } from '@wafflebase/slides/node';
3
- /**
4
- * Default `UploadImage` for the CLI: encodes each embedded image as a
5
- * `data:` URL. Matches the docs CLI's `inlineBase64Uploader` — the
6
- * frontend treats slide image `src` values as opaque strings, so a
7
- * self-contained data URL round-trips cleanly through the API and
8
- * renders without needing the workspace's `/images` endpoint.
9
- *
10
- * The trade-off is payload size: a benchmark deck with 25 unique
11
- * images and 63 references gains roughly the image-byte total to the
12
- * Yorkie document. Acceptable for a v1 CLI import — power users who
13
- * want hosted URLs can pass a custom uploader.
14
- */
15
- export declare const inlineBase64SlidesUploader: UploadImage;
16
- export interface CliPptxImportOptions {
17
- /** Override the default base64 uploader — primarily for tests. */
18
- uploadImage?: UploadImage;
19
- }
20
- /**
21
- * Parse a .pptx byte buffer into a `SlidesDocument` via the slides
22
- * parser. The DOM polyfill side-effect import installs xmldom's
23
- * `DOMParser` globally so the parser's browser-targeted XML reads work
24
- * in Node.
25
- *
26
- * Throws a wrapped `INVALID_PPTX` error when the underlying parser
27
- * cannot make sense of the buffer. The CLI command translates that
28
- * into the documented `{ error.code: 'INVALID_PPTX' }` exit body
29
- * without leaking implementation details.
30
- */
31
- export declare function importPptx(buf: Uint8Array, opts?: CliPptxImportOptions): Promise<ImportPptxResult>;
32
- export declare class InvalidPptxError extends Error {
33
- readonly code = "INVALID_PPTX";
34
- constructor(message: string);
35
- }
@@ -1,49 +0,0 @@
1
- import '../docs/dom-polyfill.js';
2
- import { importPptx as importPptxFromSlides, } from '@wafflebase/slides/node';
3
- /**
4
- * Default `UploadImage` for the CLI: encodes each embedded image as a
5
- * `data:` URL. Matches the docs CLI's `inlineBase64Uploader` — the
6
- * frontend treats slide image `src` values as opaque strings, so a
7
- * self-contained data URL round-trips cleanly through the API and
8
- * renders without needing the workspace's `/images` endpoint.
9
- *
10
- * The trade-off is payload size: a benchmark deck with 25 unique
11
- * images and 63 references gains roughly the image-byte total to the
12
- * Yorkie document. Acceptable for a v1 CLI import — power users who
13
- * want hosted URLs can pass a custom uploader.
14
- */
15
- export const inlineBase64SlidesUploader = async (bytes, mime) => {
16
- const base64 = Buffer.from(bytes).toString('base64');
17
- const mediaType = mime || 'application/octet-stream';
18
- return `data:${mediaType};base64,${base64}`;
19
- };
20
- /**
21
- * Parse a .pptx byte buffer into a `SlidesDocument` via the slides
22
- * parser. The DOM polyfill side-effect import installs xmldom's
23
- * `DOMParser` globally so the parser's browser-targeted XML reads work
24
- * in Node.
25
- *
26
- * Throws a wrapped `INVALID_PPTX` error when the underlying parser
27
- * cannot make sense of the buffer. The CLI command translates that
28
- * into the documented `{ error.code: 'INVALID_PPTX' }` exit body
29
- * without leaking implementation details.
30
- */
31
- export async function importPptx(buf, opts = {}) {
32
- const arrayBuf = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
33
- try {
34
- return await importPptxFromSlides(arrayBuf, {
35
- uploadImage: opts.uploadImage ?? inlineBase64SlidesUploader,
36
- });
37
- }
38
- catch (e) {
39
- const message = e instanceof Error ? e.message : String(e);
40
- throw new InvalidPptxError(message);
41
- }
42
- }
43
- export class InvalidPptxError extends Error {
44
- code = 'INVALID_PPTX';
45
- constructor(message) {
46
- super(message);
47
- this.name = 'InvalidPptxError';
48
- }
49
- }
@@ -1,21 +0,0 @@
1
- /**
2
- * Parse CSV text into a 2D array of strings.
3
- */
4
- export declare function parseCsv(text: string): string[][];
5
- /**
6
- * Convert a 1-based column number to a column label (1→A, 2→B, 27→AA).
7
- */
8
- export declare function toColumnLabel(col: number): string;
9
- /**
10
- * Parse a cell reference like "A1" into 1-based row and column numbers.
11
- */
12
- export declare function parseStartRef(ref: string): {
13
- row: number;
14
- col: number;
15
- };
16
- /**
17
- * Build a cell map from a 2D array for batch API.
18
- */
19
- export declare function buildCellMap(data: string[][], startRow: number, startCol: number): Record<string, {
20
- value: string;
21
- }>;
@@ -1,99 +0,0 @@
1
- /**
2
- * Parse CSV text into a 2D array of strings.
3
- */
4
- export function parseCsv(text) {
5
- const rows = [];
6
- let current = '';
7
- let inQuotes = false;
8
- let row = [];
9
- for (let i = 0; i < text.length; i++) {
10
- const ch = text[i];
11
- if (inQuotes) {
12
- if (ch === '"') {
13
- if (i + 1 < text.length && text[i + 1] === '"') {
14
- current += '"';
15
- i++;
16
- }
17
- else {
18
- inQuotes = false;
19
- }
20
- }
21
- else {
22
- current += ch;
23
- }
24
- }
25
- else if (ch === '"') {
26
- inQuotes = true;
27
- }
28
- else if (ch === ',') {
29
- row.push(current);
30
- current = '';
31
- }
32
- else if (ch === '\n' || ch === '\r') {
33
- if (ch === '\r' && i + 1 < text.length && text[i + 1] === '\n') {
34
- i++;
35
- }
36
- row.push(current);
37
- current = '';
38
- if (row.length > 0)
39
- rows.push(row);
40
- row = [];
41
- }
42
- else {
43
- current += ch;
44
- }
45
- }
46
- // last field/row
47
- row.push(current);
48
- if (row.some((cell) => cell !== ''))
49
- rows.push(row);
50
- return rows;
51
- }
52
- /**
53
- * Convert a 1-based column number to a column label (1→A, 2→B, 27→AA).
54
- */
55
- export function toColumnLabel(col) {
56
- let label = '';
57
- while (col > 0) {
58
- const rem = col % 26;
59
- if (rem === 0) {
60
- label = 'Z' + label;
61
- col = Math.floor(col / 26) - 1;
62
- }
63
- else {
64
- label = String.fromCharCode(rem + 64) + label;
65
- col = Math.floor(col / 26);
66
- }
67
- }
68
- return label;
69
- }
70
- /**
71
- * Parse a cell reference like "A1" into 1-based row and column numbers.
72
- */
73
- export function parseStartRef(ref) {
74
- const match = ref.match(/^([A-Z]+)(\d+)$/i);
75
- if (!match)
76
- return { row: 1, col: 1 };
77
- const letters = match[1].toUpperCase();
78
- let col = 0;
79
- for (const ch of letters) {
80
- col = col * 26 + (ch.charCodeAt(0) - 64);
81
- }
82
- return { row: parseInt(match[2], 10), col };
83
- }
84
- /**
85
- * Build a cell map from a 2D array for batch API.
86
- */
87
- export function buildCellMap(data, startRow, startCol) {
88
- const cells = {};
89
- for (let r = 0; r < data.length; r++) {
90
- for (let c = 0; c < data[r].length; c++) {
91
- const value = data[r][c];
92
- if (value === '')
93
- continue;
94
- const ref = toColumnLabel(startCol + c) + (startRow + r);
95
- cells[ref] = { value };
96
- }
97
- }
98
- return cells;
99
- }