@web42/cli 0.1.16 → 0.2.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.
@@ -3,92 +3,204 @@ import { join } from "path";
3
3
  import { Command } from "commander";
4
4
  import chalk from "chalk";
5
5
  import ora from "ora";
6
- import { openclawAdapter, HARDCODED_EXCLUDES } from "../platforms/openclaw/adapter.js";
6
+ import { resolvePlatform } from "../platforms/registry.js";
7
7
  import { parseSkillMd } from "../utils/skill.js";
8
- const HARDCODED_EXCLUDES_SET = new Set(HARDCODED_EXCLUDES);
8
+ /**
9
+ * Pack a single agent: run adapter.pack(), detect skills, write to dist.
10
+ */
11
+ async function packSingleAgent(opts) {
12
+ const { cwd, adapter, agentName, dryRun } = opts;
13
+ // Clone manifest so per-agent enrichment doesn't leak
14
+ const manifest = JSON.parse(JSON.stringify(opts.manifest));
15
+ const result = await adapter.pack({
16
+ cwd,
17
+ outputDir: opts.outputDir,
18
+ agentName,
19
+ });
20
+ // Detect skills from packed files and strip internal ones
21
+ const internalSkillPrefixes = [];
22
+ const detectedSkills = [];
23
+ for (const f of result.files) {
24
+ const match = f.path.match(/^skills\/([^/]+)\/SKILL\.md$/);
25
+ if (match) {
26
+ const parsed = parseSkillMd(f.content, match[1]);
27
+ if (parsed.internal) {
28
+ internalSkillPrefixes.push(`skills/${match[1]}/`);
29
+ }
30
+ else {
31
+ detectedSkills.push({ name: parsed.name, description: parsed.description });
32
+ }
33
+ }
34
+ }
35
+ if (internalSkillPrefixes.length > 0) {
36
+ result.files = result.files.filter((f) => !internalSkillPrefixes.some((prefix) => f.path.startsWith(prefix)));
37
+ }
38
+ if (detectedSkills.length > 0) {
39
+ manifest.skills = detectedSkills.sort((a, b) => a.name.localeCompare(b.name));
40
+ }
41
+ // Merge auto-generated config variables into manifest
42
+ const existingKeys = new Set((manifest.configVariables ?? []).map((v) => v.key));
43
+ for (const cv of result.configVariables) {
44
+ if (!existingKeys.has(cv.key)) {
45
+ if (!manifest.configVariables)
46
+ manifest.configVariables = [];
47
+ manifest.configVariables.push(cv);
48
+ existingKeys.add(cv.key);
49
+ }
50
+ }
51
+ if (dryRun) {
52
+ const label = agentName ? `Dry run for ${chalk.bold(agentName)}` : "Dry run";
53
+ console.log(chalk.bold(`${label} — would pack:`));
54
+ console.log();
55
+ for (const f of result.files) {
56
+ console.log(chalk.dim(` ${f.path} (${f.content.length} bytes)`));
57
+ }
58
+ console.log();
59
+ console.log(chalk.dim(`${result.files.length} files, ${result.configVariables.length} config variable(s)`));
60
+ return { manifest, result };
61
+ }
62
+ const outputDir = join(cwd, opts.outputDir);
63
+ mkdirSync(outputDir, { recursive: true });
64
+ for (const file of result.files) {
65
+ const filePath = join(outputDir, file.path);
66
+ mkdirSync(join(filePath, ".."), { recursive: true });
67
+ writeFileSync(filePath, file.content, "utf-8");
68
+ }
69
+ writeFileSync(join(outputDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
70
+ return { manifest, result };
71
+ }
9
72
  export const packCommand = new Command("pack")
10
73
  .description("Pack your agent workspace into a distributable artifact")
11
74
  .option("-o, --output <dir>", "Output directory", ".web42/dist")
12
75
  .option("--dry-run", "Preview what would be packed without writing files")
76
+ .option("--agent <name>", "Pack a specific agent (for multi-agent workspaces)")
13
77
  .action(async (opts) => {
14
78
  const cwd = process.cwd();
15
- const manifestPath = join(cwd, "manifest.json");
16
- if (!existsSync(manifestPath)) {
17
- console.log(chalk.red("No manifest.json found. Run `web42 init` first."));
18
- process.exit(1);
19
- }
20
- const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
21
- if (!manifest.name || !manifest.version || !manifest.author) {
22
- console.log(chalk.red("Invalid manifest.json. Must have name, version, and author."));
23
- process.exit(1);
24
- }
25
- const spinner = ora("Packing agent...").start();
26
- try {
27
- const result = await openclawAdapter.pack({ cwd, outputDir: opts.output });
28
- // Detect skills from packed files and strip internal ones
29
- const internalSkillPrefixes = [];
30
- const detectedSkills = [];
31
- for (const f of result.files) {
32
- const match = f.path.match(/^skills\/([^/]+)\/SKILL\.md$/);
33
- if (match) {
34
- const parsed = parseSkillMd(f.content, match[1]);
35
- if (parsed.internal) {
36
- internalSkillPrefixes.push(`skills/${match[1]}/`);
37
- }
38
- else {
39
- detectedSkills.push({ name: parsed.name, description: parsed.description });
79
+ // Determine platform: check for per-agent manifests first, then root manifest
80
+ let platform = "openclaw"; // default
81
+ let isMultiAgent = false;
82
+ const agentManifests = new Map();
83
+ // Check if this is a multi-agent workspace (per-agent manifests in .web42/{name}/)
84
+ const web42Dir = join(cwd, ".web42");
85
+ if (existsSync(web42Dir)) {
86
+ const { readdirSync } = await import("fs");
87
+ try {
88
+ const entries = readdirSync(web42Dir, { withFileTypes: true });
89
+ for (const entry of entries) {
90
+ if (!entry.isDirectory())
91
+ continue;
92
+ const agentManifestPath = join(web42Dir, entry.name, "manifest.json");
93
+ if (existsSync(agentManifestPath)) {
94
+ try {
95
+ const m = JSON.parse(readFileSync(agentManifestPath, "utf-8"));
96
+ agentManifests.set(entry.name, m);
97
+ if (m.platform)
98
+ platform = m.platform;
99
+ }
100
+ catch {
101
+ // skip invalid manifests
102
+ }
40
103
  }
41
104
  }
42
105
  }
43
- if (internalSkillPrefixes.length > 0) {
44
- result.files = result.files.filter((f) => !internalSkillPrefixes.some((prefix) => f.path.startsWith(prefix)));
106
+ catch {
107
+ // .web42 not readable
45
108
  }
46
- if (detectedSkills.length > 0) {
47
- manifest.skills = detectedSkills.sort((a, b) => a.name.localeCompare(b.name));
109
+ }
110
+ isMultiAgent = agentManifests.size > 0;
111
+ // Fall back to root manifest.json (single-agent, e.g., OpenClaw)
112
+ if (!isMultiAgent) {
113
+ const manifestPath = join(cwd, "manifest.json");
114
+ if (!existsSync(manifestPath)) {
115
+ console.log(chalk.red("No manifest.json found. Run `web42 init` first."));
116
+ process.exit(1);
48
117
  }
49
- // Merge auto-generated config variables into manifest
50
- const existingKeys = new Set((manifest.configVariables ?? []).map((v) => v.key));
51
- for (const cv of result.configVariables) {
52
- if (!existingKeys.has(cv.key)) {
53
- if (!manifest.configVariables)
54
- manifest.configVariables = [];
55
- manifest.configVariables.push(cv);
56
- existingKeys.add(cv.key);
57
- }
118
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
119
+ if (!manifest.name || !manifest.version || !manifest.author) {
120
+ console.log(chalk.red("Invalid manifest.json. Must have name, version, and author."));
121
+ process.exit(1);
58
122
  }
59
- if (opts.dryRun) {
60
- spinner.stop();
61
- const userPatterns = (result.ignorePatterns ?? []).filter((p) => !HARDCODED_EXCLUDES_SET.has(p));
62
- if (userPatterns.length > 0) {
63
- console.log(chalk.bold("Ignore patterns from .web42ignore:"));
64
- for (const p of userPatterns) {
65
- console.log(chalk.yellow(` ✕ ${p}`));
66
- }
67
- console.log();
123
+ if (manifest.platform)
124
+ platform = manifest.platform;
125
+ const adapter = resolvePlatform(platform);
126
+ const spinner = ora("Packing agent...").start();
127
+ try {
128
+ const { manifest: enriched, result } = await packSingleAgent({
129
+ cwd,
130
+ manifest,
131
+ manifestPath,
132
+ adapter,
133
+ outputDir: opts.output,
134
+ dryRun: opts.dryRun,
135
+ });
136
+ if (opts.dryRun) {
137
+ spinner.stop();
138
+ return;
68
139
  }
69
- console.log(chalk.bold("Dry run would pack:"));
70
- console.log();
71
- for (const f of result.files) {
72
- console.log(chalk.dim(` ${f.path} (${f.content.length} bytes)`));
140
+ spinner.succeed(`Packed ${chalk.bold(enriched.name)} (${result.files.length} files) ${opts.output}/`);
141
+ if (result.configVariables.length > 0) {
142
+ console.log(chalk.dim(` ${result.configVariables.length} config variable(s) detected`));
73
143
  }
74
144
  console.log();
75
- console.log(chalk.dim(`${result.files.length} files, ${result.configVariables.length} config variable(s)`));
76
- return;
145
+ console.log(chalk.dim("Run `web42 push` to publish to the marketplace."));
77
146
  }
78
- const outputDir = join(cwd, opts.output);
79
- mkdirSync(outputDir, { recursive: true });
80
- for (const file of result.files) {
81
- const filePath = join(outputDir, file.path);
82
- mkdirSync(join(filePath, ".."), { recursive: true });
83
- writeFileSync(filePath, file.content, "utf-8");
147
+ catch (error) {
148
+ spinner.fail("Pack failed");
149
+ console.error(chalk.red(error.message));
150
+ process.exit(1);
84
151
  }
85
- writeFileSync(join(outputDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
86
- spinner.succeed(`Packed ${chalk.bold(manifest.name)} (${result.files.length} files) → ${opts.output}/`);
87
- if (result.configVariables.length > 0) {
88
- console.log(chalk.dim(` ${result.configVariables.length} config variable(s) detected`));
152
+ return;
153
+ }
154
+ // Multi-agent mode
155
+ const adapter = resolvePlatform(platform);
156
+ // Determine which agents to pack
157
+ let agentsToPack;
158
+ if (opts.agent) {
159
+ const manifest = agentManifests.get(opts.agent);
160
+ if (!manifest) {
161
+ console.log(chalk.red(`Agent "${opts.agent}" not found. Available: ${[...agentManifests.keys()].join(", ")}`));
162
+ process.exit(1);
163
+ }
164
+ agentsToPack = [[opts.agent, manifest]];
165
+ }
166
+ else {
167
+ agentsToPack = [...agentManifests.entries()];
168
+ }
169
+ const spinner = ora(`Packing ${agentsToPack.length} agent(s)...`).start();
170
+ try {
171
+ for (const [agentName, manifest] of agentsToPack) {
172
+ const agentOutputDir = join(".web42", agentName, "dist");
173
+ spinner.text = `Packing ${agentName}...`;
174
+ if (opts.dryRun) {
175
+ spinner.stop();
176
+ }
177
+ const { manifest: enriched, result } = await packSingleAgent({
178
+ cwd,
179
+ manifest,
180
+ manifestPath: join(web42Dir, agentName, "manifest.json"),
181
+ adapter,
182
+ agentName,
183
+ outputDir: agentOutputDir,
184
+ dryRun: opts.dryRun,
185
+ });
186
+ if (!opts.dryRun) {
187
+ // Also write the enriched manifest back
188
+ writeFileSync(join(web42Dir, agentName, "manifest.json"), JSON.stringify(enriched, null, 2) + "\n");
189
+ console.log(chalk.green(` Packed ${chalk.bold(agentName)} (${result.files.length} files) → ${agentOutputDir}/`));
190
+ }
191
+ if (opts.dryRun) {
192
+ console.log(); // spacing between agents
193
+ spinner.start();
194
+ }
195
+ }
196
+ if (opts.dryRun) {
197
+ spinner.stop();
198
+ }
199
+ else {
200
+ spinner.succeed(`Packed ${agentsToPack.length} agent(s)`);
201
+ console.log();
202
+ console.log(chalk.dim("Run `web42 push` to publish to the marketplace."));
89
203
  }
90
- console.log();
91
- console.log(chalk.dim("Run `web42 push` to publish to the marketplace."));
92
204
  }
93
205
  catch (error) {
94
206
  spinner.fail("Pack failed");
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
2
2
  import { dirname, join } from "path";
3
3
  import { Command } from "commander";
4
4
  import chalk from "chalk";
@@ -6,125 +6,193 @@ import ora from "ora";
6
6
  import { apiGet } from "../utils/api.js";
7
7
  import { requireAuth } from "../utils/config.js";
8
8
  import { buildLocalSnapshot, computeHashFromSnapshot, readSyncState, writeSyncState, writeMarketplace, writeResourcesMeta, } from "../utils/sync.js";
9
+ /**
10
+ * Pull a single agent from the marketplace into a local directory.
11
+ */
12
+ async function pullSingleAgent(opts) {
13
+ const { manifest, manifestPath, syncDir, writeDir, distDir, config, spinner } = opts;
14
+ const name = manifest.name ?? "";
15
+ // Step 1: Resolve agent ID
16
+ let syncState = readSyncState(syncDir);
17
+ let agentId = syncState?.agent_id ?? null;
18
+ if (!agentId) {
19
+ spinner.text = `Looking up ${name}...`;
20
+ const agents = await apiGet(`/api/agents?username=${config.username}`);
21
+ const agent = agents.find((a) => a.slug === name);
22
+ if (!agent) {
23
+ spinner.fail(`Agent @${config.username}/${name} not found on the marketplace. Run \`web42 push\` first.`);
24
+ process.exit(1);
25
+ }
26
+ agentId = agent.id;
27
+ }
28
+ // Step 2: Compare remote hash with last known remote hash (unless --force)
29
+ if (!opts.force && syncState?.last_remote_hash) {
30
+ spinner.text = `Checking remote state for ${name}...`;
31
+ const remote = await apiGet(`/api/agents/${agentId}/sync`);
32
+ if (remote.hash === syncState.last_remote_hash) {
33
+ console.log(chalk.dim(` ${chalk.bold(`@${config.username}/${name}`)} is already in sync.`));
34
+ return;
35
+ }
36
+ }
37
+ // Step 3: Pull full snapshot
38
+ spinner.text = `Downloading ${name}...`;
39
+ const pullResult = await apiGet(`/api/agents/${agentId}/sync/pull`);
40
+ const { snapshot } = pullResult;
41
+ let written = 0;
42
+ // Step 4a: Write manifest.json
43
+ const updatedManifest = {
44
+ ...manifest,
45
+ ...snapshot.manifest,
46
+ name: snapshot.identity.slug,
47
+ description: snapshot.identity.description,
48
+ };
49
+ writeFileSync(manifestPath, JSON.stringify(updatedManifest, null, 2) + "\n");
50
+ written++;
51
+ // Step 4b: Write README.md
52
+ if (snapshot.readme) {
53
+ writeFileSync(join(writeDir, "README.md"), snapshot.readme, "utf-8");
54
+ written++;
55
+ }
56
+ // Step 4c: Write marketplace.json
57
+ writeMarketplace(writeDir, snapshot.marketplace);
58
+ written++;
59
+ // Step 4d: Write agent files into dist/
60
+ let skipped = 0;
61
+ mkdirSync(distDir, { recursive: true });
62
+ for (const file of snapshot.files) {
63
+ if (file.content === null || file.content === undefined) {
64
+ skipped++;
65
+ continue;
66
+ }
67
+ if (file.path === ".openclaw/config.json") {
68
+ skipped++;
69
+ continue;
70
+ }
71
+ const filePath = join(distDir, file.path);
72
+ mkdirSync(dirname(filePath), { recursive: true });
73
+ writeFileSync(filePath, file.content, "utf-8");
74
+ written++;
75
+ }
76
+ // Step 4e: Write resources metadata
77
+ if (snapshot.resources.length > 0) {
78
+ const resourcesMeta = snapshot.resources.map((r, i) => ({
79
+ file: `resource-${i}-${r.title.replace(/[^a-zA-Z0-9.-]/g, "_")}`,
80
+ title: r.title,
81
+ description: r.description ?? undefined,
82
+ type: r.type,
83
+ sort_order: r.sort_order,
84
+ }));
85
+ writeResourcesMeta(writeDir, resourcesMeta);
86
+ written++;
87
+ }
88
+ // Step 5: Save sync state
89
+ const localSnapshot = buildLocalSnapshot(syncDir, distDir);
90
+ const localHash = computeHashFromSnapshot(localSnapshot);
91
+ writeSyncState(syncDir, {
92
+ agent_id: agentId,
93
+ last_remote_hash: pullResult.hash,
94
+ last_local_hash: localHash,
95
+ synced_at: new Date().toISOString(),
96
+ });
97
+ console.log(chalk.green(` Pulled @${config.username}/${name} (${written} files${skipped > 0 ? `, ${skipped} skipped` : ""})`));
98
+ console.log(chalk.dim(` Sync hash: ${pullResult.hash.slice(0, 12)}...`));
99
+ }
9
100
  export const pullCommand = new Command("pull")
10
101
  .description("Pull latest agent state from the Web42 marketplace into the current directory")
11
102
  .option("--force", "Skip hash comparison and always pull")
103
+ .option("--agent <name>", "Pull a specific agent (for multi-agent workspaces)")
12
104
  .action(async (opts) => {
13
105
  const config = requireAuth();
14
106
  const cwd = process.cwd();
15
- const manifestPath = join(cwd, "manifest.json");
16
- if (!existsSync(manifestPath)) {
17
- console.log(chalk.red("No manifest.json found. Are you in an agent directory?"));
18
- process.exit(1);
19
- }
20
- const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
21
- if (!manifest.name) {
22
- console.log(chalk.red("manifest.json is missing a name field."));
23
- process.exit(1);
24
- }
25
- const spinner = ora(`Pulling @${config.username}/${manifest.name}...`).start();
26
- try {
27
- // -------------------------------------------------------------------
28
- // Step 1: Resolve agent ID
29
- // -------------------------------------------------------------------
30
- let syncState = readSyncState(cwd);
31
- let agentId = syncState?.agent_id ?? null;
32
- if (!agentId) {
33
- spinner.text = "Looking up agent...";
34
- const agents = await apiGet(`/api/agents?username=${config.username}`);
35
- const agent = agents.find((a) => a.slug === manifest.name);
36
- if (!agent) {
37
- spinner.fail(`Agent @${config.username}/${manifest.name} not found on the marketplace. Run \`web42 push\` first.`);
38
- process.exit(1);
107
+ // Detect multi-agent workspace (.web42/{name}/manifest.json)
108
+ const web42Dir = join(cwd, ".web42");
109
+ const agentManifests = new Map();
110
+ if (existsSync(web42Dir)) {
111
+ try {
112
+ const entries = readdirSync(web42Dir, { withFileTypes: true });
113
+ for (const entry of entries) {
114
+ if (!entry.isDirectory())
115
+ continue;
116
+ const agentManifestPath = join(web42Dir, entry.name, "manifest.json");
117
+ if (existsSync(agentManifestPath)) {
118
+ try {
119
+ const m = JSON.parse(readFileSync(agentManifestPath, "utf-8"));
120
+ agentManifests.set(entry.name, m);
121
+ }
122
+ catch {
123
+ // skip
124
+ }
125
+ }
39
126
  }
40
- agentId = agent.id;
41
127
  }
42
- // -------------------------------------------------------------------
43
- // Step 2: Compare remote hash with last known remote hash (unless --force)
44
- // -------------------------------------------------------------------
45
- if (!opts.force && syncState?.last_remote_hash) {
46
- spinner.text = "Checking remote state...";
47
- const remote = await apiGet(`/api/agents/${agentId}/sync`);
48
- if (remote.hash === syncState.last_remote_hash) {
49
- spinner.succeed(`${chalk.bold(`@${config.username}/${manifest.name}`)} is already in sync (no remote changes).`);
50
- return;
51
- }
128
+ catch {
129
+ // .web42 not readable
52
130
  }
53
- // -------------------------------------------------------------------
54
- // Step 3: Pull full snapshot
55
- // -------------------------------------------------------------------
56
- spinner.text = "Downloading snapshot...";
57
- const pullResult = await apiGet(`/api/agents/${agentId}/sync/pull`);
58
- const { snapshot } = pullResult;
59
- let written = 0;
60
- // -------------------------------------------------------------------
61
- // Step 4a: Write manifest.json (merge identity into existing manifest)
62
- // -------------------------------------------------------------------
63
- const updatedManifest = {
64
- ...manifest,
65
- ...snapshot.manifest,
66
- name: snapshot.identity.slug,
67
- description: snapshot.identity.description,
68
- };
69
- writeFileSync(manifestPath, JSON.stringify(updatedManifest, null, 2) + "\n");
70
- written++;
71
- // -------------------------------------------------------------------
72
- // Step 4b: Write README.md
73
- // -------------------------------------------------------------------
74
- if (snapshot.readme) {
75
- writeFileSync(join(cwd, "README.md"), snapshot.readme, "utf-8");
76
- written++;
131
+ }
132
+ const isMultiAgent = agentManifests.size > 0;
133
+ // Single-agent mode
134
+ if (!isMultiAgent) {
135
+ const manifestPath = join(cwd, "manifest.json");
136
+ if (!existsSync(manifestPath)) {
137
+ console.log(chalk.red("No manifest.json found. Are you in an agent directory?"));
138
+ process.exit(1);
77
139
  }
78
- // -------------------------------------------------------------------
79
- // Step 4c: Write .web42/marketplace.json
80
- // -------------------------------------------------------------------
81
- writeMarketplace(cwd, snapshot.marketplace);
82
- written++;
83
- // -------------------------------------------------------------------
84
- // Step 4d: Write agent files
85
- // -------------------------------------------------------------------
86
- let skipped = 0;
87
- for (const file of snapshot.files) {
88
- if (file.content === null || file.content === undefined) {
89
- skipped++;
90
- continue;
91
- }
92
- if (file.path === ".openclaw/config.json") {
93
- skipped++;
94
- continue;
95
- }
96
- const filePath = join(cwd, file.path);
97
- mkdirSync(dirname(filePath), { recursive: true });
98
- writeFileSync(filePath, file.content, "utf-8");
99
- written++;
140
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
141
+ if (!manifest.name) {
142
+ console.log(chalk.red("manifest.json is missing a name field."));
143
+ process.exit(1);
100
144
  }
101
- // -------------------------------------------------------------------
102
- // Step 4e: Write resources metadata
103
- // -------------------------------------------------------------------
104
- if (snapshot.resources.length > 0) {
105
- const resourcesMeta = snapshot.resources.map((r, i) => ({
106
- file: `resource-${i}-${r.title.replace(/[^a-zA-Z0-9.-]/g, "_")}`,
107
- title: r.title,
108
- description: r.description ?? undefined,
109
- type: r.type,
110
- sort_order: r.sort_order,
111
- }));
112
- writeResourcesMeta(cwd, resourcesMeta);
113
- written++;
145
+ const spinner = ora(`Pulling @${config.username}/${manifest.name}...`).start();
146
+ try {
147
+ await pullSingleAgent({
148
+ manifest,
149
+ manifestPath,
150
+ syncDir: cwd,
151
+ writeDir: cwd,
152
+ distDir: join(cwd, ".web42", "dist"),
153
+ config,
154
+ force: opts.force,
155
+ spinner,
156
+ });
157
+ spinner.succeed(`Pull complete`);
158
+ }
159
+ catch (error) {
160
+ spinner.fail("Pull failed");
161
+ console.error(chalk.red(error.message));
162
+ process.exit(1);
163
+ }
164
+ return;
165
+ }
166
+ // Multi-agent mode
167
+ let agentsToPull;
168
+ if (opts.agent) {
169
+ const manifest = agentManifests.get(opts.agent);
170
+ if (!manifest) {
171
+ console.log(chalk.red(`Agent "${opts.agent}" not found. Available: ${[...agentManifests.keys()].join(", ")}`));
172
+ process.exit(1);
173
+ }
174
+ agentsToPull = [[opts.agent, manifest]];
175
+ }
176
+ else {
177
+ agentsToPull = [...agentManifests.entries()];
178
+ }
179
+ const spinner = ora(`Pulling ${agentsToPull.length} agent(s)...`).start();
180
+ try {
181
+ for (const [agentName, manifest] of agentsToPull) {
182
+ spinner.text = `Pulling ${agentName}...`;
183
+ const agentWeb42Dir = join(web42Dir, agentName);
184
+ await pullSingleAgent({
185
+ manifest,
186
+ manifestPath: join(agentWeb42Dir, "manifest.json"),
187
+ syncDir: agentWeb42Dir,
188
+ writeDir: agentWeb42Dir,
189
+ distDir: join(agentWeb42Dir, "dist"),
190
+ config,
191
+ force: opts.force,
192
+ spinner,
193
+ });
114
194
  }
115
- // -------------------------------------------------------------------
116
- // Step 5: Save sync state (compute local hash from what we just wrote)
117
- // -------------------------------------------------------------------
118
- const localSnapshot = buildLocalSnapshot(cwd);
119
- const localHash = computeHashFromSnapshot(localSnapshot);
120
- writeSyncState(cwd, {
121
- agent_id: agentId,
122
- last_remote_hash: pullResult.hash,
123
- last_local_hash: localHash,
124
- synced_at: new Date().toISOString(),
125
- });
126
- spinner.succeed(`Pulled ${chalk.bold(`@${config.username}/${manifest.name}`)} (${written} files written${skipped > 0 ? `, ${skipped} skipped` : ""})`);
127
- console.log(chalk.dim(` Sync hash: ${pullResult.hash.slice(0, 12)}...`));
195
+ spinner.succeed(`Pulled ${agentsToPull.length} agent(s)`);
128
196
  }
129
197
  catch (error) {
130
198
  spinner.fail("Pull failed");