Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 7 additions & 21 deletions packages/agents/src/capture-event/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { ulid } from 'ulid';

import { formatUtcTimestamp } from '../kb-shared/note-helpers.ts';
import { resolveStoreByName } from '../kb-shared/resolve-store-by-name.ts';
import { parseTagList } from '../kb-shared/tag-helpers.ts';
import { readAll } from '../lib/stream-helpers.ts';
import { isEnoent } from '../lib/type-guards.ts';
import { prepareEvent } from './prepare-event.ts';
import type { CaptureContext, CaptureResult, ParsedArgs } from './types.ts';
Expand Down Expand Up @@ -211,14 +213,6 @@ function matchValueFlag(arg: string): { key: ValueFlag; inlineValue: string | nu
return null;
}

/** Splits a comma-separated tag string into individual tags, dropping empties and trimming whitespace. */
function parseTagList(value: string): string[] {
return value
.split(',')
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0);
}

/**
* Resolves the `owner/name` git remote at `cwd`, best-effort. Prefers the `origin` remote and falls back to the first
* listed remote when `origin` is absent. Both SSH (`git@host:owner/name.git`) and HTTPS (`https://host/owner/name.git`)
Expand Down Expand Up @@ -261,7 +255,11 @@ async function resolveRemoteUrl(cwd: string): Promise<string | undefined> {
}
}

/** Normalizes an SSH or HTTPS git remote URL to `owner/name`, or `undefined` when it cannot be parsed. */
/**
* Normalizes an SSH or HTTPS git remote URL to `owner/name`, or `undefined` when it cannot be parsed.
*
* @internal - Exported to allow testing.
*/
export function normalizeRemoteUrl(url: string): string | undefined {
const withoutSuffix = url.replace(/\.git$/, '');

Expand All @@ -287,16 +285,4 @@ function takeOwnerName(path: string): string | undefined {
return segments.slice(-2).join('/');
}

/** Reads a readable stream to completion as a UTF-8 string. */
async function readAll(stream: Readable): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of stream) {
if (!Buffer.isBuffer(chunk)) {
throw new TypeError('readAll: expected Buffer chunks (stream must be in binary mode)');
}
chunks.push(chunk);
}
return Buffer.concat(chunks).toString('utf8');
}

// endregion | Helpers
25 changes: 2 additions & 23 deletions packages/agents/src/kb-add/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { loadSchema } from '@codeassembly/kb/schema';
import { loadAliases } from '@codeassembly/kb/tags';

import { resolveWritableKb } from '../kb-shared/resolve-writable-kb.ts';
import { parseTagList } from '../kb-shared/tag-helpers.ts';
import { readAll } from '../lib/stream-helpers.ts';
import { prepareNote } from './prepare-note.ts';
import type { AddResult, ParsedArgs } from './types.ts';
import { writeNote } from './write-note.ts';
Expand Down Expand Up @@ -251,27 +253,4 @@ async function loadAliasesWithWarning(input: { kbRoot: KbRoot }): Promise<AliasM
}
}

/** Splits a comma-separated tag string into individual tags, dropping empties and trimming whitespace. */
function parseTagList(value: string): string[] {
return value
.split(',')
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0);
}

/**
* Reads a readable stream to completion as a UTF-8 string. Callers pass `process.stdin` (binary mode) or a
* `Readable.from([Buffer])`, both of which emit `Buffer` chunks.
*/
async function readAll(stream: Readable): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of stream) {
if (!Buffer.isBuffer(chunk)) {
throw new TypeError('readAll: expected Buffer chunks (stream must be in binary mode)');
}
chunks.push(chunk);
}
return Buffer.concat(chunks).toString('utf8');
}

// endregion | Helpers
22 changes: 2 additions & 20 deletions packages/agents/src/kb-edit/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { loadAliases } from '@codeassembly/kb/tags';

import type { ResolvedKb } from '../kb-shared/resolve-writable-kb.ts';
import { resolveWritableKb } from '../kb-shared/resolve-writable-kb.ts';
import { parseTagList } from '../kb-shared/tag-helpers.ts';
import { readAll } from '../lib/stream-helpers.ts';
import { commitSupersede } from './commit-supersede.ts';
import { loadNote } from './load-note.ts';
import { append } from './operations/append.ts';
Expand Down Expand Up @@ -417,18 +419,6 @@ async function runSupersedeWith(input: {
return success;
}

/** Reads a readable stream to completion as a UTF-8 string. */
async function readAll(stream: Readable): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of stream) {
if (!Buffer.isBuffer(chunk)) {
throw new TypeError('readAll: expected Buffer chunks (stream must be in binary mode)');
}
chunks.push(chunk);
}
return Buffer.concat(chunks).toString('utf8');
}

