@enactprotocol/cli 2.3.4 → 2.3.7

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 (36) hide show
  1. package/dist/commands/index.d.ts +1 -0
  2. package/dist/commands/index.d.ts.map +1 -1
  3. package/dist/commands/index.js +2 -0
  4. package/dist/commands/index.js.map +1 -1
  5. package/dist/commands/org/index.d.ts +16 -0
  6. package/dist/commands/org/index.d.ts.map +1 -0
  7. package/dist/commands/org/index.js +203 -0
  8. package/dist/commands/org/index.js.map +1 -0
  9. package/dist/commands/publish/index.d.ts.map +1 -1
  10. package/dist/commands/publish/index.js +5 -1
  11. package/dist/commands/publish/index.js.map +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +4 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/utils/errors.js +2 -2
  17. package/dist/utils/errors.js.map +1 -1
  18. package/package.json +6 -6
  19. package/src/commands/env/README.md +1 -1
  20. package/src/commands/index.ts +3 -0
  21. package/src/commands/install/README.md +1 -1
  22. package/src/commands/org/index.ts +263 -0
  23. package/src/commands/publish/index.ts +4 -1
  24. package/src/commands/run/README.md +1 -1
  25. package/src/index.ts +5 -1
  26. package/src/utils/errors.ts +2 -2
  27. package/tests/commands/install-integration.test.ts +5 -5
  28. package/tests/commands/publish.test.ts +5 -3
  29. package/tests/commands/sign.test.ts +1 -1
  30. package/tests/e2e.test.ts +3 -1
  31. package/tests/utils/ignore.test.ts +2 -1
  32. package/tsconfig.tsbuildinfo +1 -1
  33. /package/tests/fixtures/calculator/{skill.yaml → skill.package.yaml} +0 -0
  34. /package/tests/fixtures/env-tool/{skill.yaml → skill.package.yaml} +0 -0
  35. /package/tests/fixtures/greeter/{skill.yaml → skill.package.yaml} +0 -0
  36. /package/tests/fixtures/invalid-tool/{skill.yaml → skill.package.yaml} +0 -0
