@silicaclaw/cli 1.0.0-beta.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.
- package/ARCHITECTURE.md +137 -0
- package/CHANGELOG.md +411 -0
- package/DEMO_GUIDE.md +89 -0
- package/INSTALL.md +156 -0
- package/README.md +244 -0
- package/RELEASE_NOTES_v1.0.md +65 -0
- package/ROADMAP.md +48 -0
- package/SOCIAL_MD_SPEC.md +122 -0
- package/VERSION +1 -0
- package/apps/local-console/package.json +23 -0
- package/apps/local-console/public/assets/README.md +5 -0
- package/apps/local-console/public/assets/silicaclaw-logo.png +0 -0
- package/apps/local-console/public/index.html +1602 -0
- package/apps/local-console/src/server.ts +1656 -0
- package/apps/local-console/src/socialRoutes.ts +90 -0
- package/apps/local-console/tsconfig.json +7 -0
- package/apps/public-explorer/package.json +20 -0
- package/apps/public-explorer/public/assets/README.md +5 -0
- package/apps/public-explorer/public/assets/silicaclaw-logo.png +0 -0
- package/apps/public-explorer/public/index.html +483 -0
- package/apps/public-explorer/src/server.ts +32 -0
- package/apps/public-explorer/tsconfig.json +7 -0
- package/docs/QUICK_START.md +48 -0
- package/docs/assets/README.md +8 -0
- package/docs/assets/banner.svg +25 -0
- package/docs/assets/silicaclaw-logo.png +0 -0
- package/docs/assets/silicaclaw-og.png +0 -0
- package/docs/release/GITHUB_RELEASE_v1.0-beta.md +143 -0
- package/docs/screenshots/README.md +8 -0
- package/docs/screenshots/v0.3.1-explorer-search.svg +9 -0
- package/docs/screenshots/v0.3.1-machine-a-network.svg +9 -0
- package/docs/screenshots/v0.3.1-machine-b-peers.svg +9 -0
- package/docs/screenshots/v0.3.1-stale-transition.svg +9 -0
- package/openclaw.social.md.example +28 -0
- package/package.json +64 -0
- package/packages/core/package.json +13 -0
- package/packages/core/src/crypto.ts +55 -0
- package/packages/core/src/directory.ts +171 -0
- package/packages/core/src/identity.ts +14 -0
- package/packages/core/src/index.ts +11 -0
- package/packages/core/src/indexing.ts +42 -0
- package/packages/core/src/presence.ts +24 -0
- package/packages/core/src/profile.ts +39 -0
- package/packages/core/src/publicProfileSummary.ts +180 -0
- package/packages/core/src/socialConfig.ts +440 -0
- package/packages/core/src/socialResolver.ts +281 -0
- package/packages/core/src/socialTemplate.ts +97 -0
- package/packages/core/src/types.ts +43 -0
- package/packages/core/tsconfig.json +7 -0
- package/packages/network/package.json +10 -0
- package/packages/network/src/abstractions/messageEnvelope.ts +80 -0
- package/packages/network/src/abstractions/peerDiscovery.ts +49 -0
- package/packages/network/src/abstractions/topicCodec.ts +4 -0
- package/packages/network/src/abstractions/transport.ts +40 -0
- package/packages/network/src/codec/jsonMessageEnvelopeCodec.ts +22 -0
- package/packages/network/src/codec/jsonTopicCodec.ts +11 -0
- package/packages/network/src/discovery/heartbeatPeerDiscovery.ts +173 -0
- package/packages/network/src/index.ts +16 -0
- package/packages/network/src/localEventBus.ts +61 -0
- package/packages/network/src/mock.ts +27 -0
- package/packages/network/src/realPreview.ts +436 -0
- package/packages/network/src/transport/udpLanBroadcastTransport.ts +173 -0
- package/packages/network/src/types.ts +6 -0
- package/packages/network/src/webrtcPreview.ts +1052 -0
- package/packages/network/tsconfig.json +7 -0
- package/packages/storage/package.json +13 -0
- package/packages/storage/src/index.ts +3 -0
- package/packages/storage/src/jsonRepo.ts +25 -0
- package/packages/storage/src/repos.ts +46 -0
- package/packages/storage/src/socialRuntimeRepo.ts +51 -0
- package/packages/storage/tsconfig.json +7 -0
- package/scripts/functional-check.mjs +165 -0
- package/scripts/install-logo.sh +53 -0
- package/scripts/quickstart.sh +144 -0
- package/scripts/silicaclaw-cli.mjs +88 -0
- package/scripts/webrtc-signaling-server.mjs +249 -0
- package/social.md.example +30 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@silicaclaw/storage",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc -p tsconfig.json",
|
|
8
|
+
"check": "tsc -p tsconfig.json --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@silicaclaw/core": "0.1.0"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
|
|
4
|
+
export class JsonFileRepo<T> {
|
|
5
|
+
constructor(
|
|
6
|
+
private filePath: string,
|
|
7
|
+
private fallback: () => T
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
async get(): Promise<T> {
|
|
11
|
+
try {
|
|
12
|
+
const raw = await readFile(this.filePath, "utf8");
|
|
13
|
+
return JSON.parse(raw) as T;
|
|
14
|
+
} catch {
|
|
15
|
+
const seed = this.fallback();
|
|
16
|
+
await this.set(seed);
|
|
17
|
+
return seed;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async set(value: T): Promise<void> {
|
|
22
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
23
|
+
await writeFile(this.filePath, JSON.stringify(value, null, 2), "utf8");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { resolve } from "path";
|
|
2
|
+
import { AgentIdentity, DirectoryState, PublicProfile, createEmptyDirectoryState } from "@silicaclaw/core";
|
|
3
|
+
import { JsonFileRepo } from "./jsonRepo";
|
|
4
|
+
|
|
5
|
+
export type LogEntry = {
|
|
6
|
+
id: string;
|
|
7
|
+
level: "info" | "warn" | "error";
|
|
8
|
+
message: string;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export class IdentityRepo extends JsonFileRepo<AgentIdentity | null> {
|
|
13
|
+
constructor(rootDir = process.cwd()) {
|
|
14
|
+
super(resolve(rootDir, "data", "identity.json"), () => null);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ProfileRepo extends JsonFileRepo<PublicProfile | null> {
|
|
19
|
+
constructor(rootDir = process.cwd()) {
|
|
20
|
+
super(resolve(rootDir, "data", "profile.json"), () => null);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class CacheRepo extends JsonFileRepo<DirectoryState> {
|
|
25
|
+
constructor(rootDir = process.cwd()) {
|
|
26
|
+
super(resolve(rootDir, "data", "cache.json"), () => createEmptyDirectoryState());
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class LogRepo extends JsonFileRepo<LogEntry[]> {
|
|
31
|
+
constructor(rootDir = process.cwd()) {
|
|
32
|
+
super(resolve(rootDir, "data", "logs.json"), () => []);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async append(entry: Omit<LogEntry, "id">): Promise<void> {
|
|
36
|
+
const current = await this.get();
|
|
37
|
+
const next = [
|
|
38
|
+
{
|
|
39
|
+
id: `${entry.timestamp}-${Math.random().toString(36).slice(2, 8)}`,
|
|
40
|
+
...entry,
|
|
41
|
+
},
|
|
42
|
+
...current,
|
|
43
|
+
].slice(0, 50);
|
|
44
|
+
await this.set(next);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { resolve } from "path";
|
|
2
|
+
import { SocialRuntimeConfig } from "@silicaclaw/core";
|
|
3
|
+
import { JsonFileRepo } from "./jsonRepo";
|
|
4
|
+
|
|
5
|
+
function emptyRuntime(): SocialRuntimeConfig {
|
|
6
|
+
return {
|
|
7
|
+
enabled: true,
|
|
8
|
+
public_enabled: false,
|
|
9
|
+
source_path: null,
|
|
10
|
+
last_loaded_at: 0,
|
|
11
|
+
social_found: false,
|
|
12
|
+
parse_error: null,
|
|
13
|
+
resolved_identity: null,
|
|
14
|
+
resolved_profile: null,
|
|
15
|
+
resolved_network: {
|
|
16
|
+
mode: "lan",
|
|
17
|
+
adapter: "real-preview",
|
|
18
|
+
namespace: "silicaclaw.preview",
|
|
19
|
+
port: null,
|
|
20
|
+
signaling_url: "http://localhost:4510",
|
|
21
|
+
signaling_urls: [],
|
|
22
|
+
room: "silicaclaw-room",
|
|
23
|
+
seed_peers: [],
|
|
24
|
+
bootstrap_hints: [],
|
|
25
|
+
bootstrap_sources: [],
|
|
26
|
+
},
|
|
27
|
+
resolved_discovery: {
|
|
28
|
+
discoverable: true,
|
|
29
|
+
allow_profile_broadcast: true,
|
|
30
|
+
allow_presence_broadcast: true,
|
|
31
|
+
},
|
|
32
|
+
visibility: {
|
|
33
|
+
show_display_name: true,
|
|
34
|
+
show_bio: true,
|
|
35
|
+
show_tags: true,
|
|
36
|
+
show_agent_id: true,
|
|
37
|
+
show_last_seen: true,
|
|
38
|
+
show_capabilities_summary: true,
|
|
39
|
+
},
|
|
40
|
+
openclaw: {
|
|
41
|
+
bind_existing_identity: true,
|
|
42
|
+
use_openclaw_profile_if_available: true,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class SocialRuntimeRepo extends JsonFileRepo<SocialRuntimeConfig> {
|
|
48
|
+
constructor(rootDir = process.cwd()) {
|
|
49
|
+
super(resolve(rootDir, ".silicaclaw", "social.runtime.json"), emptyRuntime);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import vm from 'node:vm';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
function assert(condition, message) {
|
|
7
|
+
if (!condition) throw new Error(message);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function checkJson(filePath) {
|
|
11
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
12
|
+
JSON.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function checkInlineScriptSyntax(htmlPath) {
|
|
16
|
+
const html = readFileSync(htmlPath, 'utf8');
|
|
17
|
+
const start = html.indexOf('<script>');
|
|
18
|
+
const end = html.lastIndexOf('</script>');
|
|
19
|
+
assert(start >= 0 && end > start, `Missing inline script in ${htmlPath}`);
|
|
20
|
+
const js = html.slice(start + '<script>'.length, end);
|
|
21
|
+
new vm.Script(js, { filename: htmlPath });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class InMemoryLoopbackTransport {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.handlers = new Set();
|
|
27
|
+
this.started = false;
|
|
28
|
+
}
|
|
29
|
+
async start() { this.started = true; }
|
|
30
|
+
async stop() { this.started = false; }
|
|
31
|
+
async send(data) {
|
|
32
|
+
if (!this.started) return;
|
|
33
|
+
for (const h of this.handlers) {
|
|
34
|
+
h(data, { remote_address: '127.0.0.1', remote_port: 0, transport: 'in-memory' });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
onMessage(handler) {
|
|
38
|
+
this.handlers.add(handler);
|
|
39
|
+
return () => this.handlers.delete(handler);
|
|
40
|
+
}
|
|
41
|
+
inject(data) {
|
|
42
|
+
for (const h of this.handlers) {
|
|
43
|
+
h(data, { remote_address: '192.168.1.88', remote_port: 44123, transport: 'in-memory' });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
const root = process.cwd();
|
|
50
|
+
|
|
51
|
+
const criticalFiles = [
|
|
52
|
+
'README.md',
|
|
53
|
+
'ARCHITECTURE.md',
|
|
54
|
+
'ROADMAP.md',
|
|
55
|
+
'CHANGELOG.md',
|
|
56
|
+
'VERSION',
|
|
57
|
+
'apps/local-console/public/index.html',
|
|
58
|
+
'apps/public-explorer/public/index.html',
|
|
59
|
+
'data/cache.json',
|
|
60
|
+
'data/profile.json',
|
|
61
|
+
'data/identity.json',
|
|
62
|
+
'data/logs.json',
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
for (const rel of criticalFiles) {
|
|
66
|
+
const abs = path.resolve(root, rel);
|
|
67
|
+
assert(existsSync(abs), `Missing required file: ${rel}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// JSON file sanity
|
|
71
|
+
checkJson(path.resolve(root, 'data/cache.json'));
|
|
72
|
+
checkJson(path.resolve(root, 'data/logs.json'));
|
|
73
|
+
checkJson(path.resolve(root, 'data/profile.json'));
|
|
74
|
+
checkJson(path.resolve(root, 'data/identity.json'));
|
|
75
|
+
|
|
76
|
+
// Browser script syntax sanity
|
|
77
|
+
checkInlineScriptSyntax(path.resolve(root, 'apps/local-console/public/index.html'));
|
|
78
|
+
checkInlineScriptSyntax(path.resolve(root, 'apps/public-explorer/public/index.html'));
|
|
79
|
+
|
|
80
|
+
// Import built modules (requires npm run build)
|
|
81
|
+
const core = await import(pathToFileURL(path.resolve(root, 'packages/core/dist/index.js')).href);
|
|
82
|
+
const network = await import(pathToFileURL(path.resolve(root, 'packages/network/dist/index.js')).href);
|
|
83
|
+
|
|
84
|
+
// Core smoke: identity/profile/presence
|
|
85
|
+
const identity = core.createIdentity();
|
|
86
|
+
assert(identity.agent_id && identity.public_key && identity.private_key, 'Identity generation failed');
|
|
87
|
+
|
|
88
|
+
const profile = core.signProfile({
|
|
89
|
+
agent_id: identity.agent_id,
|
|
90
|
+
display_name: 'demo-agent',
|
|
91
|
+
bio: 'smoke',
|
|
92
|
+
tags: ['ai', 'lan'],
|
|
93
|
+
avatar_url: '',
|
|
94
|
+
public_enabled: true,
|
|
95
|
+
}, identity);
|
|
96
|
+
assert(core.verifyProfile(profile, identity.public_key), 'Profile signature verification failed');
|
|
97
|
+
|
|
98
|
+
const presence = core.signPresence(identity);
|
|
99
|
+
assert(core.verifyPresence(presence, identity.public_key), 'Presence signature verification failed');
|
|
100
|
+
|
|
101
|
+
// Core smoke: index + search + TTL
|
|
102
|
+
let state = core.createEmptyDirectoryState();
|
|
103
|
+
state = core.ingestProfileRecord(state, { type: 'profile', profile });
|
|
104
|
+
state = core.ingestPresenceRecord(state, presence);
|
|
105
|
+
const searchHit = core.searchDirectory(state, 'ai');
|
|
106
|
+
assert(searchHit.length >= 1, 'Directory search failed for tag index');
|
|
107
|
+
|
|
108
|
+
const cleaned = core.cleanupExpiredPresence(state, Date.now() + 999_999, 1000);
|
|
109
|
+
assert(cleaned.removed >= 1, 'Presence cleanup failed');
|
|
110
|
+
|
|
111
|
+
// Real preview adapter smoke with in-memory transport
|
|
112
|
+
const transport = new InMemoryLoopbackTransport();
|
|
113
|
+
const adapter = new network.RealNetworkAdapterPreview({
|
|
114
|
+
transport,
|
|
115
|
+
namespace: 'smoke.ns',
|
|
116
|
+
maxMessageBytes: 4 * 1024,
|
|
117
|
+
dedupeWindowMs: 60_000,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
let receivedCount = 0;
|
|
121
|
+
adapter.subscribe('profile', () => {
|
|
122
|
+
receivedCount += 1;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await adapter.start();
|
|
126
|
+
|
|
127
|
+
// Self publish should not deliver (self-message filter)
|
|
128
|
+
await adapter.publish('profile', { hello: 'self' });
|
|
129
|
+
|
|
130
|
+
// Inject remote envelope
|
|
131
|
+
const codec = new network.JsonMessageEnvelopeCodec();
|
|
132
|
+
const remoteEnvelope = {
|
|
133
|
+
version: 1,
|
|
134
|
+
message_id: 'msg-1',
|
|
135
|
+
topic: 'smoke.ns:profile',
|
|
136
|
+
source_peer_id: 'remote-peer-1',
|
|
137
|
+
timestamp: Date.now(),
|
|
138
|
+
payload: { hello: 'remote' },
|
|
139
|
+
};
|
|
140
|
+
transport.inject(codec.encode(remoteEnvelope));
|
|
141
|
+
transport.inject(codec.encode(remoteEnvelope)); // duplicate
|
|
142
|
+
|
|
143
|
+
// Namespace mismatch should be dropped
|
|
144
|
+
transport.inject(codec.encode({ ...remoteEnvelope, message_id: 'msg-2', topic: 'other.ns:profile' }));
|
|
145
|
+
|
|
146
|
+
// Malformed should be dropped
|
|
147
|
+
transport.inject(Buffer.from('{not-json'));
|
|
148
|
+
|
|
149
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
150
|
+
|
|
151
|
+
const diagnostics = adapter.getDiagnostics();
|
|
152
|
+
assert(receivedCount === 1, 'Dedup/self filter behavior unexpected');
|
|
153
|
+
assert(diagnostics.stats.dropped_duplicate >= 1, 'Expected duplicate drop counter');
|
|
154
|
+
assert(diagnostics.stats.dropped_namespace_mismatch >= 1, 'Expected namespace mismatch drop counter');
|
|
155
|
+
assert(diagnostics.stats.dropped_malformed >= 1, 'Expected malformed drop counter');
|
|
156
|
+
|
|
157
|
+
await adapter.stop();
|
|
158
|
+
|
|
159
|
+
console.log('Functional check passed: core + network preview + UI script syntax + data sanity');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
main().catch((error) => {
|
|
163
|
+
console.error('Functional check failed:', error.message);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [ "${1:-}" = "" ]; then
|
|
5
|
+
echo "Usage: npm run logo -- /absolute/path/to/logo.png"
|
|
6
|
+
exit 1
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
SRC="$1"
|
|
10
|
+
if [ ! -f "$SRC" ]; then
|
|
11
|
+
echo "Logo file not found: $SRC"
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
16
|
+
TARGETS=(
|
|
17
|
+
"$ROOT_DIR/apps/local-console/public/assets/silicaclaw-logo.png"
|
|
18
|
+
"$ROOT_DIR/apps/public-explorer/public/assets/silicaclaw-logo.png"
|
|
19
|
+
"$ROOT_DIR/docs/assets/silicaclaw-logo.png"
|
|
20
|
+
)
|
|
21
|
+
OG_TARGET="$ROOT_DIR/docs/assets/silicaclaw-og.png"
|
|
22
|
+
|
|
23
|
+
mkdir -p \
|
|
24
|
+
"$ROOT_DIR/apps/local-console/public/assets" \
|
|
25
|
+
"$ROOT_DIR/apps/public-explorer/public/assets" \
|
|
26
|
+
"$ROOT_DIR/docs/assets"
|
|
27
|
+
|
|
28
|
+
for t in "${TARGETS[@]}"; do
|
|
29
|
+
if [ "$SRC" -ef "$t" ] 2>/dev/null; then
|
|
30
|
+
echo "Skipped (same file) -> $t"
|
|
31
|
+
continue
|
|
32
|
+
fi
|
|
33
|
+
if [ -f "$t" ] && cmp -s "$SRC" "$t"; then
|
|
34
|
+
echo "Skipped (identical content) -> $t"
|
|
35
|
+
continue
|
|
36
|
+
fi
|
|
37
|
+
cp "$SRC" "$t"
|
|
38
|
+
echo "Copied logo -> $t"
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
if command -v sips >/dev/null 2>&1; then
|
|
42
|
+
if sips -z 630 1200 "$SRC" --out "$OG_TARGET" >/dev/null 2>&1; then
|
|
43
|
+
echo "Generated OG image -> $OG_TARGET (1200x630)"
|
|
44
|
+
else
|
|
45
|
+
cp "$SRC" "$OG_TARGET"
|
|
46
|
+
echo "OG resize failed, copied source -> $OG_TARGET"
|
|
47
|
+
fi
|
|
48
|
+
else
|
|
49
|
+
cp "$SRC" "$OG_TARGET"
|
|
50
|
+
echo "sips not found, copied source as OG -> $OG_TARGET"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
echo "Done. Refresh local-console/public-explorer pages to see the new logo + favicon."
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
+
cd "$ROOT_DIR"
|
|
6
|
+
|
|
7
|
+
STEP=1
|
|
8
|
+
|
|
9
|
+
line() {
|
|
10
|
+
printf '\n%s\n' "------------------------------------------------------------"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
title() {
|
|
14
|
+
line
|
|
15
|
+
printf '[Step %s] %s\n' "$STEP" "$1"
|
|
16
|
+
STEP=$((STEP + 1))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
run_cmd() {
|
|
20
|
+
local cmd="$1"
|
|
21
|
+
printf '→ %s\n' "$cmd"
|
|
22
|
+
eval "$cmd"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
ask_yes_no() {
|
|
26
|
+
local prompt="$1"
|
|
27
|
+
local default="${2:-Y}"
|
|
28
|
+
local answer
|
|
29
|
+
if [ "$default" = "Y" ]; then
|
|
30
|
+
read -r -p "$prompt [Y/n]: " answer || true
|
|
31
|
+
answer="${answer:-Y}"
|
|
32
|
+
else
|
|
33
|
+
read -r -p "$prompt [y/N]: " answer || true
|
|
34
|
+
answer="${answer:-N}"
|
|
35
|
+
fi
|
|
36
|
+
case "$answer" in
|
|
37
|
+
Y|y) return 0 ;;
|
|
38
|
+
*) return 1 ;;
|
|
39
|
+
esac
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pause_continue() {
|
|
43
|
+
read -r -p "按回车继续..." _ || true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
title "SilicaClaw Quick Start 启动"
|
|
47
|
+
echo "目录: $ROOT_DIR"
|
|
48
|
+
echo "目标: 用终端一步步完成安装与启动(类似 OpenClaw Quick Start)"
|
|
49
|
+
pause_continue
|
|
50
|
+
|
|
51
|
+
title "检查 Node.js / npm"
|
|
52
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
53
|
+
echo "未找到 node,请先安装 Node.js 18+"
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
57
|
+
echo "未找到 npm,请先安装 npm 9+"
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
NODE_VER="$(node -p "process.versions.node")"
|
|
62
|
+
NPM_VER="$(npm -v)"
|
|
63
|
+
echo "node: $NODE_VER"
|
|
64
|
+
echo "npm : $NPM_VER"
|
|
65
|
+
|
|
66
|
+
if ! node -e "const v=process.versions.node.split('.').map(Number); if (v[0] < 18) process.exit(1)"; then
|
|
67
|
+
echo "Node.js 版本过低,请升级到 18+"
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
title "安装依赖"
|
|
72
|
+
if [ ! -d "$ROOT_DIR/node_modules" ]; then
|
|
73
|
+
run_cmd "npm install"
|
|
74
|
+
else
|
|
75
|
+
if ask_yes_no "检测到 node_modules,是否仍执行 npm install 同步依赖?" "N"; then
|
|
76
|
+
run_cmd "npm install"
|
|
77
|
+
else
|
|
78
|
+
echo "跳过 npm install"
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
title "准备 social.md"
|
|
83
|
+
if [ -f "$ROOT_DIR/social.md" ]; then
|
|
84
|
+
echo "已存在 social.md,保留现有配置。"
|
|
85
|
+
else
|
|
86
|
+
if ask_yes_no "未找到 social.md,是否自动从 social.md.example 生成?" "Y"; then
|
|
87
|
+
run_cmd "cp social.md.example social.md"
|
|
88
|
+
echo "已生成: $ROOT_DIR/social.md"
|
|
89
|
+
else
|
|
90
|
+
echo "跳过生成 social.md(local-console 首启也会自动生成最小模板)"
|
|
91
|
+
fi
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
title "选择网络模式"
|
|
95
|
+
echo "1) local 单机预览(最快)"
|
|
96
|
+
echo "2) lan 局域网预览(A/B 双机)"
|
|
97
|
+
echo "3) global-preview 非局域网预览(WebRTC)"
|
|
98
|
+
read -r -p "请输入模式编号 [1/2/3] (默认 1): " MODE_PICK || true
|
|
99
|
+
MODE_PICK="${MODE_PICK:-1}"
|
|
100
|
+
|
|
101
|
+
NETWORK_MODE="local"
|
|
102
|
+
NETWORK_ADAPTER="local-event-bus"
|
|
103
|
+
WEBRTC_SIGNALING_URL_VALUE=""
|
|
104
|
+
WEBRTC_ROOM_VALUE="silicaclaw-demo"
|
|
105
|
+
|
|
106
|
+
case "$MODE_PICK" in
|
|
107
|
+
2)
|
|
108
|
+
NETWORK_MODE="lan"
|
|
109
|
+
NETWORK_ADAPTER="real-preview"
|
|
110
|
+
;;
|
|
111
|
+
3)
|
|
112
|
+
NETWORK_MODE="global-preview"
|
|
113
|
+
NETWORK_ADAPTER="webrtc-preview"
|
|
114
|
+
read -r -p "请输入 signaling URL(例如 http://your-server:4510): " WEBRTC_SIGNALING_URL_VALUE
|
|
115
|
+
if [ -z "${WEBRTC_SIGNALING_URL_VALUE:-}" ]; then
|
|
116
|
+
echo "global-preview 必须提供 signaling URL"
|
|
117
|
+
exit 1
|
|
118
|
+
fi
|
|
119
|
+
read -r -p "请输入 room(默认 silicaclaw-demo): " WEBRTC_ROOM_VALUE_INPUT || true
|
|
120
|
+
WEBRTC_ROOM_VALUE="${WEBRTC_ROOM_VALUE_INPUT:-silicaclaw-demo}"
|
|
121
|
+
;;
|
|
122
|
+
*)
|
|
123
|
+
NETWORK_MODE="local"
|
|
124
|
+
NETWORK_ADAPTER="local-event-bus"
|
|
125
|
+
;;
|
|
126
|
+
esac
|
|
127
|
+
|
|
128
|
+
echo "已选择: $NETWORK_MODE ($NETWORK_ADAPTER)"
|
|
129
|
+
|
|
130
|
+
title "启动 local-console"
|
|
131
|
+
if [ "$NETWORK_MODE" = "global-preview" ]; then
|
|
132
|
+
echo "将使用以下参数启动:"
|
|
133
|
+
echo "NETWORK_ADAPTER=$NETWORK_ADAPTER"
|
|
134
|
+
echo "WEBRTC_SIGNALING_URL=$WEBRTC_SIGNALING_URL_VALUE"
|
|
135
|
+
echo "WEBRTC_ROOM=$WEBRTC_ROOM_VALUE"
|
|
136
|
+
pause_continue
|
|
137
|
+
run_cmd "NETWORK_ADAPTER=$NETWORK_ADAPTER WEBRTC_SIGNALING_URL=$WEBRTC_SIGNALING_URL_VALUE WEBRTC_ROOM=$WEBRTC_ROOM_VALUE npm run local-console"
|
|
138
|
+
else
|
|
139
|
+
echo "将使用以下参数启动:"
|
|
140
|
+
echo "NETWORK_ADAPTER=$NETWORK_ADAPTER"
|
|
141
|
+
pause_continue
|
|
142
|
+
run_cmd "NETWORK_ADAPTER=$NETWORK_ADAPTER npm run local-console"
|
|
143
|
+
fi
|
|
144
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
const ROOT_DIR = resolve(__dirname, "..");
|
|
11
|
+
|
|
12
|
+
function run(cmd, args, extra = {}) {
|
|
13
|
+
const result = spawnSync(cmd, args, {
|
|
14
|
+
cwd: ROOT_DIR,
|
|
15
|
+
stdio: "inherit",
|
|
16
|
+
env: process.env,
|
|
17
|
+
...extra,
|
|
18
|
+
});
|
|
19
|
+
if (result.error) {
|
|
20
|
+
console.error(`[silicaclaw] failed to run ${cmd}: ${result.error.message}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
process.exit(result.status ?? 0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readVersion() {
|
|
27
|
+
const versionFile = resolve(ROOT_DIR, "VERSION");
|
|
28
|
+
if (!existsSync(versionFile)) return "unknown";
|
|
29
|
+
return readFileSync(versionFile, "utf8").trim() || "unknown";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function help() {
|
|
33
|
+
console.log(`
|
|
34
|
+
SilicaClaw CLI
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
silicaclaw onboard
|
|
38
|
+
silicaclaw local-console
|
|
39
|
+
silicaclaw explorer
|
|
40
|
+
silicaclaw signaling
|
|
41
|
+
silicaclaw doctor
|
|
42
|
+
silicaclaw version
|
|
43
|
+
silicaclaw help
|
|
44
|
+
|
|
45
|
+
Commands:
|
|
46
|
+
onboard Interactive step-by-step onboarding (recommended)
|
|
47
|
+
local-console Start local console (http://localhost:4310)
|
|
48
|
+
explorer Start public explorer (http://localhost:4311)
|
|
49
|
+
signaling Start WebRTC signaling preview server
|
|
50
|
+
doctor Run project checks (npm run health)
|
|
51
|
+
version Print SilicaClaw version
|
|
52
|
+
help Show this help
|
|
53
|
+
`.trim());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const cmd = String(process.argv[2] || "help").trim().toLowerCase();
|
|
57
|
+
|
|
58
|
+
switch (cmd) {
|
|
59
|
+
case "onboard":
|
|
60
|
+
run("bash", [resolve(ROOT_DIR, "scripts", "quickstart.sh")]);
|
|
61
|
+
break;
|
|
62
|
+
case "local-console":
|
|
63
|
+
case "console":
|
|
64
|
+
run("npm", ["run", "local-console"]);
|
|
65
|
+
break;
|
|
66
|
+
case "explorer":
|
|
67
|
+
case "public-explorer":
|
|
68
|
+
run("npm", ["run", "public-explorer"]);
|
|
69
|
+
break;
|
|
70
|
+
case "signaling":
|
|
71
|
+
case "webrtc-signaling":
|
|
72
|
+
run("npm", ["run", "webrtc-signaling"]);
|
|
73
|
+
break;
|
|
74
|
+
case "doctor":
|
|
75
|
+
run("npm", ["run", "health"]);
|
|
76
|
+
break;
|
|
77
|
+
case "version":
|
|
78
|
+
case "-v":
|
|
79
|
+
case "--version":
|
|
80
|
+
console.log(readVersion());
|
|
81
|
+
break;
|
|
82
|
+
case "help":
|
|
83
|
+
case "-h":
|
|
84
|
+
case "--help":
|
|
85
|
+
default:
|
|
86
|
+
help();
|
|
87
|
+
break;
|
|
88
|
+
}
|