@moglenny/content-workbench-agent 0.1.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 (2) hide show
  1. package/package.json +18 -0
  2. package/src/bin.mjs +207 -0
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@moglenny/content-workbench-agent",
3
+ "version": "0.1.0",
4
+ "description": "Agent installer for Content Workbench CLI, Skills, and MCP configuration",
5
+ "type": "module",
6
+ "bin": {
7
+ "content-workbench-agent": "src/bin.mjs"
8
+ },
9
+ "files": [
10
+ "src/bin.mjs"
11
+ ],
12
+ "engines": {
13
+ "node": ">=20"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ }
18
+ }
package/src/bin.mjs ADDED
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { realpathSync } from "node:fs";
5
+ import { mkdir, stat } from "node:fs/promises";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const scriptPath = fileURLToPath(import.meta.url);
11
+ const defaultRepoUrl = "https://github.com/Himog0921/topic-dashboard.git";
12
+ const defaultBranch = "main";
13
+
14
+ function expandHome(filePath) {
15
+ if (filePath === "~") return os.homedir();
16
+ if (filePath.startsWith("~/")) return path.join(os.homedir(), filePath.slice(2));
17
+ return filePath;
18
+ }
19
+
20
+ function defaultRepoDir() {
21
+ return path.join(os.homedir(), ".content-workbench", "agent", "topic-dashboard");
22
+ }
23
+
24
+ function usage() {
25
+ return `Usage:
26
+ npx @moglenny/content-workbench-agent@latest install
27
+
28
+ Options:
29
+ --dry-run Print the install plan without writing files.
30
+ --repo-url <url> Git repository URL. Defaults to the official content workbench repo.
31
+ --branch <name> Branch to install from. Defaults to main.
32
+ --repo-dir <path> Local managed clone path. Defaults to ~/.content-workbench/agent/topic-dashboard.
33
+ --skills-target <path> Skill install target. Repeatable.
34
+ --mcp-target <path> Write MCP config snippet to this JSON file.
35
+ --skip-cli Do not install the global cw CLI.
36
+ --skip-skills Do not copy Skills.
37
+ --skip-mcp Do not write MCP config.
38
+ --api-base-url <url> Base URL shown in post-install setup guidance.
39
+ --help Show this help.
40
+ `;
41
+ }
42
+
43
+ export function parseAgentInstallerArgs(argv = process.argv.slice(2)) {
44
+ const options = {
45
+ command: "install",
46
+ dryRun: false,
47
+ repoUrl: defaultRepoUrl,
48
+ branch: defaultBranch,
49
+ repoDir: defaultRepoDir(),
50
+ installArgs: ["--yes"],
51
+ help: false,
52
+ };
53
+
54
+ const args = [...argv];
55
+ if (args[0] && !args[0].startsWith("-")) {
56
+ options.command = args.shift();
57
+ }
58
+
59
+ if (options.command !== "install" && options.command !== "help") {
60
+ throw new Error(`Unknown command: ${options.command}`);
61
+ }
62
+
63
+ for (let index = 0; index < args.length; index += 1) {
64
+ const arg = args[index];
65
+ if (arg === "--help" || arg === "-h") {
66
+ options.help = true;
67
+ continue;
68
+ }
69
+ if (arg === "--dry-run") {
70
+ options.dryRun = true;
71
+ continue;
72
+ }
73
+ if (arg === "--repo-url") {
74
+ options.repoUrl = args[++index] || options.repoUrl;
75
+ continue;
76
+ }
77
+ if (arg === "--branch") {
78
+ options.branch = args[++index] || options.branch;
79
+ continue;
80
+ }
81
+ if (arg === "--repo-dir") {
82
+ options.repoDir = path.resolve(expandHome(args[++index] || ""));
83
+ continue;
84
+ }
85
+ if (arg === "--skills-target" || arg === "--mcp-target" || arg === "--api-base-url") {
86
+ const value = args[++index];
87
+ if (!value) throw new Error(`Missing value for ${arg}`);
88
+ options.installArgs.push(arg, value);
89
+ continue;
90
+ }
91
+ if (arg === "--skip-cli" || arg === "--skip-skills" || arg === "--skip-mcp") {
92
+ options.installArgs.push(arg);
93
+ continue;
94
+ }
95
+ throw new Error(`Unknown option: ${arg}`);
96
+ }
97
+
98
+ return options;
99
+ }
100
+
101
+ export function buildInstallPlan(options, repoExists) {
102
+ const bootstrapSteps = repoExists
103
+ ? [
104
+ { command: "git", args: ["-C", options.repoDir, "fetch", "--depth", "1", "origin", options.branch] },
105
+ { command: "git", args: ["-C", options.repoDir, "checkout", options.branch] },
106
+ { command: "git", args: ["-C", options.repoDir, "pull", "--ff-only", "origin", options.branch] },
107
+ ]
108
+ : [
109
+ {
110
+ command: "git",
111
+ args: ["clone", "--depth", "1", "--branch", options.branch, options.repoUrl, options.repoDir],
112
+ },
113
+ ];
114
+
115
+ return {
116
+ repoDir: options.repoDir,
117
+ dryRun: options.dryRun,
118
+ steps: [
119
+ ...bootstrapSteps,
120
+ {
121
+ command: "node",
122
+ args: [
123
+ path.join(options.repoDir, "scripts", "install-cw-agent.mjs"),
124
+ "--repo-root",
125
+ options.repoDir,
126
+ ...options.installArgs,
127
+ ],
128
+ },
129
+ ],
130
+ };
131
+ }
132
+
133
+ async function exists(filePath) {
134
+ try {
135
+ await stat(filePath);
136
+ return true;
137
+ } catch (error) {
138
+ if (error?.code === "ENOENT") return false;
139
+ throw error;
140
+ }
141
+ }
142
+
143
+ async function run(command, args) {
144
+ await new Promise((resolve, reject) => {
145
+ const child = spawn(command, args, {
146
+ stdio: "inherit",
147
+ env: process.env,
148
+ });
149
+ child.on("error", reject);
150
+ child.on("exit", (code, signal) => {
151
+ if (code === 0) {
152
+ resolve();
153
+ return;
154
+ }
155
+ reject(new Error(`${command} ${args.join(" ")} failed${signal ? ` with signal ${signal}` : ` with exit ${code}`}`));
156
+ });
157
+ });
158
+ }
159
+
160
+ function printPlan(plan, stdout) {
161
+ stdout.write(`${plan.dryRun ? "Content Workbench Agent install dry run" : "Content Workbench Agent install"}\n`);
162
+ stdout.write(`Managed repository: ${plan.repoDir}\n`);
163
+ for (const step of plan.steps) {
164
+ stdout.write(`- ${step.command} ${step.args.join(" ")}\n`);
165
+ }
166
+ if (plan.dryRun) {
167
+ stdout.write("\nRun without --dry-run to install.\n");
168
+ }
169
+ }
170
+
171
+ export async function runAgentInstaller(argv = process.argv.slice(2), io = process) {
172
+ const options = parseAgentInstallerArgs(argv);
173
+ if (options.help || options.command === "help") {
174
+ io.stdout.write(usage());
175
+ return { help: true };
176
+ }
177
+
178
+ await mkdir(path.dirname(options.repoDir), { recursive: true });
179
+ const repoExists = await (io.exists || exists)(path.join(options.repoDir, ".git"));
180
+ const plan = buildInstallPlan(options, repoExists);
181
+ printPlan(plan, io.stdout);
182
+
183
+ if (options.dryRun) return plan;
184
+
185
+ const runner = io.run || run;
186
+ for (const step of plan.steps) {
187
+ await runner(step.command, step.args);
188
+ }
189
+
190
+ io.stdout.write("Content Workbench Agent install complete\n");
191
+ return plan;
192
+ }
193
+
194
+ function isDirectInvocation() {
195
+ try {
196
+ return realpathSync(process.argv[1]) === realpathSync(scriptPath);
197
+ } catch {
198
+ return process.argv[1] === scriptPath;
199
+ }
200
+ }
201
+
202
+ if (isDirectInvocation()) {
203
+ runAgentInstaller().catch((error) => {
204
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
205
+ process.exitCode = 1;
206
+ });
207
+ }