@stigmer/cli 3.15.3-dev.20260914121148 → 3.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +3 -4
  2. package/commands/apply.d.ts.map +1 -1
  3. package/commands/apply.js +56 -4
  4. package/commands/apply.js.map +1 -1
  5. package/commands/push.d.ts.map +1 -1
  6. package/commands/push.js +122 -16
  7. package/commands/push.js.map +1 -1
  8. package/commands/validate.d.ts.map +1 -1
  9. package/commands/validate.js +39 -7
  10. package/commands/validate.js.map +1 -1
  11. package/output/command-result.d.ts +8 -0
  12. package/output/command-result.d.ts.map +1 -1
  13. package/output/command-result.js +13 -0
  14. package/output/command-result.js.map +1 -1
  15. package/package.json +6 -5
  16. package/registry/aliases.d.ts.map +1 -1
  17. package/registry/aliases.js.map +1 -1
  18. package/registry/index.d.ts +3 -3
  19. package/registry/index.d.ts.map +1 -1
  20. package/registry/index.js +3 -3
  21. package/registry/index.js.map +1 -1
  22. package/registry/metadata.d.ts.map +1 -1
  23. package/registry/metadata.js +85 -18
  24. package/registry/metadata.js.map +1 -1
  25. package/registry/registry.d.ts.map +1 -1
  26. package/registry/registry.js.map +1 -1
  27. package/registry/verb-support.d.ts.map +1 -1
  28. package/registry/verb-support.js +81 -13
  29. package/registry/verb-support.js.map +1 -1
  30. package/resources/delete.d.ts.map +1 -1
  31. package/resources/delete.js +35 -9
  32. package/resources/delete.js.map +1 -1
  33. package/resources/get-bindings.d.ts.map +1 -1
  34. package/resources/get-bindings.js +56 -13
  35. package/resources/get-bindings.js.map +1 -1
  36. package/resources/list.d.ts.map +1 -1
  37. package/resources/list.js +62 -14
  38. package/resources/list.js.map +1 -1
  39. package/resources/plugin.d.ts +80 -0
  40. package/resources/plugin.d.ts.map +1 -0
  41. package/resources/plugin.js +305 -0
  42. package/resources/plugin.js.map +1 -0
  43. package/resources/skill.d.ts +1 -0
  44. package/resources/skill.d.ts.map +1 -1
  45. package/resources/skill.js +1 -1
  46. package/resources/skill.js.map +1 -1
  47. package/src/commands/apply.ts +133 -21
  48. package/src/commands/push.ts +270 -41
  49. package/src/commands/validate.test.ts +111 -22
  50. package/src/commands/validate.ts +68 -12
  51. package/src/output/command-result.test.ts +9 -0
  52. package/src/output/command-result.ts +14 -0
  53. package/src/registry/aliases.test.ts +24 -5
  54. package/src/registry/aliases.ts +6 -1
  55. package/src/registry/index.ts +19 -3
  56. package/src/registry/metadata.ts +85 -18
  57. package/src/registry/registry.test.ts +86 -37
  58. package/src/registry/registry.ts +8 -2
  59. package/src/registry/verb-support.ts +85 -14
  60. package/src/resources/delete.ts +106 -26
  61. package/src/resources/get-bindings.ts +95 -17
  62. package/src/resources/list.ts +102 -28
  63. package/src/resources/plugin.test.ts +233 -0
  64. package/src/resources/plugin.ts +432 -0
  65. package/src/resources/skill.ts +1 -1
@@ -2,6 +2,10 @@
2
2
  //
3
3
  // File mode (-f): apply individual YAML resource files (or a directory)
4
4
  // Declarative mode: a stigmer.yaml project directory (scan + reconcile)
5
+ // Plugin track: a directory holding a plugin manifest (either flag,
6
+ // or the working directory) installs as a plugin — the
7
+ // same read, zip and push as `push plugin <dir>`, so the
8
+ // two verbs are one code path
5
9
  //
6
10
  // Resources marshal strictly from YAML to full protos and apply through the raw
7
11
  // command controllers (preserving metadata.id so updates aren't misrouted as
@@ -9,7 +13,12 @@
9
13
  import type { Command } from "commander";
10
14
  import { ensureAuthenticated, resolveOrganization } from "../config/index.js";
11
15
  import { UsageError } from "../errors/index.js";
