@pierre/storage 0.7.0 → 0.9.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.
package/README.md CHANGED
@@ -40,6 +40,16 @@ console.log(repo.id); // e.g., '123e4567-e89b-12d3-a456-426614174000'
40
40
  // Create a repository with custom ID
41
41
  const customRepo = await store.createRepo({ id: 'my-custom-repo' });
42
42
  console.log(customRepo.id); // 'my-custom-repo'
43
+
44
+ // Create a repository by forking an existing repo
45
+ const forkedRepo = await store.createRepo({
46
+ id: 'my-fork', // optional
47
+ baseRepo: {
48
+ id: 'my-template-id',
49
+ ref: 'main', // optional
50
+ },
51
+ });
52
+ // If defaultBranch is omitted, the SDK returns "main".
43
53
  ```
44
54
 
45
55
  ### Finding a Repository
@@ -116,6 +126,10 @@ const repo = await store.createRepo();
116
126
  // or
117
127
  const repo = await store.findOne({ id: 'existing-repo-id' });
118
128
 
129
+ // List repositories for the org
130
+ const repos = await store.listRepos({ limit: 20 });
131
+ console.log(repos.repos);
132
+
119
133
  // Get file content (streaming)
120
134
  const resp = await repo.getFileStream({
121
135
  path: 'README.md',
@@ -145,6 +159,27 @@ const commits = await repo.listCommits({
145
159
  });
146
160
  console.log(commits.commits);
147
161
 
162
+ // Read a git note for a commit
163
+ const note = await repo.getNote({ sha: 'abc123...' });
164
+ console.log(note.note);
165
+
166
+ // Add a git note
167
+ const noteResult = await repo.createNote({
168
+ sha: 'abc123...',
169
+ note: 'Release QA approved',
170
+ author: { name: 'Release Bot', email: 'release@example.com' },
171
+ });
172
+ console.log(noteResult.newRefSha);
173
+
174
+ // Append to an existing git note
175
+ await repo.appendNote({
176
+ sha: 'abc123...',
177
+ note: 'Follow-up review complete',
178
+ });
179
+
180
+ // Delete a git note
181
+ await repo.deleteNote({ sha: 'abc123...' });
182
+
148
183
  // Get branch diff
149
184
  const branchDiff = await repo.getBranchDiff({
150
185
  branch: 'feature-branch',
@@ -312,6 +347,19 @@ interface GitStorageOptions {
312
347
 
313
348
  interface CreateRepoOptions {
314
349
  id?: string; // Optional custom repository ID
350
+ baseRepo?:
351
+ | {
352
+ id: string; // Fork source repo ID
353
+ ref?: string; // Optional ref to fork from
354
+ sha?: string; // Optional commit SHA to fork from
355
+ }
356
+ | {
357
+ owner: string; // GitHub owner
358
+ name: string; // GitHub repository name
359
+ defaultBranch?: string;
360
+ provider?: 'github';
361
+ };
362
+ defaultBranch?: string; // Optional default branch name (defaults to "main")
315
363
  }
316
364
 
317
365
  interface FindOneOptions {
@@ -327,6 +375,10 @@ interface Repo {
327
375
  listFiles(options?: ListFilesOptions): Promise<ListFilesResult>;
328
376
  listBranches(options?: ListBranchesOptions): Promise<ListBranchesResult>;
329
377
  listCommits(options?: ListCommitsOptions): Promise<ListCommitsResult>;
378
+ getNote(options: GetNoteOptions): Promise<GetNoteResult>;
379
+ createNote(options: CreateNoteOptions): Promise<NoteWriteResult>;
380
+ appendNote(options: AppendNoteOptions): Promise<NoteWriteResult>;
381
+ deleteNote(options: DeleteNoteOptions): Promise<NoteWriteResult>;
330
382
  getBranchDiff(options: GetBranchDiffOptions): Promise<GetBranchDiffResult>;
331
383
  getCommitDiff(options: GetCommitDiffOptions): Promise<GetCommitDiffResult>;
332
384
  }
@@ -360,6 +412,52 @@ interface ListFilesResult {
360
412
  ref: string;
361
413
  }
362
414
 
415
+ interface GetNoteOptions {
416
+ sha: string; // Commit SHA to look up notes for
417
+ ttl?: number;
418
+ }
419
+
420
+ interface GetNoteResult {
421
+ sha: string;
422
+ note: string;
423
+ refSha: string;
424
+ }
425
+
426
+ interface CreateNoteOptions {
427
+ sha: string;
428
+ note: string;
429
+ expectedRefSha?: string;
430
+ author?: { name: string; email: string };
431
+ ttl?: number;
432
+ }
433
+
434
+ interface AppendNoteOptions {
435
+ sha: string;
436
+ note: string;
437
+ expectedRefSha?: string;
438
+ author?: { name: string; email: string };
439
+ ttl?: number;
440
+ }
441
+
442
+ interface DeleteNoteOptions {
443
+ sha: string;
444
+ expectedRefSha?: string;
445
+ author?: { name: string; email: string };
446
+ ttl?: number;
447
+ }
448
+
449
+ interface NoteWriteResult {
450
+ sha: string;
451
+ targetRef: string;
452
+ baseCommit?: string;
453
+ newRefSha: string;
454
+ result: {
455
+ success: boolean;
456
+ status: string;
457
+ message?: string;
458
+ };
459
+ }
460
+
363
461
  interface ListBranchesOptions {
364
462
  cursor?: string;
365
463
  limit?: number;
package/dist/index.cjs CHANGED
@@ -76,6 +76,40 @@ var listCommitsResponseSchema = zod.z.object({
76
76
  next_cursor: zod.z.string().nullable().optional(),
77
77
  has_more: zod.z.boolean()
78
78
  });
79
+ var repoBaseInfoSchema = zod.z.object({
80
+ provider: zod.z.string(),
81
+ owner: zod.z.string(),
82
+ name: zod.z.string()
83
+ });
84
+ var repoInfoSchema = zod.z.object({
85
+ repo_id: zod.z.string(),
86
+ url: zod.z.string(),
87
+ default_branch: zod.z.string(),
88
+ created_at: zod.z.string(),
89
+ base_repo: repoBaseInfoSchema.optional().nullable()
90
+ });
91
+ var listReposResponseSchema = zod.z.object({
92
+ repos: zod.z.array(repoInfoSchema),
93
+ next_cursor: zod.z.string().nullable().optional(),
94
+ has_more: zod.z.boolean()
95
+ });
96
+ var noteReadResponseSchema = zod.z.object({
97
+ sha: zod.z.string(),
98
+ note: zod.z.string(),
99
+ ref_sha: zod.z.string()
100
+ });
101
+ var noteResultSchema = zod.z.object({
102
+ success: zod.z.boolean(),
103
+ status: zod.z.string(),
104
+ message: zod.z.string().optional()
105
+ });
106
+ var noteWriteResponseSchema = zod.z.object({
107
+ sha: zod.z.string(),
108
+ target_ref: zod.z.string(),
109
+ base_commit: zod.z.string().optional(),
110
+ new_ref_sha: zod.z.string(),
111
+ result: noteResultSchema
112
+ });
79
113
  var diffStatsSchema = zod.z.object({
80
114
  files: zod.z.number(),
81
115
  additions: zod.z.number(),
@@ -472,7 +506,7 @@ function concatChunks(a, b) {
472
506
 
473
507
  // package.json
474
508
  var package_default = {
475
- version: "0.7.0"};
509
+ version: "0.9.0"};
476
510
 
477
511
  // src/version.ts
478
512
  var PACKAGE_NAME = "code-storage-sdk";
@@ -1364,6 +1398,36 @@ var RESTORE_COMMIT_ALLOWED_STATUS = [
1364
1398
  504
1365
1399
  // Gateway Timeout - long-running storage operations
1366
1400
  ];
1401
+ var NOTE_WRITE_ALLOWED_STATUS = [
1402
+ 400,
1403
+ // Bad Request - validation errors
1404
+ 401,
1405
+ // Unauthorized - missing/invalid auth header
1406
+ 403,
1407
+ // Forbidden - missing git:write scope
1408
+ 404,
1409
+ // Not Found - repo or note lookup failures
1410
+ 408,
1411
+ // Request Timeout - client cancelled
1412
+ 409,
1413
+ // Conflict - concurrent ref updates
1414
+ 412,
1415
+ // Precondition Failed - optimistic concurrency
1416
+ 422,
1417
+ // Unprocessable Entity - metadata issues
1418
+ 429,
1419
+ // Too Many Requests - upstream throttling
1420
+ 499,
1421
+ // Client Closed Request - storage cancellation
1422
+ 500,
1423
+ // Internal Server Error - generic failure
1424
+ 502,
1425
+ // Bad Gateway - storage/gateway bridge issues
1426
+ 503,
1427
+ // Service Unavailable - storage selection failures
1428
+ 504
1429
+ // Gateway Timeout - long-running storage operations
1430
+ ];
1367
1431
  function resolveInvocationTtlSeconds(options, defaultValue = DEFAULT_TOKEN_TTL_SECONDS) {
1368
1432
  if (typeof options?.ttl === "number" && options.ttl > 0) {
1369
1433
  return options.ttl;
@@ -1551,6 +1615,23 @@ function transformCreateBranchResult(raw) {
1551
1615
  commitSha: raw.commit_sha ?? void 0
1552
1616
  };
1553
1617
  }
1618
+ function transformListReposResult(raw) {
1619
+ return {
1620
+ repos: raw.repos.map((repo) => ({
1621
+ repoId: repo.repo_id,
1622
+ url: repo.url,
1623
+ defaultBranch: repo.default_branch,
1624
+ createdAt: repo.created_at,
1625
+ baseRepo: repo.base_repo ? {
1626
+ provider: repo.base_repo.provider,
1627
+ owner: repo.base_repo.owner,
1628
+ name: repo.base_repo.name
1629
+ } : void 0
1630
+ })),
1631
+ nextCursor: raw.next_cursor ?? void 0,
1632
+ hasMore: raw.has_more
1633
+ };
1634
+ }
1554
1635
  function transformGrepLine(raw) {
1555
1636
  return {
1556
1637
  lineNumber: raw.line_number,
@@ -1564,6 +1645,88 @@ function transformGrepFileMatch(raw) {
1564
1645
  lines: raw.lines.map(transformGrepLine)
1565
1646
  };
1566
1647
  }
1648
+ function transformNoteReadResult(raw) {
1649
+ return {
1650
+ sha: raw.sha,
1651
+ note: raw.note,
1652
+ refSha: raw.ref_sha
1653
+ };
1654
+ }
1655
+ function transformNoteWriteResult(raw) {
1656
+ return {
1657
+ sha: raw.sha,
1658
+ targetRef: raw.target_ref,
1659
+ baseCommit: raw.base_commit,
1660
+ newRefSha: raw.new_ref_sha,
1661
+ result: {
1662
+ success: raw.result.success,
1663
+ status: raw.result.status,
1664
+ message: raw.result.message
1665
+ }
1666
+ };
1667
+ }
1668
+ function buildNoteWriteBody(sha, note, action, options) {
1669
+ const body = {
1670
+ sha,
1671
+ action,
1672
+ note
1673
+ };
1674
+ const expectedRefSha = options.expectedRefSha?.trim();
1675
+ if (expectedRefSha) {
1676
+ body.expected_ref_sha = expectedRefSha;
1677
+ }
1678
+ if (options.author) {
1679
+ const authorName = options.author.name?.trim();
1680
+ const authorEmail = options.author.email?.trim();
1681
+ if (!authorName || !authorEmail) {
1682
+ throw new Error("note author name and email are required when provided");
1683
+ }
1684
+ body.author = {
1685
+ name: authorName,
1686
+ email: authorEmail
1687
+ };
1688
+ }
1689
+ return body;
1690
+ }
1691
+ async function parseNoteWriteResponse(response, method) {
1692
+ let jsonBody;
1693
+ const contentType = response.headers.get("content-type") ?? "";
1694
+ try {
1695
+ if (contentType.includes("application/json")) {
1696
+ jsonBody = await response.json();
1697
+ } else {
1698
+ jsonBody = await response.text();
1699
+ }
1700
+ } catch {
1701
+ jsonBody = void 0;
1702
+ }
1703
+ if (jsonBody && typeof jsonBody === "object") {
1704
+ const parsed = noteWriteResponseSchema.safeParse(jsonBody);
1705
+ if (parsed.success) {
1706
+ return transformNoteWriteResult(parsed.data);
1707
+ }
1708
+ const parsedError = errorEnvelopeSchema.safeParse(jsonBody);
1709
+ if (parsedError.success) {
1710
+ throw new ApiError({
1711
+ message: parsedError.data.error,
1712
+ status: response.status,
1713
+ statusText: response.statusText,
1714
+ method,
1715
+ url: response.url,
1716
+ body: jsonBody
1717
+ });
1718
+ }
1719
+ }
1720
+ const fallbackMessage = typeof jsonBody === "string" && jsonBody.trim() !== "" ? jsonBody.trim() : `Request ${method} ${response.url} failed with status ${response.status} ${response.statusText}`;
1721
+ throw new ApiError({
1722
+ message: fallbackMessage,
1723
+ status: response.status,
1724
+ statusText: response.statusText,
1725
+ method,
1726
+ url: response.url,
1727
+ body: jsonBody
1728
+ });
1729
+ }
1567
1730
  var RepoImpl = class {
1568
1731
  constructor(id, defaultBranch, options, generateJWT) {
1569
1732
  this.id = id;
@@ -1679,6 +1842,132 @@ var RepoImpl = class {
1679
1842
  next_cursor: raw.next_cursor ?? void 0
1680
1843
  });
1681
1844
  }
1845
+ async getNote(options) {
1846
+ const sha = options?.sha?.trim();
1847
+ if (!sha) {
1848
+ throw new Error("getNote sha is required");
1849
+ }
1850
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
1851
+ const jwt = await this.generateJWT(this.id, {
1852
+ permissions: ["git:read"],
1853
+ ttl
1854
+ });
1855
+ const response = await this.api.get({ path: "repos/notes", params: { sha } }, jwt);
1856
+ const raw = noteReadResponseSchema.parse(await response.json());
1857
+ return transformNoteReadResult(raw);
1858
+ }
1859
+ async createNote(options) {
1860
+ const sha = options?.sha?.trim();
1861
+ if (!sha) {
1862
+ throw new Error("createNote sha is required");
1863
+ }
1864
+ const note = options?.note?.trim();
1865
+ if (!note) {
1866
+ throw new Error("createNote note is required");
1867
+ }
1868
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
1869
+ const jwt = await this.generateJWT(this.id, {
1870
+ permissions: ["git:write"],
1871
+ ttl
1872
+ });
1873
+ const body = buildNoteWriteBody(sha, note, "add", {
1874
+ expectedRefSha: options.expectedRefSha,
1875
+ author: options.author
1876
+ });
1877
+ const response = await this.api.post({ path: "repos/notes", body }, jwt, {
1878
+ allowedStatus: [...NOTE_WRITE_ALLOWED_STATUS]
1879
+ });
1880
+ const result = await parseNoteWriteResponse(response, "POST");
1881
+ if (!result.result.success) {
1882
+ throw new RefUpdateError(
1883
+ result.result.message ?? `createNote failed with status ${result.result.status}`,
1884
+ {
1885
+ status: result.result.status,
1886
+ message: result.result.message,
1887
+ refUpdate: toPartialRefUpdate(result.targetRef, result.baseCommit, result.newRefSha)
1888
+ }
1889
+ );
1890
+ }
1891
+ return result;
1892
+ }
1893
+ async appendNote(options) {
1894
+ const sha = options?.sha?.trim();
1895
+ if (!sha) {
1896
+ throw new Error("appendNote sha is required");
1897
+ }
1898
+ const note = options?.note?.trim();
1899
+ if (!note) {
1900
+ throw new Error("appendNote note is required");
1901
+ }
1902
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
1903
+ const jwt = await this.generateJWT(this.id, {
1904
+ permissions: ["git:write"],
1905
+ ttl
1906
+ });
1907
+ const body = buildNoteWriteBody(sha, note, "append", {
1908
+ expectedRefSha: options.expectedRefSha,
1909
+ author: options.author
1910
+ });
1911
+ const response = await this.api.post({ path: "repos/notes", body }, jwt, {
1912
+ allowedStatus: [...NOTE_WRITE_ALLOWED_STATUS]
1913
+ });
1914
+ const result = await parseNoteWriteResponse(response, "POST");
1915
+ if (!result.result.success) {
1916
+ throw new RefUpdateError(
1917
+ result.result.message ?? `appendNote failed with status ${result.result.status}`,
1918
+ {
1919
+ status: result.result.status,
1920
+ message: result.result.message,
1921
+ refUpdate: toPartialRefUpdate(result.targetRef, result.baseCommit, result.newRefSha)
1922
+ }
1923
+ );
1924
+ }
1925
+ return result;
1926
+ }
1927
+ async deleteNote(options) {
1928
+ const sha = options?.sha?.trim();
1929
+ if (!sha) {
1930
+ throw new Error("deleteNote sha is required");
1931
+ }
1932
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
1933
+ const jwt = await this.generateJWT(this.id, {
1934
+ permissions: ["git:write"],
1935
+ ttl
1936
+ });
1937
+ const body = {
1938
+ sha
1939
+ };
1940
+ const expectedRefSha = options.expectedRefSha?.trim();
1941
+ if (expectedRefSha) {
1942
+ body.expected_ref_sha = expectedRefSha;
1943
+ }
1944
+ if (options.author) {
1945
+ const authorName = options.author.name?.trim();
1946
+ const authorEmail = options.author.email?.trim();
1947
+ if (!authorName || !authorEmail) {
1948
+ throw new Error("deleteNote author name and email are required when provided");
1949
+ }
1950
+ body.author = {
1951
+ name: authorName,
1952
+ email: authorEmail
1953
+ };
1954
+ }
1955
+ const response = await this.api.delete({ path: "repos/notes", body }, jwt, {
1956
+ allowedStatus: [...NOTE_WRITE_ALLOWED_STATUS]
1957
+ });
1958
+ const result = await parseNoteWriteResponse(response, "DELETE");
1959
+ if (!result.result.success) {
1960
+ throw new RefUpdateError(
1961
+ result.result.message ?? `deleteNote failed with status ${result.result.status}`,
1962
+ {
1963
+ status: result.result.status,
1964
+ message: result.result.message,
1965
+ refUpdate: toPartialRefUpdate(result.targetRef, result.baseCommit, result.newRefSha)
1966
+ }
1967
+ );
1968
+ }
1969
+ return result;
1970
+ }
1682
1971
  async getBranchDiff(options) {
1683
1972
  const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
1684
1973
  const jwt = await this.generateJWT(this.id, {
@@ -1984,23 +2273,83 @@ var GitStorage = class _GitStorage {
1984
2273
  permissions: ["repo:write"],
1985
2274
  ttl
1986
2275
  });
1987
- const baseRepoOptions = options?.baseRepo ? {
1988
- provider: "github",
1989
- ...snakecaseKeys__default.default(options.baseRepo)
1990
- } : null;
1991
- const defaultBranch = options?.baseRepo?.defaultBranch ?? options?.defaultBranch ?? "main";
1992
- const createRepoPath = baseRepoOptions || defaultBranch ? {
2276
+ const baseRepo = options?.baseRepo;
2277
+ const isFork = baseRepo ? "id" in baseRepo : false;
2278
+ let baseRepoOptions = null;
2279
+ let resolvedDefaultBranch;
2280
+ if (baseRepo) {
2281
+ if ("id" in baseRepo) {
2282
+ const baseRepoToken = await this.generateJWT(baseRepo.id, {
2283
+ permissions: ["git:read"],
2284
+ ttl
2285
+ });
2286
+ baseRepoOptions = {
2287
+ provider: "code",
2288
+ name: baseRepo.id,
2289
+ operation: "fork",
2290
+ auth: { token: baseRepoToken },
2291
+ ...baseRepo.ref ? { ref: baseRepo.ref } : {},
2292
+ ...baseRepo.sha ? { sha: baseRepo.sha } : {}
2293
+ };
2294
+ } else {
2295
+ baseRepoOptions = {
2296
+ provider: "github",
2297
+ ...snakecaseKeys__default.default(baseRepo)
2298
+ };
2299
+ resolvedDefaultBranch = baseRepo.defaultBranch;
2300
+ }
2301
+ }
2302
+ if (!resolvedDefaultBranch) {
2303
+ if (options?.defaultBranch) {
2304
+ resolvedDefaultBranch = options.defaultBranch;
2305
+ } else if (!isFork) {
2306
+ resolvedDefaultBranch = "main";
2307
+ }
2308
+ }
2309
+ const createRepoPath = baseRepoOptions || resolvedDefaultBranch ? {
1993
2310
  path: "repos",
1994
2311
  body: {
1995
2312
  ...baseRepoOptions && { base_repo: baseRepoOptions },
1996
- default_branch: defaultBranch
2313
+ ...resolvedDefaultBranch && { default_branch: resolvedDefaultBranch }
1997
2314
  }
1998
2315
  } : "repos";
1999
2316
  const resp = await this.api.post(createRepoPath, jwt, { allowedStatus: [409] });
2000
2317
  if (resp.status === 409) {
2001
2318
  throw new Error("Repository already exists");
2002
2319
  }
2003
- return new RepoImpl(repoId, defaultBranch, this.options, this.generateJWT.bind(this));
2320
+ return new RepoImpl(
2321
+ repoId,
2322
+ resolvedDefaultBranch ?? "main",
2323
+ this.options,
2324
+ this.generateJWT.bind(this)
2325
+ );
2326
+ }
2327
+ /**
2328
+ * List repositories for the authenticated organization
2329
+ * @returns Paginated repositories list
2330
+ */
2331
+ async listRepos(options) {
2332
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
2333
+ const jwt = await this.generateJWT("org", {
2334
+ permissions: ["org:read"],
2335
+ ttl
2336
+ });
2337
+ let params;
2338
+ if (options?.cursor || typeof options?.limit === "number") {
2339
+ params = {};
2340
+ if (options.cursor) {
2341
+ params.cursor = options.cursor;
2342
+ }
2343
+ if (typeof options.limit === "number") {
2344
+ params.limit = options.limit.toString();
2345
+ }
2346
+ }
2347
+ const response = await this.api.get({ path: "repos", params }, jwt);
2348
+ const raw = listReposResponseSchema.parse(await response.json());
2349
+ return transformListReposResult({
2350
+ ...raw,
2351
+ next_cursor: raw.next_cursor ?? void 0
2352
+ });
2004
2353
  }
2005
2354
  /**
2006
2355
  * Find a repository by ID