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.
- package/README.md +19 -2
- package/dist/index.js +1018 -109
- 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,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
|
|
11071
|
-
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";
|
|
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
|
|
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
|
|
11098
|
-
if (
|
|
11884
|
+
const header2 = tar.slice(offset, offset + BLOCK);
|
|
11885
|
+
if (header2.every((byte) => byte === 0))
|
|
11099
11886
|
break;
|
|
11100
|
-
const name = text(
|
|
11101
|
-
const prefix = text(
|
|
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(
|
|
11104
|
-
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);
|
|
11105
11892
|
if (!Number.isSafeInteger(size) || size < 0)
|
|
11106
11893
|
throw new Error(`invalid tar size for ${path}`);
|
|
11107
|
-
const typeFlag = text(
|
|
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(
|
|
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 >
|
|
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 =
|
|
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 =
|
|
11927
|
+
const root = resolve2(destination);
|
|
11141
11928
|
const entries = parseTar(archive);
|
|
11142
|
-
await
|
|
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
|
|
11933
|
+
await mkdir2(target, { recursive: true });
|
|
11147
11934
|
continue;
|
|
11148
11935
|
}
|
|
11149
|
-
await
|
|
11150
|
-
await
|
|
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
|
|
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 =
|
|
11165
|
-
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
|
+
};
|
|
11166
11960
|
}
|
|
11167
11961
|
async function exists(path) {
|
|
11168
11962
|
try {
|
|
11169
|
-
await
|
|
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
|
|
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
|
|
11985
|
+
await mkdir3(dirname3(paths.lockfile), { recursive: true });
|
|
11192
11986
|
const temporary = `${paths.lockfile}.tmp-${crypto.randomUUID()}`;
|
|
11193
|
-
await
|
|
11987
|
+
await writeFile3(temporary, `${JSON.stringify(lockfile, null, 2)}
|
|
11194
11988
|
`, { flag: "wx", mode: 384 });
|
|
11195
|
-
await
|
|
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:
|
|
11995
|
+
targets.push({ agent: "agents", directory: join2(paths.home, ".agents", "skills") });
|
|
11202
11996
|
}
|
|
11203
|
-
if (requested.has("claude-code") || requested.size === 0 && await exists(
|
|
11204
|
-
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") });
|
|
11205
11999
|
}
|
|
11206
12000
|
if (requested.has("cursor")) {
|
|
11207
|
-
targets.push({ agent: "cursor", directory:
|
|
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
|
|
11223
|
-
if (!
|
|
11224
|
-
throw new Error(`invalid skill
|
|
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 =
|
|
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) =>
|
|
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
|
|
11247
|
-
await
|
|
12052
|
+
await mkdir3(dirname3(storePath), { recursive: true });
|
|
12053
|
+
await rename2(temporary, storePath);
|
|
11248
12054
|
}
|
|
11249
12055
|
for (const link of links) {
|
|
11250
|
-
await
|
|
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
|
|
11278
|
-
if (!
|
|
11279
|
-
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;
|
|
11280
12089
|
const links = entry.agents.flatMap((agent) => {
|
|
11281
12090
|
if (agent === "claude-code")
|
|
11282
|
-
return [
|
|
12091
|
+
return [join2(paths.home, ".claude", "skills", name)];
|
|
11283
12092
|
if (agent === "cursor")
|
|
11284
|
-
return [
|
|
12093
|
+
return [join2(paths.home, ".cursor", "skills", name)];
|
|
11285
12094
|
if (agent === "agents")
|
|
11286
|
-
return [
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
|
|
11319
|
-
|
|
11320
|
-
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
|
|
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(
|
|
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@
|
|
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
|
-
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
|
|
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
|
-
|
|
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
|
|
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 } }));
|