/** A captured operation flag: its canonical name plus the value (if any) that followed it. */
interface SelectedOp {
name: OperationName;
Expand Down Expand Up @@ -563,12 +553,4 @@ function matchOperationFlag(
return null;
}

/** Splits a comma-separated tag string into individual tags, dropping empties and trimming whitespace. */
function parseTagList(value: string): string[] {
return value
.split(',')
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0);
}

// endregion | Helpers
29 changes: 29 additions & 0 deletions packages/agents/src/kb-shared/__tests__/tag-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';

import { parseTagList } from '../tag-helpers.ts';

describe(parseTagList, () => {
it('splits a comma-separated string into individual tags', () => {
expect(parseTagList('alpha,beta,gamma')).toEqual(['alpha', 'beta', 'gamma']);
});

it('trims surrounding whitespace from each tag', () => {
expect(parseTagList(' alpha , beta ,gamma ')).toEqual(['alpha', 'beta', 'gamma']);
});

it('drops empty segments from leading, trailing, and doubled commas', () => {
expect(parseTagList(',alpha,,beta,')).toEqual(['alpha', 'beta']);
});

it('returns a single-element list for a value with no commas', () => {
expect(parseTagList('alpha')).toEqual(['alpha']);
});

it('returns an empty list for an empty string', () => {
expect(parseTagList('')).toEqual([]);
});

it('returns an empty list for whitespace and commas only', () => {
expect(parseTagList(' , , ')).toEqual([]);
});
});
10 changes: 10 additions & 0 deletions packages/agents/src/kb-shared/tag-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Splits a comma-separated tag string into individual tags, dropping empties and trimming whitespace.
* @internal
*/
export function parseTagList(value: string): string[] {
return value
.split(',')
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0);
}
27 changes: 27 additions & 0 deletions packages/agents/src/lib/__tests__/stream-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Readable } from 'node:stream';

import { describe, expect, it } from 'vitest';

import { readAll } from '../stream-helpers.ts';

describe(readAll, () => {
it('concatenates multiple Buffer chunks into a single UTF-8 string', async () => {
const stream = Readable.from([Buffer.from('hello '), Buffer.from('world')]);
await expect(readAll(stream)).resolves.toBe('hello world');
});

it('decodes multi-byte UTF-8 sequences split across chunk boundaries', async () => {
const encoded = Buffer.from('café', 'utf8');
const stream = Readable.from([encoded.subarray(0, 4), encoded.subarray(4)]);
await expect(readAll(stream)).resolves.toBe('café');
});

it('returns an empty string for a stream with no chunks', async () => {
await expect(readAll(Readable.from([]))).resolves.toBe('');
});

it('throws a TypeError when a chunk is not a Buffer', async () => {
const stream = Readable.from(['not-a-buffer'], { objectMode: true });
await expect(readAll(stream)).rejects.toThrow(TypeError);
});
});
17 changes: 17 additions & 0 deletions packages/agents/src/lib/stream-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Readable } from 'node:stream';

/**
* Reads a readable stream to completion as a UTF-8 string. Callers pass `process.stdin` (binary mode) or a
* `Readable.from([Buffer])`, both of which emit `Buffer` chunks.
* @internal
*/
export async function readAll(stream: Readable): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of stream) {
if (!Buffer.isBuffer(chunk)) {
throw new TypeError('readAll: expected Buffer chunks (stream must be in binary mode)');
}
chunks.push(chunk);
}
return Buffer.concat(chunks).toString('utf8');
}
14 changes: 1 addition & 13 deletions packages/agents/src/update-jira-ticket/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,11 @@

import { realpathSync } from 'node:fs';
import process from 'node:process';
import type { Readable } from 'node:stream';
import { fileURLToPath } from 'node:url';

import { readAll } from '../lib/stream-helpers.ts';
import { check } from './check.ts';

/** Read every chunk of `stream` and concatenate into a single UTF-8 string. */
async function readAll(stream: Readable): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of stream) {
if (!Buffer.isBuffer(chunk)) {
throw new TypeError('readAll: expected Buffer chunks (stream must be in binary mode)');
}
chunks.push(chunk);
}
return Buffer.concat(chunks).toString('utf8');
}

/** Top-level entry: read stdin, run the check, emit JSON. */
async function main(): Promise<void> {
try {
Expand Down
Loading