@feralfile/cli 1.1.4 → 1.3.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/README.md +50 -0
- package/dist/index.js +4 -2
- package/dist/package.json +84 -0
- package/dist/src/ai-orchestrator/index.js +14 -2
- package/dist/src/commands/build.js +3 -3
- package/dist/src/commands/chat.js +3 -3
- package/dist/src/commands/config.js +2 -2
- package/dist/src/commands/device.js +6 -6
- package/dist/src/commands/find.js +502 -0
- package/dist/src/commands/helpers/device-discovery.js +1 -1
- package/dist/src/commands/play.js +52 -11
- package/dist/src/commands/setup.js +1 -1
- package/dist/src/commands/status.js +2 -2
- package/dist/src/intent-parser/index.js +16 -5
- package/dist/src/intent-parser/utils.js +7 -2
- package/dist/src/main.js +61 -17
- package/dist/src/utilities/ab-marketplace.js +95 -0
- package/dist/src/utilities/feral-file-artwork.js +89 -0
- package/dist/src/utilities/ff-marketplace.js +115 -0
- package/dist/src/utilities/ff1-device.js +4 -4
- package/dist/src/utilities/functions.js +1 -1
- package/dist/src/utilities/fxhash-marketplace.js +146 -0
- package/dist/src/utilities/index.js +80 -1
- package/dist/src/utilities/marketplace-url.js +373 -0
- package/dist/src/utilities/neort-marketplace.js +102 -0
- package/dist/src/utilities/nft-indexer.js +22 -14
- package/dist/src/utilities/objkt-marketplace.js +77 -0
- package/dist/src/utilities/playlist-publisher.js +9 -42
- package/dist/src/utilities/raster-client.js +252 -0
- package/dist/src/utilities/user-agent.js +20 -0
- package/docs/CONFIGURATION.md +3 -1
- package/docs/EXAMPLES.md +23 -0
- package/docs/FUNCTION_CALLING.md +3 -1
- package/docs/README.md +11 -10
- package/docs/RELEASING.md +92 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,56 @@ ff-cli config init
|
|
|
44
44
|
ff-cli config validate
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
## Find an artwork
|
|
48
|
+
|
|
49
|
+
`ff-cli find` resolves a URL, raw `chain:contract:tokenId`, or a wallet address into a playable DP-1 playlist. The focus is computational and generative art; PFP collections and pre-ERC-721 contracts have limits noted below.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Most satisfying: paste a URL, play it on your FF1
|
|
53
|
+
ff-cli find https://www.artblocks.io/collection/ringers-by-dmitri-cherniak --play
|
|
54
|
+
|
|
55
|
+
# Tezos / hic et nunc via Objkt (the alias resolves to a KT1 contract)
|
|
56
|
+
ff-cli find https://objkt.com/tokens/hicetnunc/111068 --play
|
|
57
|
+
|
|
58
|
+
# Feral File artwork (public id can be hex or numeric — both forms resolve via /api/artworks)
|
|
59
|
+
ff-cli find https://feralfile.com/exhibitions/artwork/f0240e04d64717e319584957f6a83954b029254ad1260b6320472ea8c0c5b1cf --play
|
|
60
|
+
|
|
61
|
+
# Save without playing
|
|
62
|
+
ff-cli find ethereum:0xababababab20053426ad1c782de9ea8444358070:5001410 --output send-receive.json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Sources: Art Blocks, Objkt, fxhash (canonical `/gentk/...`, live `/iteration/{slug}`, and project pages `/project/{slug}` / `/generative/{slug}`), OpenSea, SuperRare, Feral File, Neort (`/art/{id}`), raw on-chain coords, wallet addresses. Run `ff-cli find --help` for the full input list.
|
|
66
|
+
|
|
67
|
+
**Known limitations**
|
|
68
|
+
|
|
69
|
+
- **CryptoPunks (original)** don't index — the contract predates ERC-721 ([ff-indexer-v2#83](https://github.com/feral-file/ff-indexer-v2/issues/83)).
|
|
70
|
+
- **Mainstream PFPs** (Azuki, BAYC, Pudgy Penguins) build a single-item playlist instead of a series — Raster's curation is effectively the series allowlist.
|
|
71
|
+
- **Indexer 502s on burst load** are intermittent ([ff-indexer-v2#82](https://github.com/feral-file/ff-indexer-v2/issues/82)) — retry clears or use `--limit N`.
|
|
72
|
+
|
|
73
|
+
## Use with Claude Code
|
|
74
|
+
|
|
75
|
+
ff-cli ships a [Claude Code](https://docs.claude.com/en/docs/claude-code) skill at `skills/ff-control/SKILL.md`. Install it once:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
git clone --depth=1 https://github.com/feral-file/ff-cli /tmp/ff-cli \
|
|
79
|
+
&& mkdir -p ~/.claude/skills \
|
|
80
|
+
&& cp -r /tmp/ff-cli/skills/ff-control ~/.claude/skills/
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Claude Code will surface it when you ask to build a playlist, play an artwork or URL on an Art Computer, or publish to a feed. The skill validates config, builds, validates the playlist, and sends or publishes — reporting the failing command + exit code if anything breaks. Full prompt and exact flow: [`skills/ff-control/SKILL.md`](./skills/ff-control/SKILL.md).
|
|
84
|
+
|
|
85
|
+
## Use with Codex
|
|
86
|
+
|
|
87
|
+
ff-cli ships the same `ff-control` skill in Codex's native skill format. Install it once:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
git clone --depth=1 https://github.com/feral-file/ff-cli /tmp/ff-cli \
|
|
91
|
+
&& mkdir -p ~/.codex/skills \
|
|
92
|
+
&& cp -r /tmp/ff-cli/skills/ff-control ~/.codex/skills/
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Codex will surface it when you ask to build a playlist, play an artwork or URL on an Art Computer, or publish to a feed. The installed skill is `skills/ff-control/SKILL.md`, so the execution flow stays defined in one place. Codex metadata and exact flow: [`skills/ff-control/agents/openai.yaml`](./skills/ff-control/agents/openai.yaml), [`skills/ff-control/SKILL.md`](./skills/ff-control/SKILL.md).
|
|
96
|
+
|
|
47
97
|
## Dev Quick Start
|
|
48
98
|
|
|
49
99
|
**Set your LLM API key first (default Claude):** `export ANTHROPIC_API_KEY='sk-ant-your-api-key-here'`
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ const validate_1 = require("./src/commands/validate");
|
|
|
19
19
|
const sign_1 = require("./src/commands/sign");
|
|
20
20
|
const play_1 = require("./src/commands/play");
|
|
21
21
|
const publish_1 = require("./src/commands/publish");
|
|
22
|
+
const find_1 = require("./src/commands/find");
|
|
22
23
|
const build_1 = require("./src/commands/build");
|
|
23
24
|
const config_1 = require("./src/commands/config");
|
|
24
25
|
const ssh_1 = require("./src/commands/ssh");
|
|
@@ -32,11 +33,11 @@ try {
|
|
|
32
33
|
catch {
|
|
33
34
|
packageJsonPath = (0, path_1.resolve)((0, path_1.dirname)(__filename), 'package.json');
|
|
34
35
|
}
|
|
35
|
-
const { version: packageVersion } = JSON.parse((0, fs_1.readFileSync)(packageJsonPath, 'utf8'));
|
|
36
|
+
const { version: packageVersion, description: packageDescription } = JSON.parse((0, fs_1.readFileSync)(packageJsonPath, 'utf8'));
|
|
36
37
|
const program = new commander_1.Command();
|
|
37
38
|
program
|
|
38
39
|
.name('ff-cli')
|
|
39
|
-
.description(
|
|
40
|
+
.description(packageDescription)
|
|
40
41
|
.version(packageVersion)
|
|
41
42
|
.addHelpText('after', `\nQuick start:\n 1) ff-cli setup\n 2) ff-cli chat\n\nDocs: https://github.com/feral-file/ff-cli\n`);
|
|
42
43
|
program.addCommand(setup_1.setupCommand);
|
|
@@ -47,6 +48,7 @@ program.addCommand(validate_1.validateCommand);
|
|
|
47
48
|
program.addCommand(sign_1.signCommand);
|
|
48
49
|
program.addCommand(play_1.playCommand);
|
|
49
50
|
program.addCommand(publish_1.publishCommand);
|
|
51
|
+
program.addCommand(find_1.findCommand);
|
|
50
52
|
program.addCommand(build_1.buildCommand);
|
|
51
53
|
program.addCommand(config_1.configCommand);
|
|
52
54
|
program.addCommand(ssh_1.sshCommand);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@feralfile/cli",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "CLI for building DP-1 playlists of digital art using AI (Claude, Grok, ChatGPT, Gemini)",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/feral-file/ff-cli.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/feral-file/ff-cli#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/feral-file/ff-cli/issues"
|
|
12
|
+
},
|
|
13
|
+
"main": "dist/index.js",
|
|
14
|
+
"bin": {
|
|
15
|
+
"ff-cli": "dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"docs",
|
|
20
|
+
"config.json.example",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clean": "node -e \"const fs=require('fs'); fs.rmSync('dist',{ recursive:true, force:true }); fs.rmSync('release',{ recursive:true, force:true });\"",
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"bundle": "node build.js",
|
|
29
|
+
"bundle:dev": "node build.js --dev",
|
|
30
|
+
"release:asset": "./scripts/release/build-asset.sh",
|
|
31
|
+
"prepare": "husky",
|
|
32
|
+
"start": "node dist/index.js",
|
|
33
|
+
"dev": "tsx index.ts",
|
|
34
|
+
"smoke": "npm run build && node dist/index.js validate examples/sample-playlist-v11.json && node dist/index.js config validate",
|
|
35
|
+
"check": "npm run format:check && npm run lint && npm run test",
|
|
36
|
+
"verify": "npm run check && npm run smoke",
|
|
37
|
+
"prepublishOnly": "npm run verify",
|
|
38
|
+
"test": "node scripts/run-tests.js",
|
|
39
|
+
"lint": "eslint .",
|
|
40
|
+
"lint:fix": "eslint . --fix",
|
|
41
|
+
"format": "prettier --write \"**/*.{js,ts}\"",
|
|
42
|
+
"format:check": "prettier --check \"**/*.{js,ts}\""
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=22"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"nft",
|
|
52
|
+
"playlist",
|
|
53
|
+
"dp1",
|
|
54
|
+
"grok",
|
|
55
|
+
"cli"
|
|
56
|
+
],
|
|
57
|
+
"author": "",
|
|
58
|
+
"license": "MIT",
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"axios": "^1.6.2",
|
|
61
|
+
"bonjour-service": "^1.3.0",
|
|
62
|
+
"chalk": "^4.1.2",
|
|
63
|
+
"commander": "^11.1.0",
|
|
64
|
+
"dotenv": "^16.3.1",
|
|
65
|
+
"dp1-js": "2.0.0",
|
|
66
|
+
"ff1": "^1.0.0",
|
|
67
|
+
"fuzzysort": "^3.1.0",
|
|
68
|
+
"openai": "^4.58.1",
|
|
69
|
+
"viem": "^2.38.2"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@types/node": "^24.7.1",
|
|
73
|
+
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
|
74
|
+
"@typescript-eslint/parser": "^8.46.0",
|
|
75
|
+
"esbuild": "^0.25.10",
|
|
76
|
+
"eslint": "^9.36.0",
|
|
77
|
+
"eslint-config-prettier": "^10.1.8",
|
|
78
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
79
|
+
"husky": "^9.1.7",
|
|
80
|
+
"prettier": "^3.6.2",
|
|
81
|
+
"tsx": "^4.20.6",
|
|
82
|
+
"typescript": "^5.9.3"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -116,7 +116,7 @@ const functionSchemas = [
|
|
|
116
116
|
type: 'function',
|
|
117
117
|
function: {
|
|
118
118
|
name: 'query_requirement',
|
|
119
|
-
description: 'Query data for a requirement. Supports build_playlist (blockchain NFTs), query_address (all NFTs from address), and fetch_feed (feed playlists) types.',
|
|
119
|
+
description: 'Query data for a requirement. Supports build_playlist (blockchain NFTs), feral_file_artwork (Feral File artwork IDs/URLs), query_address (all NFTs from address), and fetch_feed (feed playlists) types.',
|
|
120
120
|
parameters: {
|
|
121
121
|
type: 'object',
|
|
122
122
|
properties: {
|
|
@@ -126,7 +126,7 @@ const functionSchemas = [
|
|
|
126
126
|
properties: {
|
|
127
127
|
type: {
|
|
128
128
|
type: 'string',
|
|
129
|
-
enum: ['build_playlist', 'fetch_feed', 'query_address'],
|
|
129
|
+
enum: ['build_playlist', 'feral_file_artwork', 'fetch_feed', 'query_address'],
|
|
130
130
|
description: 'Type of requirement',
|
|
131
131
|
},
|
|
132
132
|
blockchain: {
|
|
@@ -144,6 +144,10 @@ const functionSchemas = [
|
|
|
144
144
|
type: 'string',
|
|
145
145
|
},
|
|
146
146
|
},
|
|
147
|
+
artworkId: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
description: 'Feral File public artwork id or /exhibitions/artwork/{id} URL (REQUIRED for feral_file_artwork)',
|
|
150
|
+
},
|
|
147
151
|
ownerAddress: {
|
|
148
152
|
type: 'string',
|
|
149
153
|
description: 'Owner wallet address (0x... for Ethereum, tz... for Tezos) - REQUIRED for query_address',
|
|
@@ -457,6 +461,9 @@ function buildOrchestratorSystemPrompt(params) {
|
|
|
457
461
|
if (req.type === 'fetch_feed') {
|
|
458
462
|
return `${i + 1}. Fetch ${req.quantity || 5} items from playlist "${req.playlistName}"`;
|
|
459
463
|
}
|
|
464
|
+
else if (req.type === 'feral_file_artwork') {
|
|
465
|
+
return `${i + 1}. Resolve Feral File artwork ${req.artworkId}`;
|
|
466
|
+
}
|
|
460
467
|
else if (req.type === 'query_address') {
|
|
461
468
|
const quantityText = req.quantity === 'all' ? 'all ' : req.quantity ? req.quantity + ' random ' : 'all ';
|
|
462
469
|
return `${i + 1}. Query ${quantityText}tokens from address ${req.ownerAddress}`;
|
|
@@ -512,6 +519,8 @@ DECISION LOOP
|
|
|
512
519
|
1) For each requirement in order:
|
|
513
520
|
- build_playlist: call query_requirement(requirement, duration=${playlistSettings.durationPerItem || 10}).
|
|
514
521
|
Returns array with minimal item data including id field. Collect the id values.
|
|
522
|
+
- feral_file_artwork: call query_requirement(requirement, duration=${playlistSettings.durationPerItem || 10}).
|
|
523
|
+
Returns array with minimal item data including id field. Collect the id values.
|
|
515
524
|
- query_address:
|
|
516
525
|
• if ownerAddress endsWith .eth/.tez → resolve_domains([domain]); if resolved → use returned address; if not → mark failed and continue.
|
|
517
526
|
• if ownerAddress is 0x…/tz… → call query_requirement(requirement, duration=${playlistSettings.durationPerItem || 10}).
|
|
@@ -599,6 +608,9 @@ async function buildPlaylistWithAI(params, options = {}) {
|
|
|
599
608
|
if (req.type === 'fetch_feed') {
|
|
600
609
|
return `${i + 1}. Fetch ${req.quantity || 5} items from playlist "${req.playlistName}"`;
|
|
601
610
|
}
|
|
611
|
+
else if (req.type === 'feral_file_artwork') {
|
|
612
|
+
return `${i + 1}. Resolve Feral File artwork:\n - artworkId: "${req.artworkId}"`;
|
|
613
|
+
}
|
|
602
614
|
else if (req.type === 'query_address') {
|
|
603
615
|
const quantityDesc = req.quantity === 'all'
|
|
604
616
|
? 'all tokens (with pagination)'
|
|
@@ -35,9 +35,9 @@ exports.buildCommand = new commander_1.Command('build')
|
|
|
35
35
|
if (!stdin.trim()) {
|
|
36
36
|
console.error(chalk_1.default.red('No parameters provided'));
|
|
37
37
|
console.log(chalk_1.default.yellow('\nUsage:'));
|
|
38
|
-
console.log('
|
|
39
|
-
console.log(' cat params.json |
|
|
40
|
-
console.log(' echo \'{"requirements":[...]}\' |
|
|
38
|
+
console.log(' ff-cli build params.json');
|
|
39
|
+
console.log(' cat params.json | ff-cli build');
|
|
40
|
+
console.log(' echo \'{"requirements":[...]}\' | ff-cli build');
|
|
41
41
|
process.exit(1);
|
|
42
42
|
}
|
|
43
43
|
params = JSON.parse(stdin);
|
|
@@ -69,12 +69,12 @@ exports.chatCommand = new commander_1.Command('chat')
|
|
|
69
69
|
validation.errors.forEach((error) => {
|
|
70
70
|
console.error(chalk_1.default.red(` • ${error}`));
|
|
71
71
|
});
|
|
72
|
-
console.log(chalk_1.default.yellow('\nRun:
|
|
72
|
+
console.log(chalk_1.default.yellow('\nRun: ff-cli setup\n'));
|
|
73
73
|
process.exit(1);
|
|
74
74
|
}
|
|
75
75
|
// Non-interactive mode: content was passed as a positional argument.
|
|
76
76
|
if (content) {
|
|
77
|
-
console.log(chalk_1.default.blue('\
|
|
77
|
+
console.log(chalk_1.default.blue('\nff-cli chat (non-interactive)\n'));
|
|
78
78
|
console.log(chalk_1.default.dim(`Model: ${modelName}\n`));
|
|
79
79
|
console.log(chalk_1.default.yellow('Request:'), content);
|
|
80
80
|
console.log();
|
|
@@ -103,7 +103,7 @@ exports.chatCommand = new commander_1.Command('chat')
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
// Interactive mode.
|
|
106
|
-
console.log(chalk_1.default.blue('\
|
|
106
|
+
console.log(chalk_1.default.blue('\nff-cli chat\n'));
|
|
107
107
|
console.log(chalk_1.default.dim('Describe the playlist you want. Ctrl+C to exit.'));
|
|
108
108
|
console.log(chalk_1.default.dim(`Model: ${modelName}\n`));
|
|
109
109
|
console.log(chalk_1.default.dim('Examples:'));
|
|
@@ -9,7 +9,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
9
9
|
const config_1 = require("../config");
|
|
10
10
|
// `config` is a single command with an action argument rather than a
|
|
11
11
|
// commander subcommand group. Kept this way to preserve the existing
|
|
12
|
-
// CLI surface (`
|
|
12
|
+
// CLI surface (`ff-cli config init|show|validate`) used in scripts.
|
|
13
13
|
exports.configCommand = new commander_1.Command('config')
|
|
14
14
|
.description('Manage configuration')
|
|
15
15
|
.argument('<action>', 'Action: init, show, or validate')
|
|
@@ -20,7 +20,7 @@ exports.configCommand = new commander_1.Command('config')
|
|
|
20
20
|
const { userPath } = (0, config_1.getConfigPaths)();
|
|
21
21
|
const configPath = await (0, config_1.createSampleConfig)(userPath);
|
|
22
22
|
console.log(chalk_1.default.green(`Created ${configPath}`));
|
|
23
|
-
console.log(chalk_1.default.yellow('\nNext:
|
|
23
|
+
console.log(chalk_1.default.yellow('\nNext: ff-cli setup\n'));
|
|
24
24
|
}
|
|
25
25
|
else if (action === 'show') {
|
|
26
26
|
const config = (0, config_1.getConfig)();
|
|
@@ -24,14 +24,14 @@ deviceCommand
|
|
|
24
24
|
const configPath = await (0, config_files_1.resolveExistingConfigPath)();
|
|
25
25
|
if (!configPath) {
|
|
26
26
|
console.log(chalk_1.default.red('config.json not found'));
|
|
27
|
-
console.log(chalk_1.default.dim('Run:
|
|
27
|
+
console.log(chalk_1.default.dim('Run: ff-cli setup'));
|
|
28
28
|
process.exit(1);
|
|
29
29
|
}
|
|
30
30
|
const config = await (0, config_files_1.readConfigFile)(configPath);
|
|
31
31
|
const devices = config.ff1Devices?.devices || [];
|
|
32
32
|
if (devices.length === 0) {
|
|
33
33
|
console.log(chalk_1.default.yellow('\nNo devices configured'));
|
|
34
|
-
console.log(chalk_1.default.dim('Run:
|
|
34
|
+
console.log(chalk_1.default.dim('Run: ff-cli device add'));
|
|
35
35
|
console.log();
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
@@ -82,7 +82,7 @@ deviceCommand
|
|
|
82
82
|
const configPath = await (0, config_files_1.resolveExistingConfigPath)();
|
|
83
83
|
if (!configPath) {
|
|
84
84
|
console.log(chalk_1.default.red('config.json not found'));
|
|
85
|
-
console.log(chalk_1.default.dim('Run:
|
|
85
|
+
console.log(chalk_1.default.dim('Run: ff-cli setup'));
|
|
86
86
|
process.exit(1);
|
|
87
87
|
}
|
|
88
88
|
const config = await (0, config_files_1.readConfigFile)(configPath);
|
|
@@ -155,7 +155,7 @@ deviceCommand
|
|
|
155
155
|
if (nameConflict) {
|
|
156
156
|
if (options.name) {
|
|
157
157
|
console.error(chalk_1.default.red(`\nError: device name "${deviceName}" is already used by another device (${nameConflict.host}).`));
|
|
158
|
-
console.error(chalk_1.default.dim('Use a different name or run "
|
|
158
|
+
console.error(chalk_1.default.dim('Use a different name or run "ff-cli device remove" first.'));
|
|
159
159
|
closePrompt();
|
|
160
160
|
process.exit(1);
|
|
161
161
|
}
|
|
@@ -241,14 +241,14 @@ deviceCommand
|
|
|
241
241
|
const configPath = await (0, config_files_1.resolveExistingConfigPath)();
|
|
242
242
|
if (!configPath) {
|
|
243
243
|
console.log(chalk_1.default.red('config.json not found'));
|
|
244
|
-
console.log(chalk_1.default.dim('Run:
|
|
244
|
+
console.log(chalk_1.default.dim('Run: ff-cli setup'));
|
|
245
245
|
process.exit(1);
|
|
246
246
|
}
|
|
247
247
|
const config = await (0, config_files_1.readConfigFile)(configPath);
|
|
248
248
|
const existingDevices = config.ff1Devices?.devices || [];
|
|
249
249
|
if (existingDevices.length === 0) {
|
|
250
250
|
console.log(chalk_1.default.yellow('\nNo devices configured'));
|
|
251
|
-
console.log(chalk_1.default.dim('Run:
|
|
251
|
+
console.log(chalk_1.default.dim('Run: ff-cli device add\n'));
|
|
252
252
|
process.exit(1);
|
|
253
253
|
}
|
|
254
254
|
let result;
|