@getfamiliar/shared 0.1.4 → 0.1.6

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.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Escape a single value for use as a GitHub-flavoured-markdown table cell:
3
+ * backslashes stay literal, `|` no longer splits the column, and any
4
+ * newline collapses to a space so the row stays on one line. Backticks are
5
+ * left untouched so inline-code spans in a cell survive. Mirrors the
6
+ * escaping the `tools list --raw` renderer needs; that renderer's
7
+ * `escapeCell` delegates here so there is a single implementation.
8
+ *
9
+ * @param text - raw cell text
10
+ * @returns the text safe to drop between two `|` pipes
11
+ */
12
+ export declare function escapeTableCell(text: string): string;
13
+ /**
14
+ * Build a GitHub-flavoured-markdown table from a header row and body rows.
15
+ * Every cell is run through {@link escapeTableCell}. The result is meant to
16
+ * be handed to {@link writeMarkdown}, which renders it (cli-table3) on a TTY
17
+ * or emits it verbatim when piped.
18
+ *
19
+ * @param headers - column header labels
20
+ * @param rows - one string tuple per row; each row should have `headers.length` cells
21
+ * @returns a GFM table string terminated by a single newline
22
+ */
23
+ export declare function markdownTable(headers: readonly string[], rows: readonly (readonly string[])[]): string;
24
+ /**
25
+ * Write a markdown string to stdout as the single human-facing CLI/plugin
26
+ * output sink: ANSI-styled via {@link renderMarkdown} on a TTY, or the raw
27
+ * markdown verbatim when stdout is piped/redirected.
28
+ *
29
+ * The TTY check lives *inside* this function on purpose — no call site
30
+ * should read `process.stdout.isTTY` itself. A caller can only ever *force*
31
+ * raw output (e.g. to honour a `--raw` flag); it can never accidentally
32
+ * render box-drawing tables and ANSI escapes into a pipe.
33
+ *
34
+ * @param markdown - markdown source to emit
35
+ * @param opts - `raw: true` forces raw output even on a TTY; omitted/false lets the TTY check decide
36
+ */
37
+ export declare function writeMarkdown(markdown: string, opts?: {
38
+ readonly raw?: boolean;
39
+ }): void;
@@ -0,0 +1,50 @@
1
+ import { renderMarkdown } from "./markdownTerminal.js";
2
+ /**
3
+ * Escape a single value for use as a GitHub-flavoured-markdown table cell:
4
+ * backslashes stay literal, `|` no longer splits the column, and any
5
+ * newline collapses to a space so the row stays on one line. Backticks are
6
+ * left untouched so inline-code spans in a cell survive. Mirrors the
7
+ * escaping the `tools list --raw` renderer needs; that renderer's
8
+ * `escapeCell` delegates here so there is a single implementation.
9
+ *
10
+ * @param text - raw cell text
11
+ * @returns the text safe to drop between two `|` pipes
12
+ */
13
+ export function escapeTableCell(text) {
14
+ return text.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
15
+ }
16
+ /**
17
+ * Build a GitHub-flavoured-markdown table from a header row and body rows.
18
+ * Every cell is run through {@link escapeTableCell}. The result is meant to
19
+ * be handed to {@link writeMarkdown}, which renders it (cli-table3) on a TTY
20
+ * or emits it verbatim when piped.
21
+ *
22
+ * @param headers - column header labels
23
+ * @param rows - one string tuple per row; each row should have `headers.length` cells
24
+ * @returns a GFM table string terminated by a single newline
25
+ */
26
+ export function markdownTable(headers, rows) {
27
+ const headerLine = `| ${headers.map(escapeTableCell).join(" | ")} |`;
28
+ const separator = `| ${headers.map(() => "---").join(" | ")} |`;
29
+ const body = rows.map((row) => `| ${row.map(escapeTableCell).join(" | ")} |`).join("\n");
30
+ return body.length > 0
31
+ ? `${headerLine}\n${separator}\n${body}\n`
32
+ : `${headerLine}\n${separator}\n`;
33
+ }
34
+ /**
35
+ * Write a markdown string to stdout as the single human-facing CLI/plugin
36
+ * output sink: ANSI-styled via {@link renderMarkdown} on a TTY, or the raw
37
+ * markdown verbatim when stdout is piped/redirected.
38
+ *
39
+ * The TTY check lives *inside* this function on purpose — no call site
40
+ * should read `process.stdout.isTTY` itself. A caller can only ever *force*
41
+ * raw output (e.g. to honour a `--raw` flag); it can never accidentally
42
+ * render box-drawing tables and ANSI escapes into a pipe.
43
+ *
44
+ * @param markdown - markdown source to emit
45
+ * @param opts - `raw: true` forces raw output even on a TTY; omitted/false lets the TTY check decide
46
+ */
47
+ export function writeMarkdown(markdown, opts) {
48
+ const raw = opts?.raw === true || !process.stdout.isTTY;
49
+ process.stdout.write(raw ? markdown : renderMarkdown(markdown));
50
+ }
@@ -39,7 +39,7 @@ import wrapAnsi from "wrap-ansi";
39
39
  */
