@ic-reactor/cli 0.0.0-dev3 → 0.1.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.
@@ -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,6 @@
1
+ /**
2
+ * Utils barrel export
3
+ */
4
+
5
+ export * from "./config.js"
6
+ export * from "./network.js"
@@ -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
+ }