threadnote 1.2.0 → 1.3.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 +8 -0
- package/dist/mcp_server.cjs +2175 -1022
- package/dist/threadnote.cjs +1076 -18
- package/docs/index.html +11 -0
- package/docs/share.md +102 -0
- package/package.json +1 -1
package/dist/threadnote.cjs
CHANGED
|
@@ -8598,6 +8598,7 @@ function formatPlanSection(title, lines) {
|
|
|
8598
8598
|
var import_promises5 = require("node:fs/promises");
|
|
8599
8599
|
var import_node_os4 = require("node:os");
|
|
8600
8600
|
var import_node_path6 = require("node:path");
|
|
8601
|
+
var import_node_util = require("node:util");
|
|
8601
8602
|
var TEAMS_FILE_VERSION = 1;
|
|
8602
8603
|
var SHARED_SEGMENT = "shared";
|
|
8603
8604
|
var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
|
|
@@ -8605,6 +8606,15 @@ var SHAREABLE_ARTIFACT_DIR = "agent-artifacts";
|
|
|
8605
8606
|
var SHAREABLE_TOP_LEVEL_DIRS = [...SHAREABLE_MEMORY_KIND_DIRS, SHAREABLE_ARTIFACT_DIR];
|
|
8606
8607
|
var SHAREABLE_ROOT_FILES = ["README.md", "AGENTS.md", "CLAUDE.md", "SKILL.md", ".gitignore"];
|
|
8607
8608
|
var ARTIFACT_INSTALL_METADATA_VERSION = 1;
|
|
8609
|
+
var BUNDLE_MANIFEST_VERSION = 1;
|
|
8610
|
+
var BUNDLE_MANIFEST_FILE = ".threadnote-bundle.json";
|
|
8611
|
+
var BUNDLE_INSTALL_METADATA_FILE = ".threadnote-bundle-install.json";
|
|
8612
|
+
var OV_SUMMARY_FILES = [".abstract.md", ".overview.md"];
|
|
8613
|
+
var BUNDLE_IGNORE_DIR_NAMES = [".git", "node_modules", "reviews", "repos"];
|
|
8614
|
+
var PACK_INDEX_SUFFIX = ".pack.md";
|
|
8615
|
+
var PACK_MANIFEST_SUFFIX = ".pack.json";
|
|
8616
|
+
var PACK_FILES_DIR = "files";
|
|
8617
|
+
var PACK_ROOT_TOKEN = "${THREADNOTE_PACK_ROOT}";
|
|
8608
8618
|
var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
8609
8619
|
var DEFAULT_GIT_REMOTE_NAME = "origin";
|
|
8610
8620
|
var SCRUBBER_PATTERNS = [
|
|
@@ -8631,7 +8641,10 @@ var SCRUBBER_PATTERNS = [
|
|
|
8631
8641
|
// so redaction collapses an entire path to a single placeholder rather than
|
|
8632
8642
|
// leaving the subpath visible.
|
|
8633
8643
|
{ name: "macOS home path", placeholder: "<local-path>", regex: /\/Users\/[^\s)>"'`,]+/ },
|
|
8634
|
-
|
|
8644
|
+
// No leading \b: it only matches when a word char precedes the slash, which
|
|
8645
|
+
// misses the common cases (after =, :, whitespace, or line start). Mirror the
|
|
8646
|
+
// macOS pattern so /home/... is caught in every position.
|
|
8647
|
+
{ name: "linux home path", placeholder: "<local-path>", regex: /\/home\/[^\s)>"'`,]+/ }
|
|
8635
8648
|
];
|
|
8636
8649
|
function applyScrubber(content, { redact }) {
|
|
8637
8650
|
let cleaned = content;
|
|
@@ -9039,12 +9052,47 @@ async function runShareSyncQuiet(config, state, options) {
|
|
|
9039
9052
|
return void 0;
|
|
9040
9053
|
}
|
|
9041
9054
|
async function stageShareableChanges(dryRun, git, worktree) {
|
|
9055
|
+
await removeOrphanPackIndexes(dryRun, git, worktree);
|
|
9042
9056
|
const pathspecs = await existingShareablePathspecs(git, worktree);
|
|
9043
9057
|
if (pathspecs.length === 0) {
|
|
9044
9058
|
return;
|
|
9045
9059
|
}
|
|
9046
9060
|
await maybeRun(dryRun, git, ["-C", worktree, "add", "-A", "--", ...pathspecs], { allowFailure: true });
|
|
9047
9061
|
}
|
|
9062
|
+
async function removeOrphanPackIndexes(dryRun, git, worktree) {
|
|
9063
|
+
const packsRoot = (0, import_node_path6.join)(worktree, SHAREABLE_ARTIFACT_DIR, "packs");
|
|
9064
|
+
if (!await isDirectory(packsRoot)) {
|
|
9065
|
+
return;
|
|
9066
|
+
}
|
|
9067
|
+
for (const agentEntry of await (0, import_promises5.readdir)(packsRoot, { withFileTypes: true })) {
|
|
9068
|
+
if (!agentEntry.isDirectory()) {
|
|
9069
|
+
continue;
|
|
9070
|
+
}
|
|
9071
|
+
const agentDir = (0, import_node_path6.join)(packsRoot, agentEntry.name);
|
|
9072
|
+
for (const nameEntry of await (0, import_promises5.readdir)(agentDir, { withFileTypes: true })) {
|
|
9073
|
+
if (!nameEntry.isDirectory()) {
|
|
9074
|
+
continue;
|
|
9075
|
+
}
|
|
9076
|
+
const packDir = (0, import_node_path6.join)(agentDir, nameEntry.name);
|
|
9077
|
+
const indexPath = (0, import_node_path6.join)(packDir, `${nameEntry.name}${PACK_INDEX_SUFFIX}`);
|
|
9078
|
+
const manifestPath = (0, import_node_path6.join)(packDir, `${nameEntry.name}${PACK_MANIFEST_SUFFIX}`);
|
|
9079
|
+
if (!await isFile(indexPath) || await isFile(manifestPath)) {
|
|
9080
|
+
continue;
|
|
9081
|
+
}
|
|
9082
|
+
const indexRelative = (0, import_node_path6.relative)(worktree, indexPath).split(import_node_path6.sep).join("/");
|
|
9083
|
+
const tracked = await runCommand(git, ["-C", worktree, "ls-files", "--", indexRelative], { allowFailure: true });
|
|
9084
|
+
if (tracked.exitCode === 0 && tracked.stdout.trim().length > 0) {
|
|
9085
|
+
continue;
|
|
9086
|
+
}
|
|
9087
|
+
console.warn(
|
|
9088
|
+
`${dryRun ? "Would remove" : "Removing"} incomplete shared pack (missing ${nameEntry.name}${PACK_MANIFEST_SUFFIX}): ${indexRelative}`
|
|
9089
|
+
);
|
|
9090
|
+
if (!dryRun) {
|
|
9091
|
+
await (0, import_promises5.rm)(packDir, { force: true, recursive: true });
|
|
9092
|
+
}
|
|
9093
|
+
}
|
|
9094
|
+
}
|
|
9095
|
+
}
|
|
9048
9096
|
async function existingShareablePathspecs(git, worktree) {
|
|
9049
9097
|
const rootFiles = await Promise.all(
|
|
9050
9098
|
SHAREABLE_ROOT_FILES.map(
|
|
@@ -9071,7 +9119,8 @@ async function publishShareGitChange(worktree, relativePath, commitMessage, opti
|
|
|
9071
9119
|
const verb = options.verb ?? "add";
|
|
9072
9120
|
const git = await requiredExecutable("git");
|
|
9073
9121
|
const messages = [];
|
|
9074
|
-
const
|
|
9122
|
+
const paths = typeof relativePath === "string" ? [relativePath] : [...relativePath];
|
|
9123
|
+
const stageArgs = verb === "rm" ? ["-C", worktree, "rm", ...paths] : ["-C", worktree, "add", "--", ...paths];
|
|
9075
9124
|
const stageResult = await runGitCommand(dryRun, git, stageArgs, `git ${verb} failed`);
|
|
9076
9125
|
if (stageResult) {
|
|
9077
9126
|
messages.push(`git ${verb}: ${stageResult.stdout.trim() || "ok"}`);
|
|
@@ -9190,13 +9239,23 @@ async function runSharePublishArtifact(config, sourcePath, options) {
|
|
|
9190
9239
|
}
|
|
9191
9240
|
async function shareAgentArtifact(config, sourcePath, options) {
|
|
9192
9241
|
const team = await resolveTeam(config, options.team);
|
|
9193
|
-
const dryRun = options.dryRun === true;
|
|
9194
|
-
const preview = options.preview === true;
|
|
9195
9242
|
const resolvedSourcePath = expandPath(sourcePath);
|
|
9196
9243
|
if (!await isRegularFileNoSymlink(resolvedSourcePath)) {
|
|
9197
9244
|
throw new Error(`Agent artifact source is not a regular file: ${resolvedSourcePath}`);
|
|
9198
9245
|
}
|
|
9199
9246
|
const artifact = inferShareArtifact(resolvedSourcePath, options);
|
|
9247
|
+
if (artifact.kind === "skill") {
|
|
9248
|
+
const skillDir = (0, import_node_path6.dirname)(resolvedSourcePath);
|
|
9249
|
+
const members = await collectBundleMemberFiles(skillDir);
|
|
9250
|
+
if (members.length > 1) {
|
|
9251
|
+
return shareBundleArtifact(config, team, artifact, skillDir, members, options);
|
|
9252
|
+
}
|
|
9253
|
+
}
|
|
9254
|
+
return shareSingleArtifact(config, team, resolvedSourcePath, artifact, options);
|
|
9255
|
+
}
|
|
9256
|
+
async function shareSingleArtifact(config, team, resolvedSourcePath, artifact, options) {
|
|
9257
|
+
const dryRun = options.dryRun === true;
|
|
9258
|
+
const preview = options.preview === true;
|
|
9200
9259
|
const rawContent = await (0, import_promises5.readFile)(resolvedSourcePath, "utf8");
|
|
9201
9260
|
if (!rawContent.trim()) {
|
|
9202
9261
|
throw new Error(`Refusing to share empty agent artifact: ${resolvedSourcePath}`);
|
|
@@ -9264,6 +9323,702 @@ async function shareAgentArtifact(config, sourcePath, options) {
|
|
|
9264
9323
|
});
|
|
9265
9324
|
return { artifact, gitMessages, messages, sourcePath: resolvedSourcePath, targetPath, targetUri };
|
|
9266
9325
|
}
|
|
9326
|
+
async function shareBundleArtifact(config, team, artifact, skillDir, members, options) {
|
|
9327
|
+
const dryRun = options.dryRun === true;
|
|
9328
|
+
const preview = options.preview === true;
|
|
9329
|
+
const skillRootRelative = `${SHAREABLE_ARTIFACT_DIR}/skills/${artifact.agent}/${artifact.name}`;
|
|
9330
|
+
const skillRootTargetDir = (0, import_node_path6.join)(team.config.worktree, ...skillRootRelative.split("/"));
|
|
9331
|
+
const skillMdTargetPath = (0, import_node_path6.join)(skillRootTargetDir, "SKILL.md");
|
|
9332
|
+
const skillMdTargetUri = workfileToVikingUri(config, team.config, skillMdTargetPath);
|
|
9333
|
+
const skillRootTargetUri = parentUri(skillMdTargetUri);
|
|
9334
|
+
const skillMdSourcePath = (0, import_node_path6.join)(skillDir, "SKILL.md");
|
|
9335
|
+
const prepared = await Promise.all(
|
|
9336
|
+
members.map((member) => prepareBundleMember(config, team, member, skillRootTargetDir, options))
|
|
9337
|
+
);
|
|
9338
|
+
const skillMd = prepared.find((entry) => entry.relativePath === "SKILL.md");
|
|
9339
|
+
if (skillMd === void 0) {
|
|
9340
|
+
throw new Error(`Skill bundle ${artifact.agent}/${artifact.name} is missing SKILL.md.`);
|
|
9341
|
+
}
|
|
9342
|
+
if (!skillMd.binary && typeof skillMd.content === "string" && !skillMd.content.trim()) {
|
|
9343
|
+
throw new Error(`Refusing to share empty agent artifact: ${skillMdSourcePath}`);
|
|
9344
|
+
}
|
|
9345
|
+
const messages = [
|
|
9346
|
+
`${preview ? "Previewing" : dryRun ? "Would share" : "Sharing"} skill ${artifact.agent}/${artifact.name} bundle (${prepared.length} files)`,
|
|
9347
|
+
`Source: ${portablePath(skillDir)}`,
|
|
9348
|
+
`Destination: ${skillRootTargetUri}/`
|
|
9349
|
+
];
|
|
9350
|
+
const blockers = prepared.filter((entry) => entry.blocker !== void 0);
|
|
9351
|
+
if (preview) {
|
|
9352
|
+
for (const entry of prepared) {
|
|
9353
|
+
const flags = entry.binary ? ["binary"] : [];
|
|
9354
|
+
for (const redaction of entry.redactions) {
|
|
9355
|
+
flags.push(`redact ${redaction.count}\xD7 ${redaction.name}`);
|
|
9356
|
+
}
|
|
9357
|
+
const note = entry.blocker !== void 0 ? ` BLOCKED: ${entry.blocker}` : "";
|
|
9358
|
+
messages.push(` ${entry.relativePath}${flags.length > 0 ? ` [${flags.join(", ")}]` : ""}${note}`);
|
|
9359
|
+
}
|
|
9360
|
+
return {
|
|
9361
|
+
artifact,
|
|
9362
|
+
gitMessages: [],
|
|
9363
|
+
messages,
|
|
9364
|
+
previewContent: skillMd.binary ? void 0 : skillMd.content,
|
|
9365
|
+
sourcePath: skillMdSourcePath,
|
|
9366
|
+
targetPath: skillMdTargetPath,
|
|
9367
|
+
targetUri: skillMdTargetUri
|
|
9368
|
+
};
|
|
9369
|
+
}
|
|
9370
|
+
if (blockers.length > 0) {
|
|
9371
|
+
throw new Error(
|
|
9372
|
+
`Refusing to share skill ${artifact.agent}/${artifact.name}: ${blockers.map((entry) => `${entry.relativePath} (${entry.blocker})`).join("; ")}. Strip the value, pass --redact for local paths, or --allow-binary for binary files.`
|
|
9373
|
+
);
|
|
9374
|
+
}
|
|
9375
|
+
for (const entry of prepared) {
|
|
9376
|
+
for (const redaction of entry.redactions) {
|
|
9377
|
+
messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} in ${entry.relativePath} before sharing.`);
|
|
9378
|
+
}
|
|
9379
|
+
}
|
|
9380
|
+
for (const entry of prepared) {
|
|
9381
|
+
const existing = await readFileBytesIfExists(entry.targetPath);
|
|
9382
|
+
if (existing !== void 0 && sha256(existing) !== entry.sha256 && options.force !== true) {
|
|
9383
|
+
throw new Error(
|
|
9384
|
+
`Shared artifact already exists with different content: ${portablePath(entry.targetPath)}. Pass --force to replace it.`
|
|
9385
|
+
);
|
|
9386
|
+
}
|
|
9387
|
+
}
|
|
9388
|
+
if (dryRun) {
|
|
9389
|
+
messages.push(`Would write ${prepared.length} files under ${portablePath(skillRootTargetDir)}`);
|
|
9390
|
+
return {
|
|
9391
|
+
artifact,
|
|
9392
|
+
gitMessages: [],
|
|
9393
|
+
messages,
|
|
9394
|
+
sourcePath: skillMdSourcePath,
|
|
9395
|
+
targetPath: skillMdTargetPath,
|
|
9396
|
+
targetUri: skillMdTargetUri
|
|
9397
|
+
};
|
|
9398
|
+
}
|
|
9399
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
9400
|
+
const markdownMembers = orderSkillMdFirst(prepared.filter((entry) => entry.relativePath.endsWith(".md")));
|
|
9401
|
+
const otherMembers = prepared.filter((entry) => !entry.relativePath.endsWith(".md"));
|
|
9402
|
+
for (const entry of markdownMembers) {
|
|
9403
|
+
const ovHasResource = await vikingResourceExists(ov, config, entry.targetUri);
|
|
9404
|
+
await ensureSharedDirectoryChain(config, ov, entry.targetUri, dryRun, { quiet: true });
|
|
9405
|
+
await writeMemoryFile(
|
|
9406
|
+
config,
|
|
9407
|
+
ov,
|
|
9408
|
+
entry.targetUri,
|
|
9409
|
+
entry.content,
|
|
9410
|
+
ovHasResource ? "replace" : "create",
|
|
9411
|
+
dryRun,
|
|
9412
|
+
{ quiet: true }
|
|
9413
|
+
);
|
|
9414
|
+
}
|
|
9415
|
+
await ensureDirectory(skillRootTargetDir, false);
|
|
9416
|
+
for (const entry of otherMembers) {
|
|
9417
|
+
await ensureDirectory((0, import_node_path6.dirname)(entry.targetPath), false);
|
|
9418
|
+
await (0, import_promises5.writeFile)(entry.targetPath, entry.content, entry.binary ? { mode: 384 } : { encoding: "utf8", mode: 384 });
|
|
9419
|
+
}
|
|
9420
|
+
await (0, import_promises5.writeFile)((0, import_node_path6.join)(skillRootTargetDir, BUNDLE_MANIFEST_FILE), buildBundleManifest(artifact, prepared), {
|
|
9421
|
+
encoding: "utf8",
|
|
9422
|
+
mode: 384
|
|
9423
|
+
});
|
|
9424
|
+
const stagedPaths = [
|
|
9425
|
+
...prepared.map((entry) => `${skillRootRelative}/${entry.relativePath}`),
|
|
9426
|
+
`${skillRootRelative}/${BUNDLE_MANIFEST_FILE}`
|
|
9427
|
+
];
|
|
9428
|
+
const message = options.message ?? `share: publish skill ${artifact.agent}/${artifact.name} (${prepared.length} files)`;
|
|
9429
|
+
const gitMessages = await publishShareGitChange(team.config.worktree, stagedPaths, message, {
|
|
9430
|
+
dryRun,
|
|
9431
|
+
push: options.push
|
|
9432
|
+
});
|
|
9433
|
+
return {
|
|
9434
|
+
artifact,
|
|
9435
|
+
gitMessages,
|
|
9436
|
+
messages,
|
|
9437
|
+
sourcePath: skillMdSourcePath,
|
|
9438
|
+
targetPath: skillMdTargetPath,
|
|
9439
|
+
targetUri: skillMdTargetUri
|
|
9440
|
+
};
|
|
9441
|
+
}
|
|
9442
|
+
async function prepareBundleMember(config, team, member, skillRootTargetDir, options) {
|
|
9443
|
+
const buffer = await (0, import_promises5.readFile)(member.absolutePath);
|
|
9444
|
+
const targetPath = (0, import_node_path6.join)(skillRootTargetDir, ...member.relativePath.split("/"));
|
|
9445
|
+
const targetUri = workfileToVikingUri(config, team.config, targetPath);
|
|
9446
|
+
if (isProbablyBinary(buffer)) {
|
|
9447
|
+
const credential = detectBinaryCredential(buffer);
|
|
9448
|
+
const blocker = credential !== void 0 ? `possible ${credential} embedded in binary file` : options.allowBinary === true ? void 0 : "binary file (pass --allow-binary to include it unscanned)";
|
|
9449
|
+
return {
|
|
9450
|
+
binary: true,
|
|
9451
|
+
blocker,
|
|
9452
|
+
content: buffer,
|
|
9453
|
+
redactions: [],
|
|
9454
|
+
relativePath: member.relativePath,
|
|
9455
|
+
sha256: sha256(buffer),
|
|
9456
|
+
targetPath,
|
|
9457
|
+
targetUri
|
|
9458
|
+
};
|
|
9459
|
+
}
|
|
9460
|
+
const scrub = applyScrubber(buffer.toString("utf8"), { redact: options.redact === true });
|
|
9461
|
+
return {
|
|
9462
|
+
binary: false,
|
|
9463
|
+
blocker: scrub.blocker,
|
|
9464
|
+
content: scrub.cleaned,
|
|
9465
|
+
redactions: scrub.redactions,
|
|
9466
|
+
relativePath: member.relativePath,
|
|
9467
|
+
sha256: sha256(scrub.cleaned),
|
|
9468
|
+
targetPath,
|
|
9469
|
+
targetUri
|
|
9470
|
+
};
|
|
9471
|
+
}
|
|
9472
|
+
function orderSkillMdFirst(entries) {
|
|
9473
|
+
return [...entries].sort((a, b) => {
|
|
9474
|
+
if (a.relativePath === "SKILL.md") {
|
|
9475
|
+
return -1;
|
|
9476
|
+
}
|
|
9477
|
+
if (b.relativePath === "SKILL.md") {
|
|
9478
|
+
return 1;
|
|
9479
|
+
}
|
|
9480
|
+
return compareStrings(a.relativePath, b.relativePath);
|
|
9481
|
+
});
|
|
9482
|
+
}
|
|
9483
|
+
function buildBundleManifest(artifact, prepared) {
|
|
9484
|
+
const manifest = {
|
|
9485
|
+
artifact,
|
|
9486
|
+
members: prepared.map((entry) => ({ binary: entry.binary, path: entry.relativePath, sha256: entry.sha256 })).sort((a, b) => compareStrings(a.path, b.path)),
|
|
9487
|
+
version: BUNDLE_MANIFEST_VERSION
|
|
9488
|
+
};
|
|
9489
|
+
return `${JSON.stringify(manifest, void 0, 2)}
|
|
9490
|
+
`;
|
|
9491
|
+
}
|
|
9492
|
+
async function collectBundleMemberFiles(skillDir) {
|
|
9493
|
+
const out = [];
|
|
9494
|
+
async function visit(dir) {
|
|
9495
|
+
const entries = await (0, import_promises5.readdir)(dir, { withFileTypes: true });
|
|
9496
|
+
for (const entry of entries) {
|
|
9497
|
+
if (entry.isSymbolicLink()) {
|
|
9498
|
+
continue;
|
|
9499
|
+
}
|
|
9500
|
+
const full = (0, import_node_path6.join)(dir, entry.name);
|
|
9501
|
+
if (entry.isDirectory()) {
|
|
9502
|
+
if (!BUNDLE_IGNORE_DIR_NAMES.includes(entry.name)) {
|
|
9503
|
+
await visit(full);
|
|
9504
|
+
}
|
|
9505
|
+
continue;
|
|
9506
|
+
}
|
|
9507
|
+
if (!entry.isFile() || isIgnoredBundleFile(entry.name)) {
|
|
9508
|
+
continue;
|
|
9509
|
+
}
|
|
9510
|
+
out.push({ absolutePath: full, relativePath: (0, import_node_path6.relative)(skillDir, full).split(import_node_path6.sep).join("/") });
|
|
9511
|
+
}
|
|
9512
|
+
}
|
|
9513
|
+
await visit(skillDir);
|
|
9514
|
+
return out.sort((a, b) => compareStrings(a.relativePath, b.relativePath));
|
|
9515
|
+
}
|
|
9516
|
+
function isIgnoredBundleFile(name) {
|
|
9517
|
+
if (name === ".DS_Store" || name === BUNDLE_MANIFEST_FILE || name === BUNDLE_INSTALL_METADATA_FILE) {
|
|
9518
|
+
return true;
|
|
9519
|
+
}
|
|
9520
|
+
if (OV_SUMMARY_FILES.includes(name)) {
|
|
9521
|
+
return true;
|
|
9522
|
+
}
|
|
9523
|
+
return name.endsWith(".log") || name.endsWith(".threadnote-install.json");
|
|
9524
|
+
}
|
|
9525
|
+
function isProbablyBinary(buffer) {
|
|
9526
|
+
if (buffer.includes(0)) {
|
|
9527
|
+
return true;
|
|
9528
|
+
}
|
|
9529
|
+
try {
|
|
9530
|
+
new import_node_util.TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
9531
|
+
return false;
|
|
9532
|
+
} catch (_err) {
|
|
9533
|
+
return true;
|
|
9534
|
+
}
|
|
9535
|
+
}
|
|
9536
|
+
function detectBinaryCredential(buffer) {
|
|
9537
|
+
const latin1 = buffer.toString("latin1");
|
|
9538
|
+
for (const pattern of SCRUBBER_PATTERNS) {
|
|
9539
|
+
if (pattern.placeholder === void 0 && pattern.regex.test(latin1)) {
|
|
9540
|
+
return pattern.name;
|
|
9541
|
+
}
|
|
9542
|
+
}
|
|
9543
|
+
return void 0;
|
|
9544
|
+
}
|
|
9545
|
+
function detectBinaryLocalPath(buffer, rewriteRoots) {
|
|
9546
|
+
const latin1 = buffer.toString("latin1");
|
|
9547
|
+
for (const root of rewriteRoots) {
|
|
9548
|
+
if (root.length > 0 && latin1.includes(root)) {
|
|
9549
|
+
return "machine-local path";
|
|
9550
|
+
}
|
|
9551
|
+
}
|
|
9552
|
+
for (const pattern of SCRUBBER_PATTERNS) {
|
|
9553
|
+
if (pattern.placeholder !== void 0 && pattern.regex.test(latin1)) {
|
|
9554
|
+
return pattern.name;
|
|
9555
|
+
}
|
|
9556
|
+
}
|
|
9557
|
+
return void 0;
|
|
9558
|
+
}
|
|
9559
|
+
async function readFileBytesIfExists(path) {
|
|
9560
|
+
try {
|
|
9561
|
+
return await (0, import_promises5.readFile)(path);
|
|
9562
|
+
} catch (_err) {
|
|
9563
|
+
return void 0;
|
|
9564
|
+
}
|
|
9565
|
+
}
|
|
9566
|
+
function isBundleArtifact(artifact) {
|
|
9567
|
+
if (artifact.artifact.kind === "pack") {
|
|
9568
|
+
return true;
|
|
9569
|
+
}
|
|
9570
|
+
return artifact.members !== void 0 && artifact.members.length > 1;
|
|
9571
|
+
}
|
|
9572
|
+
function compareStrings(a, b) {
|
|
9573
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
9574
|
+
}
|
|
9575
|
+
async function runSharePublishBundle(config, manifestPath, options) {
|
|
9576
|
+
const result = await shareBundlePack(config, manifestPath, options);
|
|
9577
|
+
printShareArtifactResult(result, options.preview === true);
|
|
9578
|
+
}
|
|
9579
|
+
function parsePackManifest(raw, manifestPath) {
|
|
9580
|
+
const parsed = parseJsonConfigObject(raw);
|
|
9581
|
+
if (parsed === void 0) {
|
|
9582
|
+
throw new Error(`Invalid pack manifest (not a JSON object): ${manifestPath}`);
|
|
9583
|
+
}
|
|
9584
|
+
const name = parsed.name;
|
|
9585
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
9586
|
+
throw new Error(`Pack manifest must set a non-empty "name": ${manifestPath}`);
|
|
9587
|
+
}
|
|
9588
|
+
const agent = parsed.agent;
|
|
9589
|
+
if (agent !== "codex" && agent !== "claude") {
|
|
9590
|
+
throw new Error(`Pack manifest "agent" must be "codex" or "claude": ${manifestPath}`);
|
|
9591
|
+
}
|
|
9592
|
+
const stringArray2 = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
9593
|
+
const skills = stringArray2(parsed.skills);
|
|
9594
|
+
if (skills.length === 0) {
|
|
9595
|
+
throw new Error(`Pack manifest must list at least one skill in "skills": ${manifestPath}`);
|
|
9596
|
+
}
|
|
9597
|
+
const depsValue = parsed.deps ?? {};
|
|
9598
|
+
const pathRewrites = Array.isArray(parsed.pathRewrites) ? parsed.pathRewrites.map((entry) => typeof entry === "string" ? entry : entry?.from).filter((item) => typeof item === "string").map((rewrite) => rewrite.replace(/\/+$/, "")) : [];
|
|
9599
|
+
for (const rewrite of pathRewrites) {
|
|
9600
|
+
if (!(0, import_node_path6.isAbsolute)(rewrite) || rewrite.split("/").filter(Boolean).length < 2) {
|
|
9601
|
+
throw new Error(
|
|
9602
|
+
`Pack manifest pathRewrites entry must be an absolute repo-root path (got "${rewrite}"): ${manifestPath}`
|
|
9603
|
+
);
|
|
9604
|
+
}
|
|
9605
|
+
}
|
|
9606
|
+
return {
|
|
9607
|
+
agent,
|
|
9608
|
+
deps: {
|
|
9609
|
+
cli: stringArray2(depsValue.cli),
|
|
9610
|
+
mcp: stringArray2(depsValue.mcp),
|
|
9611
|
+
os: stringArray2(depsValue.os),
|
|
9612
|
+
runtime: stringArray2(depsValue.runtime)
|
|
9613
|
+
},
|
|
9614
|
+
description: typeof parsed.description === "string" ? parsed.description : void 0,
|
|
9615
|
+
include: stringArray2(parsed.include),
|
|
9616
|
+
name,
|
|
9617
|
+
pathRewrites,
|
|
9618
|
+
skills
|
|
9619
|
+
};
|
|
9620
|
+
}
|
|
9621
|
+
async function collectPackMembers(manifestDir, manifest) {
|
|
9622
|
+
const members = /* @__PURE__ */ new Map();
|
|
9623
|
+
const addEntry = async (entry) => {
|
|
9624
|
+
const normalized = entry.split("/").filter(Boolean).join("/");
|
|
9625
|
+
if (normalized.split("/").includes("..")) {
|
|
9626
|
+
throw new Error(`Pack manifest entries must stay within the pack root (got "${entry}").`);
|
|
9627
|
+
}
|
|
9628
|
+
const absolute = (0, import_node_path6.join)(manifestDir, ...normalized.split("/"));
|
|
9629
|
+
if (absolute !== manifestDir && !absolute.startsWith(manifestDir + import_node_path6.sep)) {
|
|
9630
|
+
throw new Error(`Pack manifest entry escapes the pack root: ${entry}`);
|
|
9631
|
+
}
|
|
9632
|
+
if (await isDirectory(absolute)) {
|
|
9633
|
+
for (const member of await collectBundleMemberFiles(absolute)) {
|
|
9634
|
+
const relativePath = `${normalized}/${member.relativePath}`;
|
|
9635
|
+
members.set(relativePath, { absolutePath: member.absolutePath, relativePath });
|
|
9636
|
+
}
|
|
9637
|
+
return;
|
|
9638
|
+
}
|
|
9639
|
+
if (await isRegularFileNoSymlink(absolute)) {
|
|
9640
|
+
members.set(normalized, { absolutePath: absolute, relativePath: normalized });
|
|
9641
|
+
return;
|
|
9642
|
+
}
|
|
9643
|
+
throw new Error(`Pack manifest references a missing path: ${entry}`);
|
|
9644
|
+
};
|
|
9645
|
+
for (const skill of manifest.skills) {
|
|
9646
|
+
const skillRel = skill.replace(/\/SKILL\.md$/i, "");
|
|
9647
|
+
const skillDir = (0, import_node_path6.join)(manifestDir, ...skillRel.split("/"));
|
|
9648
|
+
if (!await isFile((0, import_node_path6.join)(skillDir, "SKILL.md"))) {
|
|
9649
|
+
throw new Error(`Pack skill "${skill}" must be a directory containing SKILL.md.`);
|
|
9650
|
+
}
|
|
9651
|
+
await addEntry(skillRel);
|
|
9652
|
+
}
|
|
9653
|
+
for (const include of manifest.include) {
|
|
9654
|
+
await addEntry(include);
|
|
9655
|
+
}
|
|
9656
|
+
return [...members.values()].sort((a, b) => compareStrings(a.relativePath, b.relativePath));
|
|
9657
|
+
}
|
|
9658
|
+
function escapeRegExp2(value) {
|
|
9659
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9660
|
+
}
|
|
9661
|
+
function tokenizePackPaths(text, rewriteRoots) {
|
|
9662
|
+
let out = text;
|
|
9663
|
+
for (const root of rewriteRoots) {
|
|
9664
|
+
if (root.length > 0) {
|
|
9665
|
+
out = out.replace(
|
|
9666
|
+
new RegExp(`(?<![A-Za-z0-9/._~-])${escapeRegExp2(root)}(?=[/\\\\]|["'\`\\s),>:\\]};=]|$)`, "g"),
|
|
9667
|
+
PACK_ROOT_TOKEN
|
|
9668
|
+
);
|
|
9669
|
+
}
|
|
9670
|
+
}
|
|
9671
|
+
return out;
|
|
9672
|
+
}
|
|
9673
|
+
function residualRewriteRoot(content, rewriteRoots) {
|
|
9674
|
+
return rewriteRoots.find((root) => root.length > 0 && content.includes(root));
|
|
9675
|
+
}
|
|
9676
|
+
var PORTABLE_PATH_PREFIXES = [
|
|
9677
|
+
"/usr/",
|
|
9678
|
+
"/bin/",
|
|
9679
|
+
"/sbin/",
|
|
9680
|
+
"/lib/",
|
|
9681
|
+
"/lib64/",
|
|
9682
|
+
"/etc/",
|
|
9683
|
+
"/opt/homebrew/",
|
|
9684
|
+
"/tmp/",
|
|
9685
|
+
"/var/",
|
|
9686
|
+
"/private/var/",
|
|
9687
|
+
"/dev/",
|
|
9688
|
+
"/proc/",
|
|
9689
|
+
"/run/",
|
|
9690
|
+
"/sys/",
|
|
9691
|
+
"/Library/",
|
|
9692
|
+
"/System/",
|
|
9693
|
+
"/Applications/"
|
|
9694
|
+
];
|
|
9695
|
+
function unportableAbsolutePaths(content) {
|
|
9696
|
+
const scan = content.split(`${PACK_ROOT_TOKEN}/`).join("").split(PACK_ROOT_TOKEN).join("");
|
|
9697
|
+
const found = /* @__PURE__ */ new Set();
|
|
9698
|
+
for (const path of scan.match(/(?<![A-Za-z0-9._~$-])\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+/g) ?? []) {
|
|
9699
|
+
if (!PORTABLE_PATH_PREFIXES.some((prefix) => path.startsWith(prefix))) {
|
|
9700
|
+
found.add(path);
|
|
9701
|
+
}
|
|
9702
|
+
}
|
|
9703
|
+
for (const path of scan.match(/[A-Za-z]:\\[^\s"']+/g) ?? []) {
|
|
9704
|
+
found.add(path);
|
|
9705
|
+
}
|
|
9706
|
+
return [...found].sort((a, b) => compareStrings(a, b));
|
|
9707
|
+
}
|
|
9708
|
+
async function preparePackMember(config, team, member, filesTargetDir, rewriteRoots, options) {
|
|
9709
|
+
const buffer = await (0, import_promises5.readFile)(member.absolutePath);
|
|
9710
|
+
const targetPath = (0, import_node_path6.join)(filesTargetDir, ...member.relativePath.split("/"));
|
|
9711
|
+
const targetUri = workfileToVikingUri(config, team.config, targetPath);
|
|
9712
|
+
if (isProbablyBinary(buffer)) {
|
|
9713
|
+
const credential = detectBinaryCredential(buffer);
|
|
9714
|
+
const localPath = credential === void 0 ? detectBinaryLocalPath(buffer, rewriteRoots) : void 0;
|
|
9715
|
+
let blocker2;
|
|
9716
|
+
if (credential !== void 0) {
|
|
9717
|
+
blocker2 = `possible ${credential} embedded in binary file`;
|
|
9718
|
+
} else if (options.allowBinary !== true) {
|
|
9719
|
+
blocker2 = "binary file (pass --allow-binary to include it unscanned)";
|
|
9720
|
+
} else if (localPath !== void 0) {
|
|
9721
|
+
blocker2 = `possible ${localPath} embedded in binary file (cannot be rewritten)`;
|
|
9722
|
+
}
|
|
9723
|
+
return {
|
|
9724
|
+
binary: true,
|
|
9725
|
+
blocker: blocker2,
|
|
9726
|
+
content: buffer,
|
|
9727
|
+
redactions: [],
|
|
9728
|
+
relativePath: member.relativePath,
|
|
9729
|
+
sha256: sha256(buffer),
|
|
9730
|
+
targetPath,
|
|
9731
|
+
targetUri
|
|
9732
|
+
};
|
|
9733
|
+
}
|
|
9734
|
+
const text = buffer.toString("utf8");
|
|
9735
|
+
if (text.includes(PACK_ROOT_TOKEN)) {
|
|
9736
|
+
return {
|
|
9737
|
+
binary: false,
|
|
9738
|
+
blocker: `contains the reserved ${PACK_ROOT_TOKEN} token`,
|
|
9739
|
+
content: text,
|
|
9740
|
+
redactions: [],
|
|
9741
|
+
relativePath: member.relativePath,
|
|
9742
|
+
sha256: sha256(text),
|
|
9743
|
+
targetPath,
|
|
9744
|
+
targetUri
|
|
9745
|
+
};
|
|
9746
|
+
}
|
|
9747
|
+
const tokenized = tokenizePackPaths(text, rewriteRoots);
|
|
9748
|
+
const isMarkdown = member.relativePath.toLowerCase().endsWith(".md");
|
|
9749
|
+
const scrub = applyScrubber(tokenized, { redact: isMarkdown && options.redact === true });
|
|
9750
|
+
const residual = residualRewriteRoot(scrub.cleaned, rewriteRoots);
|
|
9751
|
+
const blocker = scrub.blocker ?? (residual !== void 0 ? `machine-local path "${residual}" not rewritten` : void 0);
|
|
9752
|
+
return {
|
|
9753
|
+
binary: false,
|
|
9754
|
+
blocker,
|
|
9755
|
+
content: scrub.cleaned,
|
|
9756
|
+
redactions: scrub.redactions,
|
|
9757
|
+
relativePath: member.relativePath,
|
|
9758
|
+
sha256: sha256(scrub.cleaned),
|
|
9759
|
+
targetPath,
|
|
9760
|
+
targetUri
|
|
9761
|
+
};
|
|
9762
|
+
}
|
|
9763
|
+
function buildPackIndex(artifact, manifest, skillNames, memberCount) {
|
|
9764
|
+
const lines = [
|
|
9765
|
+
"---",
|
|
9766
|
+
`name: ${artifact.name}`,
|
|
9767
|
+
`agent: ${artifact.agent}`,
|
|
9768
|
+
"kind: pack",
|
|
9769
|
+
`skills: [${skillNames.join(", ")}]`,
|
|
9770
|
+
"---",
|
|
9771
|
+
"",
|
|
9772
|
+
`# ${artifact.name} (skill pack)`,
|
|
9773
|
+
"",
|
|
9774
|
+
manifest.description ?? `A Threadnote skill pack bundling ${skillNames.length} skill(s) and their shared support files (${memberCount} files total).`,
|
|
9775
|
+
"",
|
|
9776
|
+
"## Skills",
|
|
9777
|
+
...skillNames.map((skill) => `- ${skill}`),
|
|
9778
|
+
"",
|
|
9779
|
+
"## Requirements",
|
|
9780
|
+
"Threadnote installs files only. Ensure these exist on the target machine before running:"
|
|
9781
|
+
];
|
|
9782
|
+
if (manifest.deps.runtime.length > 0) {
|
|
9783
|
+
lines.push(`- runtime: ${manifest.deps.runtime.join(", ")}`);
|
|
9784
|
+
}
|
|
9785
|
+
if (manifest.deps.cli.length > 0) {
|
|
9786
|
+
lines.push(`- CLI: ${manifest.deps.cli.join(", ")}`);
|
|
9787
|
+
}
|
|
9788
|
+
if (manifest.deps.os.length > 0) {
|
|
9789
|
+
lines.push(`- OS: ${manifest.deps.os.join(", ")}`);
|
|
9790
|
+
}
|
|
9791
|
+
if (manifest.deps.mcp.length > 0) {
|
|
9792
|
+
lines.push(`- MCP (configure separately): ${manifest.deps.mcp.join(", ")}`);
|
|
9793
|
+
}
|
|
9794
|
+
lines.push("", `Install: threadnote share install-artifacts --kind pack --name ${artifact.name} --apply`, "");
|
|
9795
|
+
return lines.join("\n");
|
|
9796
|
+
}
|
|
9797
|
+
function buildPackManifestJson(artifact, manifest, prepared) {
|
|
9798
|
+
const data = {
|
|
9799
|
+
artifact,
|
|
9800
|
+
deps: manifest.deps,
|
|
9801
|
+
members: prepared.map((entry) => ({ binary: entry.binary, path: entry.relativePath, sha256: entry.sha256 })).sort((a, b) => compareStrings(a.path, b.path)),
|
|
9802
|
+
version: BUNDLE_MANIFEST_VERSION
|
|
9803
|
+
};
|
|
9804
|
+
return `${JSON.stringify(data, void 0, 2)}
|
|
9805
|
+
`;
|
|
9806
|
+
}
|
|
9807
|
+
function packSkillName(skillEntry) {
|
|
9808
|
+
const trimmed = skillEntry.replace(/\/SKILL\.md$/i, "");
|
|
9809
|
+
return (0, import_node_path6.basename)(trimmed);
|
|
9810
|
+
}
|
|
9811
|
+
async function shareBundlePack(config, manifestPath, options) {
|
|
9812
|
+
const team = await resolveTeam(config, options.team);
|
|
9813
|
+
const dryRun = options.dryRun === true;
|
|
9814
|
+
const preview = options.preview === true;
|
|
9815
|
+
const resolvedManifest = expandPath(manifestPath);
|
|
9816
|
+
if (!await isRegularFileNoSymlink(resolvedManifest)) {
|
|
9817
|
+
throw new Error(`Pack manifest is not a regular file: ${resolvedManifest}`);
|
|
9818
|
+
}
|
|
9819
|
+
const manifest = parsePackManifest(await (0, import_promises5.readFile)(resolvedManifest, "utf8"), resolvedManifest);
|
|
9820
|
+
const manifestDir = (0, import_node_path6.dirname)(resolvedManifest);
|
|
9821
|
+
const artifact = { agent: manifest.agent, kind: "pack", name: uriSegment(manifest.name) };
|
|
9822
|
+
const skillNames = manifest.skills.map(packSkillName);
|
|
9823
|
+
const members = await collectPackMembers(manifestDir, manifest);
|
|
9824
|
+
const autoRoots = manifestDir.split("/").filter(Boolean).length >= 2 ? [manifestDir] : [];
|
|
9825
|
+
const rewriteRoots = [.../* @__PURE__ */ new Set([...autoRoots, ...manifest.pathRewrites])].sort((a, b) => b.length - a.length);
|
|
9826
|
+
const packRootRelative = `${SHAREABLE_ARTIFACT_DIR}/packs/${artifact.agent}/${artifact.name}`;
|
|
9827
|
+
const filesRelative = `${packRootRelative}/${PACK_FILES_DIR}`;
|
|
9828
|
+
const indexRelative = `${packRootRelative}/${artifact.name}${PACK_INDEX_SUFFIX}`;
|
|
9829
|
+
const manifestRelative = `${packRootRelative}/${artifact.name}${PACK_MANIFEST_SUFFIX}`;
|
|
9830
|
+
const filesTargetDir = (0, import_node_path6.join)(team.config.worktree, ...filesRelative.split("/"));
|
|
9831
|
+
const packRootTargetDir = (0, import_node_path6.join)(team.config.worktree, ...packRootRelative.split("/"));
|
|
9832
|
+
const indexTargetPath = (0, import_node_path6.join)(team.config.worktree, ...indexRelative.split("/"));
|
|
9833
|
+
const indexTargetUri = workfileToVikingUri(config, team.config, indexTargetPath);
|
|
9834
|
+
const prepared = await Promise.all(
|
|
9835
|
+
members.map((member) => preparePackMember(config, team, member, filesTargetDir, rewriteRoots, options))
|
|
9836
|
+
);
|
|
9837
|
+
const indexContent = tokenizePackPaths(buildPackIndex(artifact, manifest, skillNames, prepared.length), rewriteRoots);
|
|
9838
|
+
const indexScrub = applyScrubber(indexContent, { redact: options.redact === true });
|
|
9839
|
+
const messages = [
|
|
9840
|
+
`${preview ? "Previewing" : dryRun ? "Would share" : "Sharing"} pack ${artifact.agent}/${artifact.name} (${prepared.length} files, ${skillNames.length} skills)`,
|
|
9841
|
+
`Source: ${portablePath(manifestDir)}`,
|
|
9842
|
+
`Destination: ${indexTargetUri}`
|
|
9843
|
+
];
|
|
9844
|
+
const blockers = prepared.filter((entry) => entry.blocker !== void 0);
|
|
9845
|
+
if (preview) {
|
|
9846
|
+
for (const entry of prepared) {
|
|
9847
|
+
const flags = entry.binary ? ["binary"] : [];
|
|
9848
|
+
for (const redaction of entry.redactions) {
|
|
9849
|
+
flags.push(`redact ${redaction.count}\xD7 ${redaction.name}`);
|
|
9850
|
+
}
|
|
9851
|
+
const note = entry.blocker !== void 0 ? ` BLOCKED: ${entry.blocker}` : "";
|
|
9852
|
+
messages.push(` ${entry.relativePath}${flags.length > 0 ? ` [${flags.join(", ")}]` : ""}${note}`);
|
|
9853
|
+
}
|
|
9854
|
+
return {
|
|
9855
|
+
artifact,
|
|
9856
|
+
gitMessages: [],
|
|
9857
|
+
messages,
|
|
9858
|
+
previewContent: indexScrub.cleaned,
|
|
9859
|
+
sourcePath: resolvedManifest,
|
|
9860
|
+
targetPath: indexTargetPath,
|
|
9861
|
+
targetUri: indexTargetUri
|
|
9862
|
+
};
|
|
9863
|
+
}
|
|
9864
|
+
const indexResidual = residualRewriteRoot(indexScrub.cleaned, rewriteRoots);
|
|
9865
|
+
if (indexScrub.blocker !== void 0 || indexResidual !== void 0) {
|
|
9866
|
+
throw new Error(
|
|
9867
|
+
`Refusing to share pack ${artifact.agent}/${artifact.name}: index ${indexScrub.blocker ?? `machine-local path "${indexResidual}" not rewritten`}.`
|
|
9868
|
+
);
|
|
9869
|
+
}
|
|
9870
|
+
if (blockers.length > 0) {
|
|
9871
|
+
throw new Error(
|
|
9872
|
+
`Refusing to share pack ${artifact.agent}/${artifact.name}: ${blockers.map((entry) => `${entry.relativePath} (${entry.blocker})`).join("; ")}. Strip the value, pass --redact for local paths, or --allow-binary for binary files.`
|
|
9873
|
+
);
|
|
9874
|
+
}
|
|
9875
|
+
const packJson = applyScrubber(tokenizePackPaths(buildPackManifestJson(artifact, manifest, prepared), rewriteRoots), {
|
|
9876
|
+
redact: options.redact === true
|
|
9877
|
+
});
|
|
9878
|
+
const packJsonResidual = residualRewriteRoot(packJson.cleaned, rewriteRoots);
|
|
9879
|
+
if (packJson.blocker !== void 0 || packJsonResidual !== void 0) {
|
|
9880
|
+
throw new Error(
|
|
9881
|
+
`Refusing to share pack ${artifact.agent}/${artifact.name}: manifest ${packJson.blocker ?? `machine-local path "${packJsonResidual}" not rewritten`}.`
|
|
9882
|
+
);
|
|
9883
|
+
}
|
|
9884
|
+
for (const entry of prepared) {
|
|
9885
|
+
for (const redaction of entry.redactions) {
|
|
9886
|
+
messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} in ${entry.relativePath} before sharing.`);
|
|
9887
|
+
}
|
|
9888
|
+
}
|
|
9889
|
+
const unportable = /* @__PURE__ */ new Set();
|
|
9890
|
+
for (const entry of prepared) {
|
|
9891
|
+
if (!entry.binary && typeof entry.content === "string") {
|
|
9892
|
+
for (const path of unportableAbsolutePaths(entry.content)) {
|
|
9893
|
+
unportable.add(`${entry.relativePath}: ${path}`);
|
|
9894
|
+
}
|
|
9895
|
+
}
|
|
9896
|
+
}
|
|
9897
|
+
for (const path of unportableAbsolutePaths(indexScrub.cleaned)) {
|
|
9898
|
+
unportable.add(`${artifact.name}${PACK_INDEX_SUFFIX}: ${path}`);
|
|
9899
|
+
}
|
|
9900
|
+
for (const path of unportableAbsolutePaths(packJson.cleaned)) {
|
|
9901
|
+
unportable.add(`${artifact.name}${PACK_MANIFEST_SUFFIX}: ${path}`);
|
|
9902
|
+
}
|
|
9903
|
+
if (unportable.size > 0) {
|
|
9904
|
+
messages.push(
|
|
9905
|
+
`Warning: possible machine-local absolute path(s) that will not resolve on a teammate's machine (declare in pathRewrites or strip if not portable): ${[...unportable].join("; ")}`
|
|
9906
|
+
);
|
|
9907
|
+
}
|
|
9908
|
+
for (const entry of prepared) {
|
|
9909
|
+
const existing = await readFileBytesIfExists(entry.targetPath);
|
|
9910
|
+
if (existing !== void 0 && sha256(existing) !== entry.sha256 && options.force !== true) {
|
|
9911
|
+
throw new Error(
|
|
9912
|
+
`Shared pack file already exists with different content: ${portablePath(entry.targetPath)}. Pass --force to replace it.`
|
|
9913
|
+
);
|
|
9914
|
+
}
|
|
9915
|
+
}
|
|
9916
|
+
if (dryRun) {
|
|
9917
|
+
messages.push(`Would write ${prepared.length} files under ${portablePath(packRootTargetDir)}`);
|
|
9918
|
+
return {
|
|
9919
|
+
artifact,
|
|
9920
|
+
gitMessages: [],
|
|
9921
|
+
messages,
|
|
9922
|
+
sourcePath: resolvedManifest,
|
|
9923
|
+
targetPath: indexTargetPath,
|
|
9924
|
+
targetUri: indexTargetUri
|
|
9925
|
+
};
|
|
9926
|
+
}
|
|
9927
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
9928
|
+
const rollbacks = [];
|
|
9929
|
+
const manifestTargetPath = (0, import_node_path6.join)(team.config.worktree, ...manifestRelative.split("/"));
|
|
9930
|
+
try {
|
|
9931
|
+
const writeMarkdownMember = async (uri, content, worktreePath) => {
|
|
9932
|
+
const priorBytes = await readFileBytesIfExists(worktreePath);
|
|
9933
|
+
const hadResource = await vikingResourceExists(ov, config, uri);
|
|
9934
|
+
await ensureSharedDirectoryChain(config, ov, uri, dryRun, { quiet: true });
|
|
9935
|
+
await writeMemoryFile(config, ov, uri, content, hadResource ? "replace" : "create", dryRun, { quiet: true });
|
|
9936
|
+
rollbacks.push(async () => {
|
|
9937
|
+
if (priorBytes !== void 0) {
|
|
9938
|
+
await writeMemoryFile(config, ov, uri, priorBytes.toString("utf8"), "replace", false, { quiet: true });
|
|
9939
|
+
} else if (await vikingResourceExists(ov, config, uri)) {
|
|
9940
|
+
await removeMemoryUri(config, ov, uri, false, { quiet: true });
|
|
9941
|
+
}
|
|
9942
|
+
});
|
|
9943
|
+
};
|
|
9944
|
+
await writeMarkdownMember(indexTargetUri, indexScrub.cleaned, indexTargetPath);
|
|
9945
|
+
for (const entry of prepared.filter((member) => member.relativePath.endsWith(".md"))) {
|
|
9946
|
+
await writeMarkdownMember(entry.targetUri, entry.content, entry.targetPath);
|
|
9947
|
+
}
|
|
9948
|
+
await ensureDirectory(filesTargetDir, false);
|
|
9949
|
+
for (const entry of prepared.filter((member) => !member.relativePath.endsWith(".md"))) {
|
|
9950
|
+
const priorBytes = await readFileBytesIfExists(entry.targetPath);
|
|
9951
|
+
await ensureDirectory((0, import_node_path6.dirname)(entry.targetPath), false);
|
|
9952
|
+
await (0, import_promises5.writeFile)(entry.targetPath, entry.content, entry.binary ? { mode: 384 } : { encoding: "utf8", mode: 384 });
|
|
9953
|
+
rollbacks.push(async () => {
|
|
9954
|
+
if (priorBytes !== void 0) {
|
|
9955
|
+
await (0, import_promises5.writeFile)(entry.targetPath, priorBytes, { mode: 384 });
|
|
9956
|
+
} else {
|
|
9957
|
+
await (0, import_promises5.rm)(entry.targetPath, { force: true });
|
|
9958
|
+
}
|
|
9959
|
+
});
|
|
9960
|
+
}
|
|
9961
|
+
await ensureDirectory(packRootTargetDir, false);
|
|
9962
|
+
const priorManifest = await readFileBytesIfExists(manifestTargetPath);
|
|
9963
|
+
await (0, import_promises5.writeFile)(manifestTargetPath, packJson.cleaned, { encoding: "utf8", mode: 384 });
|
|
9964
|
+
rollbacks.push(async () => {
|
|
9965
|
+
if (priorManifest !== void 0) {
|
|
9966
|
+
await (0, import_promises5.writeFile)(manifestTargetPath, priorManifest, { mode: 384 });
|
|
9967
|
+
} else {
|
|
9968
|
+
await (0, import_promises5.rm)(manifestTargetPath, { force: true });
|
|
9969
|
+
}
|
|
9970
|
+
});
|
|
9971
|
+
const currentFiles = new Set(prepared.map((entry) => `${filesRelative}/${entry.relativePath}`));
|
|
9972
|
+
const git = await requiredExecutable("git");
|
|
9973
|
+
const tracked = await runCommand(git, ["-C", team.config.worktree, "ls-files", "--", filesRelative], {
|
|
9974
|
+
allowFailure: true
|
|
9975
|
+
});
|
|
9976
|
+
const stalePaths = tracked.exitCode === 0 ? tracked.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !currentFiles.has(line)) : [];
|
|
9977
|
+
for (const stale of stalePaths) {
|
|
9978
|
+
await runCommand(git, ["-C", team.config.worktree, "rm", "-f", "--ignore-unmatch", "--", stale], {
|
|
9979
|
+
allowFailure: true
|
|
9980
|
+
});
|
|
9981
|
+
if (stale.endsWith(".md")) {
|
|
9982
|
+
const staleUri = workfileToVikingUri(config, team.config, (0, import_node_path6.join)(team.config.worktree, ...stale.split("/")));
|
|
9983
|
+
try {
|
|
9984
|
+
if (await vikingResourceExists(ov, config, staleUri)) {
|
|
9985
|
+
await removeMemoryUri(config, ov, staleUri, dryRun, { quiet: true });
|
|
9986
|
+
}
|
|
9987
|
+
} catch (pruneErr) {
|
|
9988
|
+
messages.push(
|
|
9989
|
+
`Warning: could not remove stale OpenViking resource ${staleUri}: ${pruneErr instanceof Error ? pruneErr.message : String(pruneErr)}`
|
|
9990
|
+
);
|
|
9991
|
+
}
|
|
9992
|
+
}
|
|
9993
|
+
}
|
|
9994
|
+
} catch (publishErr) {
|
|
9995
|
+
for (const undo of rollbacks.reverse()) {
|
|
9996
|
+
try {
|
|
9997
|
+
await undo();
|
|
9998
|
+
} catch (_cleanupErr) {
|
|
9999
|
+
}
|
|
10000
|
+
}
|
|
10001
|
+
throw publishErr;
|
|
10002
|
+
}
|
|
10003
|
+
const stagedPaths = [
|
|
10004
|
+
indexRelative,
|
|
10005
|
+
manifestRelative,
|
|
10006
|
+
...prepared.map((entry) => `${filesRelative}/${entry.relativePath}`)
|
|
10007
|
+
];
|
|
10008
|
+
const message = options.message ?? `share: publish pack ${artifact.agent}/${artifact.name} (${prepared.length} files)`;
|
|
10009
|
+
const gitMessages = await publishShareGitChange(team.config.worktree, stagedPaths, message, {
|
|
10010
|
+
dryRun,
|
|
10011
|
+
push: options.push
|
|
10012
|
+
});
|
|
10013
|
+
return {
|
|
10014
|
+
artifact,
|
|
10015
|
+
gitMessages,
|
|
10016
|
+
messages,
|
|
10017
|
+
sourcePath: resolvedManifest,
|
|
10018
|
+
targetPath: indexTargetPath,
|
|
10019
|
+
targetUri: indexTargetUri
|
|
10020
|
+
};
|
|
10021
|
+
}
|
|
9267
10022
|
async function runShareInstallArtifacts(config, options) {
|
|
9268
10023
|
const result = await installSharedAgentArtifacts(config, options);
|
|
9269
10024
|
if (result.syncedTeams.length > 0) {
|
|
@@ -9303,6 +10058,10 @@ async function installSharedAgentArtifacts(config, options) {
|
|
|
9303
10058
|
}
|
|
9304
10059
|
let installedCount = 0;
|
|
9305
10060
|
for (const artifact of artifacts) {
|
|
10061
|
+
if (isBundleArtifact(artifact)) {
|
|
10062
|
+
installedCount += await installBundleArtifact(artifact, options, dryRun, messages);
|
|
10063
|
+
continue;
|
|
10064
|
+
}
|
|
9306
10065
|
const label = sharedArtifactLabel(artifact.artifact);
|
|
9307
10066
|
const state = await sharedArtifactInstallState(artifact);
|
|
9308
10067
|
if (dryRun) {
|
|
@@ -9828,6 +10587,9 @@ function sharedArtifactFromRelativePath(relativePath) {
|
|
|
9828
10587
|
if (parts.length === 4 && parts[1] === "commands" && parts[2] === "claude" && parts[3].endsWith(".md")) {
|
|
9829
10588
|
return { agent: "claude", kind: "command", name: parts[3].slice(0, -".md".length) };
|
|
9830
10589
|
}
|
|
10590
|
+
if (parts.length === 5 && parts[1] === "packs" && (parts[2] === "codex" || parts[2] === "claude") && parts[4] === `${parts[3]}${PACK_INDEX_SUFFIX}`) {
|
|
10591
|
+
return { agent: parts[2], kind: "pack", name: parts[3] };
|
|
10592
|
+
}
|
|
9831
10593
|
return void 0;
|
|
9832
10594
|
}
|
|
9833
10595
|
async function collectSharedArtifacts(worktree, team) {
|
|
@@ -9852,18 +10614,95 @@ async function collectSharedArtifacts(worktree, team) {
|
|
|
9852
10614
|
if (artifact === void 0) {
|
|
9853
10615
|
continue;
|
|
9854
10616
|
}
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
}
|
|
10617
|
+
const artifactDir = (0, import_node_path6.dirname)(full);
|
|
10618
|
+
if (artifact.kind === "pack" && !await isFile((0, import_node_path6.join)(artifactDir, `${artifact.name}${PACK_MANIFEST_SUFFIX}`))) {
|
|
10619
|
+
console.warn(
|
|
10620
|
+
`Skipping incomplete shared pack (missing ${artifact.name}${PACK_MANIFEST_SUFFIX}): ${relativePath}`
|
|
10621
|
+
);
|
|
10622
|
+
continue;
|
|
10623
|
+
}
|
|
10624
|
+
try {
|
|
10625
|
+
out.push({
|
|
10626
|
+
artifact,
|
|
10627
|
+
installPath: sharedArtifactInstallPath(team, artifact),
|
|
10628
|
+
members: await collectArtifactMembers(artifact, artifactDir),
|
|
10629
|
+
sourcePath: full,
|
|
10630
|
+
sourceRelativePath: relativePath,
|
|
10631
|
+
team
|
|
10632
|
+
});
|
|
10633
|
+
} catch (err) {
|
|
10634
|
+
console.warn(`Skipping shared artifact ${relativePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
10635
|
+
}
|
|
9862
10636
|
}
|
|
9863
10637
|
}
|
|
9864
10638
|
await visit(root);
|
|
9865
10639
|
return out.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
|
|
9866
10640
|
}
|
|
10641
|
+
async function collectArtifactMembers(artifact, artifactDir) {
|
|
10642
|
+
if (artifact.kind === "skill") {
|
|
10643
|
+
return collectSharedBundleMembers(artifactDir);
|
|
10644
|
+
}
|
|
10645
|
+
if (artifact.kind === "pack") {
|
|
10646
|
+
return collectSharedPackMembers(artifact, artifactDir);
|
|
10647
|
+
}
|
|
10648
|
+
return void 0;
|
|
10649
|
+
}
|
|
10650
|
+
function isContainedMemberPath(baseDir, relativePath) {
|
|
10651
|
+
if ((0, import_node_path6.isAbsolute)(relativePath) || relativePath.split("/").includes("..")) {
|
|
10652
|
+
return false;
|
|
10653
|
+
}
|
|
10654
|
+
const resolved = (0, import_node_path6.join)(baseDir, ...relativePath.split("/"));
|
|
10655
|
+
return resolved === baseDir || resolved.startsWith(baseDir + import_node_path6.sep);
|
|
10656
|
+
}
|
|
10657
|
+
async function collectSharedPackMembers(artifact, packDir) {
|
|
10658
|
+
const filesDir = (0, import_node_path6.join)(packDir, PACK_FILES_DIR);
|
|
10659
|
+
if (!await isDirectory(filesDir)) {
|
|
10660
|
+
return [];
|
|
10661
|
+
}
|
|
10662
|
+
const manifestRaw = await readFileIfExists((0, import_node_path6.join)(packDir, `${artifact.name}${PACK_MANIFEST_SUFFIX}`));
|
|
10663
|
+
if (manifestRaw !== void 0) {
|
|
10664
|
+
const rawMembers = parseJsonConfigObject(manifestRaw)?.members;
|
|
10665
|
+
if (Array.isArray(rawMembers)) {
|
|
10666
|
+
const fromManifest = [];
|
|
10667
|
+
for (const entry of rawMembers) {
|
|
10668
|
+
const path = entry?.path;
|
|
10669
|
+
if (typeof path === "string" && path.length > 0) {
|
|
10670
|
+
if (!isContainedMemberPath(filesDir, path)) {
|
|
10671
|
+
throw new Error(`Refusing pack member with an unsafe path that escapes the pack root: ${path}`);
|
|
10672
|
+
}
|
|
10673
|
+
fromManifest.push({ absolutePath: (0, import_node_path6.join)(filesDir, ...path.split("/")), relativePath: path });
|
|
10674
|
+
}
|
|
10675
|
+
}
|
|
10676
|
+
if (fromManifest.length > 0) {
|
|
10677
|
+
return fromManifest.sort((a, b) => compareStrings(a.relativePath, b.relativePath));
|
|
10678
|
+
}
|
|
10679
|
+
}
|
|
10680
|
+
}
|
|
10681
|
+
return collectBundleMemberFiles(filesDir);
|
|
10682
|
+
}
|
|
10683
|
+
async function collectSharedBundleMembers(skillDir) {
|
|
10684
|
+
const manifestRaw = await readFileIfExists((0, import_node_path6.join)(skillDir, BUNDLE_MANIFEST_FILE));
|
|
10685
|
+
if (manifestRaw !== void 0) {
|
|
10686
|
+
const parsed = parseJsonConfigObject(manifestRaw);
|
|
10687
|
+
const rawMembers = parsed?.members;
|
|
10688
|
+
if (Array.isArray(rawMembers)) {
|
|
10689
|
+
const fromManifest = [];
|
|
10690
|
+
for (const entry of rawMembers) {
|
|
10691
|
+
const path = entry?.path;
|
|
10692
|
+
if (typeof path === "string" && path.length > 0) {
|
|
10693
|
+
if (!isContainedMemberPath(skillDir, path)) {
|
|
10694
|
+
throw new Error(`Refusing skill member with an unsafe path that escapes the skill root: ${path}`);
|
|
10695
|
+
}
|
|
10696
|
+
fromManifest.push({ absolutePath: (0, import_node_path6.join)(skillDir, ...path.split("/")), relativePath: path });
|
|
10697
|
+
}
|
|
10698
|
+
}
|
|
10699
|
+
if (fromManifest.length > 0) {
|
|
10700
|
+
return fromManifest.sort((a, b) => compareStrings(a.relativePath, b.relativePath));
|
|
10701
|
+
}
|
|
10702
|
+
}
|
|
10703
|
+
}
|
|
10704
|
+
return collectBundleMemberFiles(skillDir);
|
|
10705
|
+
}
|
|
9867
10706
|
function filterSharedArtifacts(artifacts, options) {
|
|
9868
10707
|
const name = options.name === void 0 ? void 0 : uriSegment(options.name);
|
|
9869
10708
|
return artifacts.filter((artifact) => {
|
|
@@ -9885,6 +10724,218 @@ async function maybeSyncSharedArtifacts(config, options) {
|
|
|
9885
10724
|
}
|
|
9886
10725
|
return syncSharedReposBeforeAgentRead(config);
|
|
9887
10726
|
}
|
|
10727
|
+
function bundleInstallRoot(artifact) {
|
|
10728
|
+
return artifact.artifact.kind === "pack" ? artifact.installPath : (0, import_node_path6.dirname)(artifact.installPath);
|
|
10729
|
+
}
|
|
10730
|
+
function bundleInstallMetadataPath(artifact) {
|
|
10731
|
+
return (0, import_node_path6.join)(bundleInstallRoot(artifact), BUNDLE_INSTALL_METADATA_FILE);
|
|
10732
|
+
}
|
|
10733
|
+
async function readBundleInstallMetadata(artifact) {
|
|
10734
|
+
const raw = await readFileIfExists(bundleInstallMetadataPath(artifact));
|
|
10735
|
+
if (raw === void 0) {
|
|
10736
|
+
return void 0;
|
|
10737
|
+
}
|
|
10738
|
+
const parsed = parseJsonConfigObject(raw);
|
|
10739
|
+
if (parsed === void 0 || parsed.version !== ARTIFACT_INSTALL_METADATA_VERSION || !Array.isArray(parsed.members)) {
|
|
10740
|
+
return void 0;
|
|
10741
|
+
}
|
|
10742
|
+
const recordedArtifact = parsed.artifact;
|
|
10743
|
+
if (recordedArtifact?.agent !== artifact.artifact.agent || recordedArtifact?.kind !== artifact.artifact.kind || recordedArtifact?.name !== artifact.artifact.name || parsed.team !== artifact.team) {
|
|
10744
|
+
return void 0;
|
|
10745
|
+
}
|
|
10746
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
10747
|
+
for (const entry of parsed.members) {
|
|
10748
|
+
const path = entry?.path;
|
|
10749
|
+
const sourceSha256 = entry?.sourceSha256;
|
|
10750
|
+
const installedSha256 = entry?.installedSha256;
|
|
10751
|
+
if (typeof path === "string" && typeof sourceSha256 === "string" && typeof installedSha256 === "string") {
|
|
10752
|
+
map2.set(path, { installedSha256, sourceSha256 });
|
|
10753
|
+
}
|
|
10754
|
+
}
|
|
10755
|
+
return map2;
|
|
10756
|
+
}
|
|
10757
|
+
async function sharedBundleInstallStatus(artifact) {
|
|
10758
|
+
const members = artifact.members ?? [];
|
|
10759
|
+
const installRoot = bundleInstallRoot(artifact);
|
|
10760
|
+
const metadata = await readBundleInstallMetadata(artifact);
|
|
10761
|
+
const expanded = await prepareInstallMembers(members, installRoot, artifact.artifact.kind === "pack");
|
|
10762
|
+
const expectedByPath = new Map(expanded.map((entry) => [entry.relativePath, entry]));
|
|
10763
|
+
let installedCount = 0;
|
|
10764
|
+
let localChanged = false;
|
|
10765
|
+
let remoteChanged = false;
|
|
10766
|
+
const memberPaths = /* @__PURE__ */ new Set();
|
|
10767
|
+
for (const member of members) {
|
|
10768
|
+
memberPaths.add(member.relativePath);
|
|
10769
|
+
const expected = expectedByPath.get(member.relativePath);
|
|
10770
|
+
if (expected === void 0) {
|
|
10771
|
+
remoteChanged = true;
|
|
10772
|
+
continue;
|
|
10773
|
+
}
|
|
10774
|
+
const installedBytes = await readFileBytesIfExists((0, import_node_path6.join)(installRoot, ...member.relativePath.split("/")));
|
|
10775
|
+
if (installedBytes === void 0) {
|
|
10776
|
+
remoteChanged = true;
|
|
10777
|
+
continue;
|
|
10778
|
+
}
|
|
10779
|
+
installedCount += 1;
|
|
10780
|
+
const installedSha = sha256(installedBytes);
|
|
10781
|
+
const recorded = metadata?.get(member.relativePath);
|
|
10782
|
+
if (recorded === void 0) {
|
|
10783
|
+
if (installedSha !== expected?.installedSha256) {
|
|
10784
|
+
localChanged = true;
|
|
10785
|
+
}
|
|
10786
|
+
continue;
|
|
10787
|
+
}
|
|
10788
|
+
if (installedSha !== recorded.installedSha256) {
|
|
10789
|
+
localChanged = true;
|
|
10790
|
+
}
|
|
10791
|
+
if (expected?.sourceSha256 !== recorded.sourceSha256) {
|
|
10792
|
+
remoteChanged = true;
|
|
10793
|
+
}
|
|
10794
|
+
}
|
|
10795
|
+
if (metadata !== void 0) {
|
|
10796
|
+
for (const [recordedPath, recorded] of metadata) {
|
|
10797
|
+
if (memberPaths.has(recordedPath)) {
|
|
10798
|
+
continue;
|
|
10799
|
+
}
|
|
10800
|
+
remoteChanged = true;
|
|
10801
|
+
const installedBytes = await readFileBytesIfExists((0, import_node_path6.join)(installRoot, ...recordedPath.split("/")));
|
|
10802
|
+
if (installedBytes !== void 0 && sha256(installedBytes) !== recorded.installedSha256) {
|
|
10803
|
+
localChanged = true;
|
|
10804
|
+
}
|
|
10805
|
+
}
|
|
10806
|
+
}
|
|
10807
|
+
if (installedCount === 0 && metadata === void 0) {
|
|
10808
|
+
return "not_installed";
|
|
10809
|
+
}
|
|
10810
|
+
if (localChanged && remoteChanged) {
|
|
10811
|
+
return "remote_changed_and_local_modified";
|
|
10812
|
+
}
|
|
10813
|
+
if (remoteChanged) {
|
|
10814
|
+
return "update_available";
|
|
10815
|
+
}
|
|
10816
|
+
if (localChanged) {
|
|
10817
|
+
return "local_modified";
|
|
10818
|
+
}
|
|
10819
|
+
return "current";
|
|
10820
|
+
}
|
|
10821
|
+
function expandPackRoot(text, installRoot) {
|
|
10822
|
+
return text.split(PACK_ROOT_TOKEN).join(installRoot);
|
|
10823
|
+
}
|
|
10824
|
+
async function prepareInstallMembers(members, installRoot, expandTokens) {
|
|
10825
|
+
const prepared = await Promise.all(
|
|
10826
|
+
members.map(async (member) => {
|
|
10827
|
+
const sourceBytes = await readFileBytesIfExists(member.absolutePath);
|
|
10828
|
+
if (sourceBytes === void 0) {
|
|
10829
|
+
return void 0;
|
|
10830
|
+
}
|
|
10831
|
+
const installedBytes = expandTokens && !isProbablyBinary(sourceBytes) ? Buffer.from(expandPackRoot(sourceBytes.toString("utf8"), installRoot), "utf8") : sourceBytes;
|
|
10832
|
+
return {
|
|
10833
|
+
installedBytes,
|
|
10834
|
+
installedSha256: sha256(installedBytes),
|
|
10835
|
+
relativePath: member.relativePath,
|
|
10836
|
+
sourceSha256: sha256(sourceBytes)
|
|
10837
|
+
};
|
|
10838
|
+
})
|
|
10839
|
+
);
|
|
10840
|
+
return prepared.filter((member) => member !== void 0);
|
|
10841
|
+
}
|
|
10842
|
+
function serializeInstallMetadata(artifact, prepared) {
|
|
10843
|
+
const metadata = {
|
|
10844
|
+
artifact: artifact.artifact,
|
|
10845
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10846
|
+
members: prepared.map((entry) => ({
|
|
10847
|
+
installedSha256: entry.installedSha256,
|
|
10848
|
+
path: entry.relativePath,
|
|
10849
|
+
sourceSha256: entry.sourceSha256
|
|
10850
|
+
})).sort((a, b) => compareStrings(a.path, b.path)),
|
|
10851
|
+
team: artifact.team,
|
|
10852
|
+
version: ARTIFACT_INSTALL_METADATA_VERSION
|
|
10853
|
+
};
|
|
10854
|
+
return `${JSON.stringify(metadata, void 0, 2)}
|
|
10855
|
+
`;
|
|
10856
|
+
}
|
|
10857
|
+
async function installBundleArtifact(artifact, options, dryRun, messages) {
|
|
10858
|
+
const members = artifact.members ?? [];
|
|
10859
|
+
const installRoot = bundleInstallRoot(artifact);
|
|
10860
|
+
const kindLabel = artifact.artifact.kind === "pack" ? "pack" : "bundle";
|
|
10861
|
+
const label = `${sharedArtifactLabel(artifact.artifact)} ${kindLabel} (${members.length} files)`;
|
|
10862
|
+
const status = await sharedBundleInstallStatus(artifact);
|
|
10863
|
+
if (dryRun) {
|
|
10864
|
+
const verb = sharedArtifactDryRunVerb(status, options.force === true);
|
|
10865
|
+
const suffix = sharedArtifactDryRunSuffix(status, options.force === true);
|
|
10866
|
+
messages.push(`${verb} ${label}: ${portablePath(installRoot)}${suffix}`);
|
|
10867
|
+
return 0;
|
|
10868
|
+
}
|
|
10869
|
+
if ((status === "local_modified" || status === "remote_changed_and_local_modified") && options.force !== true) {
|
|
10870
|
+
throw new Error(`Refusing to overwrite ${portablePath(installRoot)}. Pass force=true or --force.`);
|
|
10871
|
+
}
|
|
10872
|
+
const prepared = await prepareInstallMembers(members, installRoot, artifact.artifact.kind === "pack");
|
|
10873
|
+
if (prepared.length < members.length && options.force !== true) {
|
|
10874
|
+
throw new Error(
|
|
10875
|
+
`Refusing to install ${portablePath(installRoot)}: ${members.length - prepared.length} declared member(s) are unreadable in the shared pack (the shared worktree may be mid-sync). Retry after sync, or pass force=true / --force.`
|
|
10876
|
+
);
|
|
10877
|
+
}
|
|
10878
|
+
if (status === "current") {
|
|
10879
|
+
await (0, import_promises5.writeFile)(bundleInstallMetadataPath(artifact), serializeInstallMetadata(artifact, prepared), { mode: 384 });
|
|
10880
|
+
messages.push(`Already installed ${label}: ${portablePath(installRoot)}`);
|
|
10881
|
+
await surfacePackRequirements(artifact, messages);
|
|
10882
|
+
return 0;
|
|
10883
|
+
}
|
|
10884
|
+
const stagingRoot = `${installRoot}.threadnote-staging`;
|
|
10885
|
+
await (0, import_promises5.rm)(stagingRoot, { force: true, recursive: true });
|
|
10886
|
+
for (const entry of prepared) {
|
|
10887
|
+
const dest = (0, import_node_path6.join)(stagingRoot, ...entry.relativePath.split("/"));
|
|
10888
|
+
await ensureDirectory((0, import_node_path6.dirname)(dest), false);
|
|
10889
|
+
await (0, import_promises5.writeFile)(dest, entry.installedBytes, { mode: 384 });
|
|
10890
|
+
}
|
|
10891
|
+
await (0, import_promises5.writeFile)((0, import_node_path6.join)(stagingRoot, BUNDLE_INSTALL_METADATA_FILE), serializeInstallMetadata(artifact, prepared), {
|
|
10892
|
+
mode: 384
|
|
10893
|
+
});
|
|
10894
|
+
await ensureDirectory((0, import_node_path6.dirname)(installRoot), false);
|
|
10895
|
+
const backupRoot = `${installRoot}.threadnote-old`;
|
|
10896
|
+
await (0, import_promises5.rm)(backupRoot, { force: true, recursive: true });
|
|
10897
|
+
const hadPriorInstall = await exists(installRoot);
|
|
10898
|
+
if (hadPriorInstall) {
|
|
10899
|
+
await (0, import_promises5.rename)(installRoot, backupRoot);
|
|
10900
|
+
}
|
|
10901
|
+
try {
|
|
10902
|
+
await (0, import_promises5.rename)(stagingRoot, installRoot);
|
|
10903
|
+
} catch (swapErr) {
|
|
10904
|
+
if (hadPriorInstall) {
|
|
10905
|
+
await (0, import_promises5.rename)(backupRoot, installRoot);
|
|
10906
|
+
}
|
|
10907
|
+
throw swapErr;
|
|
10908
|
+
}
|
|
10909
|
+
await (0, import_promises5.rm)(backupRoot, { force: true, recursive: true });
|
|
10910
|
+
messages.push(`${sharedArtifactInstallVerb(status, options.force === true)} ${label}: ${portablePath(installRoot)}`);
|
|
10911
|
+
await surfacePackRequirements(artifact, messages);
|
|
10912
|
+
return 1;
|
|
10913
|
+
}
|
|
10914
|
+
async function surfacePackRequirements(artifact, messages) {
|
|
10915
|
+
if (artifact.artifact.kind !== "pack") {
|
|
10916
|
+
return;
|
|
10917
|
+
}
|
|
10918
|
+
const raw = await readFileIfExists(
|
|
10919
|
+
(0, import_node_path6.join)((0, import_node_path6.dirname)(artifact.sourcePath), `${artifact.artifact.name}${PACK_MANIFEST_SUFFIX}`)
|
|
10920
|
+
);
|
|
10921
|
+
if (raw === void 0) {
|
|
10922
|
+
return;
|
|
10923
|
+
}
|
|
10924
|
+
const deps = parseJsonConfigObject(raw)?.deps;
|
|
10925
|
+
if (deps === void 0 || typeof deps !== "object") {
|
|
10926
|
+
return;
|
|
10927
|
+
}
|
|
10928
|
+
const stringList = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
10929
|
+
const depsRecord = deps;
|
|
10930
|
+
const tooling = [...stringList(depsRecord.runtime), ...stringList(depsRecord.cli), ...stringList(depsRecord.os)];
|
|
10931
|
+
const mcp = stringList(depsRecord.mcp);
|
|
10932
|
+
if (tooling.length > 0) {
|
|
10933
|
+
messages.push(`This pack will NOT run until these exist (Threadnote installs files only): ${tooling.join(", ")}.`);
|
|
10934
|
+
}
|
|
10935
|
+
if (mcp.length > 0) {
|
|
10936
|
+
messages.push(`Configure these MCP server(s) separately: ${mcp.join(", ")}.`);
|
|
10937
|
+
}
|
|
10938
|
+
}
|
|
9888
10939
|
async function sharedArtifactInstallState(artifact) {
|
|
9889
10940
|
const sourceContent = await (0, import_promises5.readFile)(artifact.sourcePath, "utf8");
|
|
9890
10941
|
const sourceSha = sha256(sourceContent);
|
|
@@ -10007,11 +11058,12 @@ function sharedArtifactLabel(artifact) {
|
|
|
10007
11058
|
return `${artifact.kind} ${artifact.agent}/${artifact.name}`;
|
|
10008
11059
|
}
|
|
10009
11060
|
function sharedArtifactInstallPath(team, artifact) {
|
|
10010
|
-
|
|
10011
|
-
|
|
11061
|
+
const agentDir = artifact.agent === "codex" ? ".codex" : ".claude";
|
|
11062
|
+
if (artifact.kind === "pack") {
|
|
11063
|
+
return (0, import_node_path6.join)((0, import_node_os4.homedir)(), agentDir, "skills", "threadnote-packs", team, artifact.name);
|
|
10012
11064
|
}
|
|
10013
|
-
if (artifact.kind === "skill"
|
|
10014
|
-
return (0, import_node_path6.join)((0, import_node_os4.homedir)(),
|
|
11065
|
+
if (artifact.kind === "skill") {
|
|
11066
|
+
return (0, import_node_path6.join)((0, import_node_os4.homedir)(), agentDir, "skills", "threadnote", team, artifact.name, "SKILL.md");
|
|
10015
11067
|
}
|
|
10016
11068
|
return (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".claude", "commands", "threadnote", team, `${artifact.name}.md`);
|
|
10017
11069
|
}
|
|
@@ -12384,7 +13436,7 @@ function parseGithubRelease(value) {
|
|
|
12384
13436
|
}
|
|
12385
13437
|
function titleFromReleaseName(name, version) {
|
|
12386
13438
|
const trimmed = name.trim();
|
|
12387
|
-
const withoutPrefix = trimmed.replace(new RegExp(`^v?${
|
|
13439
|
+
const withoutPrefix = trimmed.replace(new RegExp(`^v?${escapeRegExp3(version)}:\\s*`, "i"), "");
|
|
12388
13440
|
return withoutPrefix || trimmed || "Release notes";
|
|
12389
13441
|
}
|
|
12390
13442
|
function bodyLines(body) {
|
|
@@ -12402,7 +13454,7 @@ function bodyLines(body) {
|
|
|
12402
13454
|
}
|
|
12403
13455
|
return out;
|
|
12404
13456
|
}
|
|
12405
|
-
function
|
|
13457
|
+
function escapeRegExp3(value) {
|
|
12406
13458
|
return value.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
|
|
12407
13459
|
}
|
|
12408
13460
|
|
|
@@ -15435,12 +16487,18 @@ async function main() {
|
|
|
15435
16487
|
).action(async (uri, options) => {
|
|
15436
16488
|
await runSharePublish(getRuntimeConfig(program2), uri, options);
|
|
15437
16489
|
});
|
|
15438
|
-
share.command("publish-artifact").description("Publish a local Codex/Claude skill or Claude command into the shared team repo").argument("<path>", "Path to SKILL.md or a Claude command markdown file").option("--team <name>", "Team name; defaults to the configured default team").option("--agent <agent>", "Agent owner when path inference is ambiguous: codex or claude").option("--kind <kind>", "Artifact kind when path inference is ambiguous: skill or command").option("--name <name>", "Shared artifact name; defaults to skill directory or command file stem").option("--message <text>", "Commit message override").option("--force", "Replace an existing shared artifact with different content").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").option("--preview", "Print
|
|
16490
|
+
share.command("publish-artifact").description("Publish a local Codex/Claude skill or Claude command into the shared team repo").argument("<path>", "Path to SKILL.md or a Claude command markdown file").option("--team <name>", "Team name; defaults to the configured default team").option("--agent <agent>", "Agent owner when path inference is ambiguous: codex or claude").option("--kind <kind>", "Artifact kind when path inference is ambiguous: skill or command").option("--name <name>", "Shared artifact name; defaults to skill directory or command file stem").option("--message <text>", "Commit message override").option("--force", "Replace an existing shared artifact with different content").option("--allow-binary", "Include binary skill files (unscannable by the scrubber); blocked by default").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").option("--preview", "Print what would land in the shared git repo without writing or committing").option(
|
|
15439
16491
|
"--redact",
|
|
15440
16492
|
"Replace soft-leak matches (local paths) with placeholders and continue; credentials still block"
|
|
15441
16493
|
).action(async (path, options) => {
|
|
15442
16494
|
await runSharePublishArtifact(getRuntimeConfig(program2), path, options);
|
|
15443
16495
|
});
|
|
16496
|
+
share.command("publish-bundle").description("Publish a multi-skill constellation (pack) declared by a threadnote-bundle.json manifest").argument("<manifest>", "Path to a threadnote-bundle.json manifest").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message override").option("--force", "Replace existing shared pack files with different content").option("--allow-binary", "Include binary files (unscannable by the scrubber); blocked by default").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").option("--preview", "Print what would land in the shared git repo without writing or committing").option(
|
|
16497
|
+
"--redact",
|
|
16498
|
+
"Replace soft-leak matches (local paths) with placeholders and continue; credentials still block"
|
|
16499
|
+
).action(async (manifest, options) => {
|
|
16500
|
+
await runSharePublishBundle(getRuntimeConfig(program2), manifest, options);
|
|
16501
|
+
});
|
|
15444
16502
|
share.command("install-artifacts").description("Install shared Codex/Claude skills and Claude commands from a team repo").option("--team <name>", "Team name; defaults to the configured default team").option("--agent <agent>", "Install only artifacts for this agent: codex or claude").option("--kind <kind>", "Install only artifacts of this kind: skill or command").option("--name <name>", "Install only this shared artifact name").option("--apply", "Actually write files into local agent skill/command directories").option("--force", "Replace existing installed artifacts with different content").option("--no-sync", "Skip pulling shared team updates before listing/installing artifacts").option("--dry-run", "Preview install actions without writing files").action(async (options) => {
|
|
15445
16503
|
await runShareInstallArtifacts(getRuntimeConfig(program2), options);
|
|
15446
16504
|
});
|