chops-sh 0.1.0 → 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.
- package/README.md +19 -2
- package/dist/index.js +1029 -112
- 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
|
|
1233
|
+
let header2 = (startWithSpace ? indentSize : "") + chomp;
|
|
1234
1234
|
if (comment) {
|
|
1235
|
-
|
|
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 `>${
|
|
1251
|
+
return `>${header2}
|
|
1252
1252
|
${indent}${body}`;
|
|
1253
1253
|
}
|
|
1254
1254
|
value = value.replace(/\n+/g, `$&${indent}`);
|
|
1255
|
-
return `|${
|
|
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
|
|
4131
|
-
if (!
|
|
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 =
|
|
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 =
|
|
4144
|
+
const value2 = header2.chomp === "+" && lines.length > 0 ? `
|
|
4145
4145
|
`.repeat(Math.max(1, lines.length - 1)) : "";
|
|
4146
|
-
let end2 = start +
|
|
4146
|
+
let end2 = start + header2.length;
|
|
4147
4147
|
if (scalar.source)
|
|
4148
4148
|
end2 += scalar.source.length;
|
|
4149
|
-
return { value: value2, type, comment:
|
|
4149
|
+
return { value: value2, type, comment: header2.comment, range: [start, end2, end2] };
|
|
4150
4150
|
}
|
|
4151
|
-
let trimIndent = scalar.indent +
|
|
4152
|
-
let offset = scalar.offset +
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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 +
|
|
4245
|
-
return { value, type, comment:
|
|
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
|
|
5051
|
-
if (
|
|
5050
|
+
const header2 = token.props[0];
|
|
5051
|
+
if (header2.type !== "block-scalar-header")
|
|
5052
5052
|
throw new Error("Invalid block scalar header");
|
|
5053
|
-
type =
|
|
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
|
|
5088
|
-
if (
|
|
5087
|
+
const header2 = token.props[0];
|
|
5088
|
+
if (header2.type !== "block-scalar-header")
|
|
5089
5089
|
throw new Error("Invalid block scalar header");
|
|
5090
|
-
|
|
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,8 +11179,53 @@ 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
|
-
var
|
|
11228
|
+
var skillSummarySchema = exports_external.object({
|
|
10987
11229
|
slug: exports_external.string().min(1),
|
|
10988
11230
|
description: exports_external.string().nullable(),
|
|
10989
11231
|
publisher: exports_external.object({
|
|
@@ -10995,13 +11237,21 @@ var searchResultSchema = exports_external.object({
|
|
|
10995
11237
|
installs: exports_external.number().int().nonnegative(),
|
|
10996
11238
|
last_published_at: exports_external.string().datetime(),
|
|
10997
11239
|
pinned_sha: exports_external.string().min(1),
|
|
10998
|
-
install_cmd: exports_external.string().min(1)
|
|
11240
|
+
install_cmd: exports_external.string().min(1)
|
|
11241
|
+
});
|
|
11242
|
+
var searchResultSchema = skillSummarySchema.extend({
|
|
10999
11243
|
score: exports_external.number()
|
|
11000
11244
|
});
|
|
11001
11245
|
var searchResponseSchema = exports_external.object({
|
|
11002
11246
|
query: exports_external.string().min(1),
|
|
11003
11247
|
results: exports_external.array(searchResultSchema)
|
|
11004
11248
|
});
|
|
11249
|
+
var browseSortSchema = exports_external.enum(["popular", "recent"]);
|
|
11250
|
+
var browseResponseSchema = exports_external.object({
|
|
11251
|
+
total: exports_external.number().int().nonnegative(),
|
|
11252
|
+
sort: browseSortSchema,
|
|
11253
|
+
results: exports_external.array(skillSummarySchema)
|
|
11254
|
+
});
|
|
11005
11255
|
// ../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/index.js
|
|
11006
11256
|
var composer = require_composer();
|
|
11007
11257
|
var Document = require_Document();
|
|
@@ -11058,17 +11308,562 @@ var frontmatterSchema = exports_external.object({
|
|
|
11058
11308
|
metadata: exports_external.record(exports_external.string()).optional(),
|
|
11059
11309
|
"allowed-tools": exports_external.string().trim().min(1).optional()
|
|
11060
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
|
+
|
|
11061
11856
|
// src/install.ts
|
|
11062
|
-
import { lstat, mkdir as
|
|
11063
|
-
import { dirname as
|
|
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";
|
|
11064
11859
|
|
|
11065
11860
|
// src/archive.ts
|
|
11066
|
-
import { mkdir, chmod, symlink, writeFile } from "node:fs/promises";
|
|
11067
|
-
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";
|
|
11068
11863
|
import { gunzipSync } from "node:zlib";
|
|
11069
11864
|
var BLOCK = 512;
|
|
11070
11865
|
var MAX_UNCOMPRESSED_BYTES = 25 * 1024 * 1024;
|
|
11071
|
-
var
|
|
11866
|
+
var MAX_ENTRIES2 = 2000;
|
|
11072
11867
|
function text(bytes) {
|
|
11073
11868
|
const zero = bytes.indexOf(0);
|
|
11074
11869
|
return new TextDecoder().decode(zero >= 0 ? bytes.slice(0, zero) : bytes).trim();
|
|
@@ -11086,17 +11881,17 @@ function parseTar(archive) {
|
|
|
11086
11881
|
throw new Error("archive expands past 25 MiB");
|
|
11087
11882
|
const entries = [];
|
|
11088
11883
|
for (let offset = 0;offset + BLOCK <= tar.byteLength; ) {
|
|
11089
|
-
const
|
|
11090
|
-
if (
|
|
11884
|
+
const header2 = tar.slice(offset, offset + BLOCK);
|
|
11885
|
+
if (header2.every((byte) => byte === 0))
|
|
11091
11886
|
break;
|
|
11092
|
-
const name = text(
|
|
11093
|
-
const prefix = text(
|
|
11887
|
+
const name = text(header2.slice(0, 100));
|
|
11888
|
+
const prefix = text(header2.slice(345, 500));
|
|
11094
11889
|
const path = safePath(prefix ? `${prefix}/${name}` : name);
|
|
11095
|
-
const mode = Number.parseInt(text(
|
|
11096
|
-
const size = Number.parseInt(text(
|
|
11890
|
+
const mode = Number.parseInt(text(header2.slice(100, 108)) || "644", 8);
|
|
11891
|
+
const size = Number.parseInt(text(header2.slice(124, 136)) || "0", 8);
|
|
11097
11892
|
if (!Number.isSafeInteger(size) || size < 0)
|
|
11098
11893
|
throw new Error(`invalid tar size for ${path}`);
|
|
11099
|
-
const typeFlag = text(
|
|
11894
|
+
const typeFlag = text(header2.slice(156, 157)) || "0";
|
|
11100
11895
|
offset += BLOCK;
|
|
11101
11896
|
if (offset + size > tar.byteLength)
|
|
11102
11897
|
throw new Error(`truncated tar entry: ${path}`);
|
|
@@ -11107,7 +11902,7 @@ function parseTar(archive) {
|
|
|
11107
11902
|
else if (typeFlag === "5")
|
|
11108
11903
|
entries.push({ path, type: "directory", mode, data });
|
|
11109
11904
|
else if (typeFlag === "2") {
|
|
11110
|
-
const linkTarget = text(
|
|
11905
|
+
const linkTarget = text(header2.slice(157, 257));
|
|
11111
11906
|
const resolved = posix.normalize(posix.join(posix.dirname(path), linkTarget));
|
|
11112
11907
|
if (!linkTarget || linkTarget.startsWith("/") || resolved === ".." || resolved.startsWith("../")) {
|
|
11113
11908
|
throw new Error(`unsafe symlink in archive: ${path}`);
|
|
@@ -11116,35 +11911,35 @@ function parseTar(archive) {
|
|
|
11116
11911
|
} else {
|
|
11117
11912
|
throw new Error(`unsupported tar entry type ${typeFlag} for ${path}`);
|
|
11118
11913
|
}
|
|
11119
|
-
if (entries.length >
|
|
11914
|
+
if (entries.length > MAX_ENTRIES2)
|
|
11120
11915
|
throw new Error("archive contains too many entries");
|
|
11121
11916
|
}
|
|
11122
11917
|
return entries;
|
|
11123
11918
|
}
|
|
11124
11919
|
function destinationPath(root, path) {
|
|
11125
|
-
const target =
|
|
11920
|
+
const target = resolve2(root, ...path.split("/"));
|
|
11126
11921
|
const prefix = root.endsWith(sep) ? root : `${root}${sep}`;
|
|
11127
11922
|
if (target !== root && !target.startsWith(prefix))
|
|
11128
11923
|
throw new Error(`archive path escapes root: ${path}`);
|
|
11129
11924
|
return target;
|
|
11130
11925
|
}
|
|
11131
11926
|
async function extractTarGz(archive, destination) {
|
|
11132
|
-
const root =
|
|
11927
|
+
const root = resolve2(destination);
|
|
11133
11928
|
const entries = parseTar(archive);
|
|
11134
|
-
await
|
|
11929
|
+
await mkdir2(root, { recursive: true });
|
|
11135
11930
|
for (const entry of entries.filter((item) => item.type !== "symlink")) {
|
|
11136
11931
|
const target = destinationPath(root, entry.path);
|
|
11137
11932
|
if (entry.type === "directory") {
|
|
11138
|
-
await
|
|
11933
|
+
await mkdir2(target, { recursive: true });
|
|
11139
11934
|
continue;
|
|
11140
11935
|
}
|
|
11141
|
-
await
|
|
11142
|
-
await
|
|
11936
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
11937
|
+
await writeFile2(target, entry.data, { flag: "wx" });
|
|
11143
11938
|
await chmod(target, entry.mode & 73 ? 493 : 420);
|
|
11144
11939
|
}
|
|
11145
11940
|
for (const entry of entries.filter((item) => item.type === "symlink")) {
|
|
11146
11941
|
const target = destinationPath(root, entry.path);
|
|
11147
|
-
await
|
|
11942
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
11148
11943
|
await symlink(entry.linkTarget, target);
|
|
11149
11944
|
}
|
|
11150
11945
|
}
|
|
@@ -11153,12 +11948,19 @@ async function extractTarGz(archive, destination) {
|
|
|
11153
11948
|
function chopsPaths(home = process.env.HOME) {
|
|
11154
11949
|
if (!home)
|
|
11155
11950
|
throw new Error("HOME is not set");
|
|
11156
|
-
const root =
|
|
11157
|
-
return {
|
|
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
|
+
};
|
|
11158
11960
|
}
|
|
11159
11961
|
async function exists(path) {
|
|
11160
11962
|
try {
|
|
11161
|
-
await
|
|
11963
|
+
await lstat2(path);
|
|
11162
11964
|
return true;
|
|
11163
11965
|
} catch (error) {
|
|
11164
11966
|
if (error.code === "ENOENT")
|
|
@@ -11168,7 +11970,7 @@ async function exists(path) {
|
|
|
11168
11970
|
}
|
|
11169
11971
|
async function readLockfile(paths) {
|
|
11170
11972
|
try {
|
|
11171
|
-
const parsed = JSON.parse(await
|
|
11973
|
+
const parsed = JSON.parse(await readFile3(paths.lockfile, "utf8"));
|
|
11172
11974
|
if (parsed.version !== 1 || !parsed.skills || typeof parsed.skills !== "object") {
|
|
11173
11975
|
throw new Error("unsupported lockfile format");
|
|
11174
11976
|
}
|
|
@@ -11180,23 +11982,23 @@ async function readLockfile(paths) {
|
|
|
11180
11982
|
}
|
|
11181
11983
|
}
|
|
11182
11984
|
async function writeLockfile(paths, lockfile) {
|
|
11183
|
-
await
|
|
11985
|
+
await mkdir3(dirname3(paths.lockfile), { recursive: true });
|
|
11184
11986
|
const temporary = `${paths.lockfile}.tmp-${crypto.randomUUID()}`;
|
|
11185
|
-
await
|
|
11987
|
+
await writeFile3(temporary, `${JSON.stringify(lockfile, null, 2)}
|
|
11186
11988
|
`, { flag: "wx", mode: 384 });
|
|
11187
|
-
await
|
|
11989
|
+
await rename2(temporary, paths.lockfile);
|
|
11188
11990
|
}
|
|
11189
11991
|
async function agentTargets(paths, explicit = []) {
|
|
11190
11992
|
const requested = new Set(explicit.map((agent) => agent.toLowerCase()));
|
|
11191
11993
|
const targets = [];
|
|
11192
11994
|
if (requested.size === 0 || requested.has("codex") || requested.has("agents")) {
|
|
11193
|
-
targets.push({ agent: "agents", directory:
|
|
11995
|
+
targets.push({ agent: "agents", directory: join2(paths.home, ".agents", "skills") });
|
|
11194
11996
|
}
|
|
11195
|
-
if (requested.has("claude-code") || requested.size === 0 && await exists(
|
|
11196
|
-
targets.push({ agent: "claude-code", directory:
|
|
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") });
|
|
11197
11999
|
}
|
|
11198
12000
|
if (requested.has("cursor")) {
|
|
11199
|
-
targets.push({ agent: "cursor", directory:
|
|
12001
|
+
targets.push({ agent: "cursor", directory: join2(paths.home, ".cursor", "skills") });
|
|
11200
12002
|
}
|
|
11201
12003
|
if (targets.length === 0)
|
|
11202
12004
|
throw new Error("no supported agent targets were selected");
|
|
@@ -11209,24 +12011,36 @@ async function fetchManifest(apiUrl, slug) {
|
|
|
11209
12011
|
throw new Error(`manifest request failed (${response.status})`);
|
|
11210
12012
|
return manifestSchema.parse(payload);
|
|
11211
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
|
+
}
|
|
11212
12024
|
async function installSkill(options) {
|
|
11213
12025
|
const paths = options.paths ?? chopsPaths();
|
|
11214
|
-
const
|
|
11215
|
-
if (!
|
|
11216
|
-
throw new Error(`invalid skill
|
|
11217
|
-
const manifest = await fetchManifest(options.apiUrl, options.slug);
|
|
11218
|
-
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
|
+
});
|
|
11219
12033
|
if (!response.ok)
|
|
11220
12034
|
throw new Error(`tarball download failed (${response.status})`);
|
|
11221
12035
|
const archive = new Uint8Array(await response.arrayBuffer());
|
|
11222
12036
|
if (!verifyTarballSha256(archive, manifest.sha256)) {
|
|
11223
12037
|
throw new Error("tarball sha256 does not match the manifest");
|
|
11224
12038
|
}
|
|
11225
|
-
const storePath =
|
|
12039
|
+
const storePath = storePathFor(paths, ref, manifest.version.content_sha);
|
|
11226
12040
|
const temporary = `${storePath}.tmp-${crypto.randomUUID()}`;
|
|
11227
12041
|
const storeAlreadyExists = await exists(storePath);
|
|
11228
12042
|
const targets = await agentTargets(paths, options.agents);
|
|
11229
|
-
const links = targets.map((target) =>
|
|
12043
|
+
const links = targets.map((target) => join2(target.directory, manifest.skill.name));
|
|
11230
12044
|
for (const link of links) {
|
|
11231
12045
|
if (await exists(link))
|
|
11232
12046
|
throw new Error(`install target already exists: ${link}`);
|
|
@@ -11235,11 +12049,11 @@ async function installSkill(options) {
|
|
|
11235
12049
|
try {
|
|
11236
12050
|
if (!storeAlreadyExists) {
|
|
11237
12051
|
await extractTarGz(archive, temporary);
|
|
11238
|
-
await
|
|
11239
|
-
await
|
|
12052
|
+
await mkdir3(dirname3(storePath), { recursive: true });
|
|
12053
|
+
await rename2(temporary, storePath);
|
|
11240
12054
|
}
|
|
11241
12055
|
for (const link of links) {
|
|
11242
|
-
await
|
|
12056
|
+
await mkdir3(dirname3(link), { recursive: true });
|
|
11243
12057
|
await symlink2(storePath, link, process.platform === "win32" ? "junction" : "dir");
|
|
11244
12058
|
createdLinks.push(link);
|
|
11245
12059
|
}
|
|
@@ -11248,7 +12062,9 @@ async function installSkill(options) {
|
|
|
11248
12062
|
sha: manifest.version.content_sha,
|
|
11249
12063
|
tarballSha256: manifest.sha256,
|
|
11250
12064
|
agents: targets.map((target) => target.agent),
|
|
11251
|
-
installedAt: new Date().toISOString()
|
|
12065
|
+
installedAt: new Date().toISOString(),
|
|
12066
|
+
...ref.kind === "personal" ? { scope: "personal" } : {},
|
|
12067
|
+
...ref.kind === "team" ? { scope: "team", team: ref.team } : {}
|
|
11252
12068
|
};
|
|
11253
12069
|
await writeLockfile(paths, lockfile);
|
|
11254
12070
|
return { manifest, storePath, links };
|
|
@@ -11266,20 +12082,21 @@ async function removeSkill(options) {
|
|
|
11266
12082
|
const entry = lockfile.skills[options.slug];
|
|
11267
12083
|
if (!entry)
|
|
11268
12084
|
return { removed: false, links: [] };
|
|
11269
|
-
const
|
|
11270
|
-
if (!
|
|
11271
|
-
throw new Error(`invalid skill
|
|
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;
|
|
11272
12089
|
const links = entry.agents.flatMap((agent) => {
|
|
11273
12090
|
if (agent === "claude-code")
|
|
11274
|
-
return [
|
|
12091
|
+
return [join2(paths.home, ".claude", "skills", name)];
|
|
11275
12092
|
if (agent === "cursor")
|
|
11276
|
-
return [
|
|
12093
|
+
return [join2(paths.home, ".cursor", "skills", name)];
|
|
11277
12094
|
if (agent === "agents")
|
|
11278
|
-
return [
|
|
12095
|
+
return [join2(paths.home, ".agents", "skills", name)];
|
|
11279
12096
|
return [];
|
|
11280
12097
|
});
|
|
11281
12098
|
await Promise.all(links.map((link) => rm(link, { recursive: true, force: true })));
|
|
11282
|
-
const storePath =
|
|
12099
|
+
const storePath = storePathFor(paths, ref, entry.sha);
|
|
11283
12100
|
await rm(storePath, { recursive: true, force: true });
|
|
11284
12101
|
delete lockfile.skills[options.slug];
|
|
11285
12102
|
await writeLockfile(paths, lockfile);
|
|
@@ -11287,7 +12104,7 @@ async function removeSkill(options) {
|
|
|
11287
12104
|
}
|
|
11288
12105
|
|
|
11289
12106
|
// src/version.ts
|
|
11290
|
-
var CLI_VERSION = "0.
|
|
12107
|
+
var CLI_VERSION = "0.2.0";
|
|
11291
12108
|
|
|
11292
12109
|
// src/main.ts
|
|
11293
12110
|
function parseArgs(argv) {
|
|
@@ -11295,30 +12112,42 @@ function parseArgs(argv) {
|
|
|
11295
12112
|
let command;
|
|
11296
12113
|
let json = false;
|
|
11297
12114
|
let yes = false;
|
|
11298
|
-
let apiUrl = process.env.CHOPS_API_URL ?? "https://
|
|
12115
|
+
let apiUrl = process.env.CHOPS_API_URL ?? "https://api.chops.sh";
|
|
11299
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
|
+
};
|
|
11300
12127
|
for (let index = 0;index < argv.length; index++) {
|
|
11301
12128
|
const arg = argv[index];
|
|
11302
12129
|
if (arg === "--json")
|
|
11303
12130
|
json = true;
|
|
11304
12131
|
else if (arg === "--yes" || arg === "-y")
|
|
11305
12132
|
yes = true;
|
|
11306
|
-
else if (arg === "--api-url")
|
|
11307
|
-
|
|
11308
|
-
|
|
11309
|
-
|
|
11310
|
-
|
|
11311
|
-
|
|
11312
|
-
|
|
11313
|
-
|
|
11314
|
-
|
|
11315
|
-
|
|
11316
|
-
|
|
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)
|
|
11317
12146
|
command = arg;
|
|
11318
12147
|
else
|
|
11319
12148
|
positionals.push(arg);
|
|
11320
12149
|
}
|
|
11321
|
-
return { command, positionals, json, yes, apiUrl, agents };
|
|
12150
|
+
return { command, positionals, json, yes, apiUrl, agents, team, role, name, email };
|
|
11322
12151
|
}
|
|
11323
12152
|
function emit(value, json) {
|
|
11324
12153
|
if (json)
|
|
@@ -11338,6 +12167,27 @@ async function confirmInstall(slug) {
|
|
|
11338
12167
|
rl.close();
|
|
11339
12168
|
}
|
|
11340
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`;
|
|
11341
12191
|
async function main(argv = process.argv.slice(2)) {
|
|
11342
12192
|
let parsed;
|
|
11343
12193
|
try {
|
|
@@ -11346,15 +12196,36 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
11346
12196
|
console.error(`chops: ${error.message}`);
|
|
11347
12197
|
return EXIT_CODES.GENERIC;
|
|
11348
12198
|
}
|
|
12199
|
+
const ctx = { apiUrl: parsed.apiUrl, paths: chopsPaths() };
|
|
12200
|
+
const io = { emit: (value) => emit(value, parsed.json), json: parsed.json };
|
|
11349
12201
|
try {
|
|
11350
12202
|
if (parsed.command === "--version" || parsed.command === "-v") {
|
|
11351
12203
|
emit(CLI_VERSION, parsed.json);
|
|
11352
12204
|
return EXIT_CODES.OK;
|
|
11353
12205
|
}
|
|
11354
12206
|
if (parsed.command === "--help" || parsed.command === "help") {
|
|
11355
|
-
emit(
|
|
12207
|
+
emit(HELP, parsed.json);
|
|
11356
12208
|
return EXIT_CODES.OK;
|
|
11357
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
|
+
}
|
|
11358
12229
|
if (parsed.command === "find") {
|
|
11359
12230
|
const query = parsed.positionals.join(" ").trim();
|
|
11360
12231
|
if (!query)
|
|
@@ -11377,38 +12248,84 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
11377
12248
|
if (parsed.command === "add") {
|
|
11378
12249
|
const slug = parsed.positionals[0];
|
|
11379
12250
|
if (!slug)
|
|
11380
|
-
throw new Error("add requires owner/repo@
|
|
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}`);
|
|
11381
12255
|
if (!parsed.yes && !await confirmInstall(slug))
|
|
11382
12256
|
return EXIT_CODES.OK;
|
|
11383
|
-
|
|
11384
|
-
|
|
11385
|
-
|
|
11386
|
-
|
|
11387
|
-
|
|
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
|
+
}
|
|
11388
12277
|
emit({
|
|
11389
12278
|
ok: true,
|
|
11390
12279
|
slug: result.manifest.skill.slug,
|
|
11391
12280
|
sha: result.manifest.version.content_sha,
|
|
11392
12281
|
store: result.storePath,
|
|
11393
|
-
links: result.links
|
|
12282
|
+
links: result.links,
|
|
12283
|
+
...ref.kind !== "public" ? { scope: ref.kind } : {},
|
|
12284
|
+
...ref.kind === "team" ? { team: ref.team } : {}
|
|
11394
12285
|
}, parsed.json);
|
|
11395
12286
|
return EXIT_CODES.OK;
|
|
11396
12287
|
}
|
|
11397
12288
|
if (parsed.command === "list") {
|
|
11398
12289
|
const lockfile = await readLockfile(chopsPaths());
|
|
11399
|
-
|
|
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
|
+
}
|
|
11400
12300
|
return EXIT_CODES.OK;
|
|
11401
12301
|
}
|
|
11402
12302
|
if (parsed.command === "remove") {
|
|
11403
12303
|
const slug = parsed.positionals[0];
|
|
11404
12304
|
if (!slug)
|
|
11405
|
-
throw new Error("remove requires
|
|
12305
|
+
throw new Error("remove requires a skill reference");
|
|
11406
12306
|
const result = await removeSkill({ slug });
|
|
11407
12307
|
emit({ ok: true, slug, removed: result.removed, links: result.links }, parsed.json);
|
|
11408
12308
|
return EXIT_CODES.OK;
|
|
11409
12309
|
}
|
|
11410
12310
|
throw new Error(`unknown command ${parsed.command ?? "(none)"}`);
|
|
11411
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
|
+
}
|
|
11412
12329
|
const message = error.message;
|
|
11413
12330
|
if (parsed.json)
|
|
11414
12331
|
console.log(JSON.stringify({ error: { code: "failed", message } }));
|