12
- import { CommandResult, type OutputFlags, type OutputFormat, renderResult } from "../output/index.js";
16
+ import {
17
+ CommandResult,
18
+ type OutputFlags,
19
+ type OutputFormat,
20
+ renderResult,
21
+ } from "../output/index.js";
13
22
  import { addResultFlags, globalOrg, resultFormat } from "./shared.js";
14
23
 
15
24
  interface ApplyFlags extends OutputFlags {
@@ -30,7 +39,9 @@ export function registerApply(program: Command): void {
30
39
  // parity; it is intentionally a no-op client-side. Tracked as a Go follow-up.
31
40
  .option("--prune", "delete orphaned resources (declarative mode)", true)
32
41
  .option("--dry-run", "validate without applying")
33
- .action((options: ApplyFlags, command: Command) => runApply(options, command));
42
+ .action((options: ApplyFlags, command: Command) =>
43
+ runApply(options, command),
44
+ );
34
45
  addResultFlags(apply);
35
46
  }
36
47
 
@@ -38,14 +49,82 @@ async function runApply(options: ApplyFlags, command: Command): Promise<void> {
38
49
  const format = resultFormat(options);
39
50
  const orgOverride = globalOrg(command);
40
51
 
52
+ // A plugin manifest at the given directory's root selects the plugin
53
+ // track before any YAML scan: a repository holding both is applied as a
54
+ // plugin, and its `ai.stigmer/*.yaml` documents are the plugin's, not
55
+ // loose resources.
56
+ const target =
57
+ options.file !== undefined && options.file !== ""
58
+ ? options.file
59
+ : options.config !== undefined && options.config !== ""
60
+ ? options.config
61
+ : process.cwd();
62
+ const { isPluginDirectory } = await import("../resources/plugin.js");
63
+ if (isPluginDirectory(target)) {
64
+ await runPluginApply(target, orgOverride, options.dryRun === true, format);
65
+ return;
66
+ }
67
+
41
68
  if (options.file !== undefined && options.file !== "") {
42
- await runFileApply(options.file, orgOverride, options.dryRun === true, format);
69
+ await runFileApply(
70
+ options.file,
71
+ orgOverride,
72
+ options.dryRun === true,
73
+ format,
74
+ );
43
75
  return;
44
76
  }
45
77
 
46
78
  await runDeclarativeApply(options, orgOverride, format);
47
79
  }
48
80
 
81
+ // Plugin track: install or upgrade the plugin folder as one unit through the
82
+ // same read, zip and push `push plugin` runs; the org resolves like every
83
+ // other apply (--org, then the configured context).
84
+ async function runPluginApply(
85
+ dir: string,
86
+ orgOverride: string | undefined,
87
+ dryRun: boolean,
88
+ format: OutputFormat,
89
+ ): Promise<void> {
90
+ const plugin = await import("../resources/plugin.js");
91
+ const ignoreOptions = {
92
+ respectGitignore: true,
93
+ extraIgnore: [],
94
+ extraInclude: [],
95
+ };
96
+ if (dryRun) {
97
+ const { readPluginPackage } = await import("@stigmer/plugin-package");
98
+ const read = plugin.readPluginDirectory(dir, ignoreOptions);
99
+ const outcome = readPluginPackage(read.files);
100
+ if (!outcome.ok) {
101
+ throw plugin.pluginRefusal(dir, outcome.errors, outcome.warnings);
102
+ }
103
+ const result = plugin.describePackageOn(
104
+ CommandResult.success(
105
+ `Dry run: plugin '${outcome.plugin.name}' would install`,
106
+ ),
107
+ outcome.plugin,
108
+ outcome.warnings,
109
+ read.stats,
110
+ );
111
+ result.hint("Run without --dry-run to install it.");
112
+ renderResult(result, format);
113
+ return;
114
+ }
115
+ const { connectBackend } = await import("../backend.js");
116
+ const client = connectBackend();
117
+ ensureAuthenticated(client.config);
118
+ const org = resolveOrganization(client.config, orgOverride);
119
+ const outcome = await plugin.pushPlugin(client.stigmer, dir, {
120
+ org,
121
+ visibility: undefined,
122
+ message: "",
123
+ ignoreOptions,
124
+ });
125
+ renderResult(plugin.renderPushOutcome(outcome), format);
126
+ }
127
+
49
128
  // Declarative mode: detect stigmer.yaml (walk up). Absent → atomic guidance.
50
129
  // entry_point set → SDK/project synthesis track. Otherwise scan the project and
51
130
  // reconcile membership. Both project tracks converge on the shared reconciler.
@@ -54,9 +133,13 @@ async function runDeclarativeApply(
54
133
  orgOverride: string | undefined,
55
134
  format: OutputFormat,
56
135
  ): Promise<void> {
57
- const { detectTrack, applyDeclarative, previewDeclarative } = await import("../resources/apply/declarative.js");
136
+ const { detectTrack, applyDeclarative, previewDeclarative } =
137
+ await import("../resources/apply/declarative.js");
58
138
 
59
- const startDir = options.config !== undefined && options.config !== "" ? options.config : process.cwd();
139
+ const startDir =
140
+ options.config !== undefined && options.config !== ""
141
+ ? options.config
142
+ : process.cwd();
60
143
  const detect = detectTrack(startDir);
61
144
 
62
145
  if (detect.track === "atomic") {
@@ -82,7 +165,11 @@ async function runDeclarativeApply(
82
165
  const { connectBackend } = await import("../backend.js");
83
166
  const client = connectBackend();
84
167
  ensureAuthenticated(client.config);
85
- const org = resolveDeclarativeOrg(client.config, detect.project?.metadata?.org, orgOverride);
168
+ const org = resolveDeclarativeOrg(
169
+ client.config,
170
+ detect.project?.metadata?.org,
171
+ orgOverride,
172
+ );
86
173
 
87
174
  const result = await applyDeclarative(detect, {
88
175
  controller: client.controller,
@@ -104,7 +191,8 @@ async function runProjectApply(
104
191
  format: OutputFormat,
105
192
  dryRun: boolean,
106
193
  ): Promise<void> {
107
- const { applyProjectTrack, previewProjectTrack } = await import("../resources/apply/synth/project-track.js");
194
+ const { applyProjectTrack, previewProjectTrack } =
195
+ await import("../resources/apply/synth/project-track.js");
108
196
 
109
197
  if (dryRun) {
110
198
  renderResult(previewProjectTrack(detect), format);
@@ -114,7 +202,11 @@ async function runProjectApply(
114
202
  const { connectBackend } = await import("../backend.js");
115
203
  const client = connectBackend();
116
204
  ensureAuthenticated(client.config);
117
- const org = resolveDeclarativeOrg(client.config, detect.project?.metadata?.org, orgOverride);
205
+ const org = resolveDeclarativeOrg(
206
+ client.config,
207
+ detect.project?.metadata?.org,
208
+ orgOverride,
209
+ );
118
210
 
119
211
  const result = await applyProjectTrack(detect, {
120
212
  controller: client.controller,
@@ -134,7 +226,10 @@ function resolveDeclarativeOrg(
134
226
  projectOrg: string | undefined,
135
227
  override: string | undefined,
136
228
  ): string {
137
- const resolved = resolveOrganization(config, override ?? (projectOrg !== "" ? projectOrg : undefined));
229
+ const resolved = resolveOrganization(
230
+ config,
231
+ override ?? (projectOrg !== "" ? projectOrg : undefined),
232
+ );
138
233
  if (resolved === "") {
139
234
  throw new UsageError(
140
235
  "organization not set\n\n" +
@@ -153,12 +248,15 @@ async function runFileApply(
153
248
  dryRun: boolean,
154
249
  format: OutputFormat,
155
250
  ): Promise<void> {
156
- const [{ connectBackend }, { resolveApplyItems, requiresOrgContext, applyItem }, { discoverAppliedMcpServers }] =
157
- await Promise.all([
158
- import("../backend.js"),
159
- import("../resources/apply/apply.js"),
160
- import("../resources/apply/discovery.js"),
161
- ]);
251
+ const [
252
+ { connectBackend },
253
+ { resolveApplyItems, requiresOrgContext, applyItem },
254
+ { discoverAppliedMcpServers },
255
+ ] = await Promise.all([
256
+ import("../backend.js"),
257
+ import("../resources/apply/apply.js"),
258
+ import("../resources/apply/discovery.js"),
259
+ ]);
162
260
 
163
261
  const items = resolveApplyItems(path);
164
262
 
@@ -174,31 +272,45 @@ async function runFileApply(
174
272
 
175
273
  const client = connectBackend();
176
274
  ensureAuthenticated(client.config);
177
- const org = requiresOrgContext(items) ? resolveOrganization(client.config, orgOverride) : "";
275
+ const org = requiresOrgContext(items)
276
+ ? resolveOrganization(client.config, orgOverride)
277
+ : "";
178
278
 
179
279
  const appliedMcpServers = [];
180
280
  for (const item of items) {
181
281
  const outcome = await applyItem(client.controller, item, org, false);
182
282
  emitWarning(outcome.warning);
183
283
  renderResult(outcome.result, format);
184
- if (outcome.appliedMcpServer !== undefined) appliedMcpServers.push(outcome.appliedMcpServer);
284
+ if (outcome.appliedMcpServer !== undefined)
285
+ appliedMcpServers.push(outcome.appliedMcpServer);
185
286
  }
186
287
 
187
- await discoverAppliedMcpServers(client.stigmer, appliedMcpServers, org, (line) => process.stderr.write(`${line}\n`));
288
+ await discoverAppliedMcpServers(
289
+ client.stigmer,
290
+ appliedMcpServers,
291
+ org,
292
+ (line) => process.stderr.write(`${line}\n`),
293
+ );
188
294
  }
189
295
 
190
296
  function buildAtomicGuidance(): CommandResult {
191
- const result = CommandResult.warning("No stigmer.yaml found in current directory or parents");
297
+ const result = CommandResult.warning(
298
+ "No stigmer.yaml found in current directory or parents",
299
+ );
192
300
  result
193
301
  .addSection("")
194
- .item("The 'stigmer apply' command (without -f) requires a project with stigmer.yaml")
302
+ .item(
303
+ "The 'stigmer apply' command (without -f) requires a project with stigmer.yaml",
304
+ )
195
305
  .item("This enables resource discovery and project-based reconciliation");
196
306
  result
197
307
  .addSection("For single-resource deployment, use file mode")
198
308
  .item("stigmer apply -f agent.yaml")
199
309
  .item("stigmer apply -f workflow.yaml")
200
310
  .item("stigmer apply -f mcpserver.yaml");
201
- result.hint("To create a project: add a stigmer.yaml with your project name, then run 'stigmer apply'");
311
+ result.hint(
312
+ "To create a project: add a stigmer.yaml with your project name, then run 'stigmer apply'",
313
+ );
202
314
  return result;
203
315
  }
204
316
 
@@ -1,7 +1,9 @@
1
1
  // `stigmer push <type> [path]` — package and upload an artifact to the registry.
2
- // Only skills are pushable today. Two source modes: a local directory (default,
3
- // git provenance auto-detected) or a remote repo via --git-url (cloned to a
4
- // temp dir, then pushed). Heavy modules are lazy-imported so `--help` stays fast.
2
+ // Two kinds are pushable: skills (a local directory with git provenance
3
+ // auto-detected, a remote repo via --git-url, or a pre-built --archive) and
4
+ // plugins (a local plugin directory, installed or upgraded as one unit; the
5
+ // manifest's version is its tag, so --tag does not apply). Heavy modules are
6
+ // lazy-imported so `--help` stays fast.
5
7
  import { mkdtempSync, rmSync } from "node:fs";
6
8
  import { tmpdir } from "node:os";
7
9
  import { join } from "node:path";
@@ -9,11 +11,16 @@ import type { Command } from "commander";
9
11
  import { ensureAuthenticated, resolveOrganization } from "../config/index.js";
10
12
  import { UsageError } from "../errors/index.js";
11
13
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
14
+ import {
15
+ type OutputFlags,
16
+ renderResult as renderCommandResult,
17
+ } from "../output/index.js";
12
18
  import { defaultRegistry, Verb } from "../registry/index.js";
13
- import { globalOrg } from "./shared.js";
19
+ import { addResultFlags, globalOrg, resultFormat } from "./shared.js";
14
20
 
15
- interface PushFlags {
21
+ interface PushFlags extends OutputFlags {
16
22
  tag?: string;
23
+ visibility?: string;
17
24
  message?: string;
18
25
  dryRun?: boolean;
19
26
  gitUrl?: string;
@@ -26,41 +33,96 @@ interface PushFlags {
26
33
  verbose?: boolean;
27
34
  }
28
35
 
29
- const collect = (value: string, previous: string[]): string[] => [...previous, value];
36
+ const collect = (value: string, previous: string[]): string[] => [
37
+ ...previous,
38
+ value,
39
+ ];
30
40
 
31
41
  export function registerPush(program: Command): void {
32
- program
42
+ const push = program
33
43
  .command("push <type> [path]")
34
- .description("push an artifact to the registry (supported types: skill)")
35
- .option("--tag <tag>", "version tag for the artifact", "latest")
36
- .option("-m, --message <message>", "version message describing what changed")
44
+ .description(
45
+ "push an artifact to the registry (supported types: skill, plugin)",
46
+ )
47
+ .option(
48
+ "--tag <tag>",
49
+ "version tag for the artifact (skills; a plugin's tag is its manifest version)",
50
+ "latest",
51
+ )
52
+ .option(
53
+ "-m, --message <message>",
54
+ "version message describing what changed",
55
+ )
56
+ .option(
57
+ "--visibility <level>",
58
+ "plugins: visibility for the plugin and everything it installs (private, org, public, platform)",
59
+ )
37
60
  .option("--dry-run", "validate without pushing")
38
61
  .option("--git-url <url>", "push from a remote git repository URL")
39
- .option("--git-ref <ref>", "git reference (tag, branch, or commit SHA) for remote push")
40
- .option("--subdir <dir>", "subdirectory within the git repository containing the artifact")
41
- .option("--archive <file>", "push a pre-packaged ZIP archive as-is (version hash = the file's SHA-256)")
42
- .option("--ignore <pattern>", "additional pattern to ignore (repeatable)", collect, [])
43
- .option("--include <pattern>", "pattern to force-include (repeatable)", collect, [])
62
+ .option(
63
+ "--git-ref <ref>",
64
+ "git reference (tag, branch, or commit SHA) for remote push",
65
+ )
66
+ .option(
67
+ "--subdir <dir>",
68
+ "subdirectory within the git repository containing the artifact",
69
+ )
70
+ .option(
71
+ "--archive <file>",
72
+ "push a pre-packaged ZIP archive as-is (version hash = the file's SHA-256)",
73
+ )
74
+ .option(
75
+ "--ignore <pattern>",
76
+ "additional pattern to ignore (repeatable)",
77
+ collect,
78
+ [],
79
+ )
80
+ .option(
81
+ "--include <pattern>",
82
+ "pattern to force-include (repeatable)",
83
+ collect,
84
+ [],
85
+ )
44
86
  .option("--no-gitignore", "don't respect .gitignore patterns")
45
87
  .option("--verbose", "show detailed output including ignore decisions")
46
- .action((type: string, path: string | undefined, options: PushFlags, command: Command) =>
47
- runPush(type, path, options, command),
88
+ .action(
89
+ (
90
+ type: string,
91
+ path: string | undefined,
92
+ options: PushFlags,
93
+ command: Command,
94
+ ) => runPush(type, path, options, command),
48
95
  );
96
+ addResultFlags(push);
49
97
  }
50
98
 
51
- async function runPush(type: string, path: string | undefined, options: PushFlags, command: Command): Promise<void> {
99
+ async function runPush(
100
+ type: string,
101
+ path: string | undefined,
102
+ options: PushFlags,
103
+ command: Command,
104
+ ): Promise<void> {
52
105
  const info = defaultRegistry().getByAlias(type);
53
106
  if (info === undefined) {
54
- throw new UsageError(`unknown resource type: ${type}\n\nAvailable types: skill`);
107
+ throw new UsageError(
108
+ `unknown resource type: ${type}\n\nAvailable types: skill, plugin`,
109
+ );
55
110
  }
56
111
  if (!info.supportedVerbs.has(Verb.Push)) {
57
112
  throw new UsageError(`${info.displayName} does not support 'push'`);
58
113
  }
114
+ if (info.kind === ApiResourceKind.plugin) {
115
+ await runPushPlugin(path, options, command);
116
+ return;
117
+ }
59
118
  if (info.kind !== ApiResourceKind.skill) {
60
119
  throw new UsageError(`push not implemented for ${info.displayName}`);
61
120
  }
62
121
 
63
- const [{ connectBackend }, skill] = await Promise.all([import("../backend.js"), import("../resources/skill.js")]);
122
+ const [{ connectBackend }, skill] = await Promise.all([
123
+ import("../backend.js"),
124
+ import("../resources/skill.js"),
125
+ ]);
64
126
  const client = connectBackend();
65
127
  ensureAuthenticated(client.config);
66
128
  const org = resolveOrganization(client.config, globalOrg(command));
@@ -70,17 +132,33 @@ async function runPush(type: string, path: string | undefined, options: PushFlag
70
132
  extraIgnore: options.ignore ?? [],
71
133
  extraInclude: options.include ?? [],
72
134
  };
73
- const decisionSink = options.verbose === true ? (line: string) => process.stderr.write(`${line}\n`) : undefined;
135
+ const decisionSink =
136
+ options.verbose === true
137
+ ? (line: string) => process.stderr.write(`${line}\n`)
138
+ : undefined;
74
139
  const tag = options.tag ?? "latest";
75
140
  const message = options.message ?? "";
76
141
 
77
142
  if (options.archive !== undefined && options.archive !== "") {
78
- await pushArchive(client.stigmer, skill, { org, tag, message, path, options });
143
+ await pushArchive(client.stigmer, skill, {
144
+ org,
145
+ tag,
146
+ message,
147
+ path,
148
+ options,
149
+ });
79
150
  return;
80
151
  }
81
152
 
82
153
  if (options.gitUrl !== undefined && options.gitUrl !== "") {
83
- await pushRemote(client.stigmer, skill, { org, tag, message, ignoreOptions, decisionSink, options });
154
+ await pushRemote(client.stigmer, skill, {
155
+ org,
156
+ tag,
157
+ message,
158
+ ignoreOptions,
159
+ decisionSink,
160
+ options,
161
+ });
84
162
  return;
85
163
  }
86
164
 
@@ -94,14 +172,104 @@ async function runPush(type: string, path: string | undefined, options: PushFlag
94
172
  skill.parseSkillMetadata(directory);
95
173
 
96
174
  if (options.dryRun === true) {
97
- process.stdout.write(renderDryRun(directory, skill.analyzeDryRun(directory, ignoreOptions), skill));
175
+ process.stdout.write(
176
+ renderDryRun(
177
+ directory,
178
+ skill.analyzeDryRun(directory, ignoreOptions),
179
+ skill,
180
+ ),
181
+ );
98
182
  return;
99
183
  }
100
184
 
101
- const result = await skill.pushSkill(client.stigmer, directory, org, tag, message, ignoreOptions, decisionSink);
185
+ const result = await skill.pushSkill(
186
+ client.stigmer,
187
+ directory,
188
+ org,
189
+ tag,
190
+ message,
191
+ ignoreOptions,
192
+ decisionSink,
193
+ );
102
194
  process.stdout.write(renderResult(result, skill));
103
195
  }
104
196
 
197
+ /**
198
+ * `stigmer push plugin <dir>`: install or upgrade a plugin from its folder.
199
+ * The folder is read through the same walk `validate -f` uses, refused
200
+ * offline with the same sentences, zipped deterministically and pushed;
201
+ * `--dry-run` stops after the offline read and prints what would install.
202
+ * Skill-only flags (`--archive`, `--git-url`) are a contradiction here.
203
+ */
204
+ async function runPushPlugin(
205
+ path: string | undefined,
206
+ options: PushFlags,
207
+ command: Command,
208
+ ): Promise<void> {
209
+ const conflicts: string[] = [];
210
+ if (options.archive !== undefined && options.archive !== "")
211
+ conflicts.push("--archive");
212
+ if (options.gitUrl !== undefined && options.gitUrl !== "")
213
+ conflicts.push("--git-url");
214
+ if (options.gitRef !== undefined && options.gitRef !== "")
215
+ conflicts.push("--git-ref");
216
+ if (options.subdir !== undefined && options.subdir !== "")
217
+ conflicts.push("--subdir");
218
+ if (conflicts.length > 0) {
219
+ throw new UsageError(
220
+ `push plugin takes a plugin directory and cannot be combined with: ${conflicts.join(", ")}`,
221
+ );
222
+ }
223
+ const format = resultFormat(options);
224
+ const directory = path !== undefined && path !== "" ? path : process.cwd();
225
+ const plugin = await import("../resources/plugin.js");
226
+ if (!plugin.isPluginDirectory(directory)) {
227
+ throw new UsageError(
228
+ `${directory} is not a plugin directory\n\nA plugin directory holds one of: ${plugin.PLUGIN_MANIFEST_PATHS.join(", ")}`,
229
+ );
230
+ }
231
+ const ignoreOptions = {
232
+ respectGitignore: options.gitignore !== false,
233
+ extraIgnore: options.ignore ?? [],
234
+ extraInclude: options.include ?? [],
235
+ };
236
+ const { parseVisibility } = await import("../resources/skill.js");
237
+ const visibility = parseVisibility(options.visibility, "--visibility");
238
+
239
+ if (options.dryRun === true) {
240
+ const { readPluginPackage } = await import("@stigmer/plugin-package");
241
+ const read = plugin.readPluginDirectory(directory, ignoreOptions);
242
+ const outcome = readPluginPackage(read.files);
243
+ if (!outcome.ok) {
244
+ throw plugin.pluginRefusal(directory, outcome.errors, outcome.warnings);
245
+ }
246
+ const { CommandResult } = await import("../output/index.js");
247
+ const result = plugin.describePackageOn(
248
+ CommandResult.success(
249
+ `Dry run: plugin '${outcome.plugin.name}' would install`,
250
+ ),
251
+ outcome.plugin,
252
+ outcome.warnings,
253
+ read.stats,
254
+ );
255
+ result.hint("Run without --dry-run to install it.");
256
+ renderCommandResult(result, format);
257
+ return;
258
+ }
259
+
260
+ const { connectBackend } = await import("../backend.js");
261
+ const client = connectBackend();
262
+ ensureAuthenticated(client.config);
263
+ const org = resolveOrganization(client.config, globalOrg(command));
264
+ const outcome = await plugin.pushPlugin(client.stigmer, directory, {
265
+ org,
266
+ visibility,
267
+ message: options.message ?? "",
268
+ ignoreOptions,
269
+ });
270
+ renderCommandResult(plugin.renderPushOutcome(outcome), format);
271
+ }
272
+
105
273
  interface ArchiveContext {
106
274
  org: string;
107
275
  tag: string;
@@ -123,9 +291,12 @@ async function pushArchive(
123
291
  ): Promise<void> {
124
292
  const conflicts: string[] = [];
125
293
  if (ctx.path !== undefined && ctx.path !== "") conflicts.push("[path]");
126
- if (ctx.options.gitUrl !== undefined && ctx.options.gitUrl !== "") conflicts.push("--git-url");
127
- if (ctx.options.gitRef !== undefined && ctx.options.gitRef !== "") conflicts.push("--git-ref");
128
- if (ctx.options.subdir !== undefined && ctx.options.subdir !== "") conflicts.push("--subdir");
294
+ if (ctx.options.gitUrl !== undefined && ctx.options.gitUrl !== "")
295
+ conflicts.push("--git-url");
296
+ if (ctx.options.gitRef !== undefined && ctx.options.gitRef !== "")
297
+ conflicts.push("--git-ref");
298
+ if (ctx.options.subdir !== undefined && ctx.options.subdir !== "")
299
+ conflicts.push("--subdir");
129
300
  if ((ctx.options.ignore ?? []).length > 0) conflicts.push("--ignore");
130
301
  if ((ctx.options.include ?? []).length > 0) conflicts.push("--include");
131
302
  if (ctx.options.gitignore === false) conflicts.push("--no-gitignore");
@@ -138,11 +309,23 @@ async function pushArchive(
138
309
 
139
310
  const archivePath = ctx.options.archive ?? "";
140
311
  if (ctx.options.dryRun === true) {
141
- process.stdout.write(renderArchiveDryRun(archivePath, skill.readSkillArchive(archivePath), skill));
312
+ process.stdout.write(
313
+ renderArchiveDryRun(
314
+ archivePath,
315
+ skill.readSkillArchive(archivePath),
316
+ skill,
317
+ ),
318
+ );
142
319
  return;
143
320
  }
144
321
 
145
- const result = await skill.pushSkillFromArchive(client, archivePath, ctx.org, ctx.tag, ctx.message);
322
+ const result = await skill.pushSkillFromArchive(
323
+ client,
324
+ archivePath,
325
+ ctx.org,
326
+ ctx.tag,
327
+ ctx.message,
328
+ );
146
329
  process.stdout.write(renderResult(result, skill));
147
330
  }
148
331
 
@@ -173,7 +356,11 @@ interface RemoteContext {
173
356
  org: string;
174
357
  tag: string;
175
358
  message: string;
176
- ignoreOptions: { respectGitignore: boolean; extraIgnore: string[]; extraInclude: string[] };
359
+ ignoreOptions: {
360
+ respectGitignore: boolean;
361
+ extraIgnore: string[];
362
+ extraInclude: string[];
363
+ };
177
364
  decisionSink?: (line: string) => void;
178
365
  options: PushFlags;
179
366
  }
@@ -206,7 +393,15 @@ async function pushRemote(
206
393
  const result = await skill.pushSkillFromClone(
207
394
  client,
208
395
  skillDir,
209
- { gitUrl, gitRef, subdir, org: ctx.org, tag: ctx.tag, message: ctx.message, options: ctx.ignoreOptions },
396
+ {
397
+ gitUrl,
398
+ gitRef,
399
+ subdir,
400
+ org: ctx.org,
401
+ tag: ctx.tag,
402
+ message: ctx.message,
403
+ options: ctx.ignoreOptions,
404
+ },
210
405
  ctx.decisionSink,
211
406
  );
212
407
  process.stdout.write(renderResult(result, skill));
@@ -215,8 +410,15 @@ async function pushRemote(
215
410
  }
216
411
  }
217
412
 
218
- function renderResult(result: import("../resources/skill.js").PushResult, skill: typeof import("../resources/skill.js")): string {
219
- const status = result.isNewResource ? "new resource" : result.versionChanged ? "new version" : "unchanged";
413
+ function renderResult(
414
+ result: import("../resources/skill.js").PushResult,
415
+ skill: typeof import("../resources/skill.js"),
416
+ ): string {
417
+ const status = result.isNewResource
418
+ ? "new resource"
419
+ : result.versionChanged
420
+ ? "new version"
421
+ : "unchanged";
220
422
  const lines = [
221
423
  "",
222
424
  `Pushed skill '${result.skillName}'`,
@@ -250,25 +452,52 @@ function renderDryRun(
250
452
  `Directories skipped: ${stats.dirsSkipped}`,
251
453
  ];
252
454
  if (analysis.sampleIncluded.length > 0) {
253
- lines.push("", "Sample included:", ...analysis.sampleIncluded.map((f) => ` + ${f}`));
455
+ lines.push(
456
+ "",
457
+ "Sample included:",
458
+ ...analysis.sampleIncluded.map((f) => ` + ${f}`),
459
+ );
254
460
  }
255
461
  if (analysis.sampleIgnored.length > 0) {
256
- lines.push("", "Sample ignored:", ...analysis.sampleIgnored.map((f) => ` - ${f}`));
462
+ lines.push(
463
+ "",
464
+ "Sample ignored:",
465
+ ...analysis.sampleIgnored.map((f) => ` - ${f}`),
466
+ );
257
467
  }
258
468
  lines.push("", "Run without --dry-run to push the skill artifact.", "");
259
469
  return `${lines.join("\n")}\n`;
260
470
  }
261
471
 
262
- function renderRemoteDryRun(gitUrl: string, gitRef: string, subdir: string, ctx: RemoteContext): string {
263
- const lines = ["", "Dry run mode - would push skill from remote git repository:", "", "Git Source:", ` URL: ${gitUrl}`];
472
+ function renderRemoteDryRun(
473
+ gitUrl: string,
474
+ gitRef: string,
475
+ subdir: string,
476
+ ctx: RemoteContext,
477
+ ): string {
478
+ const lines = [
479
+ "",
480
+ "Dry run mode - would push skill from remote git repository:",
481
+ "",
482
+ "Git Source:",
483
+ ` URL: ${gitUrl}`,
484
+ ];
264
485
  if (gitRef !== "") lines.push(` Ref: ${gitRef}`);
265
486
  if (subdir !== "") lines.push(` Subdir: ${subdir}`);
266
487
  lines.push(` Tag: ${ctx.tag}`, "", "Ignore Configuration:");
267
- lines.push(` Gitignore: ${ctx.ignoreOptions.respectGitignore ? "enabled" : "disabled"}`);
488
+ lines.push(
489
+ ` Gitignore: ${ctx.ignoreOptions.respectGitignore ? "enabled" : "disabled"}`,
490
+ );
268
491
  lines.push(" Security defaults: enabled");
269
492
  lines.push(" Stigmerignore: will load if present");
270
- if (ctx.ignoreOptions.extraIgnore.length > 0) lines.push(` Extra ignore: ${ctx.ignoreOptions.extraIgnore.join(", ")}`);
271
- if (ctx.ignoreOptions.extraInclude.length > 0) lines.push(` Force include: ${ctx.ignoreOptions.extraInclude.join(", ")}`);
493
+ if (ctx.ignoreOptions.extraIgnore.length > 0)
494
+ lines.push(
495
+ ` Extra ignore: ${ctx.ignoreOptions.extraIgnore.join(", ")}`,
496
+ );
497
+ if (ctx.ignoreOptions.extraInclude.length > 0)
498
+ lines.push(
499
+ ` Force include: ${ctx.ignoreOptions.extraInclude.join(", ")}`,
500
+ );
272
501
  lines.push(
273
502
  "",
274
503
  "Note: Full analysis requires cloning the repository.",