40
40
  /** Upper bound on render width; full-width prose is hard to read on ultra-wide terminals. */
41
41
  const MAX_WIDTH = 180;
42
- /** Fallback width when stdout is not a TTY (no `columns`). */
42
+ /** Fallback width when stdout reports no usable `columns` (not a TTY, or a degenerate 0-width pty). */
43
43
  const FALLBACK_WIDTH = 100;
44
44
  /** Floor on the content width at deep nesting, so wrapping never degenerates. */
45
45
  const MIN_CONTENT_WIDTH = 12;
@@ -369,7 +369,12 @@ function buildRenderer(width) {
369
369
  * @returns ANSI-styled terminal output
370
370
  */
371
371
  export function renderMarkdown(input) {
372
- const width = Math.min(process.stdout.columns ?? FALLBACK_WIDTH, MAX_WIDTH);
372
+ // `columns` is undefined when stdout is not a TTY and can be a
373
+ // degenerate 0 under a zero-width pty (e.g. `script`); either way fall
374
+ // back to a healthy default rather than rendering into no space.
375
+ const columns = process.stdout.columns;
376
+ const usableWidth = typeof columns === "number" && columns > 0 ? columns : FALLBACK_WIDTH;
377
+ const width = Math.min(usableWidth, MAX_WIDTH);
373
378
  const marked = new Marked();
374
379
  marked.use({ renderer: buildRenderer(width) });
375
380
  return marked.parse(input);
@@ -0,0 +1,133 @@
1
+ import { type StorageCapabilities, type StorageConflict, type StorageDownloadResult, type StorageDrive, type StorageItem, type StorageItemKind, type StorageListOrder, type StoragePage, type StoragePageOptions, type StorageProvider, type StorageSearchHit, type StorageSearchQuery, type StorageUploadSource } from "../Storage.js";
2
+ /** Which real provider the fake imitates. */
3
+ export type FakeStorageFlavor = "onedrive" | "gdrive" | "dropbox";
4
+ /** Google-style native doc mime types and their export mapping. */
5
+ export declare const FAKE_NATIVE_EXPORTS: Readonly<Record<string, {
6
+ mime: string;
7
+ ext: string;
8
+ }>>;
9
+ /** Behaviour knobs for tests. */
10
+ export interface FakeStorageOptions {
11
+ readonly pluginId?: string;
12
+ readonly flavor?: FakeStorageFlavor;
13
+ /** Accounts and their drives (selector `null` = primary). Default: one account `a@x` with a primary drive. */
14
+ readonly accounts?: Readonly<Record<string, readonly {
15
+ selector: string | null;
16
+ name: string;
17
+ }[]>>;
18
+ /** Accounts whose calls throw `AuthExpired`. */
19
+ readonly expiredAccounts?: readonly string[];
20
+ /** Accounts whose `listDrives` throws `ProviderUnavailable`. */
21
+ readonly brokenAccounts?: readonly string[];
22
+ /** Artificial search latency (honours the abort signal). */
23
+ readonly searchDelayMs?: number;
24
+ /** Make every search throw this error. */
25
+ readonly searchError?: Error;
26
+ /** Override `contentSearch`. */
27
+ readonly contentSearch?: boolean;
28
+ /** Override `trash`. */
29
+ readonly trash?: boolean;
30
+ }
31
+ /**
32
+ * In-memory {@link StorageProvider} for service, tool, CLI and contract
33
+ * tests. Imitates OneDrive (path addressing, unique names), Google
34
+ * Drive (id addressing, duplicate names, native docs) or Dropbox
35
+ * (path addressing, no content search). Content lives in memory;
36
+ * download / upload still go through real host files so the byte paths
37
+ * are exercised.
38
+ */
39
+ export declare class FakeStorageProvider implements StorageProvider {
40
+ readonly pluginId: string;
41
+ readonly flavor: FakeStorageFlavor;
42
+ private readonly drives;
43
+ private readonly options;
44
+ private nextId;
45
+ private clock;
46
+ /**
47
+ * @param options - Behaviour knobs; see {@link FakeStorageOptions}.
48
+ */
49
+ constructor(options?: FakeStorageOptions);
50
+ /** @returns The capabilities of the imitated provider. */
51
+ capabilities(_drive: StorageDrive): StorageCapabilities;
52
+ /** @returns All configured accounts. */
53
+ listAccounts(): Promise<readonly string[]>;
54
+ /** @returns A fake login command. */
55
+ loginCommand(account: string): string;
56
+ /** @returns The account's drives, filtered and capped. */
57
+ listDrives(account: string, options: {
58
+ readonly limit: number;
59
+ readonly search?: string;
60
+ }): Promise<readonly StorageDrive[]>;
61
+ /** @returns The drive for `selector`. */
62
+ openDrive(account: string, selector: string | null): Promise<StorageDrive>;
63
+ /** @returns The root folder id. */
64
+ rootRealId(drive: StorageDrive): Promise<string>;
65
+ /** @returns The item or `null`. */
66
+ stat(drive: StorageDrive, realId: string): Promise<StorageItem | null>;
67
+ /** Native path resolution (only for path-addressed flavors). */
68
+ get resolvePath(): ((drive: StorageDrive, path: string) => Promise<readonly StorageItem[]>) | undefined;
69
+ /** @returns One page of children. */
70
+ list(drive: StorageDrive, folderRealId: string, options: StoragePageOptions & {
71
+ readonly order?: StorageListOrder;
72
+ }): Promise<StoragePage<StorageItem>>;
73
+ /** @returns One page of name / content hits. */
74
+ search(drive: StorageDrive, query: StorageSearchQuery, options: StoragePageOptions): Promise<StoragePage<StorageSearchHit>>;
75
+ /** Write the bytes (or the native export) to `destPath`. */
76
+ download(drive: StorageDrive, realId: string, destPath: string): Promise<StorageDownloadResult>;
77
+ /** Upload from a host file with conflict / revision semantics. */
78
+ upload(drive: StorageDrive, parentRealId: string, name: string, source: StorageUploadSource, options: {
79
+ readonly conflict: StorageConflict;
80
+ readonly replaceRealId?: string;
81
+ readonly ifRevision?: string;
82
+ }): Promise<StorageItem>;
83
+ /** Create a folder. */
84
+ createFolder(drive: StorageDrive, parentRealId: string, name: string, options: {
85
+ readonly conflict: "fail" | "rename" | "reuse";
86
+ }): Promise<StorageItem>;
87
+ /** Move / rename. */
88
+ move(drive: StorageDrive, realId: string, target: {
89
+ readonly parentRealId?: string;
90
+ readonly name?: string;
91
+ }, options: {
92
+ readonly ifRevision?: string;
93
+ }): Promise<StorageItem>;
94
+ /** Server-side copy (files only in the fake). */
95
+ copy(drive: StorageDrive, realId: string, target: {
96
+ readonly parentRealId: string;
97
+ readonly name?: string;
98
+ }, options: {
99
+ readonly conflict: "fail" | "rename";
100
+ }): Promise<StorageItem>;
101
+ /** Soft delete (recursive). */
102
+ trash(drive: StorageDrive, realId: string, options: {
103
+ readonly ifRevision?: string;
104
+ }): Promise<void>;
105
+ /**
106
+ * Seed an item under `parentPath` (folders are created as needed).
107
+ * Duplicates are allowed for the gdrive flavor.
108
+ *
109
+ * @returns The seeded item's realId.
110
+ */
111
+ seed(account: string, selector: string | null, path: string, options?: {
112
+ kind?: StorageItemKind;
113
+ content?: string;
114
+ mimeType?: string;
115
+ }): string;
116
+ /** Test helper: raw content of an item, or `null`. */
117
+ contentOf(account: string, selector: string | null, realId: string): string | null;
118
+ /** Test helper: whether an item is in the trash. */
119
+ isTrashed(account: string, selector: string | null, realId: string): boolean;
120
+ private state;
121
+ private assertAccount;
122
+ private requireNode;
123
+ private requireFolder;
124
+ private checkRevision;
125
+ private children;
126
+ private childNamed;
127
+ private freeName;
128
+ private pathOf;
129
+ private toItem;
130
+ private makeNode;
131
+ private newId;
132
+ private tick;
133
+ }