@elixpo/lixblogs-cli 1.1.2 → 1.3.1

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.
@@ -4,6 +4,7 @@ import { metadataFromOptions, resolveMarkdownInput } from './input.js';
4
4
  import { validateBlogInput } from '../../content/validate.js';
5
5
  import { promises as fs } from 'node:fs';
6
6
  import path from 'node:path';
7
+ import { requireConfirmation } from '../../cli/contract.js';
7
8
 
8
9
  export async function blogList({ client, options }) {
9
10
  return client.list({ status: options.status, limit: options.limit, cursor: options.cursor });
@@ -55,6 +56,7 @@ export async function blogPublish({ client, id, options }) {
55
56
  const current = await client.get(id);
56
57
  validateBlogInput(current, { publishing: true });
57
58
  if (options['dry-run']) return { dryRun: true, id, from: current.status, to: 'published' };
59
+ requireConfirmation(options, 'Publishing this blog');
58
60
  return client.publish(id, { etag: options.etag || current.etag, idempotencyKey: options['idempotency-key'] });
59
61
  }
60
62
 
@@ -62,6 +64,7 @@ export async function blogUnpublish({ client, id, options }) {
62
64
  if (!id) throw new Error('A blog ID is required.');
63
65
  const current = await client.get(id);
64
66
  if (options['dry-run']) return { dryRun: true, id, from: current.status, to: 'draft' };
67
+ requireConfirmation(options, 'Unpublishing this blog');
65
68
  return client.unpublish(id, { etag: options.etag || current.etag });
66
69
  }
67
70
 
@@ -77,5 +80,6 @@ export async function blogRestore({ client, id, options }) {
77
80
  if (!id) throw new Error('A blog ID is required.');
78
81
  const current = await client.get(id);
79
82
  if (options['dry-run']) return { dryRun: true, id, restoreTo: current.preDeleteStatus || 'draft' };
83
+ requireConfirmation(options, 'Restoring this blog');
80
84
  return client.restore(id, { etag: options.etag || current.etag });
81
85
  }
@@ -0,0 +1,53 @@
1
+ import { requireConfirmation } from '../../cli/contract.js';
2
+
3
+ function requireBlogId(id) {
4
+ if (!id) throw new Error('A blog ID is required.');
5
+ }
6
+
7
+ export async function collabList({ client, id }) {
8
+ requireBlogId(id);
9
+ return client.list(id);
10
+ }
11
+
12
+ export async function collabInvitations({ client }) {
13
+ return client.invitations();
14
+ }
15
+
16
+ export async function collabInvite({ client, id, options }) {
17
+ requireBlogId(id);
18
+ if (!options.user) throw new Error('--user is required.');
19
+ if (!['viewer', 'editor', 'admin'].includes(options.role)) throw new Error('--role must be viewer, editor, or admin.');
20
+ if (options['dry-run']) return { dryRun: true, action: 'invite', blogId: id, user: options.user, role: options.role };
21
+ requireConfirmation(options, 'Inviting this collaborator');
22
+ return client.invite(id, { user: options.user, role: options.role, idempotencyKey: options['idempotency-key'] });
23
+ }
24
+
25
+ export async function collabRole({ client, id, options }) {
26
+ requireBlogId(id);
27
+ if (!options.user) throw new Error('--user is required.');
28
+ if (!['viewer', 'editor', 'admin'].includes(options.role)) throw new Error('--role must be viewer, editor, or admin.');
29
+ if (options['dry-run']) return { dryRun: true, action: 'role', blogId: id, user: options.user, role: options.role };
30
+ requireConfirmation(options, 'Changing this collaborator role');
31
+ return client.role(id, { user: options.user, role: options.role, idempotencyKey: options['idempotency-key'] });
32
+ }
33
+
34
+ export async function collabRemove({ client, id, options }) {
35
+ requireBlogId(id);
36
+ if (options['dry-run']) return { dryRun: true, action: 'remove', blogId: id, user: options.user || 'self' };
37
+ requireConfirmation(options, 'Removing this collaborator or invitation');
38
+ return client.remove(id, { user: options.user, idempotencyKey: options['idempotency-key'] });
39
+ }
40
+
41
+ export async function collabAccept({ client, id, options }) {
42
+ requireBlogId(id);
43
+ if (options['dry-run']) return { dryRun: true, action: 'accept', blogId: id, showOnProfile: !options['hide-on-profile'] };
44
+ requireConfirmation(options, 'Accepting this collaboration invitation');
45
+ return client.resolveInvitation(id, { action: 'accept', showOnProfile: !options['hide-on-profile'], idempotencyKey: options['idempotency-key'] });
46
+ }
47
+
48
+ export async function collabDecline({ client, id, options }) {
49
+ requireBlogId(id);
50
+ if (options['dry-run']) return { dryRun: true, action: 'decline', blogId: id };
51
+ requireConfirmation(options, 'Declining this collaboration invitation');
52
+ return client.resolveInvitation(id, { action: 'decline', idempotencyKey: options['idempotency-key'] });
53
+ }
@@ -0,0 +1,22 @@
1
+ export async function orgList({ client }) {
2
+ return client.list();
3
+ }
4
+
5
+ export async function orgGet({ client, id }) {
6
+ if (!id) throw new Error("An organization ID or handle is required.");
7
+ return client.get(id);
8
+ }
9
+
10
+ export async function orgCollections({ client, id }) {
11
+ if (!id) throw new Error("An organization ID or handle is required.");
12
+ return client.collections(id);
13
+ }
14
+
15
+ export async function orgMembers({ client, id }) {
16
+ if (!id) throw new Error("An organization ID or handle is required.");
17
+ return client.members(id);
18
+ }
19
+
20
+ export async function orgTargets({ client }) {
21
+ return client.targets();
22
+ }
@@ -0,0 +1,83 @@
1
+ import { access, cp, readFile, readdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { requireConfirmation } from '../../cli/contract.js';
5
+
6
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
7
+ const bundledRoot = path.join(packageRoot, 'skills');
8
+ const developmentRoot = path.resolve(packageRoot, '../..', '.agents', 'skills');
9
+
10
+ async function exists(candidate) {
11
+ try { await access(candidate); return true; } catch { return false; }
12
+ }
13
+
14
+ async function root() {
15
+ if (await exists(bundledRoot)) return bundledRoot;
16
+ if (await exists(developmentRoot)) return developmentRoot;
17
+ const error = new Error('No bundled LixBlogs skills were found. Reinstall @elixpo/lixblogs-cli.');
18
+ error.code = 'skills_unavailable';
19
+ throw error;
20
+ }
21
+
22
+ function validateName(name) {
23
+ if (!/^lixblogs-[a-z0-9-]+$/.test(name || '')) {
24
+ const error = new Error('A valid lixblogs-* skill name is required.');
25
+ error.code = 'invalid_skill_name';
26
+ throw error;
27
+ }
28
+ return name;
29
+ }
30
+
31
+ async function metadata(directory, name) {
32
+ const content = await readFile(path.join(directory, name, 'SKILL.md'), 'utf8');
33
+ const description = content.match(/^description:\s*(.+)$/m)?.[1] || content.match(/^description:\s*>-\s*\n\s*(.+)$/m)?.[1] || '';
34
+ const minimumCliVersion = content.match(/`@elixpo\/lixblogs-cli`\s+([0-9.]+)/)?.[1] || null;
35
+ return { name, description: description.trim(), minimumCliVersion, content };
36
+ }
37
+
38
+ export async function skillList() {
39
+ const directory = await root();
40
+ const entries = await readdir(directory, { withFileTypes: true });
41
+ return Promise.all(entries
42
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith('lixblogs-'))
43
+ .map((entry) => metadata(directory, entry.name))
44
+ ).then((skills) => skills.map(({ content: _content, ...skill }) => skill).sort((a, b) => a.name.localeCompare(b.name)));
45
+ }
46
+
47
+ export async function skillInspect({ name }) {
48
+ const directory = await root();
49
+ const skillName = validateName(name);
50
+ if (!(await exists(path.join(directory, skillName, 'SKILL.md')))) {
51
+ const error = new Error(`Skill "${skillName}" is not bundled.`);
52
+ error.code = 'skill_not_found';
53
+ throw error;
54
+ }
55
+ return metadata(directory, skillName);
56
+ }
57
+
58
+ export async function skillInstall({ name, options }) {
59
+ const directory = await root();
60
+ const skillName = validateName(name);
61
+ const source = path.join(directory, skillName);
62
+ if (!(await exists(path.join(source, 'SKILL.md')))) {
63
+ const error = new Error(`Skill "${skillName}" is not bundled.`);
64
+ error.code = 'skill_not_found';
65
+ throw error;
66
+ }
67
+ const targetRoot = path.resolve(options.target || '.agents/skills');
68
+ const target = path.join(targetRoot, skillName);
69
+ if (options['dry-run']) return { dryRun: true, name: skillName, target, replace: await exists(target) };
70
+ if (await exists(target)) {
71
+ if (!options.force) {
72
+ const error = new Error(`Skill already exists at ${target}.`);
73
+ error.code = 'skill_exists';
74
+ error.hint = 'Inspect the existing skill or re-run with --force --yes to replace it.';
75
+ throw error;
76
+ }
77
+ requireConfirmation(options, `Replacing ${target}`);
78
+ } else {
79
+ requireConfirmation(options, `Installing ${skillName} into ${targetRoot}`);
80
+ }
81
+ await cp(source, target, { recursive: true, force: Boolean(options.force) });
82
+ return { installed: true, name: skillName, target };
83
+ }
@@ -29,7 +29,7 @@ export function createAuthProvider(config) {
29
29
  accountsBaseUrl: config.accountsBaseUrl,
30
30
  clientId: config.clientId,
31
31
  audience: config.audience,
32
- cliVersion: config.cliVersion || "1.1.0",
32
+ cliVersion: config.cliVersion || "1.2.0",
33
33
  fetchImpl: config.fetchImpl,
34
34
  });
35
35