chops-sh 0.1.1 → 0.2.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 (3) hide show
  1. package/README.md +19 -2
  2. package/dist/index.js +1018 -109
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1230,9 +1230,9 @@ ${indent}`) + "'";
1230
1230
  start = start.replace(/\n+/g, `$&${indent}`);
1231
1231
  }
1232
1232
  const indentSize = indent ? "2" : "1";
1233
- let header = (startWithSpace ? indentSize : "") + chomp;
1233
+ let header2 = (startWithSpace ? indentSize : "") + chomp;
1234
1234
  if (comment) {
1235
- header += " " + commentString(comment.replace(/ ?[\r\n]+/g, " "));
1235
+ header2 += " " + commentString(comment.replace(/ ?[\r\n]+/g, " "));
1236
1236
  if (onComment)
1237
1237
  onComment();
1238
1238
  }
@@ -1248,11 +1248,11 @@ $&`).replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g, "$1$2").replace(/
1248
1248
  }
1249
1249
  const body = foldFlowLines.foldFlowLines(`${start}${foldedValue}${end}`, indent, foldFlowLines.FOLD_BLOCK, foldOptions);
1250
1250
  if (!literalFallback)
1251
- return `>${header}
1251
+ return `>${header2}
1252
1252
  ${indent}${body}`;
1253
1253
  }
1254
1254
  value = value.replace(/\n+/g, `$&${indent}`);
1255
- return `|${header}
1255
+ return `|${header2}
1256
1256
  ${indent}${start}${value}${end}`;
1257
1257
  }
