forgedock 1.0.20 → 1.0.22
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 +146 -77
- package/bin/demo.mjs +386 -0
- package/bin/forgedock.mjs +357 -30
- package/bin/init-detect.mjs +95 -0
- package/bin/runner.mjs +780 -0
- package/commands/ci-audit.md +469 -0
- package/commands/cleanup.md +85 -39
- package/commands/deploy-info.md +6 -2
- package/commands/forge-stats.md +1 -1
- package/commands/optimize.md +1 -1
- package/commands/orchestrate.md +684 -33
- package/commands/quality-gate.md +166 -46
- package/commands/review-pr-agents.md +106 -34
- package/commands/review-pr.md +33 -2
- package/commands/scope.md +11 -3
- package/commands/security-audit.md +24 -16
- package/commands/sync-ecosystem.md +2 -2
- package/commands/work-on/build/architect.md +47 -3
- package/commands/work-on/build/context.md +36 -5
- package/commands/work-on/build/validate.md +16 -7
- package/commands/work-on/close.md +129 -1
- package/commands/work-on/investigate.md +24 -1
- package/commands/work-on.md +226 -8
- package/docs/comparison.md +135 -0
- package/docs/design/developer-objections-ai-context-tools.md +244 -0
- package/docs/design/messaging-angles-research.md +179 -0
- package/docs/design/show-hn-draft.md +86 -0
- package/docs/site/annotations-explained.md +55 -0
- package/docs/site/command-learning-path.md +104 -0
- package/docs/site/command-reference.md +3 -0
- package/docs/site/getting-started.md +36 -14
- package/docs/site/troubleshooting.md +208 -0
- package/docs/spec/forge-protocol-v1.md +768 -0
- package/examples/forgedock-demo/README.md +113 -0
- package/examples/forgedock-demo/bootstrap.sh +125 -0
- package/examples/forgedock-demo/issues/01-bug-delete-missing-auth.md +37 -0
- package/examples/forgedock-demo/issues/02-feature-filter-notes.md +37 -0
- package/examples/forgedock-demo/issues/03-refactor-router-table.md +33 -0
- package/examples/forgedock-demo/issues/04-perf-index-lookup.md +35 -0
- package/examples/forgedock-demo/issues/05-docs-api-reference.md +39 -0
- package/examples/forgedock-demo/labels.json +21 -0
- package/examples/forgedock-demo/package.json +16 -0
- package/examples/forgedock-demo/scripts/smoke.js +61 -0
- package/examples/forgedock-demo/src/auth.js +28 -0
- package/examples/forgedock-demo/src/db.js +66 -0
- package/examples/forgedock-demo/src/routes/notes.js +64 -0
- package/examples/forgedock-demo/src/server.js +79 -0
- package/package.json +6 -2
- package/templates/forge.yaml.minimal +44 -0
package/bin/demo.mjs
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* bin/demo.mjs — `forgedock demo` one-command demo mode (issue #1145).
|
|
6
|
+
*
|
|
7
|
+
* Stands up a runnable ForgeDock demo at a PREDICTABLE location with ZERO
|
|
8
|
+
* required decisions, then prints the exact next steps. It removes the
|
|
9
|
+
* clone → cd → init → open friction that loses first-time users.
|
|
10
|
+
*
|
|
11
|
+
* Source precedence (robust + offline-friendly):
|
|
12
|
+
* 1. `git clone` the live demo repo (RapierCraftStudios/forgedock-demo).
|
|
13
|
+
* 2. On failure (repo absent / no network), fall back to copying the bundled
|
|
14
|
+
* scaffold shipped inside the installed package at
|
|
15
|
+
* `FORGE_HOME/examples/forgedock-demo`, then `git init` + initial commit.
|
|
16
|
+
*
|
|
17
|
+
* Re-runs are idempotent: an existing git clone is fast-forwarded
|
|
18
|
+
* (`git pull --ff-only`); a scaffold copy is reused, never clobbered.
|
|
19
|
+
*
|
|
20
|
+
* Design notes:
|
|
21
|
+
* - All side effects (process spawning, filesystem, PATH lookup) are injected
|
|
22
|
+
* via `opts` so the unit tests in bin/tests/demo.test.mjs run with no
|
|
23
|
+
* network and no git/claude on the host.
|
|
24
|
+
* - Pure helpers (parseDemoArgs, resolveTargetDir, renderNextSteps,
|
|
25
|
+
* demoUsage, demoForgeYaml) are exported for direct testing.
|
|
26
|
+
*
|
|
27
|
+
* Exports:
|
|
28
|
+
* DEFAULT_DEMO_REPO, DEFAULT_DEMO_DIRNAME
|
|
29
|
+
* parseDemoArgs(args) → {positional, dir, open, repo, help, error}
|
|
30
|
+
* resolveTargetDir(opts) → string (absolute target path)
|
|
31
|
+
* isGitRepo(dir, fsx) → boolean
|
|
32
|
+
* demoUsage() → string
|
|
33
|
+
* demoForgeYaml(repoSlug) → string
|
|
34
|
+
* renderNextSteps(ctx) → string
|
|
35
|
+
* runDemo(opts) → Promise<{status, ...}>
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
existsSync,
|
|
40
|
+
mkdirSync,
|
|
41
|
+
cpSync,
|
|
42
|
+
readdirSync,
|
|
43
|
+
writeFileSync,
|
|
44
|
+
} from "fs";
|
|
45
|
+
import { join, dirname, resolve, isAbsolute, basename } from "path";
|
|
46
|
+
import { homedir } from "os";
|
|
47
|
+
import { spawnSync, spawn } from "child_process";
|
|
48
|
+
|
|
49
|
+
export const DEFAULT_DEMO_REPO = "RapierCraftStudios/forgedock-demo";
|
|
50
|
+
export const DEFAULT_DEMO_DIRNAME = "forgedock-demo";
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Argument parsing
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse `forgedock demo` arguments.
|
|
58
|
+
*
|
|
59
|
+
* Flags:
|
|
60
|
+
* --dir <path> | --dir=<path> Target directory (default: ~/forgedock-demo).
|
|
61
|
+
* --repo <slug> | --repo=<slug> Override the demo repo (owner/name).
|
|
62
|
+
* --open Launch Claude Code in the demo dir when found.
|
|
63
|
+
* --no-open Explicitly disable launching (the default).
|
|
64
|
+
* --help | -h Show usage.
|
|
65
|
+
*
|
|
66
|
+
* The first non-flag argument is treated as the target directory (positional
|
|
67
|
+
* convenience equivalent to --dir).
|
|
68
|
+
*
|
|
69
|
+
* @param {string[]} args
|
|
70
|
+
* @returns {{positional: string|undefined, dir: string|undefined,
|
|
71
|
+
* repo: string|undefined, open: boolean, help: boolean,
|
|
72
|
+
* error: string|null}}
|
|
73
|
+
*/
|
|
74
|
+
export function parseDemoArgs(args = []) {
|
|
75
|
+
const out = {
|
|
76
|
+
positional: undefined,
|
|
77
|
+
dir: undefined,
|
|
78
|
+
repo: undefined,
|
|
79
|
+
open: false,
|
|
80
|
+
help: false,
|
|
81
|
+
error: null,
|
|
82
|
+
};
|
|
83
|
+
for (let i = 0; i < args.length; i++) {
|
|
84
|
+
const a = args[i];
|
|
85
|
+
if (a === "--help" || a === "-h") {
|
|
86
|
+
out.help = true;
|
|
87
|
+
} else if (a === "--open") {
|
|
88
|
+
out.open = true;
|
|
89
|
+
} else if (a === "--no-open") {
|
|
90
|
+
out.open = false;
|
|
91
|
+
} else if (a === "--dir") {
|
|
92
|
+
out.dir = args[++i];
|
|
93
|
+
if (out.dir === undefined) out.error = "--dir requires a path argument";
|
|
94
|
+
} else if (a.startsWith("--dir=")) {
|
|
95
|
+
out.dir = a.slice("--dir=".length);
|
|
96
|
+
} else if (a === "--repo") {
|
|
97
|
+
out.repo = args[++i];
|
|
98
|
+
if (out.repo === undefined) out.error = "--repo requires an owner/name argument";
|
|
99
|
+
} else if (a.startsWith("--repo=")) {
|
|
100
|
+
out.repo = a.slice("--repo=".length);
|
|
101
|
+
} else if (a.startsWith("--")) {
|
|
102
|
+
out.error = `Unknown flag: ${a}`;
|
|
103
|
+
} else if (out.positional === undefined) {
|
|
104
|
+
out.positional = a;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Resolve the predictable target directory. Priority: --dir, then positional,
|
|
112
|
+
* then ~/forgedock-demo. Always returned as an absolute path.
|
|
113
|
+
*
|
|
114
|
+
* @param {{dir?: string, positional?: string, home?: string, cwd?: string}} opts
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
export function resolveTargetDir({ dir, positional, home = homedir(), cwd = process.cwd() } = {}) {
|
|
118
|
+
const chosen = dir || positional;
|
|
119
|
+
if (chosen && chosen.trim() !== "") {
|
|
120
|
+
return isAbsolute(chosen) ? chosen : resolve(cwd, chosen);
|
|
121
|
+
}
|
|
122
|
+
return join(home, DEFAULT_DEMO_DIRNAME);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} dir
|
|
127
|
+
* @param {{existsSync: Function}} fsx
|
|
128
|
+
* @returns {boolean} true if dir contains a `.git` entry (worktree or repo).
|
|
129
|
+
*/
|
|
130
|
+
export function isGitRepo(dir, fsx) {
|
|
131
|
+
return fsx.existsSync(join(dir, ".git"));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isEmptyDir(dir, fsx) {
|
|
135
|
+
try {
|
|
136
|
+
return fsx.readdirSync(dir).length === 0;
|
|
137
|
+
} catch {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Rendering
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
export function demoUsage() {
|
|
147
|
+
return [
|
|
148
|
+
"Usage: forgedock demo [dir] [--dir <path>] [--repo <owner/name>] [--open]",
|
|
149
|
+
"",
|
|
150
|
+
"Stand up a runnable ForgeDock demo at a predictable location (default",
|
|
151
|
+
"~/forgedock-demo) and print the exact next steps. No configuration needed.",
|
|
152
|
+
"",
|
|
153
|
+
" [dir] Target directory (same as --dir).",
|
|
154
|
+
" --dir <path> Where to create the demo (default: ~/forgedock-demo).",
|
|
155
|
+
" --repo <slug> Demo repo to clone (default: RapierCraftStudios/forgedock-demo).",
|
|
156
|
+
" --open Launch Claude Code in the demo directory if available.",
|
|
157
|
+
" --no-open Do not launch Claude Code (default).",
|
|
158
|
+
" -h, --help Show this help.",
|
|
159
|
+
].join("\n");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Minimal, valid forge.yaml for the demo repo so /work-on is usable immediately.
|
|
164
|
+
* @param {string} repoSlug - "owner/name"
|
|
165
|
+
* @returns {string}
|
|
166
|
+
*/
|
|
167
|
+
export function demoForgeYaml(repoSlug) {
|
|
168
|
+
const [owner = "your-org", repo = DEFAULT_DEMO_DIRNAME] = String(repoSlug).split("/");
|
|
169
|
+
return [
|
|
170
|
+
"# forge.yaml — minimal ForgeDock config for the demo repo.",
|
|
171
|
+
"# Auto-generated by: npx forgedock demo",
|
|
172
|
+
"project:",
|
|
173
|
+
' name: "ForgeDock Demo"',
|
|
174
|
+
` owner: "${owner}"`,
|
|
175
|
+
` repo: "${repo}"`,
|
|
176
|
+
' description: "A tiny Notes API for trying ForgeDock risk-free."',
|
|
177
|
+
"paths:",
|
|
178
|
+
' root: "."',
|
|
179
|
+
' worktree_base: ".claude/worktrees"',
|
|
180
|
+
"branches:",
|
|
181
|
+
' default: "main"',
|
|
182
|
+
' staging: "main"',
|
|
183
|
+
"",
|
|
184
|
+
].join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Render the success / next-steps card.
|
|
189
|
+
* @param {{target: string, source: string, claudeAvailable: boolean}} ctx
|
|
190
|
+
* @returns {string}
|
|
191
|
+
*/
|
|
192
|
+
export function renderNextSteps({ target, source, claudeAvailable }) {
|
|
193
|
+
const sourceNote = {
|
|
194
|
+
cloned: "Cloned the live demo repo.",
|
|
195
|
+
pulled: "Demo already present — pulled the latest.",
|
|
196
|
+
scaffold: "Live repo unavailable — built the demo from the bundled scaffold.",
|
|
197
|
+
exists: "Reused the existing demo directory.",
|
|
198
|
+
}[source] || "";
|
|
199
|
+
|
|
200
|
+
const lines = [
|
|
201
|
+
"",
|
|
202
|
+
`✔ Demo repo ready at ${target}`,
|
|
203
|
+
...(sourceNote ? [` (${sourceNote})`] : []),
|
|
204
|
+
"",
|
|
205
|
+
"Next: open Claude Code in that directory and run:",
|
|
206
|
+
"",
|
|
207
|
+
" /work-on 1",
|
|
208
|
+
"",
|
|
209
|
+
"This picks up a pre-written bug issue and runs the full",
|
|
210
|
+
"investigate → architect → build → review → merge pipeline.",
|
|
211
|
+
"",
|
|
212
|
+
` cd ${target}`,
|
|
213
|
+
claudeAvailable
|
|
214
|
+
? " claude # open Claude Code here (or re-run with --open)"
|
|
215
|
+
: " # then open Claude Code in this directory",
|
|
216
|
+
];
|
|
217
|
+
return lines.join("\n");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Default injectable side-effect implementations
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
const defaultFsx = { existsSync, mkdirSync, cpSync, readdirSync, writeFileSync };
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Run a command without throwing. Returns a normalized result.
|
|
228
|
+
* @returns {{status: number, stdout: string, stderr: string}}
|
|
229
|
+
*/
|
|
230
|
+
function defaultExec(cmd, cmdArgs, opts = {}) {
|
|
231
|
+
const r = spawnSync(cmd, cmdArgs, { encoding: "utf8", ...opts });
|
|
232
|
+
return {
|
|
233
|
+
status: r.error ? 127 : (typeof r.status === "number" ? r.status : 1),
|
|
234
|
+
stdout: r.stdout || "",
|
|
235
|
+
stderr: r.stderr || "",
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** @returns {boolean} whether `bin` resolves on PATH. */
|
|
240
|
+
function defaultCommandExists(bin) {
|
|
241
|
+
const probe = process.platform === "win32" ? "where" : "which";
|
|
242
|
+
const r = spawnSync(probe, [bin], { encoding: "utf8" });
|
|
243
|
+
return !r.error && r.status === 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Launch an interactive process in `cwd`, inheriting stdio. */
|
|
247
|
+
function defaultLaunch(bin, launchArgs, opts = {}) {
|
|
248
|
+
spawn(bin, launchArgs, { stdio: "inherit", ...opts });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const defaultLogger = {
|
|
252
|
+
log: (m) => process.stdout.write(`${m}\n`),
|
|
253
|
+
error: (m) => process.stderr.write(`${m}\n`),
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// Orchestrator
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Stand up the demo.
|
|
262
|
+
*
|
|
263
|
+
* @param {object} opts
|
|
264
|
+
* @param {string} opts.forgeHome - Package root (for bundled scaffold).
|
|
265
|
+
* @param {string[]} [opts.args] - CLI args after `demo`.
|
|
266
|
+
* @param {string} [opts.home] - Home dir (default os.homedir()).
|
|
267
|
+
* @param {string} [opts.cwd] - Working dir (default process.cwd()).
|
|
268
|
+
* @param {string} [opts.demoRepo] - Override demo repo slug.
|
|
269
|
+
* @param {Function} [opts.exec] - (cmd, args, {cwd}) => {status,...}.
|
|
270
|
+
* @param {object} [opts.fsx] - Filesystem facade.
|
|
271
|
+
* @param {Function} [opts.commandExists] - (bin) => boolean.
|
|
272
|
+
* @param {Function} [opts.launch] - (bin, args, {cwd}) => void.
|
|
273
|
+
* @param {{log:Function,error:Function}} [opts.logger]
|
|
274
|
+
* @returns {Promise<{status: string, target?: string, source?: string, error?: string}>}
|
|
275
|
+
*/
|
|
276
|
+
export async function runDemo(opts = {}) {
|
|
277
|
+
const {
|
|
278
|
+
forgeHome,
|
|
279
|
+
args = [],
|
|
280
|
+
home = homedir(),
|
|
281
|
+
cwd = process.cwd(),
|
|
282
|
+
exec = defaultExec,
|
|
283
|
+
fsx = defaultFsx,
|
|
284
|
+
commandExists = defaultCommandExists,
|
|
285
|
+
launch = defaultLaunch,
|
|
286
|
+
logger = defaultLogger,
|
|
287
|
+
} = opts;
|
|
288
|
+
|
|
289
|
+
const parsed = parseDemoArgs(args);
|
|
290
|
+
if (parsed.help) {
|
|
291
|
+
logger.log(demoUsage());
|
|
292
|
+
return { status: "help" };
|
|
293
|
+
}
|
|
294
|
+
if (parsed.error) {
|
|
295
|
+
logger.error(parsed.error);
|
|
296
|
+
logger.error(demoUsage());
|
|
297
|
+
return { status: "error", error: parsed.error };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const demoRepo =
|
|
301
|
+
parsed.repo || opts.demoRepo || process.env.FORGEDOCK_DEMO_REPO || DEFAULT_DEMO_REPO;
|
|
302
|
+
const target = resolveTargetDir({ dir: parsed.dir, positional: parsed.positional, home, cwd });
|
|
303
|
+
const scaffold = forgeHome
|
|
304
|
+
? join(forgeHome, "examples", "forgedock-demo")
|
|
305
|
+
: null;
|
|
306
|
+
|
|
307
|
+
let source;
|
|
308
|
+
|
|
309
|
+
if (fsx.existsSync(target) && !isEmptyDir(target, fsx)) {
|
|
310
|
+
// Idempotent re-run — never clobber an existing demo.
|
|
311
|
+
if (isGitRepo(target, fsx)) {
|
|
312
|
+
const pull = exec("git", ["pull", "--ff-only"], { cwd: target });
|
|
313
|
+
source = pull.status === 0 ? "pulled" : "exists";
|
|
314
|
+
if (pull.status !== 0) {
|
|
315
|
+
logger.log("Could not fast-forward the existing demo — leaving it untouched.");
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
source = "exists";
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
// Fresh setup: prefer the live repo, fall back to the bundled scaffold.
|
|
322
|
+
fsx.mkdirSync(dirname(target), { recursive: true });
|
|
323
|
+
const clone = exec(
|
|
324
|
+
"git",
|
|
325
|
+
["clone", "--depth", "1", `https://github.com/${demoRepo}.git`, target],
|
|
326
|
+
{ cwd },
|
|
327
|
+
);
|
|
328
|
+
if (clone.status === 0) {
|
|
329
|
+
source = "cloned";
|
|
330
|
+
} else {
|
|
331
|
+
if (!scaffold || !fsx.existsSync(scaffold)) {
|
|
332
|
+
const msg =
|
|
333
|
+
`Could not clone ${demoRepo} and no bundled scaffold was found` +
|
|
334
|
+
(scaffold ? ` at ${scaffold}` : "") +
|
|
335
|
+
". Check your network or reinstall ForgeDock.";
|
|
336
|
+
logger.error(msg);
|
|
337
|
+
return { status: "error", error: msg };
|
|
338
|
+
}
|
|
339
|
+
// Copy everything except bootstrap.sh (that script is for standing up a
|
|
340
|
+
// *live* GitHub repo and is not needed for a local demo).
|
|
341
|
+
fsx.mkdirSync(target, { recursive: true });
|
|
342
|
+
fsx.cpSync(scaffold, target, {
|
|
343
|
+
recursive: true,
|
|
344
|
+
filter: (src) => basename(src) !== "bootstrap.sh",
|
|
345
|
+
});
|
|
346
|
+
// Best-effort local git history so the demo feels like a real repo.
|
|
347
|
+
exec("git", ["init", "-q"], { cwd: target });
|
|
348
|
+
exec("git", ["add", "."], { cwd: target });
|
|
349
|
+
exec(
|
|
350
|
+
"git",
|
|
351
|
+
[
|
|
352
|
+
"-c",
|
|
353
|
+
"user.name=forgedock-demo",
|
|
354
|
+
"-c",
|
|
355
|
+
"user.email=demo@forgedock.dev",
|
|
356
|
+
"commit",
|
|
357
|
+
"-q",
|
|
358
|
+
"-m",
|
|
359
|
+
"chore: initial demo scaffold",
|
|
360
|
+
],
|
|
361
|
+
{ cwd: target },
|
|
362
|
+
);
|
|
363
|
+
source = "scaffold";
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Ensure a usable forge.yaml exists (the bundled scaffold ships without one).
|
|
368
|
+
const forgeYamlPath = join(target, "forge.yaml");
|
|
369
|
+
if (!fsx.existsSync(forgeYamlPath)) {
|
|
370
|
+
fsx.writeFileSync(forgeYamlPath, demoForgeYaml(demoRepo));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const claudeAvailable = commandExists("claude");
|
|
374
|
+
logger.log(renderNextSteps({ target, source, claudeAvailable }));
|
|
375
|
+
|
|
376
|
+
if (parsed.open) {
|
|
377
|
+
if (claudeAvailable) {
|
|
378
|
+
logger.log("Launching Claude Code…");
|
|
379
|
+
launch("claude", [], { cwd: target });
|
|
380
|
+
} else {
|
|
381
|
+
logger.log("`claude` was not found on PATH — open Claude Code manually in the demo directory.");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { status: "ok", target, source };
|
|
386
|
+
}
|