@ic-reactor/cli 0.0.0-dev3 → 0.2.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/dist/index.js +191 -731
- package/package.json +14 -12
- package/schema.json +24 -2
- package/src/commands/add.ts +447 -0
- package/src/commands/fetch.ts +600 -0
- package/src/commands/index.ts +9 -0
- package/src/commands/init.ts +224 -0
- package/src/commands/list.ts +144 -0
- package/src/commands/sync.ts +259 -0
- package/src/generators/index.ts +8 -0
- package/src/generators/infiniteQuery.ts +34 -0
- package/src/generators/mutation.ts +29 -0
- package/src/generators/query.ts +32 -0
- package/src/generators/reactor.ts +41 -0
- package/src/index.ts +63 -0
- package/src/types.ts +26 -0
- package/src/utils/config.ts +114 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/network.ts +139 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query hook generator
|
|
3
|
+
*
|
|
4
|
+
* Generates createQuery-based hooks for canister query methods.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type MethodInfo,
|
|
9
|
+
type HookType,
|
|
10
|
+
generateQueryHook as generateQueryHookFromCodegen,
|
|
11
|
+
} from "@ic-reactor/codegen"
|
|
12
|
+
import type { ReactorConfig } from "../types.js"
|
|
13
|
+
|
|
14
|
+
export interface QueryHookOptions {
|
|
15
|
+
canisterName: string
|
|
16
|
+
method: MethodInfo
|
|
17
|
+
config: ReactorConfig // Not used by codegen but kept for CLI consistency if needed
|
|
18
|
+
type?: HookType
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate a query hook file content
|
|
23
|
+
*/
|
|
24
|
+
export function generateQueryHook(options: QueryHookOptions): string {
|
|
25
|
+
const { canisterName, method, type } = options
|
|
26
|
+
|
|
27
|
+
return generateQueryHookFromCodegen({
|
|
28
|
+
canisterName,
|
|
29
|
+
method,
|
|
30
|
+
type,
|
|
31
|
+
})
|
|
32
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactor file generator
|
|
3
|
+
*
|
|
4
|
+
* Generates the shared reactor instance file for a canister.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type CanisterConfig,
|
|
9
|
+
generateReactorFile as generateReactorFileFromCodegen,
|
|
10
|
+
} from "@ic-reactor/codegen"
|
|
11
|
+
import type { ReactorConfig } from "../types.js"
|
|
12
|
+
|
|
13
|
+
export interface ReactorGeneratorOptions {
|
|
14
|
+
canisterName: string
|
|
15
|
+
canisterConfig: CanisterConfig
|
|
16
|
+
config: ReactorConfig
|
|
17
|
+
outDir: string
|
|
18
|
+
hasDeclarations?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate the reactor.ts file content
|
|
23
|
+
*/
|
|
24
|
+
export function generateReactorFile(options: ReactorGeneratorOptions): string {
|
|
25
|
+
const {
|
|
26
|
+
canisterName,
|
|
27
|
+
canisterConfig,
|
|
28
|
+
config,
|
|
29
|
+
hasDeclarations = true,
|
|
30
|
+
} = options
|
|
31
|
+
|
|
32
|
+
return generateReactorFileFromCodegen({
|
|
33
|
+
canisterName,
|
|
34
|
+
canisterConfig,
|
|
35
|
+
hasDeclarations,
|
|
36
|
+
globalClientManagerPath: config.clientManagerPath,
|
|
37
|
+
// CLI doesn't currently expose advanced mode per-canister, but we default to false (simple mode)
|
|
38
|
+
// If we want to support it, we'd add 'advanced' to CanisterConfig or ReactorGeneratorOptions
|
|
39
|
+
advanced: false,
|
|
40
|
+
})
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @ic-reactor/cli
|
|
4
|
+
*
|
|
5
|
+
* CLI tool to generate shadcn-style React hooks for ICP canisters.
|
|
6
|
+
* Gives users full control over generated code - no magic, just scaffolding.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Command } from "commander"
|
|
10
|
+
import { initCommand } from "./commands/init.js"
|
|
11
|
+
import { addCommand } from "./commands/add.js"
|
|
12
|
+
import { syncCommand } from "./commands/sync.js"
|
|
13
|
+
import { listCommand } from "./commands/list.js"
|
|
14
|
+
import { fetchCommand } from "./commands/fetch.js"
|
|
15
|
+
import pc from "picocolors"
|
|
16
|
+
|
|
17
|
+
const program = new Command()
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.name("ic-reactor")
|
|
21
|
+
.description(
|
|
22
|
+
pc.cyan("🔧 Generate shadcn-style React hooks for ICP canisters")
|
|
23
|
+
)
|
|
24
|
+
.version("3.0.0")
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.command("init")
|
|
28
|
+
.description("Initialize ic-reactor configuration in your project")
|
|
29
|
+
.option("-y, --yes", "Skip prompts and use defaults")
|
|
30
|
+
.option("-o, --out-dir <path>", "Output directory for generated hooks")
|
|
31
|
+
.action(initCommand)
|
|
32
|
+
|
|
33
|
+
program
|
|
34
|
+
.command("add")
|
|
35
|
+
.description("Add hooks for canister methods (from local .did file)")
|
|
36
|
+
.option("-c, --canister <name>", "Canister name to add hooks for")
|
|
37
|
+
.option("-m, --methods <methods...>", "Method names to generate hooks for")
|
|
38
|
+
.option("-a, --all", "Add hooks for all methods")
|
|
39
|
+
.action(addCommand)
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command("fetch")
|
|
43
|
+
.description("Fetch Candid from a live canister and generate hooks")
|
|
44
|
+
.option("-i, --canister-id <id>", "Canister ID to fetch from")
|
|
45
|
+
.option("-n, --network <network>", "Network: 'ic' or 'local'", "ic")
|
|
46
|
+
.option("--name <name>", "Name for the canister in generated code")
|
|
47
|
+
.option("-m, --methods <methods...>", "Method names to generate hooks for")
|
|
48
|
+
.option("-a, --all", "Add hooks for all methods")
|
|
49
|
+
.action(fetchCommand)
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.command("sync")
|
|
53
|
+
.description("Sync hooks with .did file changes")
|
|
54
|
+
.option("-c, --canister <name>", "Canister to sync")
|
|
55
|
+
.action(syncCommand)
|
|
56
|
+
|
|
57
|
+
program
|
|
58
|
+
.command("list")
|
|
59
|
+
.description("List available methods from a canister")
|
|
60
|
+
.option("-c, --canister <name>", "Canister to list methods from")
|
|
61
|
+
.action(listCommand)
|
|
62
|
+
|
|
63
|
+
program.parse()
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-specific Types
|
|
3
|
+
*
|
|
4
|
+
* Shared types (MethodInfo, CanisterConfig, HookType, GeneratorOptions)
|
|
5
|
+
* are now in @ic-reactor/codegen.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CanisterConfig, HookType } from "@ic-reactor/codegen"
|
|
9
|
+
|
|
10
|
+
export interface HookConfig {
|
|
11
|
+
name: string
|
|
12
|
+
type?: HookType
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ReactorConfig {
|
|
16
|
+
/** Schema version */
|
|
17
|
+
$schema?: string
|
|
18
|
+
/** Output directory for generated files */
|
|
19
|
+
outDir: string
|
|
20
|
+
/** Default path to client manager import (can be overridden per canister) */
|
|
21
|
+
clientManagerPath?: string
|
|
22
|
+
/** Canister configurations */
|
|
23
|
+
canisters: Record<string, CanisterConfig>
|
|
24
|
+
/** Track which hooks have been generated */
|
|
25
|
+
generatedHooks: Record<string, Array<string | HookConfig>>
|
|
26
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration file utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs"
|
|
6
|
+
import path from "node:path"
|
|
7
|
+
import type { ReactorConfig } from "../types.js"
|
|
8
|
+
|
|
9
|
+
export const CONFIG_FILE_NAME = "reactor.config.json"
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_CONFIG: ReactorConfig = {
|
|
12
|
+
$schema:
|
|
13
|
+
"https://raw.githubusercontent.com/B3Pay/ic-reactor/main/packages/cli/schema.json",
|
|
14
|
+
outDir: "src/canisters",
|
|
15
|
+
canisters: {},
|
|
16
|
+
generatedHooks: {},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Find the config file in the current directory or parent directories
|
|
21
|
+
*/
|
|
22
|
+
export function findConfigFile(
|
|
23
|
+
startDir: string = process.cwd()
|
|
24
|
+
): string | null {
|
|
25
|
+
let currentDir = startDir
|
|
26
|
+
|
|
27
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
28
|
+
const configPath = path.join(currentDir, CONFIG_FILE_NAME)
|
|
29
|
+
if (fs.existsSync(configPath)) {
|
|
30
|
+
return configPath
|
|
31
|
+
}
|
|
32
|
+
currentDir = path.dirname(currentDir)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load the reactor config file
|
|
40
|
+
*/
|
|
41
|
+
export function loadConfig(configPath?: string): ReactorConfig | null {
|
|
42
|
+
const filePath = configPath ?? findConfigFile()
|
|
43
|
+
|
|
44
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const content = fs.readFileSync(filePath, "utf-8")
|
|
50
|
+
return JSON.parse(content) as ReactorConfig
|
|
51
|
+
} catch {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Save the reactor config file
|
|
58
|
+
*/
|
|
59
|
+
export function saveConfig(
|
|
60
|
+
config: ReactorConfig,
|
|
61
|
+
configPath: string = path.join(process.cwd(), CONFIG_FILE_NAME)
|
|
62
|
+
): void {
|
|
63
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n")
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the project root directory.
|
|
68
|
+
*
|
|
69
|
+
* Priority:
|
|
70
|
+
* 1. Directory containing reactor.config.json (if found)
|
|
71
|
+
* 2. Current working directory (default for new projects)
|
|
72
|
+
*
|
|
73
|
+
* Note: We intentionally don't traverse up looking for package.json
|
|
74
|
+
* as this can cause issues when running in subdirectories or when
|
|
75
|
+
* parent directories have their own package.json files.
|
|
76
|
+
*/
|
|
77
|
+
export function getProjectRoot(): string {
|
|
78
|
+
// First, check if there's a reactor.config.json in the current directory or parents
|
|
79
|
+
const configPath = findConfigFile()
|
|
80
|
+
if (configPath) {
|
|
81
|
+
return path.dirname(configPath)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Default to current working directory for new projects
|
|
85
|
+
return process.cwd()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Ensure a directory exists
|
|
90
|
+
*/
|
|
91
|
+
export function ensureDir(dirPath: string): void {
|
|
92
|
+
if (!fs.existsSync(dirPath)) {
|
|
93
|
+
fs.mkdirSync(dirPath, { recursive: true })
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a file exists
|
|
99
|
+
*/
|
|
100
|
+
export function fileExists(filePath: string): boolean {
|
|
101
|
+
return fs.existsSync(filePath)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Calculate relative path from one file to another
|
|
106
|
+
*/
|
|
107
|
+
export function getRelativePath(from: string, to: string): string {
|
|
108
|
+
const relativePath = path.relative(path.dirname(from), to)
|
|
109
|
+
// Ensure it starts with ./ or ../
|
|
110
|
+
if (!relativePath.startsWith(".")) {
|
|
111
|
+
return "./" + relativePath
|
|
112
|
+
}
|
|
113
|
+
return relativePath
|
|
114
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IC Network utilities
|
|
3
|
+
*
|
|
4
|
+
* Fetches Candid interfaces from live canisters on the IC network.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { HttpAgent, Actor } from "@icp-sdk/core/agent"
|
|
8
|
+
import { Principal } from "@icp-sdk/core/principal"
|
|
9
|
+
import { IDL } from "@icp-sdk/core/candid"
|
|
10
|
+
import type { MethodInfo } from "@ic-reactor/codegen"
|
|
11
|
+
import { extractMethods } from "@ic-reactor/codegen"
|
|
12
|
+
|
|
13
|
+
// IC mainnet host
|
|
14
|
+
const IC_HOST = "https://icp-api.io"
|
|
15
|
+
|
|
16
|
+
// Local replica host
|
|
17
|
+
const LOCAL_HOST = "http://127.0.0.1:4943"
|
|
18
|
+
|
|
19
|
+
export type NetworkType = "ic" | "local"
|
|
20
|
+
|
|
21
|
+
export interface FetchCandidOptions {
|
|
22
|
+
canisterId: string
|
|
23
|
+
network?: NetworkType
|
|
24
|
+
host?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CandidResult {
|
|
28
|
+
candidSource: string
|
|
29
|
+
methods: MethodInfo[]
|
|
30
|
+
canisterId: string
|
|
31
|
+
network: NetworkType
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fetch Candid interface from a live canister
|
|
36
|
+
*
|
|
37
|
+
* Uses the `__get_candid_interface_tmp_hack` method which is available
|
|
38
|
+
* on most canisters that were compiled with candid export.
|
|
39
|
+
*/
|
|
40
|
+
export async function fetchCandidFromCanister(
|
|
41
|
+
options: FetchCandidOptions
|
|
42
|
+
): Promise<CandidResult> {
|
|
43
|
+
const { canisterId, network = "ic", host } = options
|
|
44
|
+
|
|
45
|
+
// Validate canister ID
|
|
46
|
+
let principal: Principal
|
|
47
|
+
try {
|
|
48
|
+
principal = Principal.fromText(canisterId)
|
|
49
|
+
} catch {
|
|
50
|
+
throw new Error(`Invalid canister ID: ${canisterId}`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Determine host
|
|
54
|
+
const agentHost = host ?? (network === "local" ? LOCAL_HOST : IC_HOST)
|
|
55
|
+
|
|
56
|
+
// Create agent
|
|
57
|
+
const agent = await HttpAgent.create({
|
|
58
|
+
host: agentHost,
|
|
59
|
+
// Don't verify signatures for CLI queries
|
|
60
|
+
verifyQuerySignatures: false,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// For local network, fetch root key (unsafe for mainnet)
|
|
64
|
+
if (network === "local") {
|
|
65
|
+
try {
|
|
66
|
+
await agent.fetchRootKey()
|
|
67
|
+
} catch {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Failed to connect to local replica at ${agentHost}. Is it running?`
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Try to fetch Candid interface using the temporary hack method
|
|
75
|
+
// This method is added by the Rust CDK and Motoko compiler
|
|
76
|
+
const candidInterface = IDL.Service({
|
|
77
|
+
__get_candid_interface_tmp_hack: IDL.Func([], [IDL.Text], ["query"]),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const actor = Actor.createActor(() => candidInterface, {
|
|
81
|
+
agent,
|
|
82
|
+
canisterId: principal,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const candidSource =
|
|
87
|
+
(await actor.__get_candid_interface_tmp_hack()) as string
|
|
88
|
+
|
|
89
|
+
if (!candidSource || candidSource.trim() === "") {
|
|
90
|
+
throw new Error("Canister returned empty Candid interface")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Parse the Candid to extract methods
|
|
94
|
+
const methods = extractMethods(candidSource)
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
candidSource,
|
|
98
|
+
methods,
|
|
99
|
+
canisterId,
|
|
100
|
+
network,
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
104
|
+
|
|
105
|
+
// Check for common errors
|
|
106
|
+
if (message.includes("Replica Error")) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Canister ${canisterId} does not expose a Candid interface. ` +
|
|
109
|
+
`The canister may not support the __get_candid_interface_tmp_hack method.`
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (message.includes("not found") || message.includes("404")) {
|
|
114
|
+
throw new Error(`Canister ${canisterId} not found on ${network} network.`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
throw new Error(`Failed to fetch Candid from canister: ${message}`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validate a canister ID string
|
|
123
|
+
*/
|
|
124
|
+
export function isValidCanisterId(canisterId: string): boolean {
|
|
125
|
+
try {
|
|
126
|
+
Principal.fromText(canisterId)
|
|
127
|
+
return true
|
|
128
|
+
} catch {
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get a shortened display version of a canister ID
|
|
135
|
+
*/
|
|
136
|
+
export function shortenCanisterId(canisterId: string): string {
|
|
137
|
+
if (canisterId.length <= 15) return canisterId
|
|
138
|
+
return `${canisterId.slice(0, 5)}...${canisterId.slice(-5)}`
|
|
139
|
+
}
|