1258
1258
  function plainString(item, ctx, onComment, onChompKeep) {
@@ -4127,10 +4127,10 @@ var require_resolve_block_scalar = __commonJS((exports) => {
4127
4127
  var Scalar = require_Scalar();
4128
4128
  function resolveBlockScalar(ctx, scalar, onError) {
4129
4129
  const start = scalar.offset;
4130
- const header = parseBlockScalarHeader(scalar, ctx.options.strict, onError);
4131
- if (!header)
4130
+ const header2 = parseBlockScalarHeader(scalar, ctx.options.strict, onError);
4131
+ if (!header2)
4132
4132
  return { value: "", type: null, comment: "", range: [start, start, start] };
4133
- const type = header.mode === ">" ? Scalar.Scalar.BLOCK_FOLDED : Scalar.Scalar.BLOCK_LITERAL;
4133
+ const type = header2.mode === ">" ? Scalar.Scalar.BLOCK_FOLDED : Scalar.Scalar.BLOCK_LITERAL;
4134
4134
  const lines = scalar.source ? splitLines(scalar.source) : [];
4135
4135
  let chompStart = lines.length;
4136
4136
  for (let i = lines.length - 1;i >= 0; --i) {
@@ -4141,27 +4141,27 @@ var require_resolve_block_scalar = __commonJS((exports) => {
4141
4141
  break;
4142
4142
  }
4143
4143
  if (chompStart === 0) {
4144
- const value2 = header.chomp === "+" && lines.length > 0 ? `
4144
+ const value2 = header2.chomp === "+" && lines.length > 0 ? `
4145
4145
  `.repeat(Math.max(1, lines.length - 1)) : "";
4146
- let end2 = start + header.length;
4146
+ let end2 = start + header2.length;
4147
4147
  if (scalar.source)
4148
4148
  end2 += scalar.source.length;
4149
- return { value: value2, type, comment: header.comment, range: [start, end2, end2] };
4149
+ return { value: value2, type, comment: header2.comment, range: [start, end2, end2] };
4150
4150
  }
4151
- let trimIndent = scalar.indent + header.indent;
4152
- let offset = scalar.offset + header.length;
4151
+ let trimIndent = scalar.indent + header2.indent;
4152
+ let offset = scalar.offset + header2.length;
4153
4153
  let contentStart = 0;
4154
4154
  for (let i = 0;i < chompStart; ++i) {
4155
4155
  const [indent, content] = lines[i];
4156
4156
  if (content === "" || content === "\r") {
4157
- if (header.indent === 0 && indent.length > trimIndent)
4157
+ if (header2.indent === 0 && indent.length > trimIndent)
4158
4158
  trimIndent = indent.length;
4159
4159
  } else {
4160
4160
  if (indent.length < trimIndent) {
4161
4161
  const message = "Block scalars with more-indented leading empty lines must use an explicit indentation indicator";
4162
4162
  onError(offset + indent.length, "MISSING_CHAR", message);
4163
4163
  }
4164
- if (header.indent === 0)
4164
+ if (header2.indent === 0)
4165
4165
  trimIndent = indent.length;
4166
4166
  contentStart = i;
4167
4167
  if (trimIndent === 0 && !ctx.atRoot) {
@@ -4189,7 +4189,7 @@ var require_resolve_block_scalar = __commonJS((exports) => {
4189
4189
  if (crlf)
4190
4190
  content = content.slice(0, -1);
4191
4191
  if (content && indent.length < trimIndent) {
4192
- const src = header.indent ? "explicit indentation indicator" : "first line";
4192
+ const src = header2.indent ? "explicit indentation indicator" : "first line";
4193
4193
  const message = `Block scalar lines must not be less indented than their ${src}`;
4194
4194
  onError(offset - content.length - (crlf ? 2 : 1), "BAD_INDENT", message);
4195
4195
  indent = "";
@@ -4225,7 +4225,7 @@ var require_resolve_block_scalar = __commonJS((exports) => {
4225
4225
  prevMoreIndented = false;
4226
4226
  }
4227
4227
  }
4228
- switch (header.chomp) {
4228
+ switch (header2.chomp) {
4229
4229
  case "-":
4230
4230
  break;
4231
4231
  case "+":
@@ -4241,8 +4241,8 @@ var require_resolve_block_scalar = __commonJS((exports) => {
4241
4241
  value += `
4242
4242
  `;
4243
4243
  }
4244
- const end = start + header.length + scalar.source.length;
4245
- return { value, type, comment: header.comment, range: [start, end, end] };
4244
+ const end = start + header2.length + scalar.source.length;
4245
+ return { value, type, comment: header2.comment, range: [start, end, end] };
4246
4246
  }
4247
4247
  function parseBlockScalarHeader({ offset, props }, strict, onError) {
4248
4248
  if (props[0].type !== "block-scalar-header") {
@@ -5047,10 +5047,10 @@ var require_cst_scalar = __commonJS((exports) => {
5047
5047
  type = "QUOTE_DOUBLE";
5048
5048
  break;
5049
5049
  case "block-scalar": {
5050
- const header = token.props[0];
5051
- if (header.type !== "block-scalar-header")
5050
+ const header2 = token.props[0];
5051
+ if (header2.type !== "block-scalar-header")
5052
5052
  throw new Error("Invalid block scalar header");
5053
- type = header.source[0] === ">" ? "BLOCK_FOLDED" : "BLOCK_LITERAL";
5053
+ type = header2.source[0] === ">" ? "BLOCK_FOLDED" : "BLOCK_LITERAL";
5054
5054
  break;
5055
5055
  }
5056
5056
  default:
@@ -5084,10 +5084,10 @@ var require_cst_scalar = __commonJS((exports) => {
5084
5084
  const body = source.substring(he + 1) + `
5085
5085
  `;
5086
5086
  if (token.type === "block-scalar") {
5087
- const header = token.props[0];
5088
- if (header.type !== "block-scalar-header")
5087
+ const header2 = token.props[0];
5088
+ if (header2.type !== "block-scalar-header")
5089
5089
  throw new Error("Invalid block scalar header");
5090
- header.source = head;
5090
+ header2.source = head;
5091
5091
  token.source = body;
5092
5092
  } else {
5093
5093
  const { offset } = token;
@@ -6958,18 +6958,6 @@ var require_public_api = __commonJS((exports) => {
6958
6958
  import { createInterface } from "node:readline/promises";
6959
6959
  import { stdin, stdout } from "node:process";
6960
6960
 
6961
- // ../core/src/exit-codes.ts
6962
- var EXIT_CODES = {
6963
- OK: 0,
6964
- GENERIC: 1,
6965
- VERIFIED_ONLY: 42,
6966
- VERIFICATION_WARNING: 43,
6967
- TEAM_POLICY_BLOCK: 44,
6968
- RATE_LIMITED: 45
6969
- };
6970
- // ../core/src/manifest.ts
6971
- import { createHash } from "node:crypto";
6972
-
6973
6961
  // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
6974
6962
  var exports_external = {};
6975
6963
  __export(exports_external, {
@@ -10943,7 +10931,216 @@ var coerce = {
10943
10931
  date: (arg) => ZodDate.create({ ...arg, coerce: true })
10944
10932
  };
10945
10933
  var NEVER = INVALID;
10934
+ // ../core/src/api-auth.ts
10935
+ var apiErrorSchema = exports_external.object({
10936
+ error: exports_external.object({
10937
+ code: exports_external.string(),
10938
+ message: exports_external.string(),
10939
+ lift: exports_external.object({ action: exports_external.string(), hint: exports_external.string() }).optional(),
10940
+ retry_after: exports_external.number().int().nonnegative().optional()
10941
+ })
10942
+ });
10943
+ var authUserSchema = exports_external.object({
10944
+ id: exports_external.string().uuid(),
10945
+ email: exports_external.string().email(),
10946
+ handle: exports_external.string().nullable()
10947
+ });
10948
+ var otpStartResponseSchema = exports_external.object({
10949
+ ok: exports_external.literal(true),
10950
+ dev_code: exports_external.string().optional()
10951
+ });
10952
+ var tokenPairSchema = exports_external.object({
10953
+ access_jwt: exports_external.string().min(1),
10954
+ refresh_token: exports_external.string().min(1),
10955
+ expires_in: exports_external.number().int().positive()
10956
+ });
10957
+ var otpVerifyResponseSchema = tokenPairSchema.extend({
10958
+ user: authUserSchema
10959
+ });
10960
+ var teamRoleSchema = exports_external.enum(["owner", "admin", "member"]);
10961
+ var teamSummarySchema = exports_external.object({
10962
+ id: exports_external.string().uuid(),
10963
+ slug: exports_external.string(),
10964
+ name: exports_external.string(),
10965
+ role: teamRoleSchema,
10966
+ member_count: exports_external.number().int().positive()
10967
+ });
10968
+ var createTeamResponseSchema = exports_external.object({
10969
+ team: teamSummarySchema
10970
+ });
10971
+ var meTeamsResponseSchema = exports_external.object({
10972
+ teams: exports_external.array(teamSummarySchema)
10973
+ });
10974
+ var teamMemberSchema = exports_external.object({
10975
+ user_id: exports_external.string().uuid(),
10976
+ email: exports_external.string().email(),
10977
+ handle: exports_external.string().nullable(),
10978
+ role: teamRoleSchema,
10979
+ joined_at: exports_external.string()
10980
+ });
10981
+ var pendingInviteSchema = exports_external.object({
10982
+ id: exports_external.string().uuid(),
10983
+ email: exports_external.string().email(),
10984
+ role: teamRoleSchema,
10985
+ invited_by: exports_external.string(),
10986
+ expires_at: exports_external.string()
10987
+ });
10988
+ var teamMembersResponseSchema = exports_external.object({
10989
+ members: exports_external.array(teamMemberSchema),
10990
+ invites: exports_external.array(pendingInviteSchema).optional()
10991
+ });
10992
+ var inviteSendResultSchema = exports_external.object({
10993
+ email: exports_external.string(),
10994
+ status: exports_external.enum(["sent", "already_member", "invalid_email"]),
10995
+ expires_at: exports_external.string().optional()
10996
+ });
10997
+ var sendInvitesResponseSchema = exports_external.object({
10998
+ results: exports_external.array(inviteSendResultSchema)
10999
+ });
11000
+ var invitePreviewSchema = exports_external.object({
11001
+ team: exports_external.object({ slug: exports_external.string(), name: exports_external.string() }),
11002
+ inviter: exports_external.string(),
11003
+ invited_email_masked: exports_external.string(),
11004
+ status: exports_external.enum(["pending", "accepted", "revoked", "expired"]),
11005
+ expires_at: exports_external.string()
11006
+ });
11007
+ var meInviteSchema = exports_external.object({
11008
+ id: exports_external.string().uuid(),
11009
+ team: exports_external.object({ slug: exports_external.string(), name: exports_external.string() }),
11010
+ role: teamRoleSchema,
11011
+ invited_by: exports_external.string(),
11012
+ expires_at: exports_external.string()
11013
+ });
11014
+ var meInvitesResponseSchema = exports_external.object({
11015
+ invites: exports_external.array(meInviteSchema)
11016
+ });
11017
+ var acceptInviteResponseSchema = exports_external.object({
11018
+ team: exports_external.object({ slug: exports_external.string(), name: exports_external.string() }),
11019
+ role: teamRoleSchema
11020
+ });
11021
+ var privateSkillSummarySchema = exports_external.object({
11022
+ ref: exports_external.string(),
11023
+ name: exports_external.string(),
11024
+ description: exports_external.string().nullable(),
11025
+ version: exports_external.number().int().positive(),
11026
+ content_sha: exports_external.string(),
11027
+ size_bytes: exports_external.number().int().nonnegative(),
11028
+ published_by: exports_external.string(),
11029
+ updated_at: exports_external.string()
11030
+ });
11031
+ var privateSkillsResponseSchema = exports_external.object({
11032
+ skills: exports_external.array(privateSkillSummarySchema)
11033
+ });
11034
+ var privatePublishMetaSchema = exports_external.object({
11035
+ name: exports_external.string().min(1).max(64),
11036
+ description: exports_external.string().max(1024).nullish(),
11037
+ content_sha: exports_external.string().regex(/^[a-f0-9]{64}$/),
11038
+ size_bytes: exports_external.number().int().positive()
11039
+ });
11040
+ var privatePublishResponseSchema = exports_external.object({
11041
+ ref: exports_external.string(),
11042
+ name: exports_external.string(),
11043
+ version: exports_external.number().int().positive(),
11044
+ content_sha: exports_external.string(),
11045
+ dedup: exports_external.boolean()
11046
+ });
11047
+ // ../core/src/archive.ts
11048
+ import { gzipSync } from "node:zlib";
11049
+ var BLOCK_SIZE = 512;
11050
+ function safeArchivePath(path) {
11051
+ const normalized = path.replaceAll("\\", "/").replace(/^\.\//, "");
11052
+ if (!normalized || normalized.startsWith("/") || normalized.split("/").some((part) => part === "" || part === "." || part === "..")) {
11053
+ throw new Error(`unsafe archive path: ${path}`);
11054
+ }
11055
+ return normalized;
11056
+ }
11057
+ function writeText(target, offset, length, value) {
11058
+ const bytes = new TextEncoder().encode(value);
11059
+ if (bytes.length > length)
11060
+ throw new Error(`tar header value is too long: ${value}`);
11061
+ target.set(bytes, offset);
11062
+ }
11063
+ function octal(value, length) {
11064
+ const body = Math.trunc(value).toString(8);
11065
+ if (body.length > length - 1)
11066
+ throw new Error(`tar numeric value is too large: ${value}`);
11067
+ return `${body.padStart(length - 1, "0")}\x00`;
11068
+ }
11069
+ function splitTarPath(path) {
11070
+ if (Buffer.byteLength(path) <= 100)
11071
+ return { name: path, prefix: "" };
11072
+ for (let index = path.lastIndexOf("/");index > 0; index = path.lastIndexOf("/", index - 1)) {
11073
+ const prefix = path.slice(0, index);
11074
+ const name = path.slice(index + 1);
11075
+ if (Buffer.byteLength(prefix) <= 155 && Buffer.byteLength(name) <= 100) {
11076
+ return { name, prefix };
11077
+ }
11078
+ }
11079
+ throw new Error(`tar path is too long: ${path}`);
11080
+ }
11081
+ function header(entry) {
11082
+ const path = safeArchivePath(entry.path);
11083
+ const { name, prefix } = splitTarPath(path);
11084
+ const block = new Uint8Array(BLOCK_SIZE);
11085
+ const symlinkTarget = entry.symlink ? new TextDecoder().decode(entry.data) : "";
11086
+ const size = entry.symlink ? 0 : entry.data.byteLength;
11087
+ writeText(block, 0, 100, name);
11088
+ writeText(block, 100, 8, octal(entry.executable ? 493 : 420, 8));
11089
+ writeText(block, 108, 8, octal(0, 8));
11090
+ writeText(block, 116, 8, octal(0, 8));
11091
+ writeText(block, 124, 12, octal(size, 12));
11092
+ writeText(block, 136, 12, octal(0, 12));
11093
+ block.fill(32, 148, 156);
11094
+ writeText(block, 156, 1, entry.symlink ? "2" : "0");
11095
+ if (entry.symlink)
11096
+ writeText(block, 157, 100, symlinkTarget);
11097
+ writeText(block, 257, 6, "ustar\x00");
11098
+ writeText(block, 263, 2, "00");
11099
+ writeText(block, 345, 155, prefix);
11100
+ const checksum = block.reduce((sum, byte) => sum + byte, 0);
11101
+ writeText(block, 148, 8, `${checksum.toString(8).padStart(6, "0")}\x00 `);
11102
+ return block;
11103
+ }
11104
+ function buildTarGz(entries) {
11105
+ const chunks = [];
11106
+ let total = BLOCK_SIZE * 2;
11107
+ for (const entry of [...entries].sort((a, b) => a.path.localeCompare(b.path))) {
11108
+ const block = header(entry);
11109
+ chunks.push(block);
11110
+ total += block.byteLength;
11111
+ if (!entry.symlink) {
11112
+ chunks.push(entry.data);
11113
+ total += entry.data.byteLength;
11114
+ const padding = (BLOCK_SIZE - entry.data.byteLength % BLOCK_SIZE) % BLOCK_SIZE;
11115
+ if (padding) {
11116
+ chunks.push(new Uint8Array(padding));
11117
+ total += padding;
11118
+ }
11119
+ }
11120
+ }
11121
+ chunks.push(new Uint8Array(BLOCK_SIZE * 2));
11122
+ const tar = new Uint8Array(total);
11123
+ let offset = 0;
11124
+ for (const chunk of chunks) {
11125
+ tar.set(chunk, offset);
11126
+ offset += chunk.byteLength;
11127
+ }
11128
+ const compressed = Uint8Array.from(gzipSync(tar, { level: 9 }));
11129
+ if (compressed.byteLength > 9)
11130
+ compressed[9] = 255;
11131
+ return compressed;
11132
+ }
11133
+ // ../core/src/exit-codes.ts
11134
+ var EXIT_CODES = {
11135
+ OK: 0,
11136
+ GENERIC: 1,
11137
+ VERIFIED_ONLY: 42,
11138
+ VERIFICATION_WARNING: 43,
11139
+ TEAM_POLICY_BLOCK: 44,
11140
+ RATE_LIMITED: 45
11141
+ };
10946
11142
  // ../core/src/manifest.ts
11143
+ import { createHash } from "node:crypto";
10947
11144
  var manifestSchema = exports_external.object({
10948
11145
  skill: exports_external.object({
10949
11146
  slug: exports_external.string().min(1),
@@ -10982,6 +11179,51 @@ function parseSkillSlug(input) {
10982
11179
  });
10983
11180
  return parsed.success ? parsed.data : null;
10984
11181
  }
11182
+
11183
+ // ../core/src/skill-ref.ts
11184
+ var PRIVATE_NAME = /^(?!-)(?!.*--)[a-z0-9]+(?:-[a-z0-9]+)*$/;
11185
+ var TEAM_SLUG = /^(?!-)(?!.*--)[a-z0-9]+(?:-[a-z0-9]+)*$/;
11186
+ var PERSONAL_NAMESPACE = "me";
11187
+ var RESERVED_NAMESPACES = new Set([
11188
+ PERSONAL_NAMESPACE,
11189
+ "admin",
11190
+ "api",
11191
+ "chops",
11192
+ "docs",
11193
+ "invites",
11194
+ "skills",
11195
+ "teams",
11196
+ "www"
11197
+ ]);
11198
+ function parseSkillRef(input) {
11199
+ const trimmed = input.trim();
11200
+ if (trimmed.startsWith("@")) {
11201
+ const match = /^@([^/@\s]+)\/([^/@\s]+)$/.exec(trimmed);
11202
+ if (!match)
11203
+ return null;
11204
+ const namespace = match[1];
11205
+ const name = match[2];
11206
+ if (!PRIVATE_NAME.test(name))
11207
+ return null;
11208
+ if (namespace === PERSONAL_NAMESPACE)
11209
+ return { kind: "personal", name };
11210
+ if (!TEAM_SLUG.test(namespace))
11211
+ return null;
11212
+ return { kind: "team", team: namespace, name };
11213
+ }
11214
+ const slug = parseSkillSlug(trimmed);
11215
+ return slug ? { kind: "public", ...slug } : null;
11216
+ }
11217
+ function formatSkillRef(ref) {
11218
+ switch (ref.kind) {
11219
+ case "public":
11220
+ return `${ref.owner}/${ref.repo}@${ref.skill}`;
11221
+ case "personal":
11222
+ return `@${PERSONAL_NAMESPACE}/${ref.name}`;
11223
+ case "team":
11224
+ return `@${ref.team}/${ref.name}`;
11225
+ }
11226
+ }
10985
11227
  // ../core/src/search.ts
10986
11228
  var skillSummarySchema = exports_external.object({
10987
11229
  slug: exports_external.string().min(1),
@@ -11066,17 +11308,562 @@ var frontmatterSchema = exports_external.object({
11066
11308
  metadata: exports_external.record(exports_external.string()).optional(),
11067
11309
  "allowed-tools": exports_external.string().trim().min(1).optional()
11068
11310
  }).passthrough();
11311
+ function splitFrontmatter(input) {
11312
+ const text = input.startsWith("\uFEFF") ? input.slice(1) : input;
11313
+ const lines = text.split(/\r?\n/);
11314
+ if (lines[0]?.trim() !== "---") {
11315
+ return {
11316
+ ok: false,
11317
+ error: {
11318
+ code: "missing-frontmatter",
11319
+ message: "SKILL.md must start with a YAML frontmatter delimiter"
11320
+ }
11321
+ };
11322
+ }
11323
+ const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
11324
+ if (closing < 0) {
11325
+ return {
11326
+ ok: false,
11327
+ error: {
11328
+ code: "unterminated-frontmatter",
11329
+ message: "SKILL.md frontmatter is missing its closing delimiter"
11330
+ }
11331
+ };
11332
+ }
11333
+ return {
11334
+ ok: true,
11335
+ yaml: lines.slice(1, closing).join(`
11336
+ `),
11337
+ body: lines.slice(closing + 1).join(`
11338
+ `).trim()
11339
+ };
11340
+ }
11341
+ function parseSkillDocument(input, options = {}) {
11342
+ const split = splitFrontmatter(input);
11343
+ if (!split.ok)
11344
+ return { ok: false, errors: [split.error] };
11345
+ const document = $parseDocument(split.yaml, {
11346
+ prettyErrors: false,
11347
+ uniqueKeys: true
11348
+ });
11349
+ if (document.errors.length > 0) {
11350
+ return {
11351
+ ok: false,
11352
+ errors: document.errors.map((error) => ({
11353
+ code: "invalid-yaml",
11354
+ message: error.message
11355
+ }))
11356
+ };
11357
+ }
11358
+ let raw;
11359
+ try {
11360
+ raw = document.toJS({ maxAliasCount: 0 });
11361
+ } catch (error) {
11362
+ return {
11363
+ ok: false,
11364
+ errors: [{ code: "invalid-yaml", message: error.message }]
11365
+ };
11366
+ }
11367
+ const parsed = frontmatterSchema.safeParse(raw);
11368
+ if (!parsed.success) {
11369
+ return {
11370
+ ok: false,
11371
+ errors: parsed.error.issues.map((issue) => ({
11372
+ code: "invalid-frontmatter",
11373
+ message: `${issue.path.join(".") || "frontmatter"}: ${issue.message}`
11374
+ }))
11375
+ };
11376
+ }
11377
+ if (options.directoryName && parsed.data.name !== options.directoryName) {
11378
+ return {
11379
+ ok: false,
11380
+ errors: [
11381
+ {
11382
+ code: "name-directory-mismatch",
11383
+ message: `name "${parsed.data.name}" must match directory "${options.directoryName}"`
11384
+ }
11385
+ ]
11386
+ };
11387
+ }
11388
+ return { ok: true, value: { frontmatter: parsed.data, body: split.body } };
11389
+ }
11390
+ // src/credentials.ts
11391
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
11392
+ import { dirname } from "node:path";
11393
+ var emptyCredentials = (apiUrl) => ({ version: 1, apiUrl });
11394
+ async function writePrivateJson(path, value) {
11395
+ await mkdir(dirname(path), { recursive: true });
11396
+ const temporary = `${path}.tmp-${crypto.randomUUID()}`;
11397
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}
11398
+ `, { flag: "wx", mode: 384 });
11399
+ await rename(temporary, path);
11400
+ }
11401
+ async function readCredentials(paths, apiUrl) {
11402
+ try {
11403
+ const parsed = JSON.parse(await readFile(paths.credentials, "utf8"));
11404
+ if (parsed.version !== 1 || typeof parsed.apiUrl !== "string")
11405
+ return emptyCredentials(apiUrl);
11406
+ if (parsed.apiUrl !== apiUrl)
11407
+ return emptyCredentials(apiUrl);
11408
+ return parsed;
11409
+ } catch {
11410
+ return emptyCredentials(apiUrl);
11411
+ }
11412
+ }
11413
+ async function writeCredentials(paths, creds) {
11414
+ await writePrivateJson(paths.credentials, creds);
11415
+ }
11416
+ async function clearAuth(paths, apiUrl) {
11417
+ const creds = await readCredentials(paths, apiUrl);
11418
+ delete creds.auth;
11419
+ await writeCredentials(paths, creds);
11420
+ }
11421
+ async function ensureDeviceId(paths) {
11422
+ try {
11423
+ const parsed = JSON.parse(await readFile(paths.device, "utf8"));
11424
+ if (typeof parsed.deviceId === "string" && parsed.deviceId)
11425
+ return parsed.deviceId;
11426
+ } catch {}
11427
+ const identity2 = {
11428
+ deviceId: crypto.randomUUID(),
11429
+ createdAt: new Date().toISOString()
11430
+ };
11431
+ await writePrivateJson(paths.device, identity2);
11432
+ return identity2.deviceId;
11433
+ }
11434
+
11435
+ // src/client.ts
11436
+ class ApiError extends Error {
11437
+ code;
11438
+ exitCode;
11439
+ lift;
11440
+ retryAfter;
11441
+ constructor(code, message, options) {
11442
+ super(message);
11443
+ this.code = code;
11444
+ this.exitCode = exitCodeFor(code);
11445
+ this.lift = options?.lift;
11446
+ this.retryAfter = options?.retryAfter;
11447
+ }
11448
+ }
11449
+ function exitCodeFor(code) {
11450
+ if (code === "verified_only" || code === "auth_required")
11451
+ return EXIT_CODES.VERIFIED_ONLY;
11452
+ if (code.startsWith("team_policy"))
11453
+ return EXIT_CODES.TEAM_POLICY_BLOCK;
11454
+ if (code === "rate_limited")
11455
+ return EXIT_CODES.RATE_LIMITED;
11456
+ return EXIT_CODES.GENERIC;
11457
+ }
11458
+ var REFRESH_SKEW_MS = 60000;
11459
+ async function refreshAuth(ctx, auth) {
11460
+ const doFetch = ctx.fetchImpl ?? fetch;
11461
+ const response = await doFetch(`${ctx.apiUrl}/auth/token/refresh`, {
11462
+ method: "POST",
11463
+ headers: { "content-type": "application/json" },
11464
+ body: JSON.stringify({ refresh_token: auth.refreshToken })
11465
+ });
11466
+ if (!response.ok) {
11467
+ await clearAuth(ctx.paths, ctx.apiUrl);
11468
+ return null;
11469
+ }
11470
+ const body = await response.json();
11471
+ const rotated = {
11472
+ ...auth,
11473
+ ...body.user ? { userId: body.user.id, email: body.user.email, handle: body.user.handle } : {},
11474
+ accessJwt: body.access_jwt,
11475
+ refreshToken: body.refresh_token,
11476
+ accessExpiresAt: new Date(Date.now() + body.expires_in * 1000).toISOString()
11477
+ };
11478
+ const creds = await readCredentials(ctx.paths, ctx.apiUrl);
11479
+ creds.auth = rotated;
11480
+ await writeCredentials(ctx.paths, creds);
11481
+ return rotated;
11482
+ }
11483
+ async function currentAuth(ctx) {
11484
+ const creds = await readCredentials(ctx.paths, ctx.apiUrl);
11485
+ if (!creds.auth)
11486
+ return null;
11487
+ const expiresAt = Date.parse(creds.auth.accessExpiresAt);
11488
+ if (Number.isFinite(expiresAt) && expiresAt - Date.now() > REFRESH_SKEW_MS)
11489
+ return creds.auth;
11490
+ return refreshAuth(ctx, creds.auth);
11491
+ }
11492
+ var signedOutError = () => new ApiError("auth_required", "not signed in — run: chops auth start <email>", {
11493
+ lift: { action: "verify_email", hint: "chops auth start <email>" }
11494
+ });
11495
+ async function apiFetch(ctx, path, init = {}, options = {}) {
11496
+ const doFetch = ctx.fetchImpl ?? fetch;
11497
+ const authMode = options.auth ?? "none";
11498
+ const deviceId = await ensureDeviceId(ctx.paths);
11499
+ let auth = authMode === "none" ? null : await currentAuth(ctx);
11500
+ if (authMode === "required" && !auth)
11501
+ throw signedOutError();
11502
+ const send = (token) => {
11503
+ const headers = new Headers(init.headers);
11504
+ headers.set("x-chops-device", deviceId);
11505
+ if (!headers.has("content-type") && typeof init.body === "string") {
11506
+ headers.set("content-type", "application/json");
11507
+ }
11508
+ if (token)
11509
+ headers.set("authorization", `Bearer ${token}`);
11510
+ return doFetch(`${ctx.apiUrl}${path}`, { ...init, headers });
11511
+ };
11512
+ let response = await send(auth?.accessJwt ?? null);
11513
+ if (response.status === 401 && auth) {
11514
+ auth = await refreshAuth(ctx, auth);
11515
+ if (!auth) {
11516
+ if (authMode === "required")
11517
+ throw signedOutError();
11518
+ } else {
11519
+ response = await send(auth.accessJwt);
11520
+ }
11521
+ }
11522
+ if (!response.ok)
11523
+ throw await toApiError(response);
11524
+ return response;
11525
+ }
11526
+ async function toApiError(response) {
11527
+ const fallback = new ApiError("failed", `request failed (${response.status})`);
11528
+ try {
11529
+ const parsed = apiErrorSchema.safeParse(await response.json());
11530
+ if (!parsed.success)
11531
+ return fallback;
11532
+ const { code, message, lift, retry_after } = parsed.data.error;
11533
+ return new ApiError(code, message, {
11534
+ ...lift ? { lift } : {},
11535
+ ...retry_after !== undefined ? { retryAfter: retry_after } : {}
11536
+ });
11537
+ } catch {
11538
+ return fallback;
11539
+ }
11540
+ }
11541
+
11542
+ // src/commands/auth.ts
11543
+ async function authCommand(ctx, io, sub, positionals, flags) {
11544
+ if (sub === "start")
11545
+ return authStart(ctx, io, positionals[0]);
11546
+ if (sub === "verify")
11547
+ return authVerify(ctx, io, positionals[0], flags.email);
11548
+ if (sub === "status")
11549
+ return authStatus(ctx, io);
11550
+ if (sub === "logout")
11551
+ return authLogout(ctx, io);
11552
+ throw new Error("usage: chops auth <start|verify|status|logout>");
11553
+ }
11554
+ async function authStart(ctx, io, email) {
11555
+ if (!email)
11556
+ throw new Error("usage: chops auth start <email>");
11557
+ const response = await apiFetch(ctx, "/auth/otp/start", {
11558
+ method: "POST",
11559
+ body: JSON.stringify({ email })
11560
+ });
11561
+ const body = await response.json();
11562
+ const creds = await readCredentials(ctx.paths, ctx.apiUrl);
11563
+ creds.pendingAuth = { email: email.trim().toLowerCase(), startedAt: new Date().toISOString() };
11564
+ await writeCredentials(ctx.paths, creds);
11565
+ if (io.json) {
11566
+ io.emit({
11567
+ ok: true,
11568
+ email: creds.pendingAuth.email,
11569
+ expires_in: 600,
11570
+ next: "chops auth verify <code>",
11571
+ ...body.dev_code ? { dev_code: body.dev_code } : {}
11572
+ });
11573
+ } else {
11574
+ io.emit(`Code sent to ${creds.pendingAuth.email} (expires in 10 minutes).
11575
+ Run: chops auth verify <code>`);
11576
+ if (body.dev_code)
11577
+ io.emit(`dev code: ${body.dev_code}`);
11578
+ }
11579
+ return EXIT_CODES.OK;
11580
+ }
11581
+ async function authVerify(ctx, io, code, emailFlag) {
11582
+ if (!code)
11583
+ throw new Error("usage: chops auth verify <code>");
11584
+ const creds = await readCredentials(ctx.paths, ctx.apiUrl);
11585
+ const email = emailFlag?.trim().toLowerCase() ?? creds.pendingAuth?.email;
11586
+ if (!email) {
11587
+ throw new Error("no pending sign-in — run: chops auth start <email> (or pass --email)");
11588
+ }
11589
+ const response = await apiFetch(ctx, "/auth/otp/verify", {
11590
+ method: "POST",
11591
+ body: JSON.stringify({ email, code, scope: "cli" })
11592
+ });
11593
+ const body = await response.json();
11594
+ creds.auth = {
11595
+ email: body.user.email,
11596
+ userId: body.user.id,
11597
+ handle: body.user.handle,
11598
+ accessJwt: body.access_jwt,
11599
+ refreshToken: body.refresh_token,
11600
+ accessExpiresAt: new Date(Date.now() + body.expires_in * 1000).toISOString()
11601
+ };
11602
+ delete creds.pendingAuth;
11603
+ await writeCredentials(ctx.paths, creds);
11604
+ if (io.json)
11605
+ io.emit({ ok: true, user: body.user });
11606
+ else
11607
+ io.emit(`Verified as ${body.user.email}.`);
11608
+ return EXIT_CODES.OK;
11609
+ }
11610
+ async function authStatus(ctx, io) {
11611
+ const creds = await readCredentials(ctx.paths, ctx.apiUrl);
11612
+ if (!creds.auth) {
11613
+ if (io.json)
11614
+ io.emit({ authenticated: false, api_url: ctx.apiUrl });
11615
+ else
11616
+ io.emit("Not signed in. Run: chops auth start <email>");
11617
+ return EXIT_CODES.OK;
11618
+ }
11619
+ if (io.json) {
11620
+ io.emit({
11621
+ authenticated: true,
11622
+ email: creds.auth.email,
11623
+ user_id: creds.auth.userId,
11624
+ handle: creds.auth.handle,
11625
+ access_expires_at: creds.auth.accessExpiresAt,
11626
+ api_url: ctx.apiUrl
11627
+ });
11628
+ } else {
11629
+ io.emit(`Signed in as ${creds.auth.email} (${ctx.apiUrl})`);
11630
+ }
11631
+ return EXIT_CODES.OK;
11632
+ }
11633
+ async function authLogout(ctx, io) {
11634
+ const auth = await currentAuth(ctx).catch(() => null);
11635
+ let revoked = false;
11636
+ if (auth) {
11637
+ try {
11638
+ await Promise.race([
11639
+ apiFetch(ctx, "/auth/revoke", { method: "POST", body: JSON.stringify({ refresh_token: auth.refreshToken }) }, { auth: "required" }),
11640
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 2000))
11641
+ ]);
11642
+ revoked = true;
11643
+ } catch {
11644
+ revoked = false;
11645
+ }
11646
+ }
11647
+ await clearAuth(ctx.paths, ctx.apiUrl);
11648
+ if (io.json)
11649
+ io.emit({ ok: true, revoked });
11650
+ else
11651
+ io.emit("Signed out.");
11652
+ return EXIT_CODES.OK;
11653
+ }
11654
+
11655
+ // src/commands/publish.ts
11656
+ import { lstat, readdir, readFile as readFile2 } from "node:fs/promises";
11657
+ import { basename, join, relative, resolve } from "node:path";
11658
+
11659
+ // src/commands/team.ts
11660
+ async function teamCommand(ctx, io, sub, positionals, flags) {
11661
+ if (sub === "create")
11662
+ return teamCreate(ctx, io, positionals[0], flags);
11663
+ if (sub === "list")
11664
+ return teamList(ctx, io);
11665
+ if (sub === "invite")
11666
+ return teamInvite(ctx, io, positionals, flags);
11667
+ if (sub === "join")
11668
+ return teamJoin(ctx, io, positionals[0], flags);
11669
+ if (sub === "members")
11670
+ return teamMembers(ctx, io, flags);
11671
+ throw new Error("usage: chops team <create|list|invite|join|members>");
11672
+ }
11673
+ async function myTeams(ctx) {
11674
+ const response = await apiFetch(ctx, "/me/teams", {}, { auth: "required" });
11675
+ return meTeamsResponseSchema.parse(await response.json()).teams;
11676
+ }
11677
+ async function resolveTeamSlug(ctx, flag) {
11678
+ if (flag)
11679
+ return flag.toLowerCase();
11680
+ const teams = await myTeams(ctx);
11681
+ if (teams.length === 1)
11682
+ return teams[0].slug;
11683
+ if (teams.length === 0)
11684
+ throw new Error("you are not in any team — run: chops team create <slug>");
11685
+ throw new Error(`you are in several teams — pass --team <slug> (${teams.map((t) => t.slug).join(", ")})`);
11686
+ }
11687
+ async function teamCreate(ctx, io, slug, flags) {
11688
+ if (!slug)
11689
+ throw new Error("usage: chops team create <slug> [--name <display name>]");
11690
+ const response = await apiFetch(ctx, "/teams", { method: "POST", body: JSON.stringify({ slug, ...flags.name ? { name: flags.name } : {} }) }, { auth: "required" });
11691
+ const { team } = createTeamResponseSchema.parse(await response.json());
11692
+ if (io.json)
11693
+ io.emit({ ok: true, team });
11694
+ else {
11695
+ io.emit(`Team ${team.slug} ready (${team.name}). You are the owner.`);
11696
+ io.emit(`Invite teammates with: chops team invite <email> --team ${team.slug}`);
11697
+ }
11698
+ return EXIT_CODES.OK;
11699
+ }
11700
+ async function teamList(ctx, io) {
11701
+ const teams = await myTeams(ctx);
11702
+ if (io.json)
11703
+ io.emit({ teams });
11704
+ else if (teams.length === 0)
11705
+ io.emit("No teams yet. Run: chops team create <slug>");
11706
+ else {
11707
+ for (const team of teams) {
11708
+ io.emit(`${team.slug.padEnd(20)} ${team.role.padEnd(8)} ${team.member_count} member(s) ${team.name}`);
11709
+ }
11710
+ }
11711
+ return EXIT_CODES.OK;
11712
+ }
11713
+ async function teamInvite(ctx, io, emails, flags) {
11714
+ if (emails.length === 0) {
11715
+ throw new Error("usage: chops team invite <email> [<email>...] [--team <slug>] [--role member|admin]");
11716
+ }
11717
+ const role = flags.role ?? "member";
11718
+ if (role !== "member" && role !== "admin")
11719
+ throw new Error("--role must be member or admin");
11720
+ const slug = await resolveTeamSlug(ctx, flags.team);
11721
+ const response = await apiFetch(ctx, `/teams/${encodeURIComponent(slug)}/invites`, { method: "POST", body: JSON.stringify({ emails, role }) }, { auth: "required" });
11722
+ const { results } = sendInvitesResponseSchema.parse(await response.json());
11723
+ if (io.json)
11724
+ io.emit({ ok: true, team: slug, invites: results });
11725
+ else {
11726
+ for (const result of results) {
11727
+ io.emit(result.status === "sent" ? `Invited ${result.email} to ${slug} (expires ${result.expires_at ?? "in 7 days"}).` : `${result.email}: ${result.status.replaceAll("_", " ")}`);
11728
+ }
11729
+ }
11730
+ return EXIT_CODES.OK;
11731
+ }
11732
+ async function teamJoin(ctx, io, token, flags) {
11733
+ let body;
11734
+ if (token) {
11735
+ body = { token };
11736
+ } else {
11737
+ const response2 = await apiFetch(ctx, "/me/invites", {}, { auth: "required" });
11738
+ const { invites } = meInvitesResponseSchema.parse(await response2.json());
11739
+ const candidates = flags.team ? invites.filter((invite) => invite.team.slug === flags.team.toLowerCase()) : invites;
11740
+ if (candidates.length === 0) {
11741
+ throw new Error(flags.team ? `no pending invite for team ${flags.team}` : "no pending invites for your email");
11742
+ }
11743
+ if (candidates.length > 1) {
11744
+ throw new Error(`several pending invites — pass --team <slug> (${candidates.map((i) => i.team.slug).join(", ")})`);
11745
+ }
11746
+ body = { invite_id: candidates[0].id };
11747
+ }
11748
+ const response = await apiFetch(ctx, "/invites/accept", { method: "POST", body: JSON.stringify(body) }, { auth: "required" });
11749
+ const accepted = acceptInviteResponseSchema.parse(await response.json());
11750
+ if (io.json)
11751
+ io.emit({ ok: true, team: accepted.team, role: accepted.role });
11752
+ else
11753
+ io.emit(`Joined ${accepted.team.name} (${accepted.team.slug}) as ${accepted.role}.`);
11754
+ return EXIT_CODES.OK;
11755
+ }
11756
+ async function teamMembers(ctx, io, flags) {
11757
+ const slug = await resolveTeamSlug(ctx, flags.team);
11758
+ const response = await apiFetch(ctx, `/teams/${encodeURIComponent(slug)}/members`, {}, { auth: "required" });
11759
+ const parsed = teamMembersResponseSchema.parse(await response.json());
11760
+ if (io.json)
11761
+ io.emit({ team: slug, ...parsed });
11762
+ else {
11763
+ for (const member of parsed.members) {
11764
+ io.emit(`${member.email.padEnd(36)} ${member.role.padEnd(8)} joined ${member.joined_at.slice(0, 10)}`);
11765
+ }
11766
+ for (const invite of parsed.invites ?? []) {
11767
+ io.emit(`${invite.email.padEnd(36)} ${invite.role.padEnd(8)} invited (expires ${invite.expires_at.slice(0, 10)})`);
11768
+ }
11769
+ }
11770
+ return EXIT_CODES.OK;
11771
+ }
11772
+
11773
+ // src/commands/publish.ts
11774
+ var MAX_ENTRIES = 2000;
11775
+ var MAX_TOTAL_BYTES = 25 * 1024 * 1024;
11776
+ var EXCLUDED_DIRS = new Set([".git", "node_modules"]);
11777
+ async function packSkillDirectory(path) {
11778
+ const root = resolve(path);
11779
+ const skillPath = join(root, "SKILL.md");
11780
+ const content = await readFile2(skillPath, "utf8").catch(() => {
11781
+ throw new Error(`no SKILL.md found in ${root}`);
11782
+ });
11783
+ const document = parseSkillDocument(content, { directoryName: basename(root) });
11784
+ if (!document.ok) {
11785
+ throw new Error(`invalid SKILL.md:
11786
+ ${document.errors.map((e) => ` - ${e.message}`).join(`
11787
+ `)}`);
11788
+ }
11789
+ const entries = [];
11790
+ let totalBytes = 0;
11791
+ const walk = async (directory) => {
11792
+ for (const dirent of await readdir(directory, { withFileTypes: true })) {
11793
+ const absolute = join(directory, dirent.name);
11794
+ if (dirent.isSymbolicLink()) {
11795
+ throw new Error(`symlinks are not allowed in skills: ${relative(root, absolute)}`);
11796
+ }
11797
+ if (dirent.isDirectory()) {
11798
+ if (!EXCLUDED_DIRS.has(dirent.name))
11799
+ await walk(absolute);
11800
+ continue;
11801
+ }
11802
+ if (!dirent.isFile())
11803
+ continue;
11804
+ const data = new Uint8Array(await readFile2(absolute));
11805
+ totalBytes += data.byteLength;
11806
+ if (entries.length + 1 > MAX_ENTRIES)
11807
+ throw new Error("skill has too many files (max 2000)");
11808
+ if (totalBytes > MAX_TOTAL_BYTES)
11809
+ throw new Error("skill is too large (max 25 MiB)");
11810
+ const stats = await lstat(absolute);
11811
+ entries.push({
11812
+ path: relative(root, absolute).replaceAll("\\", "/"),
11813
+ data,
11814
+ ...(stats.mode & 73) !== 0 ? { executable: true } : {}
11815
+ });
11816
+ }
11817
+ };
11818
+ await walk(root);
11819
+ const tarball = buildTarGz(entries);
11820
+ return {
11821
+ name: document.value.frontmatter.name,
11822
+ description: document.value.frontmatter.description,
11823
+ tarball,
11824
+ contentSha: tarballSha256(tarball)
11825
+ };
11826
+ }
11827
+ async function publishCommand(ctx, io, path, flags) {
11828
+ if (!path)
11829
+ throw new Error("usage: chops publish <path> [--team <slug>]");
11830
+ const packed = await packSkillDirectory(path);
11831
+ const teamSlug = flags.team ? await resolveTeamSlug(ctx, flags.team) : null;
11832
+ const endpoint = teamSlug ? `/teams/${encodeURIComponent(teamSlug)}/skills` : "/me/skills";
11833
+ const form = new FormData;
11834
+ form.set("meta", JSON.stringify({
11835
+ name: packed.name,
11836
+ description: packed.description,
11837
+ content_sha: packed.contentSha,
11838
+ size_bytes: packed.tarball.byteLength
11839
+ }));
11840
+ form.set("tarball", new File([packed.tarball.slice().buffer], `${packed.name}.tgz`, {
11841
+ type: "application/gzip"
11842
+ }));
11843
+ const response = await apiFetch(ctx, endpoint, { method: "POST", body: form }, { auth: "required" });
11844
+ const result = privatePublishResponseSchema.parse(await response.json());
11845
+ if (io.json) {
11846
+ io.emit({ ...result, ok: true, install_cmd: `chops add ${result.ref}` });
11847
+ } else if (result.dedup) {
11848
+ io.emit(`Already published — ${result.ref} v${result.version} matches this content.`);
11849
+ } else {
11850
+ io.emit(`Published ${result.ref} v${result.version} (sha ${result.content_sha.slice(0, 7)}).`);
11851
+ io.emit(teamSlug ? `Team members install it with: chops add ${result.ref}` : `Install it anywhere you're signed in with: chops add ${result.ref}`);
11852
+ }
11853
+ return EXIT_CODES.OK;
11854
+ }
11855
+
11069
11856
  // src/install.ts
11070
- import { lstat, mkdir as mkdir2, readFile, rename, rm, symlink as symlink2, writeFile as writeFile2 } from "node:fs/promises";
11071
- import { dirname as dirname2, join } from "node:path";
11857
+ import { lstat as lstat2, mkdir as mkdir3, readFile as readFile3, rename as rename2, rm, symlink as symlink2, writeFile as writeFile3 } from "node:fs/promises";
11858
+ import { dirname as dirname3, join as join2 } from "node:path";
11072
11859
 
11073
11860
  // src/archive.ts
11074
- import { mkdir, chmod, symlink, writeFile } from "node:fs/promises";
11075
- import { dirname, isAbsolute, posix, resolve, sep } from "node:path";
11861
+ import { mkdir as mkdir2, chmod, symlink, writeFile as writeFile2 } from "node:fs/promises";
11862
+ import { dirname as dirname2, isAbsolute, posix, resolve as resolve2, sep } from "node:path";
11076
11863
  import { gunzipSync } from "node:zlib";
11077
11864
  var BLOCK = 512;
11078
11865
  var MAX_UNCOMPRESSED_BYTES = 25 * 1024 * 1024;
11079
- var MAX_ENTRIES = 2000;
11866
+ var MAX_ENTRIES2 = 2000;
11080
11867
  function text(bytes) {
11081
11868
  const zero = bytes.indexOf(0);
11082
11869
  return new TextDecoder().decode(zero >= 0 ? bytes.slice(0, zero) : bytes).trim();
@@ -11094,17 +11881,17 @@ function parseTar(archive) {
11094
11881
  throw new Error("archive expands past 25 MiB");
11095
11882
  const entries = [];
11096
11883
  for (let offset = 0;offset + BLOCK <= tar.byteLength; ) {
11097
- const header = tar.slice(offset, offset + BLOCK);
11098
- if (header.every((byte) => byte === 0))
11884
+ const header2 = tar.slice(offset, offset + BLOCK);
11885
+ if (header2.every((byte) => byte === 0))
11099
11886
  break;
11100
- const name = text(header.slice(0, 100));
11101
- const prefix = text(header.slice(345, 500));
11887
+ const name = text(header2.slice(0, 100));
11888
+ const prefix = text(header2.slice(345, 500));
11102
11889
  const path = safePath(prefix ? `${prefix}/${name}` : name);
11103
- const mode = Number.parseInt(text(header.slice(100, 108)) || "644", 8);
11104
- const size = Number.parseInt(text(header.slice(124, 136)) || "0", 8);
11890
+ const mode = Number.parseInt(text(header2.slice(100, 108)) || "644", 8);
11891
+ const size = Number.parseInt(text(header2.slice(124, 136)) || "0", 8);
11105
11892
  if (!Number.isSafeInteger(size) || size < 0)
11106
11893
  throw new Error(`invalid tar size for ${path}`);
11107
- const typeFlag = text(header.slice(156, 157)) || "0";
11894
+ const typeFlag = text(header2.slice(156, 157)) || "0";
11108
11895
  offset += BLOCK;
11109
11896
  if (offset + size > tar.byteLength)
11110
11897
  throw new Error(`truncated tar entry: ${path}`);
@@ -11115,7 +11902,7 @@ function parseTar(archive) {
11115
11902
  else if (typeFlag === "5")
11116
11903
  entries.push({ path, type: "directory", mode, data });
11117
11904
  else if (typeFlag === "2") {
11118
- const linkTarget = text(header.slice(157, 257));
11905
+ const linkTarget = text(header2.slice(157, 257));
11119
11906
  const resolved = posix.normalize(posix.join(posix.dirname(path), linkTarget));
11120
11907
  if (!linkTarget || linkTarget.startsWith("/") || resolved === ".." || resolved.startsWith("../")) {
11121
11908
  throw new Error(`unsafe symlink in archive: ${path}`);
@@ -11124,35 +11911,35 @@ function parseTar(archive) {
11124
11911
  } else {
11125
11912
  throw new Error(`unsupported tar entry type ${typeFlag} for ${path}`);
11126
11913
  }
11127
- if (entries.length > MAX_ENTRIES)
11914
+ if (entries.length > MAX_ENTRIES2)
11128
11915
  throw new Error("archive contains too many entries");
11129
11916
  }
11130
11917
  return entries;
11131
11918
  }
11132
11919
  function destinationPath(root, path) {
11133
- const target = resolve(root, ...path.split("/"));
11920
+ const target = resolve2(root, ...path.split("/"));
11134
11921
  const prefix = root.endsWith(sep) ? root : `${root}${sep}`;
11135
11922
  if (target !== root && !target.startsWith(prefix))
11136
11923
  throw new Error(`archive path escapes root: ${path}`);
11137
11924
  return target;
11138
11925
  }
11139
11926
  async function extractTarGz(archive, destination) {
11140
- const root = resolve(destination);
11927
+ const root = resolve2(destination);
11141
11928
  const entries = parseTar(archive);
11142
- await mkdir(root, { recursive: true });
11929
+ await mkdir2(root, { recursive: true });
11143
11930
  for (const entry of entries.filter((item) => item.type !== "symlink")) {
11144
11931
  const target = destinationPath(root, entry.path);
11145
11932
  if (entry.type === "directory") {
11146
- await mkdir(target, { recursive: true });
11933
+ await mkdir2(target, { recursive: true });
11147
11934
  continue;
11148
11935
  }
11149
- await mkdir(dirname(target), { recursive: true });
11150
- await writeFile(target, entry.data, { flag: "wx" });
11936
+ await mkdir2(dirname2(target), { recursive: true });
11937
+ await writeFile2(target, entry.data, { flag: "wx" });
11151
11938
  await chmod(target, entry.mode & 73 ? 493 : 420);
11152
11939
  }
11153
11940
  for (const entry of entries.filter((item) => item.type === "symlink")) {
11154
11941
  const target = destinationPath(root, entry.path);
11155
- await mkdir(dirname(target), { recursive: true });
11942
+ await mkdir2(dirname2(target), { recursive: true });
11156
11943
  await symlink(entry.linkTarget, target);
11157
11944
  }
11158
11945
  }
@@ -11161,12 +11948,19 @@ async function extractTarGz(archive, destination) {
11161
11948
  function chopsPaths(home = process.env.HOME) {
11162
11949
  if (!home)
11163
11950
  throw new Error("HOME is not set");
11164
- const root = join(home, ".chops");
11165
- return { home, root, store: join(root, "store"), lockfile: join(root, "chops.lock") };
11951
+ const root = join2(home, ".chops");
11952
+ return {
11953
+ home,
11954
+ root,
11955
+ store: join2(root, "store"),
11956
+ lockfile: join2(root, "chops.lock"),
11957
+ credentials: join2(root, "credentials.json"),
11958
+ device: join2(root, "device.json")
11959
+ };
11166
11960
  }
11167
11961
  async function exists(path) {
11168
11962
  try {
11169
- await lstat(path);
11963
+ await lstat2(path);
11170
11964
  return true;
11171
11965
  } catch (error) {
11172
11966
  if (error.code === "ENOENT")
@@ -11176,7 +11970,7 @@ async function exists(path) {
11176
11970
  }
11177
11971
  async function readLockfile(paths) {
11178
11972
  try {
11179
- const parsed = JSON.parse(await readFile(paths.lockfile, "utf8"));
11973
+ const parsed = JSON.parse(await readFile3(paths.lockfile, "utf8"));
11180
11974
  if (parsed.version !== 1 || !parsed.skills || typeof parsed.skills !== "object") {
11181
11975
  throw new Error("unsupported lockfile format");
11182
11976
  }
@@ -11188,23 +11982,23 @@ async function readLockfile(paths) {
11188
11982
  }
11189
11983
  }
11190
11984
  async function writeLockfile(paths, lockfile) {
11191
- await mkdir2(dirname2(paths.lockfile), { recursive: true });
11985
+ await mkdir3(dirname3(paths.lockfile), { recursive: true });
11192
11986
  const temporary = `${paths.lockfile}.tmp-${crypto.randomUUID()}`;
11193
- await writeFile2(temporary, `${JSON.stringify(lockfile, null, 2)}
11987
+ await writeFile3(temporary, `${JSON.stringify(lockfile, null, 2)}
11194
11988
  `, { flag: "wx", mode: 384 });
11195
- await rename(temporary, paths.lockfile);
11989
+ await rename2(temporary, paths.lockfile);
11196
11990
  }
11197
11991
  async function agentTargets(paths, explicit = []) {
11198
11992
  const requested = new Set(explicit.map((agent) => agent.toLowerCase()));
11199
11993
  const targets = [];
11200
11994
  if (requested.size === 0 || requested.has("codex") || requested.has("agents")) {
11201
- targets.push({ agent: "agents", directory: join(paths.home, ".agents", "skills") });
11995
+ targets.push({ agent: "agents", directory: join2(paths.home, ".agents", "skills") });
11202
11996
  }
11203
- if (requested.has("claude-code") || requested.size === 0 && await exists(join(paths.home, ".claude"))) {
11204
- targets.push({ agent: "claude-code", directory: join(paths.home, ".claude", "skills") });
11997
+ if (requested.has("claude-code") || requested.size === 0 && await exists(join2(paths.home, ".claude"))) {
11998
+ targets.push({ agent: "claude-code", directory: join2(paths.home, ".claude", "skills") });
11205
11999
  }
11206
12000
  if (requested.has("cursor")) {
11207
- targets.push({ agent: "cursor", directory: join(paths.home, ".cursor", "skills") });
12001
+ targets.push({ agent: "cursor", directory: join2(paths.home, ".cursor", "skills") });
11208
12002
  }
11209
12003
  if (targets.length === 0)
11210
12004
  throw new Error("no supported agent targets were selected");
@@ -11217,24 +12011,36 @@ async function fetchManifest(apiUrl, slug) {
11217
12011
  throw new Error(`manifest request failed (${response.status})`);
11218
12012
  return manifestSchema.parse(payload);
11219
12013
  }
12014
+ function storePathFor(paths, ref, sha) {
12015
+ switch (ref.kind) {
12016
+ case "public":
12017
+ return join2(paths.store, ref.owner, ref.repo, `${ref.skill}@${sha}`);
12018
+ case "personal":
12019
+ return join2(paths.store, "_me", `${ref.name}@${sha}`);
12020
+ case "team":
12021
+ return join2(paths.store, "_teams", ref.team, `${ref.name}@${sha}`);
12022
+ }
12023
+ }
11220
12024
  async function installSkill(options) {
11221
12025
  const paths = options.paths ?? chopsPaths();
11222
- const parsedSlug = parseSkillSlug(options.slug);
11223
- if (!parsedSlug)
11224
- throw new Error(`invalid skill slug: ${options.slug}`);
11225
- const manifest = await fetchManifest(options.apiUrl, options.slug);
11226
- const response = await fetch(manifest.tarball_url);
12026
+ const ref = parseSkillRef(options.slug);
12027
+ if (!ref)
12028
+ throw new Error(`invalid skill reference: ${options.slug}`);
12029
+ const manifest = options.manifest ?? await fetchManifest(options.apiUrl, options.slug);
12030
+ const response = await fetch(manifest.tarball_url, {
12031
+ ...options.tarballHeaders ? { headers: options.tarballHeaders } : {}
12032
+ });
11227
12033
  if (!response.ok)
11228
12034
  throw new Error(`tarball download failed (${response.status})`);
11229
12035
  const archive = new Uint8Array(await response.arrayBuffer());
11230
12036
  if (!verifyTarballSha256(archive, manifest.sha256)) {
11231
12037
  throw new Error("tarball sha256 does not match the manifest");
11232
12038
  }
11233
- const storePath = join(paths.store, parsedSlug.owner, parsedSlug.repo, `${parsedSlug.skill}@${manifest.version.content_sha}`);
12039
+ const storePath = storePathFor(paths, ref, manifest.version.content_sha);
11234
12040
  const temporary = `${storePath}.tmp-${crypto.randomUUID()}`;
11235
12041
  const storeAlreadyExists = await exists(storePath);
11236
12042
  const targets = await agentTargets(paths, options.agents);
11237
- const links = targets.map((target) => join(target.directory, manifest.skill.name));
12043
+ const links = targets.map((target) => join2(target.directory, manifest.skill.name));
11238
12044
  for (const link of links) {
11239
12045
  if (await exists(link))
11240
12046
  throw new Error(`install target already exists: ${link}`);
@@ -11243,11 +12049,11 @@ async function installSkill(options) {
11243
12049
  try {
11244
12050
  if (!storeAlreadyExists) {
11245
12051
  await extractTarGz(archive, temporary);
11246
- await mkdir2(dirname2(storePath), { recursive: true });
11247
- await rename(temporary, storePath);
12052
+ await mkdir3(dirname3(storePath), { recursive: true });
12053
+ await rename2(temporary, storePath);
11248
12054
  }
11249
12055
  for (const link of links) {
11250
- await mkdir2(dirname2(link), { recursive: true });
12056
+ await mkdir3(dirname3(link), { recursive: true });
11251
12057
  await symlink2(storePath, link, process.platform === "win32" ? "junction" : "dir");
11252
12058
  createdLinks.push(link);
11253
12059
  }
@@ -11256,7 +12062,9 @@ async function installSkill(options) {
11256
12062
  sha: manifest.version.content_sha,
11257
12063
  tarballSha256: manifest.sha256,
11258
12064
  agents: targets.map((target) => target.agent),
11259
- installedAt: new Date().toISOString()
12065
+ installedAt: new Date().toISOString(),
12066
+ ...ref.kind === "personal" ? { scope: "personal" } : {},
12067
+ ...ref.kind === "team" ? { scope: "team", team: ref.team } : {}
11260
12068
  };
11261
12069
  await writeLockfile(paths, lockfile);
11262
12070
  return { manifest, storePath, links };
@@ -11274,20 +12082,21 @@ async function removeSkill(options) {
11274
12082
  const entry = lockfile.skills[options.slug];
11275
12083
  if (!entry)
11276
12084
  return { removed: false, links: [] };
11277
- const parsed = parseSkillSlug(options.slug);
11278
- if (!parsed)
11279
- throw new Error(`invalid skill slug in lockfile: ${options.slug}`);
12085
+ const ref = parseSkillRef(options.slug);
12086
+ if (!ref)
12087
+ throw new Error(`invalid skill reference in lockfile: ${options.slug}`);
12088
+ const name = ref.kind === "public" ? ref.skill : ref.name;
11280
12089
  const links = entry.agents.flatMap((agent) => {
11281
12090
  if (agent === "claude-code")
11282
- return [join(paths.home, ".claude", "skills", parsed.skill)];
12091
+ return [join2(paths.home, ".claude", "skills", name)];
11283
12092
  if (agent === "cursor")
11284
- return [join(paths.home, ".cursor", "skills", parsed.skill)];
12093
+ return [join2(paths.home, ".cursor", "skills", name)];
11285
12094
  if (agent === "agents")
11286
- return [join(paths.home, ".agents", "skills", parsed.skill)];
12095
+ return [join2(paths.home, ".agents", "skills", name)];
11287
12096
  return [];
11288
12097
  });
11289
12098
  await Promise.all(links.map((link) => rm(link, { recursive: true, force: true })));
11290
- const storePath = join(paths.store, parsed.owner, parsed.repo, `${parsed.skill}@${entry.sha}`);
12099
+ const storePath = storePathFor(paths, ref, entry.sha);
11291
12100
  await rm(storePath, { recursive: true, force: true });
11292
12101
  delete lockfile.skills[options.slug];
11293
12102
  await writeLockfile(paths, lockfile);
@@ -11295,7 +12104,7 @@ async function removeSkill(options) {
11295
12104
  }
11296
12105
 
11297
12106
  // src/version.ts
11298
- var CLI_VERSION = "0.1.1";
12107
+ var CLI_VERSION = "0.2.0";
11299
12108
 
11300
12109
  // src/main.ts
11301
12110
  function parseArgs(argv) {
@@ -11305,28 +12114,40 @@ function parseArgs(argv) {
11305
12114
  let yes = false;
11306
12115
  let apiUrl = process.env.CHOPS_API_URL ?? "https://api.chops.sh";
11307
12116
  const agents = [];
12117
+ let team;
12118
+ let role;
12119
+ let name;
12120
+ let email;
12121
+ const valueFlag = (argv2, index, flag) => {
12122
+ const value = argv2[index];
12123
+ if (!value)
12124
+ throw new Error(`${flag} requires a value`);
12125
+ return value;
12126
+ };
11308
12127
  for (let index = 0;index < argv.length; index++) {
11309
12128
  const arg = argv[index];
11310
12129
  if (arg === "--json")
11311
12130
  json = true;
11312
12131
  else if (arg === "--yes" || arg === "-y")
11313
12132
  yes = true;
11314
- else if (arg === "--api-url") {
11315
- const value = argv[++index];
11316
- if (!value)
11317
- throw new Error("--api-url requires a value");
11318
- apiUrl = value.replace(/\/$/, "");
11319
- } else if (arg === "--agent") {
11320
- const value = argv[++index];
11321
- if (!value)
11322
- throw new Error("--agent requires a value");
11323
- agents.push(...value.split(",").filter(Boolean));
11324
- } else if (!command)
12133
+ else if (arg === "--api-url")
12134
+ apiUrl = valueFlag(argv, ++index, arg).replace(/\/$/, "");
12135
+ else if (arg === "--agent")
12136
+ agents.push(...valueFlag(argv, ++index, arg).split(",").filter(Boolean));
12137
+ else if (arg === "--team")
12138
+ team = valueFlag(argv, ++index, arg);
12139
+ else if (arg === "--role")
12140
+ role = valueFlag(argv, ++index, arg);
12141
+ else if (arg === "--name")
12142
+ name = valueFlag(argv, ++index, arg);
12143
+ else if (arg === "--email")
12144
+ email = valueFlag(argv, ++index, arg);
12145
+ else if (!command)
11325
12146
  command = arg;
11326
12147
  else
11327
12148
  positionals.push(arg);
11328
12149
  }
11329
- return { command, positionals, json, yes, apiUrl, agents };
12150
+ return { command, positionals, json, yes, apiUrl, agents, team, role, name, email };
11330
12151
  }
11331
12152
  function emit(value, json) {
11332
12153
  if (json)
@@ -11346,6 +12167,27 @@ async function confirmInstall(slug) {
11346
12167
  rl.close();
11347
12168
  }
11348
12169
  }
12170
+ var HELP = `Usage: chops <command> [args]
12171
+
12172
+ Registry
12173
+ find <query> search public skills
12174
+ add <ref> [--yes] [--agent a,b] install a skill (owner/repo@name, @me/name, @team/name)
12175
+ list installed skills
12176
+ remove <ref> uninstall a skill
12177
+
12178
+ Account
12179
+ auth start <email> email a one-time sign-in code
12180
+ auth verify <code> [--email e] finish signing in
12181
+ auth status | auth logout
12182
+
12183
+ Teams & private skills
12184
+ team create <slug> [--name n] create a team
12185
+ team invite <email>... [--team t] [--role member|admin]
12186
+ team join [token] [--team t] accept a pending invite
12187
+ team list | team members [--team t]
12188
+ publish <path> [--team t] publish a private skill (@me/... or @team/...)
12189
+
12190
+ Flags: --json machine-readable output --api-url <url> -y/--yes`;
11349
12191
  async function main(argv = process.argv.slice(2)) {
11350
12192
  let parsed;
11351
12193
  try {
@@ -11354,15 +12196,36 @@ async function main(argv = process.argv.slice(2)) {
11354
12196
  console.error(`chops: ${error.message}`);
11355
12197
  return EXIT_CODES.GENERIC;
11356
12198
  }
12199
+ const ctx = { apiUrl: parsed.apiUrl, paths: chopsPaths() };
12200
+ const io = { emit: (value) => emit(value, parsed.json), json: parsed.json };
11357
12201
  try {
11358
12202
  if (parsed.command === "--version" || parsed.command === "-v") {
11359
12203
  emit(CLI_VERSION, parsed.json);
11360
12204
  return EXIT_CODES.OK;
11361
12205
  }
11362
12206
  if (parsed.command === "--help" || parsed.command === "help") {
11363
- emit("Usage: chops <find|add|list|remove> [args] [--json] [--yes]", parsed.json);
12207
+ emit(HELP, parsed.json);
11364
12208
  return EXIT_CODES.OK;
11365
12209
  }
12210
+ if (parsed.command === "auth") {
12211
+ return await authCommand(ctx, io, parsed.positionals[0], parsed.positionals.slice(1), {
12212
+ email: parsed.email
12213
+ });
12214
+ }
12215
+ if (parsed.command === "team") {
12216
+ return await teamCommand(ctx, io, parsed.positionals[0], parsed.positionals.slice(1), {
12217
+ team: parsed.team,
12218
+ role: parsed.role,
12219
+ name: parsed.name,
12220
+ yes: parsed.yes
12221
+ });
12222
+ }
12223
+ if (parsed.command === "publish") {
12224
+ return await publishCommand(ctx, io, parsed.positionals[0], {
12225
+ team: parsed.team,
12226
+ yes: parsed.yes
12227
+ });
12228
+ }
11366
12229
  if (parsed.command === "find") {
11367
12230
  const query = parsed.positionals.join(" ").trim();
11368
12231
  if (!query)
@@ -11385,38 +12248,84 @@ async function main(argv = process.argv.slice(2)) {
11385
12248
  if (parsed.command === "add") {
11386
12249
  const slug = parsed.positionals[0];
11387
12250
  if (!slug)
11388
- throw new Error("add requires owner/repo@skill");
12251
+ throw new Error("add requires a skill reference (owner/repo@name or @scope/name)");
12252
+ const ref = parseSkillRef(slug);
12253
+ if (!ref)
12254
+ throw new Error(`invalid skill reference: ${slug}`);
11389
12255
  if (!parsed.yes && !await confirmInstall(slug))
11390
12256
  return EXIT_CODES.OK;
11391
- const result = await installSkill({
11392
- apiUrl: parsed.apiUrl,
11393
- slug,
11394
- ...parsed.agents.length ? { agents: parsed.agents } : {}
11395
- });
12257
+ let result;
12258
+ if (ref.kind === "public") {
12259
+ result = await installSkill({
12260
+ apiUrl: parsed.apiUrl,
12261
+ slug,
12262
+ ...parsed.agents.length ? { agents: parsed.agents } : {}
12263
+ });
12264
+ } else {
12265
+ const manifestPath = ref.kind === "personal" ? `/me/skills/${encodeURIComponent(ref.name)}/manifest` : `/teams/${encodeURIComponent(ref.team)}/skills/${encodeURIComponent(ref.name)}/manifest`;
12266
+ const response = await apiFetch(ctx, manifestPath, {}, { auth: "required" });
12267
+ const manifest = manifestSchema.parse(await response.json());
12268
+ const auth = await currentAuth(ctx);
12269
+ result = await installSkill({
12270
+ apiUrl: parsed.apiUrl,
12271
+ slug: formatSkillRef(ref),
12272
+ manifest,
12273
+ ...auth ? { tarballHeaders: { authorization: `Bearer ${auth.accessJwt}` } } : {},
12274
+ ...parsed.agents.length ? { agents: parsed.agents } : {}
12275
+ });
12276
+ }
11396
12277
  emit({
11397
12278
  ok: true,
11398
12279
  slug: result.manifest.skill.slug,
11399
12280
  sha: result.manifest.version.content_sha,
11400
12281
  store: result.storePath,
11401
- links: result.links
12282
+ links: result.links,
12283
+ ...ref.kind !== "public" ? { scope: ref.kind } : {},
12284
+ ...ref.kind === "team" ? { team: ref.team } : {}
11402
12285
  }, parsed.json);
11403
12286
  return EXIT_CODES.OK;
11404
12287
  }
11405
12288
  if (parsed.command === "list") {
11406
12289
  const lockfile = await readLockfile(chopsPaths());
11407
- emit({ skills: lockfile.skills }, parsed.json);
12290
+ if (parsed.json)
12291
+ emit({ skills: lockfile.skills }, true);
12292
+ else if (Object.keys(lockfile.skills).length === 0)
12293
+ emit("No skills installed.", false);
12294
+ else {
12295
+ for (const [slug, entry] of Object.entries(lockfile.skills)) {
12296
+ const scope = entry.scope === "personal" ? "private" : entry.scope === "team" ? `team ${entry.team ?? ""}`.trim() : "public";
12297
+ emit(`${slug.padEnd(44)} ${scope.padEnd(16)} sha ${entry.sha.slice(0, 7)}`, false);
12298
+ }
12299
+ }
11408
12300
  return EXIT_CODES.OK;
11409
12301
  }
11410
12302
  if (parsed.command === "remove") {
11411
12303
  const slug = parsed.positionals[0];
11412
12304
  if (!slug)
11413
- throw new Error("remove requires owner/repo@skill");
12305
+ throw new Error("remove requires a skill reference");
11414
12306
  const result = await removeSkill({ slug });
11415
12307
  emit({ ok: true, slug, removed: result.removed, links: result.links }, parsed.json);
11416
12308
  return EXIT_CODES.OK;
11417
12309
  }
11418
12310
  throw new Error(`unknown command ${parsed.command ?? "(none)"}`);
11419
12311
  } catch (error) {
12312
+ if (error instanceof ApiError) {
12313
+ if (parsed.json) {
12314
+ emit({
12315
+ error: {
12316
+ code: error.code,
12317
+ message: error.message,
12318
+ ...error.lift ? { lift: error.lift } : {},
12319
+ ...error.retryAfter !== undefined ? { retry_after: error.retryAfter } : {}
12320
+ }
12321
+ }, true);
12322
+ } else {
12323
+ console.error(`chops: ${error.message}`);
12324
+ if (error.lift)
12325
+ console.error(` hint: ${error.lift.hint}`);
12326
+ }
12327
+ return error.exitCode;
12328
+ }
11420
12329
  const message = error.message;
11421
12330
  if (parsed.json)
11422
12331
  console.log(JSON.stringify({ error: { code: "failed", message } }));