@moglenny/content-workbench-agent 0.1.1 → 0.1.3
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/package.json +1 -1
- package/src/bin.mjs +115 -7
package/package.json
CHANGED
package/src/bin.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
|
-
import { mkdir, stat } from "node:fs/promises";
|
|
5
|
+
import { mkdir, rename, rm, stat } from "node:fs/promises";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -10,6 +10,16 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
const scriptPath = fileURLToPath(import.meta.url);
|
|
11
11
|
const defaultRepoUrl = "https://github.com/Himog0921/topic-dashboard.git";
|
|
12
12
|
const defaultBranch = "main";
|
|
13
|
+
const cloneRetryAttempts = 3;
|
|
14
|
+
const sparseCheckoutPaths = [
|
|
15
|
+
".cw-agent",
|
|
16
|
+
".claude/skills",
|
|
17
|
+
"bin",
|
|
18
|
+
"packages/cli",
|
|
19
|
+
"packages/mcp-server",
|
|
20
|
+
"scripts",
|
|
21
|
+
"public/releases/cw",
|
|
22
|
+
];
|
|
13
23
|
|
|
14
24
|
function expandHome(filePath) {
|
|
15
25
|
if (filePath === "~") return os.homedir();
|
|
@@ -30,9 +40,12 @@ Options:
|
|
|
30
40
|
--repo-url <url> Git repository URL. Defaults to the official content workbench repo.
|
|
31
41
|
--branch <name> Branch to install from. Defaults to main.
|
|
32
42
|
--repo-dir <path> Local managed clone path. Defaults to ~/.content-workbench/agent/topic-dashboard.
|
|
43
|
+
--cli-prefix <path> User-owned npm prefix for cw. Defaults to ~/.content-workbench/npm.
|
|
33
44
|
--skills-target <path> Skill install target. Repeatable.
|
|
34
45
|
--mcp-target <path> Write MCP config snippet to this JSON file.
|
|
35
|
-
--
|
|
46
|
+
--shell-profile <path> Shell profile that should include cw in PATH.
|
|
47
|
+
--skip-cli Do not install the user-level cw CLI.
|
|
48
|
+
--skip-shell-profile Do not add cw to the shell profile PATH.
|
|
36
49
|
--skip-skills Do not copy Skills.
|
|
37
50
|
--skip-mcp Do not write MCP config.
|
|
38
51
|
--api-base-url <url> Base URL shown in post-install setup guidance.
|
|
@@ -82,13 +95,13 @@ export function parseAgentInstallerArgs(argv = process.argv.slice(2)) {
|
|
|
82
95
|
options.repoDir = path.resolve(expandHome(args[++index] || ""));
|
|
83
96
|
continue;
|
|
84
97
|
}
|
|
85
|
-
if (arg === "--skills-target" || arg === "--mcp-target" || arg === "--api-base-url") {
|
|
98
|
+
if (arg === "--skills-target" || arg === "--mcp-target" || arg === "--api-base-url" || arg === "--cli-prefix" || arg === "--shell-profile") {
|
|
86
99
|
const value = args[++index];
|
|
87
100
|
if (!value) throw new Error(`Missing value for ${arg}`);
|
|
88
101
|
options.installArgs.push(arg, value);
|
|
89
102
|
continue;
|
|
90
103
|
}
|
|
91
|
-
if (arg === "--skip-cli" || arg === "--skip-skills" || arg === "--skip-mcp") {
|
|
104
|
+
if (arg === "--skip-cli" || arg === "--skip-shell-profile" || arg === "--skip-skills" || arg === "--skip-mcp") {
|
|
92
105
|
options.installArgs.push(arg);
|
|
93
106
|
continue;
|
|
94
107
|
}
|
|
@@ -108,7 +121,22 @@ export function buildInstallPlan(options, repoExists) {
|
|
|
108
121
|
: [
|
|
109
122
|
{
|
|
110
123
|
command: "git",
|
|
111
|
-
args: [
|
|
124
|
+
args: [
|
|
125
|
+
"clone",
|
|
126
|
+
"--depth",
|
|
127
|
+
"1",
|
|
128
|
+
"--filter=blob:none",
|
|
129
|
+
"--sparse",
|
|
130
|
+
"--single-branch",
|
|
131
|
+
"--branch",
|
|
132
|
+
options.branch,
|
|
133
|
+
options.repoUrl,
|
|
134
|
+
options.repoDir,
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
command: "git",
|
|
139
|
+
args: ["-C", options.repoDir, "sparse-checkout", "set", "--cone", ...sparseCheckoutPaths],
|
|
112
140
|
},
|
|
113
141
|
];
|
|
114
142
|
|
|
@@ -157,6 +185,59 @@ async function run(command, args) {
|
|
|
157
185
|
});
|
|
158
186
|
}
|
|
159
187
|
|
|
188
|
+
function gitVerb(step) {
|
|
189
|
+
if (step.command !== "git") return null;
|
|
190
|
+
if (step.args[0] === "-C") return step.args[2] || null;
|
|
191
|
+
return step.args[0] || null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function shouldRetryStep(step) {
|
|
195
|
+
return ["clone", "fetch", "pull"].includes(gitVerb(step));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isCloneStep(step) {
|
|
199
|
+
return step.command === "git" && step.args[0] === "clone";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sleep(ms) {
|
|
203
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function runStepWithRetry(step, options) {
|
|
207
|
+
const maxAttempts = shouldRetryStep(step) ? cloneRetryAttempts : 1;
|
|
208
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
209
|
+
try {
|
|
210
|
+
await options.runner(step.command, step.args);
|
|
211
|
+
return;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (attempt >= maxAttempts) throw error;
|
|
214
|
+
if (isCloneStep(step)) {
|
|
215
|
+
await options.removeDir(options.repoDir, { recursive: true, force: true });
|
|
216
|
+
}
|
|
217
|
+
options.stdout.write(`Retrying interrupted ${step.command} ${gitVerb(step)} (${attempt + 1}/${maxAttempts})...\n`);
|
|
218
|
+
await options.sleep(attempt * 1000);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function runSteps(steps, options) {
|
|
224
|
+
for (const step of steps) {
|
|
225
|
+
await runStepWithRetry(step, options);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function clearManagedRepo(options) {
|
|
230
|
+
try {
|
|
231
|
+
await options.removeDir(options.repoDir, { recursive: true, force: true });
|
|
232
|
+
return null;
|
|
233
|
+
} catch {
|
|
234
|
+
const quarantineDir = `${options.repoDir}.broken-${options.now()}`;
|
|
235
|
+
await options.renameDir(options.repoDir, quarantineDir);
|
|
236
|
+
options.stdout.write(`Moved inaccessible managed repository to ${quarantineDir}\n`);
|
|
237
|
+
return quarantineDir;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
160
241
|
function printPlan(plan, stdout) {
|
|
161
242
|
stdout.write(`${plan.dryRun ? "Content Workbench Agent install dry run" : "Content Workbench Agent install"}\n`);
|
|
162
243
|
stdout.write(`Managed repository: ${plan.repoDir}\n`);
|
|
@@ -183,8 +264,35 @@ export async function runAgentInstaller(argv = process.argv.slice(2), io = proce
|
|
|
183
264
|
if (options.dryRun) return plan;
|
|
184
265
|
|
|
185
266
|
const runner = io.run || run;
|
|
186
|
-
|
|
187
|
-
|
|
267
|
+
const removeDir = io.removeDir || rm;
|
|
268
|
+
const renameDir = io.renameDir || rename;
|
|
269
|
+
const wait = io.sleep || sleep;
|
|
270
|
+
const runnerOptions = {
|
|
271
|
+
runner,
|
|
272
|
+
removeDir,
|
|
273
|
+
renameDir,
|
|
274
|
+
repoDir: options.repoDir,
|
|
275
|
+
sleep: wait,
|
|
276
|
+
now: io.now || Date.now,
|
|
277
|
+
stdout: io.stdout,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
if (repoExists) {
|
|
281
|
+
const updateSteps = plan.steps.slice(0, 3);
|
|
282
|
+
const installStep = plan.steps.at(-1);
|
|
283
|
+
try {
|
|
284
|
+
await runSteps(updateSteps, runnerOptions);
|
|
285
|
+
} catch {
|
|
286
|
+
io.stdout.write("Existing managed repository could not be updated. Recreating a clean managed repository...\n");
|
|
287
|
+
const quarantinedRepoDir = await clearManagedRepo(runnerOptions);
|
|
288
|
+
const cleanPlan = buildInstallPlan(options, false);
|
|
289
|
+
await runSteps(cleanPlan.steps, runnerOptions);
|
|
290
|
+
io.stdout.write("Content Workbench Agent install complete\n");
|
|
291
|
+
return { ...cleanPlan, recreatedExistingRepo: true, quarantinedRepoDir };
|
|
292
|
+
}
|
|
293
|
+
await runSteps([installStep], runnerOptions);
|
|
294
|
+
} else {
|
|
295
|
+
await runSteps(plan.steps, runnerOptions);
|
|
188
296
|
}
|
|
189
297
|
|
|
190
298
|
io.stdout.write("Content Workbench Agent install complete\n");
|