@tankpkg/cli 0.15.7 → 0.15.8

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/dist/bin/tank.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as VERSION, i as USER_AGENT, l as setConfig, n as flushLogs, o as getConfig, s as getConfigDir, t as authFlowLog } from "../debug-logger-B6uLj2h_.js";
2
+ import { a as VERSION, i as USER_AGENT, l as setConfig, n as flushLogs, o as getConfig, s as getConfigDir, t as authFlowLog } from "../debug-logger-BCwL85ni.js";
3
3
  import { t as logger } from "../logger-BhULz3Uz.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { Command } from "commander";
@@ -231,6 +231,54 @@ z.enum([
231
231
  "org.member.remove",
232
232
  "org.delete"
233
233
  ]);
234
+ const commandSchema$1 = z.string().min(1, "command must not be empty");
235
+ const argSchema$1 = z.array(z.string()).default([]);
236
+ const envSchema$1 = z.record(z.string(), z.string()).optional();
237
+ const remoteUrlSchema$1 = z.string().url("remote must be a valid URL");
238
+ const localMcpServerSchema = z.object({
239
+ command: commandSchema$1,
240
+ args: argSchema$1,
241
+ env: envSchema$1,
242
+ requires_auth: z.literal(false).optional()
243
+ }).strict();
244
+ const remoteMcpServerSchema = z.object({
245
+ remote: remoteUrlSchema$1,
246
+ requires_auth: z.boolean().default(false),
247
+ env: envSchema$1
248
+ }).strict();
249
+ const mcpServerSchema$1 = z.union([localMcpServerSchema, remoteMcpServerSchema]);
250
+ function isRemoteMcpServer(server) {
251
+ return "remote" in server;
252
+ }
253
+ const baseManifestFields$1 = {
254
+ name: z.string().min(1, "Name must not be empty").max(214, `Name must be 214 characters or fewer`).regex(/^@[a-z0-9-]+\/[a-z0-9][a-z0-9-]*$/, "Name must be scoped (@org/name), lowercase alphanumeric and hyphens"),
255
+ version: z.string().regex(/^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/, "Version must be valid semver"),
256
+ description: z.string().max(500, `Description must be 500 characters or fewer`).optional(),
257
+ skills: z.record(z.string(), z.string()).optional(),
258
+ permissions: permissionsSchema$1.optional(),
259
+ repository: z.string().url("Repository must be a valid URL").optional(),
260
+ visibility: z.enum(["public", "private"]).optional(),
261
+ audit: z.object({ min_score: z.number().min(0).max(10) }).strict().optional(),
262
+ mcp_server: mcpServerSchema$1.optional()
263
+ };
264
+ /** Legacy skills.json schema — strict, no atoms. Used for backward-compatible consumers. */
265
+ const skillsJsonSchema = z.object(baseManifestFields$1).strict();
266
+ const publishConfigSchema$1 = z.object({
267
+ build: z.string().min(1, "publish.build must be a non-empty shell command").optional(),
268
+ files: z.array(z.string().min(1)).optional()
269
+ }).strict();
270
+ /**
271
+ * Publish manifest schema — accepts both legacy skills.json AND atom-enriched tank.json.
272
+ * The `atoms` and `includes` fields are passed through as opaque JSON arrays,
273
+ * validated only at surface level. Full atom IR validation happens at build time.
274
+ * The `publish` block is a CLI-only lifecycle config (build hook + files allow-list).
275
+ */
276
+ const publishManifestSchema = z.object({
277
+ ...baseManifestFields$1,
278
+ atoms: z.array(z.record(z.string(), z.unknown())).optional(),
279
+ includes: z.array(z.string()).optional(),
280
+ publish: publishConfigSchema$1.optional()
281
+ }).strict();
234
282
  const promptIRSchema$1 = z.object({
235
283
  kind: z.literal("prompt"),
236
284
  name: z.string().min(1, "Prompt name must not be empty"),
@@ -269,8 +317,9 @@ const mcpServerConfigSchema$1 = z.object({
269
317
  args: z.array(z.string()).optional(),
270
318
  env: z.record(z.string(), z.string()).optional(),
271
319
  runtime: z.string().min(1).optional(),
272
- entry: z.string().min(1).optional()
273
- }).strict().refine((data) => data.command || data.runtime && data.entry, "MCP config must have either \"command\" or both \"runtime\" and \"entry\"");
320
+ entry: z.string().min(1).optional(),
321
+ package: z.string().min(1).optional()
322
+ }).strict().refine((data) => Boolean(data.command) || Boolean(data.runtime && (data.entry || data.package)), "MCP config must have either \"command\" or \"runtime\" plus one of \"entry\"/\"package\"");
274
323
  const toolIRSchema$1 = z.object({
275
324
  kind: z.literal("tool"),
276
325
  name: z.string().min(1, "Tool name must not be empty"),
@@ -299,27 +348,9 @@ const packageIRSchema = z.object({
299
348
  permissions: permissionsSchema$1.optional(),
300
349
  repository: z.string().url("Repository must be a valid URL").optional(),
301
350
  visibility: z.enum(["public", "private"]).optional(),
302
- audit: z.object({ min_score: z.number().min(0).max(10) }).strict().optional()
303
- }).strict();
304
- const commandSchema$1 = z.string().min(1, "command must not be empty");
305
- const argSchema$1 = z.array(z.string()).default([]);
306
- const envSchema$1 = z.record(z.string(), z.string()).optional();
307
- const remoteUrlSchema$1 = z.string().url("remote must be a valid URL");
308
- const localMcpServerSchema = z.object({
309
- command: commandSchema$1,
310
- args: argSchema$1,
311
- env: envSchema$1,
312
- requires_auth: z.literal(false).optional()
313
- }).strict();
314
- const remoteMcpServerSchema = z.object({
315
- remote: remoteUrlSchema$1,
316
- requires_auth: z.boolean().default(false),
317
- env: envSchema$1
351
+ audit: z.object({ min_score: z.number().min(0).max(10) }).strict().optional(),
352
+ publish: publishConfigSchema$1.optional()
318
353
  }).strict();
319
- const mcpServerSchema$1 = z.union([localMcpServerSchema, remoteMcpServerSchema]);
320
- function isRemoteMcpServer(server) {
321
- return "remote" in server;
322
- }
323
354
  const perToolOverrideSchema$1 = z.object({
324
355
  scan: z.boolean().optional(),
325
356
  blockOnMatch: z.boolean().optional()
@@ -330,29 +361,6 @@ z.object({
330
361
  resetPinsOnMismatch: z.boolean().optional(),
331
362
  perTool: z.record(z.string(), perToolOverrideSchema$1).optional()
332
363
  }).strict();
333
- const baseManifestFields$1 = {
334
- name: z.string().min(1, "Name must not be empty").max(214, `Name must be 214 characters or fewer`).regex(/^@[a-z0-9-]+\/[a-z0-9][a-z0-9-]*$/, "Name must be scoped (@org/name), lowercase alphanumeric and hyphens"),
335
- version: z.string().regex(/^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/, "Version must be valid semver"),
336
- description: z.string().max(500, `Description must be 500 characters or fewer`).optional(),
337
- skills: z.record(z.string(), z.string()).optional(),
338
- permissions: permissionsSchema$1.optional(),
339
- repository: z.string().url("Repository must be a valid URL").optional(),
340
- visibility: z.enum(["public", "private"]).optional(),
341
- audit: z.object({ min_score: z.number().min(0).max(10) }).strict().optional(),
342
- mcp_server: mcpServerSchema$1.optional()
343
- };
344
- /** Legacy skills.json schema — strict, no atoms. Used for backward-compatible consumers. */
345
- const skillsJsonSchema = z.object(baseManifestFields$1).strict();
346
- /**
347
- * Publish manifest schema — accepts both legacy skills.json AND atom-enriched tank.json.
348
- * The `atoms` and `includes` fields are passed through as opaque JSON arrays,
349
- * validated only at surface level. Full atom IR validation happens at build time.
350
- */
351
- const publishManifestSchema = z.object({
352
- ...baseManifestFields$1,
353
- atoms: z.array(z.record(z.string(), z.unknown())).optional(),
354
- includes: z.array(z.string()).optional()
355
- }).strict();
356
364
  const SKILL_SOURCES$1 = [
357
365
  "registry",
358
366
  "github",
@@ -633,6 +641,89 @@ async function auditCommand(options) {
633
641
  }
634
642
  //#endregion
635
643
  //#region ../adapters/dist/index.mjs
644
+ function resolveMcpCommand(atom, adapterName) {
645
+ if (atom.mcp) return resolveFromMcpBlock(atom.mcp);
646
+ const fromExtensions = resolveFromExtensions(atom.extensions, adapterName);
647
+ if (fromExtensions) return fromExtensions;
648
+ return null;
649
+ }
650
+ function resolveFromMcpBlock(mcp) {
651
+ const env = mcp.env;
652
+ if (mcp.command) return {
653
+ command: mcp.command,
654
+ args: mcp.args ?? [],
655
+ ...env ? { env } : {}
656
+ };
657
+ if (!mcp.runtime) return null;
658
+ const args = mcp.args ?? [];
659
+ switch (mcp.runtime) {
660
+ case "uvx":
661
+ if (!mcp.package) return null;
662
+ return {
663
+ command: "uvx",
664
+ args: [mcp.package, ...args],
665
+ ...env ? { env } : {}
666
+ };
667
+ case "npx":
668
+ if (!mcp.package) return null;
669
+ return {
670
+ command: "npx",
671
+ args: [
672
+ "-y",
673
+ mcp.package,
674
+ ...args
675
+ ],
676
+ ...env ? { env } : {}
677
+ };
678
+ case "bunx":
679
+ if (!mcp.package) return null;
680
+ return {
681
+ command: "bunx",
682
+ args: [mcp.package, ...args],
683
+ ...env ? { env } : {}
684
+ };
685
+ case "pipx":
686
+ if (!mcp.package) return null;
687
+ return {
688
+ command: "pipx",
689
+ args: [
690
+ "run",
691
+ mcp.package,
692
+ ...args
693
+ ],
694
+ ...env ? { env } : {}
695
+ };
696
+ case "node":
697
+ if (!mcp.entry) return null;
698
+ return {
699
+ command: "node",
700
+ args: [mcp.entry, ...args],
701
+ ...env ? { env } : {}
702
+ };
703
+ case "python":
704
+ if (!mcp.entry) return null;
705
+ return {
706
+ command: "python",
707
+ args: [mcp.entry, ...args],
708
+ ...env ? { env } : {}
709
+ };
710
+ default: return null;
711
+ }
712
+ }
713
+ function resolveFromExtensions(extensions, adapterName) {
714
+ if (!extensions) return null;
715
+ const bag = extensions[adapterName];
716
+ if (!bag || typeof bag !== "object") return null;
717
+ const candidate = bag;
718
+ if (typeof candidate.command !== "string" || candidate.command.length === 0) return null;
719
+ const args = Array.isArray(candidate.args) ? candidate.args.filter((a) => typeof a === "string") : [];
720
+ const env = candidate.env && typeof candidate.env === "object" && !Array.isArray(candidate.env) ? Object.fromEntries(Object.entries(candidate.env).filter(([, v]) => typeof v === "string")) : void 0;
721
+ return {
722
+ command: candidate.command,
723
+ args,
724
+ ...env ? { env } : {}
725
+ };
726
+ }
636
727
  function emitInstruction$5(atom) {
637
728
  const globs = atom.globs?.length ? atom.globs : void 0;
638
729
  if (globs) {
@@ -733,7 +824,8 @@ function emitAgent$5(atom) {
733
824
  };
734
825
  }
735
826
  function emitTool$5(atom) {
736
- if (!atom.mcp) return {
827
+ const resolved = resolveMcpCommand(atom, "claude-code");
828
+ if (!resolved) return {
737
829
  files: [],
738
830
  warnings: [{
739
831
  level: "skipped",
@@ -742,9 +834,9 @@ function emitTool$5(atom) {
742
834
  }]
743
835
  };
744
836
  const mcpConfig = { mcpServers: { [atom.name]: {
745
- command: atom.mcp.command,
746
- args: atom.mcp.args ?? [],
747
- ...atom.mcp.env ? { env: atom.mcp.env } : {}
837
+ command: resolved.command,
838
+ args: resolved.args,
839
+ ...resolved.env ? { env: resolved.env } : {}
748
840
  } } };
749
841
  return {
750
842
  files: [{
@@ -944,7 +1036,8 @@ function emitAgent$4(atom) {
944
1036
  };
945
1037
  }
946
1038
  function emitTool$4(atom) {
947
- if (!atom.mcp) return {
1039
+ const resolved = resolveMcpCommand(atom, "cline");
1040
+ if (!resolved) return {
948
1041
  files: [],
949
1042
  warnings: [{
950
1043
  level: "skipped",
@@ -953,10 +1046,10 @@ function emitTool$4(atom) {
953
1046
  }]
954
1047
  };
955
1048
  const config = { mcpServers: { [atom.name]: {
956
- command: atom.mcp.command,
957
- args: atom.mcp.args ?? [],
1049
+ command: resolved.command,
1050
+ args: resolved.args,
958
1051
  disabled: false,
959
- ...atom.mcp.env ? { env: atom.mcp.env } : {}
1052
+ ...resolved.env ? { env: resolved.env } : {}
960
1053
  } } };
961
1054
  return {
962
1055
  files: [{
@@ -1126,7 +1219,8 @@ function emitAgent$3(atom) {
1126
1219
  };
1127
1220
  }
1128
1221
  function emitTool$3(atom) {
1129
- if (!atom.mcp) return {
1222
+ const resolved = resolveMcpCommand(atom, "cursor");
1223
+ if (!resolved) return {
1130
1224
  files: [],
1131
1225
  warnings: [{
1132
1226
  level: "skipped",
@@ -1135,9 +1229,9 @@ function emitTool$3(atom) {
1135
1229
  }]
1136
1230
  };
1137
1231
  const config = { mcpServers: { [atom.name]: {
1138
- command: atom.mcp.command,
1139
- args: atom.mcp.args ?? [],
1140
- ...atom.mcp.env ? { env: atom.mcp.env } : {}
1232
+ command: resolved.command,
1233
+ args: resolved.args,
1234
+ ...resolved.env ? { env: resolved.env } : {}
1141
1235
  } } };
1142
1236
  return {
1143
1237
  files: [{
@@ -1324,7 +1418,8 @@ function emitAgent$2(atom) {
1324
1418
  };
1325
1419
  }
1326
1420
  function emitTool$2(atom) {
1327
- if (!atom.mcp) return {
1421
+ const resolved = resolveMcpCommand(atom, "opencode");
1422
+ if (!resolved) return {
1328
1423
  files: [],
1329
1424
  warnings: [{
1330
1425
  level: "skipped",
@@ -1334,8 +1429,8 @@ function emitTool$2(atom) {
1334
1429
  };
1335
1430
  const config = { [atom.name]: {
1336
1431
  type: "local",
1337
- command: [atom.mcp.command, ...atom.mcp.args ?? []],
1338
- ...atom.mcp.env ? { environment: atom.mcp.env } : {}
1432
+ command: [resolved.command, ...resolved.args],
1433
+ ...resolved.env ? { environment: resolved.env } : {}
1339
1434
  } };
1340
1435
  return {
1341
1436
  files: [{
@@ -1598,7 +1693,8 @@ function emitAgent$1(atom) {
1598
1693
  };
1599
1694
  }
1600
1695
  function emitTool$1(atom) {
1601
- if (!atom.mcp) return {
1696
+ const resolved = resolveMcpCommand(atom, "roo-code");
1697
+ if (!resolved) return {
1602
1698
  files: [],
1603
1699
  warnings: [{
1604
1700
  level: "skipped",
@@ -1607,10 +1703,10 @@ function emitTool$1(atom) {
1607
1703
  }]
1608
1704
  };
1609
1705
  const config = { mcpServers: { [atom.name]: {
1610
- command: atom.mcp.command,
1611
- args: atom.mcp.args ?? [],
1706
+ command: resolved.command,
1707
+ args: resolved.args,
1612
1708
  disabled: false,
1613
- ...atom.mcp.env ? { env: atom.mcp.env } : {}
1709
+ ...resolved.env ? { env: resolved.env } : {}
1614
1710
  } } };
1615
1711
  return {
1616
1712
  files: [{
@@ -1768,7 +1864,8 @@ function emitAgent(atom) {
1768
1864
  };
1769
1865
  }
1770
1866
  function emitTool(atom) {
1771
- if (!atom.mcp) return {
1867
+ const resolved = resolveMcpCommand(atom, "windsurf");
1868
+ if (!resolved) return {
1772
1869
  files: [],
1773
1870
  warnings: [{
1774
1871
  level: "skipped",
@@ -1777,9 +1874,9 @@ function emitTool(atom) {
1777
1874
  }]
1778
1875
  };
1779
1876
  const config = { mcpServers: { [atom.name]: {
1780
- command: atom.mcp.command,
1781
- args: atom.mcp.args ?? [],
1782
- ...atom.mcp.env ? { env: atom.mcp.env } : {}
1877
+ command: resolved.command,
1878
+ args: resolved.args,
1879
+ ...resolved.env ? { env: resolved.env } : {}
1783
1880
  } } };
1784
1881
  return {
1785
1882
  files: [{
@@ -7065,6 +7162,42 @@ _enum([
7065
7162
  "org.member.remove",
7066
7163
  "org.delete"
7067
7164
  ]);
7165
+ const commandSchema = string().min(1, "command must not be empty");
7166
+ const argSchema = array(string()).default([]);
7167
+ const envSchema = record(string(), string()).optional();
7168
+ const remoteUrlSchema = string().url("remote must be a valid URL");
7169
+ const mcpServerSchema = union([object({
7170
+ command: commandSchema,
7171
+ args: argSchema,
7172
+ env: envSchema,
7173
+ requires_auth: literal(false).optional()
7174
+ }).strict(), object({
7175
+ remote: remoteUrlSchema,
7176
+ requires_auth: boolean().default(false),
7177
+ env: envSchema
7178
+ }).strict()]);
7179
+ const baseManifestFields = {
7180
+ name: string().min(1, "Name must not be empty").max(214, `Name must be 214 characters or fewer`).regex(/^@[a-z0-9-]+\/[a-z0-9][a-z0-9-]*$/, "Name must be scoped (@org/name), lowercase alphanumeric and hyphens"),
7181
+ version: string().regex(/^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/, "Version must be valid semver"),
7182
+ description: string().max(500, `Description must be 500 characters or fewer`).optional(),
7183
+ skills: record(string(), string()).optional(),
7184
+ permissions: permissionsSchema.optional(),
7185
+ repository: string().url("Repository must be a valid URL").optional(),
7186
+ visibility: _enum(["public", "private"]).optional(),
7187
+ audit: object({ min_score: number().min(0).max(10) }).strict().optional(),
7188
+ mcp_server: mcpServerSchema.optional()
7189
+ };
7190
+ object(baseManifestFields).strict();
7191
+ const publishConfigSchema = object({
7192
+ build: string().min(1, "publish.build must be a non-empty shell command").optional(),
7193
+ files: array(string().min(1)).optional()
7194
+ }).strict();
7195
+ object({
7196
+ ...baseManifestFields,
7197
+ atoms: array(record(string(), unknown())).optional(),
7198
+ includes: array(string()).optional(),
7199
+ publish: publishConfigSchema.optional()
7200
+ }).strict();
7068
7201
  const promptIRSchema = object({
7069
7202
  kind: literal("prompt"),
7070
7203
  name: string().min(1, "Prompt name must not be empty"),
@@ -7103,8 +7236,9 @@ const mcpServerConfigSchema = object({
7103
7236
  args: array(string()).optional(),
7104
7237
  env: record(string(), string()).optional(),
7105
7238
  runtime: string().min(1).optional(),
7106
- entry: string().min(1).optional()
7107
- }).strict().refine((data) => data.command || data.runtime && data.entry, "MCP config must have either \"command\" or both \"runtime\" and \"entry\"");
7239
+ entry: string().min(1).optional(),
7240
+ package: string().min(1).optional()
7241
+ }).strict().refine((data) => Boolean(data.command) || Boolean(data.runtime && (data.entry || data.package)), "MCP config must have either \"command\" or \"runtime\" plus one of \"entry\"/\"package\"");
7108
7242
  const toolIRSchema = object({
7109
7243
  kind: literal("tool"),
7110
7244
  name: string().min(1, "Tool name must not be empty"),
@@ -7133,22 +7267,9 @@ object({
7133
7267
  permissions: permissionsSchema.optional(),
7134
7268
  repository: string().url("Repository must be a valid URL").optional(),
7135
7269
  visibility: _enum(["public", "private"]).optional(),
7136
- audit: object({ min_score: number().min(0).max(10) }).strict().optional()
7270
+ audit: object({ min_score: number().min(0).max(10) }).strict().optional(),
7271
+ publish: publishConfigSchema.optional()
7137
7272
  }).strict();
7138
- const commandSchema = string().min(1, "command must not be empty");
7139
- const argSchema = array(string()).default([]);
7140
- const envSchema = record(string(), string()).optional();
7141
- const remoteUrlSchema = string().url("remote must be a valid URL");
7142
- const mcpServerSchema = union([object({
7143
- command: commandSchema,
7144
- args: argSchema,
7145
- env: envSchema,
7146
- requires_auth: literal(false).optional()
7147
- }).strict(), object({
7148
- remote: remoteUrlSchema,
7149
- requires_auth: boolean().default(false),
7150
- env: envSchema
7151
- }).strict()]);
7152
7273
  const perToolOverrideSchema = object({
7153
7274
  scan: boolean().optional(),
7154
7275
  blockOnMatch: boolean().optional()
@@ -7159,23 +7280,6 @@ object({
7159
7280
  resetPinsOnMismatch: boolean().optional(),
7160
7281
  perTool: record(string(), perToolOverrideSchema).optional()
7161
7282
  }).strict();
7162
- const baseManifestFields = {
7163
- name: string().min(1, "Name must not be empty").max(214, `Name must be 214 characters or fewer`).regex(/^@[a-z0-9-]+\/[a-z0-9][a-z0-9-]*$/, "Name must be scoped (@org/name), lowercase alphanumeric and hyphens"),
7164
- version: string().regex(/^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/, "Version must be valid semver"),
7165
- description: string().max(500, `Description must be 500 characters or fewer`).optional(),
7166
- skills: record(string(), string()).optional(),
7167
- permissions: permissionsSchema.optional(),
7168
- repository: string().url("Repository must be a valid URL").optional(),
7169
- visibility: _enum(["public", "private"]).optional(),
7170
- audit: object({ min_score: number().min(0).max(10) }).strict().optional(),
7171
- mcp_server: mcpServerSchema.optional()
7172
- };
7173
- object(baseManifestFields).strict();
7174
- object({
7175
- ...baseManifestFields,
7176
- atoms: array(record(string(), unknown())).optional(),
7177
- includes: array(string()).optional()
7178
- }).strict();
7179
7283
  const SKILL_SOURCES = [
7180
7284
  "registry",
7181
7285
  "github",
@@ -10607,6 +10711,31 @@ async function proxyCommand(options) {
10607
10711
  process.exit(code);
10608
10712
  }
10609
10713
  //#endregion
10714
+ //#region src/lib/build-hook.ts
10715
+ function runBuildHook(directory, command) {
10716
+ return new Promise((resolve, reject) => {
10717
+ const child = spawn(command, {
10718
+ cwd: directory,
10719
+ shell: true,
10720
+ stdio: "inherit"
10721
+ });
10722
+ child.on("error", (err) => {
10723
+ reject(/* @__PURE__ */ new Error(`Build hook failed to start: ${err.message}`));
10724
+ });
10725
+ child.on("close", (code, signal) => {
10726
+ if (code === 0) {
10727
+ resolve();
10728
+ return;
10729
+ }
10730
+ if (signal) {
10731
+ reject(/* @__PURE__ */ new Error(`Build hook terminated by signal ${signal}`));
10732
+ return;
10733
+ }
10734
+ reject(/* @__PURE__ */ new Error(`Build hook exited with code ${code ?? "unknown"}`));
10735
+ });
10736
+ });
10737
+ }
10738
+ //#endregion
10610
10739
  //#region src/lib/packer.ts
10611
10740
  const MAX_PACKAGE_SIZE = 50 * 1024 * 1024;
10612
10741
  const MAX_FILE_COUNT = 1e3;
@@ -10620,18 +10749,7 @@ const DEFAULT_IGNORES = [
10620
10749
  ];
10621
10750
  const ALWAYS_IGNORED = ["node_modules", ".git"];
10622
10751
  const IGNORE_FILES = [".tankignore", ".gitignore"];
10623
- /**
10624
- * Pack a skill directory into a .tgz tarball with integrity hashing.
10625
- *
10626
- * Validates:
10627
- * - tank.json (or skills.json) exists and is valid
10628
- * - No symlinks or hardlinks
10629
- * - No path traversal (.. components)
10630
- * - No absolute paths
10631
- * - File count <= 1000
10632
- * - Tarball size <= 50MB
10633
- */
10634
- async function pack(directory) {
10752
+ async function pack(directory, options = {}) {
10635
10753
  const absDir = path.resolve(directory);
10636
10754
  if (!fs.existsSync(absDir)) throw new Error(`Directory does not exist: ${absDir}`);
10637
10755
  if (!fs.statSync(absDir).isDirectory()) throw new Error(`Not a directory: ${absDir}`);
@@ -10672,7 +10790,7 @@ async function pack(directory) {
10672
10790
  } catch {
10673
10791
  readmeContent = "";
10674
10792
  }
10675
- const files = collectFiles(absDir, absDir, buildIgnoreFilter(absDir));
10793
+ const files = options.files && options.files.length > 0 ? collectFromAllowList(absDir, options.files, manifestFilename) : collectFiles(absDir, absDir, buildIgnoreFilter(absDir));
10676
10794
  if (files.length > MAX_FILE_COUNT) throw new Error(`Too many files: ${files.length} exceeds maximum of ${MAX_FILE_COUNT}`);
10677
10795
  let totalSize = 0;
10678
10796
  for (const file of files) {
@@ -10741,9 +10859,15 @@ async function packForScan(directory) {
10741
10859
  files
10742
10860
  };
10743
10861
  }
10744
- /**
10745
- * Build an ignore filter from .tankignore, .gitignore, or defaults.
10746
- */
10862
+ function collectFromAllowList(baseDir, globs, manifestFilename) {
10863
+ const securityFilter = ignore().add(ALWAYS_IGNORED);
10864
+ const allowMatcher = ignore().add(globs);
10865
+ const all = collectFiles(baseDir, baseDir, securityFilter);
10866
+ const always = new Set([manifestFilename]);
10867
+ if (fs.existsSync(path.join(baseDir, "SKILL.md"))) always.add("SKILL.md");
10868
+ if (fs.existsSync(path.join(baseDir, "README.md"))) always.add("README.md");
10869
+ return all.filter((rel) => always.has(rel) || allowMatcher.ignores(rel));
10870
+ }
10747
10871
  function buildIgnoreFilter(dir) {
10748
10872
  const ig = ignore();
10749
10873
  ig.add(ALWAYS_IGNORED);
@@ -10848,14 +10972,21 @@ async function publishCommand(options = {}) {
10848
10972
  if (effectiveVisibility) manifest.visibility = effectiveVisibility;
10849
10973
  const name = manifest.name;
10850
10974
  const version = manifest.version;
10975
+ const publishConfig = manifest.publish ?? void 0;
10976
+ if (publishConfig?.build) {
10977
+ logger.info(`Running build: ${publishConfig.build}`);
10978
+ await runBuildHook(directory, publishConfig.build);
10979
+ }
10851
10980
  const spinner = ora("Packing...").start();
10852
10981
  let packResult;
10853
10982
  try {
10854
- packResult = await pack(directory);
10983
+ packResult = await pack(directory, publishConfig?.files ? { files: publishConfig.files } : {});
10855
10984
  } catch (err) {
10856
10985
  spinner.fail("Packing failed");
10857
10986
  throw err;
10858
10987
  }
10988
+ const outboundManifest = { ...manifest };
10989
+ delete outboundManifest.publish;
10859
10990
  const { tarball, integrity, fileCount, totalSize, readme, files } = packResult;
10860
10991
  if (dryRun) {
10861
10992
  spinner.stop();
@@ -10891,7 +11022,7 @@ async function publishCommand(options = {}) {
10891
11022
  method: "POST",
10892
11023
  headers,
10893
11024
  body: JSON.stringify({
10894
- manifest,
11025
+ manifest: outboundManifest,
10895
11026
  readme,
10896
11027
  files
10897
11028
  })