@web42/cli 0.1.6 → 0.1.8
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/dist/commands/init.js +65 -1
- package/dist/commands/pack.js +11 -2
- package/dist/commands/pull.js +88 -17
- package/dist/commands/push.js +130 -66
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +98 -0
- package/dist/generated/embedded-skills.js +17 -5
- package/dist/index.js +2 -0
- package/dist/platforms/base.d.ts +1 -0
- package/dist/platforms/openclaw/adapter.d.ts +1 -0
- package/dist/platforms/openclaw/adapter.js +2 -2
- package/dist/types/sync.d.ts +74 -0
- package/dist/types/sync.js +7 -0
- package/dist/utils/api.d.ts +2 -0
- package/dist/utils/api.js +26 -0
- package/dist/utils/sync.d.ts +12 -0
- package/dist/utils/sync.js +197 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/web42-publish-prep/SKILL.md +84 -23
- package/skills/web42-publish-prep/_meta.json +3 -3
- package/skills/web42-publish-prep/assets/readme-template.md +9 -5
- package/skills/web42-publish-prep/references/file-hygiene.md +30 -9
- package/skills/web42-publish-prep/references/manifest-fields.md +4 -1
- package/skills/web42-publish-prep/references/marketplace-config.md +99 -0
- package/skills/web42-publish-prep/references/resources-guide.md +136 -0
- package/skills/web42-publish-prep/references/web42-folder.md +120 -0
package/dist/commands/init.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeFileSync, existsSync, readdirSync, readFileSync, } from "fs";
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync, readdirSync, readFileSync, } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import chalk from "chalk";
|
|
@@ -7,6 +7,8 @@ import { requireAuth } from "../utils/config.js";
|
|
|
7
7
|
import { parseSkillMd } from "../utils/skill.js";
|
|
8
8
|
import { listBundledSkills, copySkillToWorkspace } from "../utils/bundled-skills.js";
|
|
9
9
|
import { resolvePlatform, listPlatforms } from "../platforms/registry.js";
|
|
10
|
+
import { writeMarketplace, writeResourcesMeta } from "../utils/sync.js";
|
|
11
|
+
import { DEFAULT_MARKETPLACE } from "../types/sync.js";
|
|
10
12
|
import { AGENTS_MD, IDENTITY_MD, SOUL_MD, TOOLS_MD, USER_MD, HEARTBEAT_MD, INIT_BOOTSTRAP_MD, } from "../platforms/openclaw/templates.js";
|
|
11
13
|
function detectWorkspaceSkills(cwd) {
|
|
12
14
|
const skillsDir = join(cwd, "skills");
|
|
@@ -147,6 +149,67 @@ export const initCommand = new Command("init")
|
|
|
147
149
|
if (skipped.length > 0) {
|
|
148
150
|
console.log(chalk.dim(` Skipped (already exist): ${skipped.join(", ")}`));
|
|
149
151
|
}
|
|
152
|
+
// Scaffold .web42/ metadata folder
|
|
153
|
+
const web42Dir = join(cwd, ".web42");
|
|
154
|
+
mkdirSync(web42Dir, { recursive: true });
|
|
155
|
+
const marketplacePath = join(web42Dir, "marketplace.json");
|
|
156
|
+
if (!existsSync(marketplacePath)) {
|
|
157
|
+
writeMarketplace(cwd, { ...DEFAULT_MARKETPLACE });
|
|
158
|
+
console.log(chalk.green(` Created ${chalk.bold(".web42/marketplace.json")}`));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
console.log(chalk.dim(" Skipped .web42/marketplace.json (already exists)"));
|
|
162
|
+
}
|
|
163
|
+
const resourcesJsonPath = join(web42Dir, "resources.json");
|
|
164
|
+
if (!existsSync(resourcesJsonPath)) {
|
|
165
|
+
writeResourcesMeta(cwd, []);
|
|
166
|
+
console.log(chalk.green(` Created ${chalk.bold(".web42/resources.json")}`));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.log(chalk.dim(" Skipped .web42/resources.json (already exists)"));
|
|
170
|
+
}
|
|
171
|
+
mkdirSync(join(web42Dir, "resources"), { recursive: true });
|
|
172
|
+
const ignorePath = join(cwd, ".web42ignore");
|
|
173
|
+
if (!existsSync(ignorePath)) {
|
|
174
|
+
writeFileSync(ignorePath, [
|
|
175
|
+
"# .web42ignore — files excluded from web42 pack / push",
|
|
176
|
+
"# Syntax: glob patterns, one per line. Lines starting with # are comments.",
|
|
177
|
+
"# NOTE: .git, node_modules, .web42/, manifest.json, and other internals",
|
|
178
|
+
"# are always excluded automatically.",
|
|
179
|
+
"",
|
|
180
|
+
"# Working notes & drafts",
|
|
181
|
+
"TODO.md",
|
|
182
|
+
"NOTES.md",
|
|
183
|
+
"drafts/**",
|
|
184
|
+
"",
|
|
185
|
+
"# Environment & secrets",
|
|
186
|
+
".env",
|
|
187
|
+
".env.*",
|
|
188
|
+
"",
|
|
189
|
+
"# IDE / editor",
|
|
190
|
+
".vscode/**",
|
|
191
|
+
".idea/**",
|
|
192
|
+
".cursor/**",
|
|
193
|
+
"",
|
|
194
|
+
"# Test & CI",
|
|
195
|
+
"tests/**",
|
|
196
|
+
"__tests__/**",
|
|
197
|
+
".github/**",
|
|
198
|
+
"",
|
|
199
|
+
"# Build artifacts",
|
|
200
|
+
"dist/**",
|
|
201
|
+
"build/**",
|
|
202
|
+
"",
|
|
203
|
+
"# Large media not needed at runtime",
|
|
204
|
+
"# *.mp4",
|
|
205
|
+
"# *.mov",
|
|
206
|
+
"",
|
|
207
|
+
].join("\n"), "utf-8");
|
|
208
|
+
console.log(chalk.green(` Created ${chalk.bold(".web42ignore")}`));
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
console.log(chalk.dim(" Skipped .web42ignore (already exists)"));
|
|
212
|
+
}
|
|
150
213
|
// Offer bundled starter skills
|
|
151
214
|
const bundled = listBundledSkills();
|
|
152
215
|
if (bundled.length > 0) {
|
|
@@ -190,5 +253,6 @@ export const initCommand = new Command("init")
|
|
|
190
253
|
}
|
|
191
254
|
}
|
|
192
255
|
console.log();
|
|
256
|
+
console.log(chalk.dim("Edit .web42/marketplace.json to set price, tags, license, and visibility."));
|
|
193
257
|
console.log(chalk.dim("Run `web42 pack` to bundle your agent, or `web42 push` to pack and publish."));
|
|
194
258
|
});
|
package/dist/commands/pack.js
CHANGED
|
@@ -3,11 +3,12 @@ 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 } from "../platforms/openclaw/adapter.js";
|
|
6
|
+
import { openclawAdapter, HARDCODED_EXCLUDES } from "../platforms/openclaw/adapter.js";
|
|
7
7
|
import { parseSkillMd } from "../utils/skill.js";
|
|
8
|
+
const HARDCODED_EXCLUDES_SET = new Set(HARDCODED_EXCLUDES);
|
|
8
9
|
export const packCommand = new Command("pack")
|
|
9
10
|
.description("Pack your agent workspace into a distributable artifact")
|
|
10
|
-
.option("-o, --output <dir>", "Output directory", ".web42")
|
|
11
|
+
.option("-o, --output <dir>", "Output directory", ".web42/dist")
|
|
11
12
|
.option("--dry-run", "Preview what would be packed without writing files")
|
|
12
13
|
.action(async (opts) => {
|
|
13
14
|
const cwd = process.cwd();
|
|
@@ -57,6 +58,14 @@ export const packCommand = new Command("pack")
|
|
|
57
58
|
}
|
|
58
59
|
if (opts.dryRun) {
|
|
59
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();
|
|
68
|
+
}
|
|
60
69
|
console.log(chalk.bold("Dry run — would pack:"));
|
|
61
70
|
console.log();
|
|
62
71
|
for (const f of result.files) {
|
package/dist/commands/pull.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import ora from "ora";
|
|
6
6
|
import { apiGet } from "../utils/api.js";
|
|
7
7
|
import { requireAuth } from "../utils/config.js";
|
|
8
|
+
import { buildLocalSnapshot, computeHashFromSnapshot, readSyncState, writeSyncState, writeMarketplace, writeResourcesMeta, } from "../utils/sync.js";
|
|
8
9
|
export const pullCommand = new Command("pull")
|
|
9
|
-
.description("Pull latest agent
|
|
10
|
-
.
|
|
10
|
+
.description("Pull latest agent state from the Web42 marketplace into the current directory")
|
|
11
|
+
.option("--force", "Skip hash comparison and always pull")
|
|
12
|
+
.action(async (opts) => {
|
|
11
13
|
const config = requireAuth();
|
|
12
14
|
const cwd = process.cwd();
|
|
13
15
|
const manifestPath = join(cwd, "manifest.json");
|
|
@@ -22,25 +24,71 @@ export const pullCommand = new Command("pull")
|
|
|
22
24
|
}
|
|
23
25
|
const spinner = ora(`Pulling @${config.username}/${manifest.name}...`).start();
|
|
24
26
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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);
|
|
39
|
+
}
|
|
40
|
+
agentId = agent.id;
|
|
30
41
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
}
|
|
35
52
|
}
|
|
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;
|
|
36
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++;
|
|
77
|
+
}
|
|
78
|
+
// -------------------------------------------------------------------
|
|
79
|
+
// Step 4c: Write .web42/marketplace.json
|
|
80
|
+
// -------------------------------------------------------------------
|
|
81
|
+
writeMarketplace(cwd, snapshot.marketplace);
|
|
82
|
+
written++;
|
|
83
|
+
// -------------------------------------------------------------------
|
|
84
|
+
// Step 4d: Write agent files
|
|
85
|
+
// -------------------------------------------------------------------
|
|
37
86
|
let skipped = 0;
|
|
38
|
-
for (const file of files) {
|
|
87
|
+
for (const file of snapshot.files) {
|
|
39
88
|
if (file.content === null || file.content === undefined) {
|
|
40
89
|
skipped++;
|
|
41
90
|
continue;
|
|
42
91
|
}
|
|
43
|
-
// Skip packaging artifacts that are regenerated on push
|
|
44
92
|
if (file.path === ".openclaw/config.json") {
|
|
45
93
|
skipped++;
|
|
46
94
|
continue;
|
|
@@ -50,10 +98,33 @@ export const pullCommand = new Command("pull")
|
|
|
50
98
|
writeFileSync(filePath, file.content, "utf-8");
|
|
51
99
|
written++;
|
|
52
100
|
}
|
|
53
|
-
|
|
54
|
-
|
|
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++;
|
|
55
114
|
}
|
|
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
|
+
});
|
|
56
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)}...`));
|
|
57
128
|
}
|
|
58
129
|
catch (error) {
|
|
59
130
|
spinner.fail("Pull failed");
|
package/dist/commands/push.js
CHANGED
|
@@ -1,48 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { join, relative } from "path";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
4
3
|
import { Command } from "commander";
|
|
5
4
|
import chalk from "chalk";
|
|
6
5
|
import ora from "ora";
|
|
7
|
-
import { apiPost } from "../utils/api.js";
|
|
6
|
+
import { apiPost, apiFormData } from "../utils/api.js";
|
|
8
7
|
import { requireAuth } from "../utils/config.js";
|
|
9
8
|
import { openclawAdapter } from "../platforms/openclaw/adapter.js";
|
|
10
9
|
import { parseSkillMd } from "../utils/skill.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (entry.name === "manifest.json" && currentDir === dir)
|
|
25
|
-
continue;
|
|
26
|
-
const stat = statSync(fullPath);
|
|
27
|
-
if (stat.size > 1024 * 1024)
|
|
28
|
-
continue;
|
|
29
|
-
try {
|
|
30
|
-
const content = readFileSync(fullPath, "utf-8");
|
|
31
|
-
const relPath = relative(dir, fullPath);
|
|
32
|
-
files.push({ path: relPath, content, hash: hashContent(content) });
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
// Skip binary files
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
walk(dir);
|
|
41
|
-
return files;
|
|
10
|
+
import { buildLocalSnapshot, computeHashFromSnapshot, findLocalAvatar, readResourcesMeta, readSyncState, writeSyncState, } from "../utils/sync.js";
|
|
11
|
+
function mimeFromExtension(ext) {
|
|
12
|
+
const map = {
|
|
13
|
+
png: "image/png",
|
|
14
|
+
jpg: "image/jpeg",
|
|
15
|
+
jpeg: "image/jpeg",
|
|
16
|
+
webp: "image/webp",
|
|
17
|
+
svg: "image/svg+xml",
|
|
18
|
+
mp4: "video/mp4",
|
|
19
|
+
webm: "video/webm",
|
|
20
|
+
pdf: "application/pdf",
|
|
21
|
+
};
|
|
22
|
+
return map[ext.toLowerCase()] ?? "application/octet-stream";
|
|
42
23
|
}
|
|
43
24
|
export const pushCommand = new Command("push")
|
|
44
25
|
.description("Push your agent package to the Web42 marketplace")
|
|
45
|
-
.
|
|
26
|
+
.option("--force", "Skip hash comparison and always push")
|
|
27
|
+
.action(async (opts) => {
|
|
46
28
|
const config = requireAuth();
|
|
47
29
|
const cwd = process.cwd();
|
|
48
30
|
const manifestPath = join(cwd, "manifest.json");
|
|
@@ -56,19 +38,23 @@ export const pushCommand = new Command("push")
|
|
|
56
38
|
process.exit(1);
|
|
57
39
|
}
|
|
58
40
|
const spinner = ora("Preparing agent package...").start();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
41
|
+
// -----------------------------------------------------------------------
|
|
42
|
+
// Step 1: Pack into .web42/dist/
|
|
43
|
+
// -----------------------------------------------------------------------
|
|
44
|
+
const distDir = join(cwd, ".web42", "dist");
|
|
45
|
+
if (existsSync(distDir)) {
|
|
46
|
+
spinner.text = "Reading packed artifact from .web42/dist/...";
|
|
47
|
+
const packedManifestPath = join(distDir, "manifest.json");
|
|
65
48
|
if (existsSync(packedManifestPath)) {
|
|
66
49
|
manifest = JSON.parse(readFileSync(packedManifestPath, "utf-8"));
|
|
67
50
|
}
|
|
68
51
|
}
|
|
69
52
|
else {
|
|
70
|
-
spinner.text = "
|
|
71
|
-
const result = await openclawAdapter.pack({
|
|
53
|
+
spinner.text = "Packing agent into .web42/dist/...";
|
|
54
|
+
const result = await openclawAdapter.pack({
|
|
55
|
+
cwd,
|
|
56
|
+
outputDir: ".web42/dist",
|
|
57
|
+
});
|
|
72
58
|
const internalPrefixes = [];
|
|
73
59
|
for (const f of result.files) {
|
|
74
60
|
const skillMatch = f.path.match(/^skills\/([^/]+)\/SKILL\.md$/);
|
|
@@ -81,7 +67,6 @@ export const pushCommand = new Command("push")
|
|
|
81
67
|
if (internalPrefixes.length > 0) {
|
|
82
68
|
result.files = result.files.filter((f) => !internalPrefixes.some((p) => f.path.startsWith(p)));
|
|
83
69
|
}
|
|
84
|
-
processedFiles = result.files;
|
|
85
70
|
const existingKeys = new Set((manifest.configVariables ?? []).map((v) => v.key));
|
|
86
71
|
for (const cv of result.configVariables) {
|
|
87
72
|
if (!existingKeys.has(cv.key)) {
|
|
@@ -91,45 +76,124 @@ export const pushCommand = new Command("push")
|
|
|
91
76
|
existingKeys.add(cv.key);
|
|
92
77
|
}
|
|
93
78
|
}
|
|
94
|
-
mkdirSync(
|
|
79
|
+
mkdirSync(distDir, { recursive: true });
|
|
95
80
|
for (const file of result.files) {
|
|
96
|
-
const filePath = join(
|
|
81
|
+
const filePath = join(distDir, file.path);
|
|
97
82
|
mkdirSync(join(filePath, ".."), { recursive: true });
|
|
98
83
|
writeFileSync(filePath, file.content, "utf-8");
|
|
99
84
|
}
|
|
100
|
-
writeFileSync(join(
|
|
85
|
+
writeFileSync(join(distDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
101
86
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
87
|
+
// -----------------------------------------------------------------------
|
|
88
|
+
// Step 2: Resolve agent ID (create if first push)
|
|
89
|
+
// -----------------------------------------------------------------------
|
|
90
|
+
spinner.text = "Resolving agent...";
|
|
91
|
+
let syncState = readSyncState(cwd);
|
|
92
|
+
let agentId = syncState?.agent_id ?? null;
|
|
93
|
+
let isCreated = false;
|
|
94
|
+
if (!agentId) {
|
|
95
|
+
let readme = "";
|
|
96
|
+
const readmePath = join(cwd, "README.md");
|
|
97
|
+
if (existsSync(readmePath)) {
|
|
98
|
+
readme = readFileSync(readmePath, "utf-8");
|
|
99
|
+
}
|
|
109
100
|
const agentResult = await apiPost("/api/agents", {
|
|
110
101
|
slug: manifest.name,
|
|
111
102
|
name: manifest.name,
|
|
112
|
-
description: manifest.description,
|
|
103
|
+
description: manifest.description ?? "",
|
|
113
104
|
readme,
|
|
114
105
|
manifest,
|
|
115
106
|
demo_video_url: manifest.demoVideoUrl,
|
|
116
107
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
108
|
+
agentId = agentResult.agent.id;
|
|
109
|
+
isCreated = !!agentResult.created;
|
|
110
|
+
}
|
|
111
|
+
// -----------------------------------------------------------------------
|
|
112
|
+
// Step 3: Build local snapshot and compute hash
|
|
113
|
+
// -----------------------------------------------------------------------
|
|
114
|
+
spinner.text = "Building snapshot...";
|
|
115
|
+
const snapshot = buildLocalSnapshot(cwd);
|
|
116
|
+
const localHash = computeHashFromSnapshot(snapshot);
|
|
117
|
+
// -----------------------------------------------------------------------
|
|
118
|
+
// Step 4: Compare local hash with last known local hash (unless --force)
|
|
119
|
+
// -----------------------------------------------------------------------
|
|
120
|
+
if (!opts.force && !isCreated && syncState?.last_local_hash) {
|
|
121
|
+
if (localHash === syncState.last_local_hash) {
|
|
122
|
+
spinner.succeed(`${chalk.bold(`@${config.username}/${manifest.name}`)} has no local changes since last sync.`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// -----------------------------------------------------------------------
|
|
127
|
+
// Step 5: Push snapshot
|
|
128
|
+
// -----------------------------------------------------------------------
|
|
129
|
+
spinner.text = `Pushing ${snapshot.files.length} files...`;
|
|
130
|
+
try {
|
|
131
|
+
const pushResult = await apiPost(`/api/agents/${agentId}/sync/push`, snapshot);
|
|
132
|
+
let finalHash = pushResult.hash;
|
|
133
|
+
// -------------------------------------------------------------------
|
|
134
|
+
// Step 6: Upload avatar if present
|
|
135
|
+
// -------------------------------------------------------------------
|
|
136
|
+
const avatarPath = findLocalAvatar(cwd);
|
|
137
|
+
if (avatarPath) {
|
|
138
|
+
spinner.text = "Uploading avatar...";
|
|
139
|
+
const ext = avatarPath.split(".").pop() ?? "png";
|
|
140
|
+
const avatarBuffer = readFileSync(avatarPath);
|
|
141
|
+
const avatarBlob = new Blob([avatarBuffer], {
|
|
142
|
+
type: mimeFromExtension(ext),
|
|
143
|
+
});
|
|
144
|
+
const avatarForm = new FormData();
|
|
145
|
+
avatarForm.append("avatar", avatarBlob, `avatar.${ext}`);
|
|
146
|
+
const avatarResult = await apiFormData(`/api/agents/${agentId}/sync/avatar`, avatarForm);
|
|
147
|
+
finalHash = avatarResult.hash;
|
|
148
|
+
}
|
|
149
|
+
// -------------------------------------------------------------------
|
|
150
|
+
// Step 7: Upload resources if present
|
|
151
|
+
// -------------------------------------------------------------------
|
|
152
|
+
const resourcesMeta = readResourcesMeta(cwd);
|
|
153
|
+
if (resourcesMeta.length > 0) {
|
|
154
|
+
spinner.text = "Uploading resources...";
|
|
155
|
+
const resForm = new FormData();
|
|
156
|
+
const metadataForApi = resourcesMeta.map((meta, i) => ({
|
|
157
|
+
file_key: `resource_${i}`,
|
|
158
|
+
title: meta.title,
|
|
159
|
+
description: meta.description,
|
|
160
|
+
type: meta.type,
|
|
161
|
+
sort_order: meta.sort_order,
|
|
162
|
+
}));
|
|
163
|
+
resForm.append("metadata", JSON.stringify(metadataForApi));
|
|
164
|
+
for (let i = 0; i < resourcesMeta.length; i++) {
|
|
165
|
+
const meta = resourcesMeta[i];
|
|
166
|
+
const resFilePath = join(cwd, ".web42", "resources", meta.file);
|
|
167
|
+
if (existsSync(resFilePath)) {
|
|
168
|
+
const resBuffer = readFileSync(resFilePath);
|
|
169
|
+
const ext = meta.file.split(".").pop() ?? "";
|
|
170
|
+
const blob = new Blob([resBuffer], {
|
|
171
|
+
type: mimeFromExtension(ext),
|
|
172
|
+
});
|
|
173
|
+
resForm.append(`resource_${i}`, blob, meta.file);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const resResult = await apiFormData(`/api/agents/${agentId}/sync/resources`, resForm);
|
|
177
|
+
finalHash = resResult.hash;
|
|
178
|
+
}
|
|
179
|
+
// -------------------------------------------------------------------
|
|
180
|
+
// Step 8: Save sync state
|
|
181
|
+
// -------------------------------------------------------------------
|
|
182
|
+
writeSyncState(cwd, {
|
|
183
|
+
agent_id: agentId,
|
|
184
|
+
last_remote_hash: finalHash,
|
|
185
|
+
last_local_hash: localHash,
|
|
186
|
+
synced_at: new Date().toISOString(),
|
|
187
|
+
});
|
|
188
|
+
spinner.succeed(`Pushed ${chalk.bold(`@${config.username}/${manifest.name}`)} (${snapshot.files.length} files)`);
|
|
189
|
+
if (isCreated) {
|
|
127
190
|
console.log(chalk.green(" New agent created!"));
|
|
128
191
|
}
|
|
129
192
|
else {
|
|
130
193
|
console.log(chalk.green(" Agent updated."));
|
|
131
194
|
}
|
|
132
195
|
console.log(chalk.dim(` View at: ${config.apiUrl ? config.apiUrl.replace("https://", "") : "web42.ai"}/${config.username}/${manifest.name}`));
|
|
196
|
+
console.log(chalk.dim(` Sync hash: ${finalHash.slice(0, 12)}...`));
|
|
133
197
|
}
|
|
134
198
|
catch (error) {
|
|
135
199
|
spinner.fail("Push failed");
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
import { apiGet } from "../utils/api.js";
|
|
7
|
+
import { requireAuth } from "../utils/config.js";
|
|
8
|
+
import { buildLocalSnapshot, computeHashFromSnapshot, readSyncState, } from "../utils/sync.js";
|
|
9
|
+
export const syncCommand = new Command("sync")
|
|
10
|
+
.description("Check sync status between local workspace and the marketplace")
|
|
11
|
+
.action(async () => {
|
|
12
|
+
const config = requireAuth();
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
const manifestPath = join(cwd, "manifest.json");
|
|
15
|
+
if (!existsSync(manifestPath)) {
|
|
16
|
+
console.log(chalk.red("No manifest.json found. Are you in an agent directory?"));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
20
|
+
if (!manifest.name) {
|
|
21
|
+
console.log(chalk.red("manifest.json is missing a name field."));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const spinner = ora("Checking sync status...").start();
|
|
25
|
+
try {
|
|
26
|
+
// Resolve agent ID
|
|
27
|
+
const syncState = readSyncState(cwd);
|
|
28
|
+
let agentId = syncState?.agent_id ?? null;
|
|
29
|
+
if (!agentId) {
|
|
30
|
+
const agents = await apiGet(`/api/agents?username=${config.username}`);
|
|
31
|
+
const agent = agents.find((a) => a.slug === manifest.name);
|
|
32
|
+
if (!agent) {
|
|
33
|
+
spinner.fail(`Agent @${config.username}/${manifest.name} not found on the marketplace. Run \`web42 push\` first.`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
agentId = agent.id;
|
|
37
|
+
}
|
|
38
|
+
// Compute local hash
|
|
39
|
+
const distDir = join(cwd, ".web42", "dist");
|
|
40
|
+
let localHash = null;
|
|
41
|
+
if (existsSync(distDir)) {
|
|
42
|
+
const snapshot = buildLocalSnapshot(cwd);
|
|
43
|
+
localHash = computeHashFromSnapshot(snapshot);
|
|
44
|
+
}
|
|
45
|
+
// Fetch remote hash
|
|
46
|
+
const remote = await apiGet(`/api/agents/${agentId}/sync`);
|
|
47
|
+
const remoteHash = remote.hash;
|
|
48
|
+
const lastRemoteHash = syncState?.last_remote_hash ?? null;
|
|
49
|
+
const lastLocalHash = syncState?.last_local_hash ?? null;
|
|
50
|
+
spinner.stop();
|
|
51
|
+
console.log();
|
|
52
|
+
console.log(chalk.bold(` Sync status for @${config.username}/${manifest.name}`));
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(` Remote hash: ${chalk.cyan(remoteHash.slice(0, 12))}...`);
|
|
55
|
+
if (localHash) {
|
|
56
|
+
console.log(` Local hash: ${chalk.cyan(localHash.slice(0, 12))}...`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log(` Local hash: ${chalk.dim("(not packed yet — run web42 pack first)")}`);
|
|
60
|
+
}
|
|
61
|
+
if (lastRemoteHash) {
|
|
62
|
+
console.log(` Last synced: ${chalk.dim(syncState.synced_at)}`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log(` Last synced: ${chalk.dim("never")}`);
|
|
66
|
+
}
|
|
67
|
+
console.log();
|
|
68
|
+
// Determine sync status by comparing each side against its last-known hash
|
|
69
|
+
if (!lastRemoteHash || !lastLocalHash) {
|
|
70
|
+
console.log(chalk.yellow(" Status: Never synced — run `web42 push` or `web42 pull`"));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const localChanged = localHash !== null && localHash !== lastLocalHash;
|
|
74
|
+
const remoteChanged = remoteHash !== lastRemoteHash;
|
|
75
|
+
if (localChanged && remoteChanged) {
|
|
76
|
+
console.log(chalk.red(" Status: Both local and remote have changed since last sync"));
|
|
77
|
+
console.log(chalk.dim(" Use `web42 push --force` or `web42 pull --force` to resolve"));
|
|
78
|
+
}
|
|
79
|
+
else if (localChanged) {
|
|
80
|
+
console.log(chalk.yellow(" Status: Local changes (push to sync)"));
|
|
81
|
+
console.log(chalk.dim(" Run `web42 push` to update the marketplace"));
|
|
82
|
+
}
|
|
83
|
+
else if (remoteChanged) {
|
|
84
|
+
console.log(chalk.yellow(" Status: Remote changes (pull to sync)"));
|
|
85
|
+
console.log(chalk.dim(" Run `web42 pull` to update local files"));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log(chalk.green(" Status: In sync"));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
spinner.fail("Sync status check failed");
|
|
95
|
+
console.error(chalk.red(error.message));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
});
|