@stigmer/cli 3.15.3-dev.20260914100347 → 3.15.3-dev.20260916211208
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 +3 -4
- package/commands/apply.d.ts.map +1 -1
- package/commands/apply.js +56 -4
- package/commands/apply.js.map +1 -1
- package/commands/push.d.ts.map +1 -1
- package/commands/push.js +122 -16
- package/commands/push.js.map +1 -1
- package/commands/validate.d.ts.map +1 -1
- package/commands/validate.js +39 -7
- package/commands/validate.js.map +1 -1
- package/output/command-result.d.ts +8 -0
- package/output/command-result.d.ts.map +1 -1
- package/output/command-result.js +13 -0
- package/output/command-result.js.map +1 -1
- package/package.json +6 -5
- package/registry/aliases.d.ts.map +1 -1
- package/registry/aliases.js.map +1 -1
- package/registry/index.d.ts +3 -3
- package/registry/index.d.ts.map +1 -1
- package/registry/index.js +3 -3
- package/registry/index.js.map +1 -1
- package/registry/metadata.d.ts.map +1 -1
- package/registry/metadata.js +85 -18
- package/registry/metadata.js.map +1 -1
- package/registry/registry.d.ts.map +1 -1
- package/registry/registry.js.map +1 -1
- package/registry/verb-support.d.ts.map +1 -1
- package/registry/verb-support.js +81 -13
- package/registry/verb-support.js.map +1 -1
- package/resources/delete.d.ts.map +1 -1
- package/resources/delete.js +35 -9
- package/resources/delete.js.map +1 -1
- package/resources/get-bindings.d.ts.map +1 -1
- package/resources/get-bindings.js +56 -13
- package/resources/get-bindings.js.map +1 -1
- package/resources/list.d.ts.map +1 -1
- package/resources/list.js +62 -14
- package/resources/list.js.map +1 -1
- package/resources/plugin.d.ts +80 -0
- package/resources/plugin.d.ts.map +1 -0
- package/resources/plugin.js +305 -0
- package/resources/plugin.js.map +1 -0
- package/resources/skill.d.ts +1 -0
- package/resources/skill.d.ts.map +1 -1
- package/resources/skill.js +1 -1
- package/resources/skill.js.map +1 -1
- package/src/commands/apply.ts +133 -21
- package/src/commands/push.ts +270 -41
- package/src/commands/validate.test.ts +111 -22
- package/src/commands/validate.ts +68 -12
- package/src/output/command-result.test.ts +9 -0
- package/src/output/command-result.ts +14 -0
- package/src/registry/aliases.test.ts +24 -5
- package/src/registry/aliases.ts +6 -1
- package/src/registry/index.ts +19 -3
- package/src/registry/metadata.ts +85 -18
- package/src/registry/registry.test.ts +86 -37
- package/src/registry/registry.ts +8 -2
- package/src/registry/verb-support.ts +85 -14
- package/src/resources/delete.ts +106 -26
- package/src/resources/get-bindings.ts +95 -17
- package/src/resources/list.ts +102 -28
- package/src/resources/plugin.test.ts +233 -0
- package/src/resources/plugin.ts +432 -0
- package/src/resources/skill.ts +1 -1
|
@@ -10,6 +10,7 @@ import { AgentInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/agentins
|
|
|
10
10
|
import { ChannelAppSchema } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/api_pb";
|
|
11
11
|
import { EnvironmentSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
|
|
12
12
|
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
13
|
+
import { PluginSchema } from "@stigmer/protos/ai/stigmer/agentic/plugin/v1/api_pb";
|
|
13
14
|
import { ScheduleSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
14
15
|
import { SkillSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
15
16
|
import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
@@ -26,24 +27,87 @@ export interface ResourceResult {
|
|
|
26
27
|
readonly message: Message;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
type Getter = (
|
|
30
|
+
type Getter = (
|
|
31
|
+
client: Stigmer,
|
|
32
|
+
ref: ParsedReference,
|
|
33
|
+
) => Promise<ResourceResult>;
|
|
30
34
|
|
|
31
35
|
// Every kind declaring Get in the verb matrix must have an entry here (or a
|
|
32
36
|
// documented special case, e.g. organization) — the conformance test in
|
|
33
37
|
// registry/registry.test.ts enforces it, so the two cannot drift.
|
|
34
38
|
export const GET_BINDINGS: ReadonlyMap<ApiResourceKind, Getter> = new Map([
|
|
35
|
-
[
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
[
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
[
|
|
40
|
+
ApiResourceKind.agent,
|
|
41
|
+
refGetter(ApiResourceKind.agent, AgentSchema, (c) => c.agent),
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
ApiResourceKind.agent_instance,
|
|
45
|
+
refGetter(
|
|
46
|
+
ApiResourceKind.agent_instance,
|
|
47
|
+
AgentInstanceSchema,
|
|
48
|
+
(c) => c.agentInstance,
|
|
49
|
+
),
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
ApiResourceKind.workflow,
|
|
53
|
+
refGetter(ApiResourceKind.workflow, WorkflowSchema, (c) => c.workflow),
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
ApiResourceKind.workflow_instance,
|
|
57
|
+
refGetter(
|
|
58
|
+
ApiResourceKind.workflow_instance,
|
|
59
|
+
WorkflowInstanceSchema,
|
|
60
|
+
(c) => c.workflowInstance,
|
|
61
|
+
),
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
ApiResourceKind.mcp_server,
|
|
65
|
+
refGetter(ApiResourceKind.mcp_server, McpServerSchema, (c) => c.mcpServer),
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
ApiResourceKind.project,
|
|
69
|
+
refGetter(ApiResourceKind.project, ProjectSchema, (c) => c.project),
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
ApiResourceKind.environment,
|
|
73
|
+
refGetter(
|
|
74
|
+
ApiResourceKind.environment,
|
|
75
|
+
EnvironmentSchema,
|
|
76
|
+
(c) => c.environment,
|
|
77
|
+
),
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
ApiResourceKind.agent_channel,
|
|
81
|
+
refGetter(
|
|
82
|
+
ApiResourceKind.agent_channel,
|
|
83
|
+
AgentChannelSchema,
|
|
84
|
+
(c) => c.agentChannel,
|
|
85
|
+
),
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
ApiResourceKind.channel_app,
|
|
89
|
+
refGetter(
|
|
90
|
+
ApiResourceKind.channel_app,
|
|
91
|
+
ChannelAppSchema,
|
|
92
|
+
(c) => c.channelapp,
|
|
93
|
+
),
|
|
94
|
+
],
|
|
95
|
+
[
|
|
96
|
+
ApiResourceKind.schedule,
|
|
97
|
+
refGetter(ApiResourceKind.schedule, ScheduleSchema, (c) => c.schedule),
|
|
98
|
+
],
|
|
99
|
+
[
|
|
100
|
+
ApiResourceKind.skill,
|
|
101
|
+
refGetter(ApiResourceKind.skill, SkillSchema, (c) => c.skill),
|
|
102
|
+
],
|
|
103
|
+
[
|
|
104
|
+
ApiResourceKind.plugin,
|
|
105
|
+
refGetter(ApiResourceKind.plugin, PluginSchema, (c) => c.plugin),
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
ApiResourceKind.api_key,
|
|
109
|
+
idOnlyGetter(ApiKeySchema, (c) => c.apiKey, "API keys"),
|
|
110
|
+
],
|
|
47
111
|
]);
|
|
48
112
|
|
|
49
113
|
export function getterFor(kind: ApiResourceKind): Getter | undefined {
|
|
@@ -53,20 +117,34 @@ export function getterFor(kind: ApiResourceKind): Getter | undefined {
|
|
|
53
117
|
// A resource fetchable by ID or by org/slug reference.
|
|
54
118
|
interface RefClient {
|
|
55
119
|
get(id: string): Promise<Message>;
|
|
56
|
-
getByReference(ref: {
|
|
120
|
+
getByReference(ref: {
|
|
121
|
+
org: string;
|
|
122
|
+
slug: string;
|
|
123
|
+
kind: number;
|
|
124
|
+
}): Promise<Message>;
|
|
57
125
|
}
|
|
58
126
|
|
|
59
|
-
function refGetter(
|
|
127
|
+
function refGetter(
|
|
128
|
+
kind: ApiResourceKind,
|
|
129
|
+
schema: DescMessage,
|
|
130
|
+
pick: (c: Stigmer) => RefClient,
|
|
131
|
+
): Getter {
|
|
60
132
|
return async (client, ref) => {
|
|
61
133
|
const sub = pick(client);
|
|
62
134
|
const message =
|
|
63
|
-
ref.kind === "id"
|
|
135
|
+
ref.kind === "id"
|
|
136
|
+
? await sub.get(ref.id)
|
|
137
|
+
: await sub.getByReference({ org: ref.org, slug: ref.slug, kind });
|
|
64
138
|
return { schema, message };
|
|
65
139
|
};
|
|
66
140
|
}
|
|
67
141
|
|
|
68
142
|
// A resource fetchable only by ID (e.g. API keys are not slug-addressable).
|
|
69
|
-
function idOnlyGetter(
|
|
143
|
+
function idOnlyGetter(
|
|
144
|
+
schema: DescMessage,
|
|
145
|
+
pick: (c: Stigmer) => { get(id: string): Promise<Message> },
|
|
146
|
+
label: string,
|
|
147
|
+
): Getter {
|
|
70
148
|
return async (client, ref) => {
|
|
71
149
|
if (ref.kind !== "id") {
|
|
72
150
|
throw new UsageError(`${label} can only be fetched by ID`);
|
package/src/resources/list.ts
CHANGED
|
@@ -35,7 +35,14 @@ import { OrganizationSchema } from "@stigmer/protos/ai/stigmer/tenancy/organizat
|
|
|
35
35
|
import type { Stigmer } from "@stigmer/sdk";
|
|
36
36
|
import { UsageError } from "../errors/index.js";
|
|
37
37
|
import type { OutputFormat } from "../output/index.js";
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
bool,
|
|
40
|
+
type JsonObject,
|
|
41
|
+
obj,
|
|
42
|
+
renderCollection,
|
|
43
|
+
str,
|
|
44
|
+
type TableShape,
|
|
45
|
+
} from "./render.js";
|
|
39
46
|
|
|
40
47
|
// Kinds that list through the SearchService (list mode: empty query, org
|
|
41
48
|
// scope). Must stay in step with the server's SearchableKinds allowlist
|
|
@@ -43,14 +50,16 @@ import { bool, type JsonObject, obj, renderCollection, str, type TableShape } fr
|
|
|
43
50
|
// search_criteria.go): a kind present here but absent there silently lists
|
|
44
51
|
// as empty. The LIST_HANDLERS kinds are deliberately NOT here — see the
|
|
45
52
|
// header: not search-indexed, or better served by their dedicated RPC.
|
|
46
|
-
export const SEARCH_KINDS: ReadonlySet<ApiResourceKind> =
|
|
47
|
-
ApiResourceKind
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
export const SEARCH_KINDS: ReadonlySet<ApiResourceKind> =
|
|
54
|
+
new Set<ApiResourceKind>([
|
|
55
|
+
ApiResourceKind.agent,
|
|
56
|
+
ApiResourceKind.workflow,
|
|
57
|
+
ApiResourceKind.mcp_server,
|
|
58
|
+
ApiResourceKind.project,
|
|
59
|
+
ApiResourceKind.skill,
|
|
60
|
+
ApiResourceKind.plugin,
|
|
61
|
+
ApiResourceKind.environment,
|
|
62
|
+
]);
|
|
54
63
|
|
|
55
64
|
// One fetched page of a listing: what to render (entries + schema) and how
|
|
56
65
|
// (table). Rendering and --limit truncation happen centrally in
|
|
@@ -64,12 +73,19 @@ interface ListPage {
|
|
|
64
73
|
// Where the RPC paginates, the handler still forwards `limit` as the page
|
|
65
74
|
// size so the server does the bounding; the dispatcher's slice is then a
|
|
66
75
|
// no-op. Where it doesn't, the slice IS the bound.
|
|
67
|
-
type ListFn = (
|
|
76
|
+
type ListFn = (
|
|
77
|
+
client: Stigmer,
|
|
78
|
+
org: string,
|
|
79
|
+
limit: number,
|
|
80
|
+
) => Promise<ListPage>;
|
|
68
81
|
|
|
69
82
|
// Dedicated-RPC list handlers for the non-search-indexed kinds. Every kind
|
|
70
83
|
// declaring List in the verb matrix must appear here or in SEARCH_KINDS —
|
|
71
84
|
// the conformance test enforces it, so the two cannot drift.
|
|
72
|
-
export const LIST_HANDLERS: ReadonlyMap<ApiResourceKind, ListFn> = new Map<
|
|
85
|
+
export const LIST_HANDLERS: ReadonlyMap<ApiResourceKind, ListFn> = new Map<
|
|
86
|
+
ApiResourceKind,
|
|
87
|
+
ListFn
|
|
88
|
+
>([
|
|
73
89
|
[
|
|
74
90
|
ApiResourceKind.organization,
|
|
75
91
|
async (client) => {
|
|
@@ -77,7 +93,11 @@ export const LIST_HANDLERS: ReadonlyMap<ApiResourceKind, ListFn> = new Map<ApiRe
|
|
|
77
93
|
// a caller's org set is small by nature). The paginated `find` RPC is
|
|
78
94
|
// platform-admin-only — the wrong surface for `list organization`.
|
|
79
95
|
const result = await client.organization.findMyOrganizations();
|
|
80
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
schema: OrganizationSchema,
|
|
98
|
+
entries: result.entries,
|
|
99
|
+
table: ORG_TABLE,
|
|
100
|
+
};
|
|
81
101
|
},
|
|
82
102
|
],
|
|
83
103
|
[
|
|
@@ -85,25 +105,43 @@ export const LIST_HANDLERS: ReadonlyMap<ApiResourceKind, ListFn> = new Map<ApiRe
|
|
|
85
105
|
async (client) => {
|
|
86
106
|
// findAll is caller-scoped and unpaginated (Empty request).
|
|
87
107
|
const result = await client.apiKey.findAll();
|
|
88
|
-
return {
|
|
108
|
+
return {
|
|
109
|
+
schema: ApiKeySchema,
|
|
110
|
+
entries: result.entries,
|
|
111
|
+
table: APIKEY_TABLE,
|
|
112
|
+
};
|
|
89
113
|
},
|
|
90
114
|
],
|
|
91
115
|
[
|
|
92
116
|
ApiResourceKind.agent_instance,
|
|
93
117
|
async (client, org, limit) => {
|
|
94
118
|
const result = await client.agentInstance.list(
|
|
95
|
-
create(ListAgentInstancesRequestSchema, {
|
|
119
|
+
create(ListAgentInstancesRequestSchema, {
|
|
120
|
+
org,
|
|
121
|
+
pageInfo: create(PageInfoSchema, { num: 1, size: limit }),
|
|
122
|
+
}),
|
|
96
123
|
);
|
|
97
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
schema: AgentInstanceSchema,
|
|
126
|
+
entries: result.items,
|
|
127
|
+
table: INSTANCE_TABLE,
|
|
128
|
+
};
|
|
98
129
|
},
|
|
99
130
|
],
|
|
100
131
|
[
|
|
101
132
|
ApiResourceKind.agent_channel,
|
|
102
133
|
async (client, org, limit) => {
|
|
103
134
|
const result = await client.agentChannel.list(
|
|
104
|
-
create(ListAgentChannelsRequestSchema, {
|
|
135
|
+
create(ListAgentChannelsRequestSchema, {
|
|
136
|
+
org,
|
|
137
|
+
pageInfo: create(PageInfoSchema, { num: 1, size: limit }),
|
|
138
|
+
}),
|
|
105
139
|
);
|
|
106
|
-
return {
|
|
140
|
+
return {
|
|
141
|
+
schema: AgentChannelSchema,
|
|
142
|
+
entries: result.items,
|
|
143
|
+
table: AGENT_CHANNEL_TABLE,
|
|
144
|
+
};
|
|
107
145
|
},
|
|
108
146
|
],
|
|
109
147
|
[
|
|
@@ -112,8 +150,14 @@ export const LIST_HANDLERS: ReadonlyMap<ApiResourceKind, ListFn> = new Map<ApiRe
|
|
|
112
150
|
// listByOrg has no pagination on its contract (the per-org set is small
|
|
113
151
|
// by design); the dispatcher's slice bounds the output.
|
|
114
152
|
// `channelapp` (not `channelApp`) is a recorded SDK codegen naming quirk.
|
|
115
|
-
const result = await client.channelapp.listByOrg(
|
|
116
|
-
|
|
153
|
+
const result = await client.channelapp.listByOrg(
|
|
154
|
+
create(ListChannelAppsByOrgInputSchema, { org }),
|
|
155
|
+
);
|
|
156
|
+
return {
|
|
157
|
+
schema: ChannelAppSchema,
|
|
158
|
+
entries: result.entries,
|
|
159
|
+
table: CHANNEL_APP_TABLE,
|
|
160
|
+
};
|
|
117
161
|
},
|
|
118
162
|
],
|
|
119
163
|
[
|
|
@@ -128,9 +172,16 @@ export const LIST_HANDLERS: ReadonlyMap<ApiResourceKind, ListFn> = new Map<ApiRe
|
|
|
128
172
|
);
|
|
129
173
|
}
|
|
130
174
|
const result = await client.schedule.list(
|
|
131
|
-
create(ListSchedulesRequestSchema, {
|
|
175
|
+
create(ListSchedulesRequestSchema, {
|
|
176
|
+
org,
|
|
177
|
+
pageInfo: create(PageInfoSchema, { num: 1, size: limit }),
|
|
178
|
+
}),
|
|
132
179
|
);
|
|
133
|
-
return {
|
|
180
|
+
return {
|
|
181
|
+
schema: ScheduleSchema,
|
|
182
|
+
entries: result.items,
|
|
183
|
+
table: SCHEDULE_TABLE,
|
|
184
|
+
};
|
|
134
185
|
},
|
|
135
186
|
],
|
|
136
187
|
[
|
|
@@ -142,8 +193,14 @@ export const LIST_HANDLERS: ReadonlyMap<ApiResourceKind, ListFn> = new Map<ApiRe
|
|
|
142
193
|
// api_key above — the agent_instance precedent, not an oversight.
|
|
143
194
|
// Promoted from a bespoke pre-gate route in commands/list.ts by
|
|
144
195
|
// stigmer/stigmer#469 (it had shipped working-but-unadvertised).
|
|
145
|
-
const result = await client.session.list(
|
|
146
|
-
|
|
196
|
+
const result = await client.session.list(
|
|
197
|
+
create(ListSessionsRequestSchema, { pageSize: limit }),
|
|
198
|
+
);
|
|
199
|
+
return {
|
|
200
|
+
schema: SessionSchema,
|
|
201
|
+
entries: result.entries,
|
|
202
|
+
table: SESSION_TABLE,
|
|
203
|
+
};
|
|
147
204
|
},
|
|
148
205
|
],
|
|
149
206
|
]);
|
|
@@ -162,7 +219,12 @@ export async function listResources(
|
|
|
162
219
|
// rather than per-handler is what keeps the flag honest for every kind —
|
|
163
220
|
// organization and api_key shipped ignoring it when handlers owned this
|
|
164
221
|
// (stigmer/stigmer#312).
|
|
165
|
-
return renderCollection(
|
|
222
|
+
return renderCollection(
|
|
223
|
+
page.schema,
|
|
224
|
+
page.entries.slice(0, limit),
|
|
225
|
+
format,
|
|
226
|
+
page.table,
|
|
227
|
+
);
|
|
166
228
|
}
|
|
167
229
|
|
|
168
230
|
async function fetchListPage(
|
|
@@ -178,8 +240,16 @@ async function fetchListPage(
|
|
|
178
240
|
if (!SEARCH_KINDS.has(kind)) {
|
|
179
241
|
throw new UsageError("list is not implemented for this resource type");
|
|
180
242
|
}
|
|
181
|
-
const result = await client.search.query({
|
|
182
|
-
|
|
243
|
+
const result = await client.search.query({
|
|
244
|
+
kinds: [kind],
|
|
245
|
+
org,
|
|
246
|
+
page: { num: 1, size: limit },
|
|
247
|
+
});
|
|
248
|
+
return {
|
|
249
|
+
schema: SearchResultSchema,
|
|
250
|
+
entries: result.entries,
|
|
251
|
+
table: SEARCH_TABLE,
|
|
252
|
+
};
|
|
183
253
|
}
|
|
184
254
|
|
|
185
255
|
// Shared with `search` — both render SearchService results identically.
|
|
@@ -258,7 +328,9 @@ const CHANNEL_APP_TABLE: TableShape = {
|
|
|
258
328
|
str(metadata, "id"),
|
|
259
329
|
str(metadata, "slug"),
|
|
260
330
|
providerOf(obj(json, "spec")),
|
|
261
|
-
date(
|
|
331
|
+
date(
|
|
332
|
+
str(obj(obj(obj(json, "status"), "audit"), "spec_audit"), "created_at"),
|
|
333
|
+
),
|
|
262
334
|
];
|
|
263
335
|
},
|
|
264
336
|
};
|
|
@@ -332,7 +404,9 @@ function apiKeyExpiry(spec: JsonObject): string {
|
|
|
332
404
|
|
|
333
405
|
function truncate(text: string, max: number): string {
|
|
334
406
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
335
|
-
return normalized.length > max
|
|
407
|
+
return normalized.length > max
|
|
408
|
+
? `${normalized.slice(0, max - 1)}…`
|
|
409
|
+
: normalized;
|
|
336
410
|
}
|
|
337
411
|
|
|
338
412
|
function date(timestamp: string): string {
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Pins the plugin directory walker: detection by any of the four manifests,
|
|
2
|
+
// sorted sized entries, the ignore matcher applied (defaults, .gitignore,
|
|
3
|
+
// .stigmerignore), symlinks never followed, and the JSON projection that
|
|
4
|
+
// reduces overlay documents to their paths.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
mkdirSync,
|
|
8
|
+
mkdtempSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
symlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
16
|
+
import { readPluginPackage } from "@stigmer/plugin-package";
|
|
17
|
+
import {
|
|
18
|
+
describePlugin,
|
|
19
|
+
isPluginDirectory,
|
|
20
|
+
readPluginDirectory,
|
|
21
|
+
} from "./plugin.js";
|
|
22
|
+
|
|
23
|
+
let root: string;
|
|
24
|
+
|
|
25
|
+
function write(rel: string, content: string): void {
|
|
26
|
+
const full = join(root, rel);
|
|
27
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
28
|
+
writeFileSync(full, content);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
root = mkdtempSync(join(tmpdir(), "stigmer-plugin-walk-"));
|
|
33
|
+
write(
|
|
34
|
+
".cursor-plugin/plugin.json",
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
name: "walker",
|
|
37
|
+
skills: "./skills/",
|
|
38
|
+
mcpServers: "./mcp.json",
|
|
39
|
+
variables: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: { TOKEN: { type: "string" } },
|
|
42
|
+
required: ["TOKEN"],
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
write(
|
|
47
|
+
"mcp.json",
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
mcpServers: {
|
|
50
|
+
api: {
|
|
51
|
+
type: "http",
|
|
52
|
+
url: "https://api.example.com/mcp",
|
|
53
|
+
headers: { Authorization: "Bearer ${TOKEN}" },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
write(
|
|
59
|
+
"skills/greet/SKILL.md",
|
|
60
|
+
"---\nname: greet\ndescription: Says hello.\n---\nSay hello.\n",
|
|
61
|
+
);
|
|
62
|
+
write("skills/greet/scripts/hello.sh", "echo hello");
|
|
63
|
+
write("skills/greet/secrets.yaml", "never: included");
|
|
64
|
+
write("skills/greet/.env", "SECRET=1");
|
|
65
|
+
write("node_modules/dep/index.js", "module.exports = 1");
|
|
66
|
+
write("ai.stigmer/agent.yaml", "kind: Agent\n");
|
|
67
|
+
write(".gitignore", "*.log\n");
|
|
68
|
+
write("debug.log", "ignored by .gitignore");
|
|
69
|
+
write(".stigmerignore", "drafts/\n");
|
|
70
|
+
write("drafts/idea.md", "ignored by .stigmerignore");
|
|
71
|
+
symlinkSync(join(root, "mcp.json"), join(root, "mcp-link.json"));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterAll(() => {
|
|
75
|
+
rmSync(root, { recursive: true, force: true });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("isPluginDirectory", () => {
|
|
79
|
+
it("is true for a directory holding any of the four manifests and false otherwise", () => {
|
|
80
|
+
expect(isPluginDirectory(root)).toBe(true);
|
|
81
|
+
expect(isPluginDirectory(join(root, "skills"))).toBe(false);
|
|
82
|
+
expect(isPluginDirectory(join(root, "mcp.json"))).toBe(false);
|
|
83
|
+
expect(isPluginDirectory(join(root, "does-not-exist"))).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("readPluginDirectory", () => {
|
|
88
|
+
it("lists sorted, sized, contained entries and applies every ignore source", () => {
|
|
89
|
+
const { files, stats } = readPluginDirectory(root);
|
|
90
|
+
expect(files.entries.map((e) => e.path)).toEqual([
|
|
91
|
+
".cursor-plugin/plugin.json",
|
|
92
|
+
".gitignore",
|
|
93
|
+
".stigmerignore",
|
|
94
|
+
"ai.stigmer/agent.yaml",
|
|
95
|
+
"mcp.json",
|
|
96
|
+
"skills/greet/SKILL.md",
|
|
97
|
+
"skills/greet/scripts/hello.sh",
|
|
98
|
+
]);
|
|
99
|
+
expect(
|
|
100
|
+
files.entries.find((e) => e.path === "skills/greet/scripts/hello.sh")
|
|
101
|
+
?.size,
|
|
102
|
+
).toBe("echo hello".length);
|
|
103
|
+
// .env and secrets.yaml (security defaults), debug.log (.gitignore); the
|
|
104
|
+
// symlink is neither a file nor a directory and is skipped uncounted.
|
|
105
|
+
expect(stats).toMatchObject({
|
|
106
|
+
filesIncluded: 7,
|
|
107
|
+
filesIgnored: 3,
|
|
108
|
+
dirsSkipped: 2,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("reads exactly the listed bytes", () => {
|
|
113
|
+
const { files } = readPluginDirectory(root);
|
|
114
|
+
expect(new TextDecoder().decode(files.read("ai.stigmer/agent.yaml"))).toBe(
|
|
115
|
+
"kind: Agent\n",
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("feeds the reader a plugin it accepts", () => {
|
|
120
|
+
const { files, stats } = readPluginDirectory(root);
|
|
121
|
+
const outcome = readPluginPackage(files);
|
|
122
|
+
expect(outcome.ok).toBe(true);
|
|
123
|
+
if (!outcome.ok) return;
|
|
124
|
+
const description = describePlugin(outcome.plugin, outcome.warnings, stats);
|
|
125
|
+
expect(description.plugin.overlay).toEqual({
|
|
126
|
+
agent: "ai.stigmer/agent.yaml",
|
|
127
|
+
workflows: [],
|
|
128
|
+
mcpServers: [],
|
|
129
|
+
});
|
|
130
|
+
expect(description.excludedFiles).toBe(3);
|
|
131
|
+
expect(JSON.stringify(description)).not.toContain("bytes");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("zipPluginFiles", () => {
|
|
136
|
+
it("writes the walked list deterministically and round-trips through the library's own reader", async () => {
|
|
137
|
+
const { zipPluginFiles } = await import("./plugin.js");
|
|
138
|
+
const { unzipSync } = await import("fflate");
|
|
139
|
+
const directory = readPluginDirectory(root);
|
|
140
|
+
const first = zipPluginFiles(directory.files);
|
|
141
|
+
const second = zipPluginFiles(directory.files);
|
|
142
|
+
expect(Buffer.from(first).equals(Buffer.from(second))).toBe(true);
|
|
143
|
+
const entries = Object.keys(unzipSync(first)).sort();
|
|
144
|
+
expect(entries).toEqual(directory.files.entries.map((e) => e.path));
|
|
145
|
+
expect(entries).not.toContain("debug.log");
|
|
146
|
+
expect(entries).not.toContain("mcp-link.json");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("renderPushOutcome", () => {
|
|
151
|
+
it("summarises the install by kind, lists members and carries warnings", async () => {
|
|
152
|
+
const { renderPushOutcome } = await import("./plugin.js");
|
|
153
|
+
const { create } = await import("@bufbuild/protobuf");
|
|
154
|
+
const { PluginSchema } =
|
|
155
|
+
await import("@stigmer/protos/ai/stigmer/agentic/plugin/v1/api_pb");
|
|
156
|
+
const { PluginMemberSchema } =
|
|
157
|
+
await import("@stigmer/protos/ai/stigmer/agentic/plugin/v1/io_pb");
|
|
158
|
+
const { PluginDialect } =
|
|
159
|
+
await import("@stigmer/protos/ai/stigmer/agentic/plugin/v1/spec_pb");
|
|
160
|
+
const { PluginState } =
|
|
161
|
+
await import("@stigmer/protos/ai/stigmer/agentic/plugin/v1/status_pb");
|
|
162
|
+
const { ApiResourceKind } =
|
|
163
|
+
await import("@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb");
|
|
164
|
+
const { renderResult } = await import("../output/command-result.js");
|
|
165
|
+
|
|
166
|
+
const plugin = create(PluginSchema, {
|
|
167
|
+
metadata: { id: "plg_1", slug: "thermos" },
|
|
168
|
+
spec: {
|
|
169
|
+
name: "thermos",
|
|
170
|
+
version: "1.2.0",
|
|
171
|
+
dialect: PluginDialect.CURSOR,
|
|
172
|
+
},
|
|
173
|
+
status: {
|
|
174
|
+
digest: "a".repeat(64),
|
|
175
|
+
state: PluginState.READY,
|
|
176
|
+
materialized: { skills: 2, mcpServers: 1, agents: 1, workflows: 0 },
|
|
177
|
+
warnings: [
|
|
178
|
+
{
|
|
179
|
+
kind: "model-hint-unresolved",
|
|
180
|
+
message:
|
|
181
|
+
"sub-agent 'reviewer' names model 'sonnet'; it runs on the session's model",
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
const members = [
|
|
187
|
+
create(PluginMemberSchema, {
|
|
188
|
+
kind: ApiResourceKind.skill,
|
|
189
|
+
id: "skl_1",
|
|
190
|
+
slug: "review",
|
|
191
|
+
name: "review",
|
|
192
|
+
}),
|
|
193
|
+
create(PluginMemberSchema, {
|
|
194
|
+
kind: ApiResourceKind.skill,
|
|
195
|
+
id: "skl_2",
|
|
196
|
+
slug: "triage",
|
|
197
|
+
name: "triage",
|
|
198
|
+
}),
|
|
199
|
+
create(PluginMemberSchema, {
|
|
200
|
+
kind: ApiResourceKind.mcp_server,
|
|
201
|
+
id: "mcp_1",
|
|
202
|
+
slug: "github",
|
|
203
|
+
name: "github",
|
|
204
|
+
}),
|
|
205
|
+
create(PluginMemberSchema, {
|
|
206
|
+
kind: ApiResourceKind.agent,
|
|
207
|
+
id: "agt_1",
|
|
208
|
+
slug: "thermos",
|
|
209
|
+
name: "thermos",
|
|
210
|
+
}),
|
|
211
|
+
];
|
|
212
|
+
const result = renderPushOutcome({ plugin, members, archiveBytes: 2048 });
|
|
213
|
+
const lines: string[] = [];
|
|
214
|
+
const original = process.stderr.write.bind(process.stderr);
|
|
215
|
+
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
216
|
+
lines.push(String(chunk));
|
|
217
|
+
return true;
|
|
218
|
+
}) as typeof process.stderr.write;
|
|
219
|
+
try {
|
|
220
|
+
renderResult(result, "human");
|
|
221
|
+
} finally {
|
|
222
|
+
process.stderr.write = original;
|
|
223
|
+
}
|
|
224
|
+
const text = lines.join("");
|
|
225
|
+
expect(text).toContain(
|
|
226
|
+
"Installed plugin 'thermos' (2 skills, 1 MCP server, 1 agent) with 1 warning",
|
|
227
|
+
);
|
|
228
|
+
expect(text).toContain("Skills");
|
|
229
|
+
expect(text).toContain("review, triage");
|
|
230
|
+
expect(text).toContain("Cursor plugin");
|
|
231
|
+
expect(text).toContain("runs on the session's model");
|
|
232
|
+
});
|
|
233
|
+
});
|