@@ -0,0 +1,263 @@
1
+ /**
2
+ * enact org command
3
+ *
4
+ * Manage organizations for @org scoped tool namespaces.
5
+ *
6
+ * Subcommands:
7
+ * - create: Create a new organization
8
+ * - info: Show organization details
9
+ * - list: List your organizations (TODO: requires user orgs endpoint)
10
+ * - add-member: Add a member to an organization
11
+ * - remove-member: Remove a member from an organization
12
+ * - set-role: Change a member's role
13
+ */
14
+
15
+ import {
16
+ addOrgMember,
17
+ createApiClient,
18
+ createOrg,
19
+ getOrg,
20
+ listOrgMembers,
21
+ removeOrgMember,
22
+ updateOrgMemberRole,
23
+ } from "@enactprotocol/api";
24
+ import { getSecret } from "@enactprotocol/secrets";
25
+ import { loadConfig } from "@enactprotocol/shared";
26
+ import type { OrgRole } from "@enactprotocol/shared";
27
+ import type { Command } from "commander";
28
+ import type { GlobalOptions } from "../../types";
29
+ import {
30
+ type TableColumn,
31
+ dim,
32
+ error,
33
+ formatError,
34
+ header,
35
+ json,
36
+ keyValue,
37
+ newline,
38
+ success,
39
+ table,
40
+ } from "../../utils";
41
+
42
+ const AUTH_NAMESPACE = "enact:auth";
43
+ const ACCESS_TOKEN_KEY = "access_token";
44
+
45
+ const VALID_ROLES = ["owner", "admin", "member"] as const;
46
+
47
+ async function getClient() {
48
+ const config = loadConfig();
49
+ const token = await getSecret(AUTH_NAMESPACE, ACCESS_TOKEN_KEY);
50
+ return createApiClient({
51
+ baseUrl: config.registry?.url,
52
+ authToken: token ?? undefined,
53
+ });
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Handlers
58
+ // ---------------------------------------------------------------------------
59
+
60
+ async function createHandler(
61
+ name: string,
62
+ options: { displayName?: string; description?: string } & GlobalOptions
63
+ ) {
64
+ try {
65
+ const client = await getClient();
66
+ const org = await createOrg(client, {
67
+ name,
68
+ displayName: options.displayName,
69
+ description: options.description,
70
+ });
71
+
72
+ if (options.json) {
73
+ json(org);
74
+ return;
75
+ }
76
+
77
+ success(`Organization "@${org.name}" created`);
78
+ newline();
79
+ keyValue("Name", `@${org.name}`);
80
+ if (org.display_name) keyValue("Display Name", org.display_name);
81
+ if (org.description) keyValue("Description", org.description);
82
+ newline();
83
+ dim("You are the owner. Publish tools with:");
84
+ dim(` enact publish (with name: "@${org.name}/your-tool" in SKILL.md)`);
85
+ } catch (err) {
86
+ error(formatError(err));
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ async function infoHandler(name: string, options: GlobalOptions) {
92
+ try {
93
+ // Strip @ prefix if provided
94
+ const orgName = name.startsWith("@") ? name.substring(1) : name;
95
+ const client = await getClient();
96
+
97
+ const org = await getOrg(client, orgName);
98
+ const members = await listOrgMembers(client, orgName);
99
+
100
+ if (options.json) {
101
+ json({ ...org, members });
102
+ return;
103
+ }
104
+
105
+ header(`@${org.name}`);
106
+ if (org.display_name) keyValue("Display Name", org.display_name);
107
+ if (org.description) keyValue("Description", org.description);
108
+ keyValue("Tools", String(org.tool_count));
109
+ keyValue("Members", String(org.member_count));
110
+ keyValue("Created", org.created_at);
111
+
112
+ newline();
113
+ header("Members");
114
+ const columns: TableColumn[] = [
115
+ { header: "Username", key: "username", width: 20 },
116
+ { header: "Role", key: "role", width: 10 },
117
+ { header: "Added", key: "added_at", width: 20 },
118
+ ];
119
+ table(
120
+ members.map((m) => ({
121
+ username: m.username,
122
+ role: m.role,
123
+ added_at: m.added_at,
124
+ })),
125
+ columns
126
+ );
127
+ } catch (err) {
128
+ error(formatError(err));
129
+ process.exit(1);
130
+ }
131
+ }
132
+
133
+ async function addMemberHandler(
134
+ orgName: string,
135
+ username: string,
136
+ options: { role?: string } & GlobalOptions
137
+ ) {
138
+ try {
139
+ const org = orgName.startsWith("@") ? orgName.substring(1) : orgName;
140
+ const role = (options.role ?? "member") as OrgRole;
141
+
142
+ if (!VALID_ROLES.includes(role as (typeof VALID_ROLES)[number])) {
143
+ error(`Invalid role: ${role}. Must be one of: ${VALID_ROLES.join(", ")}`);
144
+ process.exit(1);
145
+ }
146
+
147
+ const client = await getClient();
148
+ await addOrgMember(client, org, { username, role });
149
+
150
+ if (options.json) {
151
+ json({ username, role, org });
152
+ return;
153
+ }
154
+
155
+ success(`Added "${username}" to @${org} as ${role}`);
156
+ } catch (err) {
157
+ error(formatError(err));
158
+ process.exit(1);
159
+ }
160
+ }
161
+
162
+ async function removeMemberHandler(orgName: string, username: string, options: GlobalOptions) {
163
+ try {
164
+ const org = orgName.startsWith("@") ? orgName.substring(1) : orgName;
165
+ const client = await getClient();
166
+ await removeOrgMember(client, org, username);
167
+
168
+ if (options.json) {
169
+ json({ removed: username, org });
170
+ return;
171
+ }
172
+
173
+ success(`Removed "${username}" from @${org}`);
174
+ } catch (err) {
175
+ error(formatError(err));
176
+ process.exit(1);
177
+ }
178
+ }
179
+
180
+ async function setRoleHandler(
181
+ orgName: string,
182
+ username: string,
183
+ role: string,
184
+ options: GlobalOptions
185
+ ) {
186
+ try {
187
+ const org = orgName.startsWith("@") ? orgName.substring(1) : orgName;
188
+
189
+ if (!VALID_ROLES.includes(role as (typeof VALID_ROLES)[number])) {
190
+ error(`Invalid role: ${role}. Must be one of: ${VALID_ROLES.join(", ")}`);
191
+ process.exit(1);
192
+ }
193
+
194
+ const client = await getClient();
195
+ await updateOrgMemberRole(client, org, username, role as OrgRole);
196
+
197
+ if (options.json) {
198
+ json({ username, role, org });
199
+ return;
200
+ }
201
+
202
+ success(`Set "${username}" role to ${role} in @${org}`);
203
+ } catch (err) {
204
+ error(formatError(err));
205
+ process.exit(1);
206
+ }
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Command Configuration
211
+ // ---------------------------------------------------------------------------
212
+
213
+ export function configureOrgCommand(program: Command): void {
214
+ const org = program.command("org").description("Manage organizations (@org scoped namespaces)");
215
+
216
+ // enact org create <name>
217
+ org
218
+ .command("create <name>")
219
+ .description("Create a new organization")
220
+ .option("--display-name <name>", "Display name for the organization")
221
+ .option("--description <desc>", "Organization description")
222
+ .option("--json", "Output as JSON")
223
+ .action(async (name: string, options) => {
224
+ await createHandler(name, options);
225
+ });
226
+
227
+ // enact org info <name>
228
+ org
229
+ .command("info <name>")
230
+ .description("Show organization details and members")
231
+ .option("--json", "Output as JSON")
232
+ .action(async (name: string, options) => {
233
+ await infoHandler(name, options);
234
+ });
235
+
236
+ // enact org add-member <org> <username>
237
+ org
238
+ .command("add-member <org> <username>")
239
+ .description("Add a member to an organization")
240
+ .option("--role <role>", "Member role (owner, admin, member)", "member")
241
+ .option("--json", "Output as JSON")
242
+ .action(async (orgName: string, username: string, options) => {
243
+ await addMemberHandler(orgName, username, options);
244
+ });
245
+
246
+ // enact org remove-member <org> <username>
247
+ org
248
+ .command("remove-member <org> <username>")
249
+ .description("Remove a member from an organization")
250
+ .option("--json", "Output as JSON")
251
+ .action(async (orgName: string, username: string, options) => {
252
+ await removeMemberHandler(orgName, username, options);
253
+ });
254
+
255
+ // enact org set-role <org> <username> <role>
256
+ org
257
+ .command("set-role <org> <username> <role>")
258
+ .description("Change a member's role (owner, admin, member)")
259
+ .option("--json", "Output as JSON")
260
+ .action(async (orgName: string, username: string, role: string, options) => {
261
+ await setRoleHandler(orgName, username, role, options);
262
+ });
263
+ }
@@ -320,7 +320,10 @@ async function publishHandler(
320
320
  const currentUsername = await getCurrentUsername();
321
321
  if (currentUsername) {
322
322
  const toolNamespace = extractNamespace(toolName);
323
- if (toolNamespace !== currentUsername) {
323
+ if (toolNamespace.startsWith("@")) {
324
+ // Org-scoped tool — server will enforce org membership
325
+ dim(`Publishing to organization namespace "${toolNamespace}"...`);
326
+ } else if (toolNamespace !== currentUsername) {
324
327
  error(
325
328
  `Namespace mismatch: Tool namespace "${toolNamespace}" does not match your username "${currentUsername}".`
326
329
  );
@@ -10,7 +10,7 @@ enact run <tool> [options]
10
10
 
11
11
  ## Description
12
12
 
13
- The `run` command executes a tool using the command defined in its manifest (`skill.yaml` or `SKILL.md`). The tool runs in an isolated container environment with:
13
+ The `run` command executes a tool using the command defined in its manifest (`skill.package.yml` or `SKILL.md`). The tool runs in an isolated container environment with:
14
14
 
15
15
  - Input validation against the tool's JSON Schema
16
16
  - Automatic secret resolution from the OS keyring
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  configureLearnCommand,
23
23
  configureListCommand,
24
24
  configureMcpCommand,
25
+ configureOrgCommand,
25
26
  configurePublishCommand,
26
27
  configureReportCommand,
27
28
  configureRunCommand,
@@ -37,7 +38,7 @@ import {
37
38
  } from "./commands";
38
39
  import { error, formatError } from "./utils";
39
40
 
40
- export const version = "2.3.4";
41
+ export const version = "2.3.7";
41
42
 
42
43
  // Export types for external use
43
44
  export type { GlobalOptions, CommandContext } from "./types";
@@ -93,6 +94,9 @@ async function main() {
93
94
  // Self-hosted registry
94
95
  configureServeCommand(program);
95
96
 
97
+ // Organizations
98
+ configureOrgCommand(program);
99
+
96
100
  // Global error handler - handle Commander's help/version exits gracefully
97
101
  program.exitOverride((err) => {
98
102
  // Commander throws errors for help, version, and other "exit" scenarios
@@ -84,7 +84,7 @@ export class ManifestError extends CliError {
84
84
  super(
85
85
  fullMessage,
86
86
  EXIT_MANIFEST_ERROR,
87
- "Ensure the directory contains a valid skill.yaml or SKILL.md file."
87
+ "Ensure the directory contains a valid skill.package.yaml, skill.package.yml, or SKILL.md file."
88
88
  );
89
89
  this.name = "ManifestError";
90
90
  }
@@ -377,7 +377,7 @@ export const ErrorMessages = {
377
377
  message: `No manifest found in ${dir}`,
378
378
  suggestions: [
379
379
  `Create a manifest: ${colors.command("enact init")}`,
380
- "Ensure the directory contains skill.yaml or SKILL.md",
380
+ "Ensure the directory contains skill.package.yaml, skill.package.yml, or SKILL.md",
381
381
  ],
382
382
  }),
383
383
 
@@ -46,7 +46,7 @@ describe("install integration", () => {
46
46
  mkdirSync(join(TEST_GLOBAL_HOME, ".enact", "cache"), { recursive: true });
47
47
 
48
48
  // Create sample tool source
49
- writeFileSync(join(TEST_TOOL_SRC, "skill.yaml"), SAMPLE_MANIFEST);
49
+ writeFileSync(join(TEST_TOOL_SRC, "skill.package.yml"), SAMPLE_MANIFEST);
50
50
  });
51
51
 
52
52
  beforeEach(() => {
@@ -92,7 +92,7 @@ describe("install integration", () => {
92
92
 
93
93
  // Verify installation
94
94
  expect(existsSync(destPath)).toBe(true);
95
- expect(existsSync(join(destPath, "skill.yaml"))).toBe(true);
95
+ expect(existsSync(join(destPath, "skill.package.yml"))).toBe(true);
96
96
 
97
97
  // Verify manifest can be loaded from destination
98
98
  const installedManifest = loadManifestFromDir(destPath);
@@ -118,7 +118,7 @@ describe("install integration", () => {
118
118
  // Create v2 source
119
119
  const v2Source = join(TEST_BASE, "source-tool-v2");
120
120
  mkdirSync(v2Source, { recursive: true });
121
- writeFileSync(join(v2Source, "skill.yaml"), SAMPLE_MANIFEST_V2);
121
+ writeFileSync(join(v2Source, "skill.package.yml"), SAMPLE_MANIFEST_V2);
122
122
 
123
123
  // Simulate force overwrite
124
124
  rmSync(destPath, { recursive: true, force: true });
@@ -240,7 +240,7 @@ describe("install integration", () => {
240
240
  const cachePath = join(TEST_GLOBAL_HOME, ".enact", "cache", "test", "cached-tool", "v1.0.0");
241
241
  mkdirSync(cachePath, { recursive: true });
242
242
  writeFileSync(
243
- join(cachePath, "skill.yaml"),
243
+ join(cachePath, "skill.package.yml"),
244
244
  `
245
245
  name: test/cached-tool
246
246
  version: 1.0.0
@@ -251,7 +251,7 @@ command: echo "cached"
251
251
 
252
252
  // The resolver should be able to find this tool once it's registered
253
253
  // Note: Full resolver testing is in resolver.test.ts
254
- expect(existsSync(join(cachePath, "skill.yaml"))).toBe(true);
254
+ expect(existsSync(join(cachePath, "skill.package.yml"))).toBe(true);
255
255
  });
256
256
  });
257
257
  });
@@ -110,7 +110,8 @@ describe("publish command", () => {
110
110
  test("identifies manifest files", () => {
111
111
  const isManifest = (filename: string): boolean => {
112
112
  return (
113
- filename === "skill.yaml" ||
113
+ filename === "skill.package.yaml" ||
114
+ filename === "skill.package.yml" ||
114
115
  filename === "skill.yml" ||
115
116
  filename === "enact.yaml" ||
116
117
  filename === "enact.yml" ||
@@ -118,7 +119,8 @@ describe("publish command", () => {
118
119
  );
119
120
  };
120
121
 
121
- expect(isManifest("skill.yaml")).toBe(true);
122
+ expect(isManifest("skill.package.yaml")).toBe(true);
123
+ expect(isManifest("skill.package.yml")).toBe(true);
122
124
  expect(isManifest("skill.yml")).toBe(true);
123
125
  expect(isManifest("enact.yaml")).toBe(true);
124
126
  expect(isManifest("enact.yml")).toBe(true);
@@ -132,7 +134,7 @@ describe("publish command", () => {
132
134
  return lastDot === -1 ? "" : filename.slice(lastDot + 1);
133
135
  };
134
136
 
135
- expect(getExtension("skill.yaml")).toBe("yaml");
137
+ expect(getExtension("skill.package.yaml")).toBe("yaml");
136
138
  expect(getExtension("skill.yml")).toBe("yml");
137
139
  expect(getExtension("enact.yaml")).toBe("yaml");
138
140
  expect(getExtension("enact.yml")).toBe("yml");
@@ -17,7 +17,7 @@ describe("sign command", () => {
17
17
 
18
18
  // Create a test manifest
19
19
  writeFileSync(
20
- join(FIXTURES_DIR, "skill.yaml"),
20
+ join(FIXTURES_DIR, "skill.package.yml"),
21
21
  `enact: "2.0.0"
22
22
  name: "test/sign-tool"
23
23
  version: "1.0.0"
package/tests/e2e.test.ts CHANGED
@@ -126,7 +126,9 @@ describe("E2E: Tool Installation Flow", () => {
126
126
  expect(manifest.name).toBe("test/greeter");
127
127
  expect(existsSync(destPath)).toBe(true);
128
128
  expect(
129
- existsSync(join(destPath, "enact.yaml")) || existsSync(join(destPath, "skill.yaml"))
129
+ existsSync(join(destPath, "enact.yaml")) ||
130
+ existsSync(join(destPath, "skill.package.yaml")) ||
131
+ existsSync(join(destPath, "skill.package.yml"))
130
132
  ).toBe(true);
131
133
 
132
134
  // Verify installed manifest can be loaded
@@ -281,7 +281,8 @@ dist `;
281
281
  });
282
282
 
283
283
  test("allows manifest files", () => {
284
- expect(shouldIgnore("skill.yaml", "skill.yaml")).toBe(false);
284
+ expect(shouldIgnore("skill.package.yaml", "skill.package.yaml")).toBe(false);
285
+ expect(shouldIgnore("skill.package.yml", "skill.package.yml")).toBe(false);
285
286
  expect(shouldIgnore("skill.yml", "skill.yml")).toBe(false);
286
287
  expect(shouldIgnore("enact.md", "enact.md")).toBe(false);
287
288
  expect(shouldIgnore("enact.yaml", "enact.yaml")).toBe(false);