@ic-reactor/cli 0.4.1 → 0.5.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/schema.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "title": "IC Reactor CLI Configuration",
4
- "description": "Configuration file for @ic-reactor/cli",
3
+ "title": "IC Reactor Configuration",
4
+ "description": "Configuration file for @ic-reactor/cli and @ic-reactor/vite-plugin",
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "$schema": {
@@ -10,8 +10,13 @@
10
10
  },
11
11
  "outDir": {
12
12
  "type": "string",
13
- "description": "Output directory for generated hooks (relative to project root)",
14
- "default": "src/canisters"
13
+ "description": "Default output directory for generated files (relative to project root)",
14
+ "default": "src/declarations"
15
+ },
16
+ "clientManagerPath": {
17
+ "type": "string",
18
+ "description": "Default import path for the client manager (relative from generated files)",
19
+ "default": "../../clients"
15
20
  },
16
21
  "canisters": {
17
22
  "type": "object",
@@ -19,58 +24,28 @@
19
24
  "additionalProperties": {
20
25
  "type": "object",
21
26
  "properties": {
27
+ "name": {
28
+ "type": "string",
29
+ "description": "Canister name (required)"
30
+ },
22
31
  "didFile": {
23
32
  "type": "string",
24
- "description": "Path to the .did file (relative to project root)"
33
+ "description": "Path to the .did file (required)"
25
34
  },
26
- "clientManagerPath": {
35
+ "outDir": {
27
36
  "type": "string",
28
- "description": "Import path to the client manager (relative from generated hooks)",
29
- "default": "../../lib/client"
37
+ "description": "Override output directory for this canister"
30
38
  },
31
- "useDisplayReactor": {
32
- "type": "boolean",
33
- "description": "Use DisplayReactor for automatic type transformations (bigint → string, etc.)",
34
- "default": true
39
+ "clientManagerPath": {
40
+ "type": "string",
41
+ "description": "Override client manager import path for this canister"
35
42
  },
36
43
  "canisterId": {
37
44
  "type": "string",
38
- "description": "Optional fixed canister ID (defaults to environment-based resolution)"
45
+ "description": "Optional fixed canister ID"
39
46
  }
40
47
  },
41
- "required": ["didFile"]
42
- }
43
- },
44
- "generatedHooks": {
45
- "type": "object",
46
- "description": "Tracks which methods have generated hooks (managed by CLI)",
47
- "additionalProperties": {
48
- "items": {
49
- "anyOf": [
50
- {
51
- "type": "string"
52
- },
53
- {
54
- "type": "object",
55
- "properties": {
56
- "name": {
57
- "type": "string"
58
- },
59
- "type": {
60
- "type": "string",
61
- "enum": [
62
- "query",
63
- "mutation",
64
- "suspenseQuery",
65
- "infiniteQuery",
66
- "suspenseInfiniteQuery"
67
- ]
68
- }
69
- },
70
- "required": ["name"]
71
- }
72
- ]
73
- }
48
+ "required": ["name", "didFile"]
74
49
  }
75
50
  }
76
51
  },
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Generate Command
3
+ *
4
+ * Runs the codegen pipeline for configured canisters.
5
+ */
6
+
7
+ import * as p from "@clack/prompts"
8
+ import pc from "picocolors"
9
+ import { loadConfig, findConfigFile, getProjectRoot } from "../utils/config.js"
10
+ import { runCanisterPipeline } from "@ic-reactor/codegen"
11
+ import type { GenerateOptions } from "../types.js"
12
+
13
+ export async function generateCommand(options: GenerateOptions) {
14
+ console.log()
15
+ p.intro(pc.cyan("🔄 Generate Hooks"))
16
+
17
+ // Load config
18
+ const configPath = findConfigFile()
19
+ if (!configPath) {
20
+ p.log.error(
21
+ `No ${pc.yellow("ic-reactor.json")} found. Run ${pc.cyan("npx ic-reactor init")} first.`
22
+ )
23
+ process.exit(1)
24
+ }
25
+
26
+ const config = loadConfig(configPath)
27
+ if (!config) {
28
+ p.log.error(`Failed to load config from ${pc.yellow(configPath)}`)
29
+ process.exit(1)
30
+ }
31
+
32
+ const projectRoot = getProjectRoot()
33
+ const canisterNames = Object.keys(config.canisters)
34
+
35
+ if (canisterNames.length === 0) {
36
+ p.log.error("No canisters configured.")
37
+ process.exit(1)
38
+ }
39
+
40
+ // Determine which canisters to process
41
+ let canistersToProcess: string[] = []
42
+
43
+ if (options.canister) {
44
+ if (!config.canisters[options.canister]) {
45
+ p.log.error(
46
+ `Canister ${pc.yellow(options.canister)} not found in config.`
47
+ )
48
+ process.exit(1)
49
+ }
50
+ canistersToProcess = [options.canister]
51
+ } else {
52
+ canistersToProcess = canisterNames
53
+ }
54
+
55
+ const spinner = p.spinner()
56
+ spinner.start(
57
+ `Generating hooks for ${canistersToProcess.length} canisters...`
58
+ )
59
+
60
+ let successCount = 0
61
+ let errorCount = 0
62
+ const errorMessages: string[] = []
63
+
64
+ // Run pipeline for each canister
65
+ for (const name of canistersToProcess) {
66
+ const canisterConfig = config.canisters[name]
67
+
68
+ spinner.message(`Processing ${pc.cyan(name)}...`)
69
+
70
+ try {
71
+ const result = await runCanisterPipeline({
72
+ canisterConfig,
73
+ projectRoot,
74
+ globalConfig: config,
75
+ })
76
+
77
+ if (result.success) {
78
+ successCount++
79
+ } else {
80
+ errorCount++
81
+ errorMessages.push(`${name}: ${result.error}`)
82
+ }
83
+ } catch (err) {
84
+ errorCount++
85
+ errorMessages.push(
86
+ `${name}: ${err instanceof Error ? err.message : String(err)}`
87
+ )
88
+ }
89
+ }
90
+
91
+ spinner.stop("Generation complete")
92
+
93
+ if (errorMessages.length > 0) {
94
+ console.log()
95
+ p.log.error("Errors encountered:")
96
+ for (const msg of errorMessages) {
97
+ console.log(` ${pc.red("•")} ${msg}`)
98
+ }
99
+ }
100
+
101
+ console.log()
102
+ p.note(
103
+ `Success: ${pc.green(successCount.toString())}\n` +
104
+ `Failed: ${pc.red(errorCount.toString())}`,
105
+ "Summary"
106
+ )
107
+
108
+ if (errorCount > 0) {
109
+ p.outro(pc.red("✖ Generation failed with errors."))
110
+ process.exit(1)
111
+ } else {
112
+ p.outro(pc.green("✓ All hooks generated successfully!"))
113
+ }
114
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Init command
2
+ * Init Command
3
3
  *
4
- * Initializes ic-reactor configuration in the project.
4
+ * Initializes ic-reactor.json configuration.
5
5
  */
6
6
 
7
7
  import * as p from "@clack/prompts"
@@ -16,7 +16,8 @@ import {
16
16
  getProjectRoot,
17
17
  ensureDir,
18
18
  } from "../utils/config.js"
19
- import type { ReactorConfig, CanisterConfig } from "../types.js"
19
+ import type { CodegenConfig, CanisterConfig } from "../types.js"
20
+ import { generateClientFile } from "@ic-reactor/codegen"
20
21
 
21
22
  interface InitOptions {
22
23
  yes?: boolean
@@ -42,183 +43,115 @@ export async function initCommand(options: InitOptions) {
42
43
  }
43
44
 
44
45
  const projectRoot = getProjectRoot()
45
- let config: ReactorConfig
46
+ let config: CodegenConfig
46
47
 
47
48
  if (options.yes) {
48
- // Use defaults
49
49
  config = { ...DEFAULT_CONFIG }
50
50
  if (options.outDir) {
51
51
  config.outDir = options.outDir
52
52
  }
53
53
  } else {
54
- // Interactive mode
54
+ // Interactive Setup
55
+
56
+ // Output Directory
55
57
  const outDir = await p.text({
56
- message: "Where should generated hooks be placed?",
57
- placeholder: "src/lib/canisters",
58
- defaultValue: "src/lib/canisters",
59
- validate: (value) => {
60
- if (!value) return "Output directory is required"
61
- return undefined
62
- },
58
+ message: "Where should generated files be placed?",
59
+ placeholder: "src/declarations",
60
+ defaultValue: "src/declarations",
63
61
  })
62
+ if (p.isCancel(outDir)) process.exit(0)
64
63
 
65
- if (p.isCancel(outDir)) {
66
- p.cancel("Setup cancelled.")
67
- process.exit(0)
68
- }
69
-
70
- // Ask if they want to add a canister now
71
- const addCanister = await p.confirm({
72
- message: "Would you like to add a canister now?",
73
- initialValue: true,
64
+ // Client Manager Path
65
+ const clientManagerPath = await p.text({
66
+ message: "Relative path for the client manager import?",
67
+ placeholder: "../../clients",
68
+ defaultValue: "../../clients",
74
69
  })
75
-
76
- if (p.isCancel(addCanister)) {
77
- p.cancel("Setup cancelled.")
78
- process.exit(0)
79
- }
70
+ if (p.isCancel(clientManagerPath)) process.exit(0)
80
71
 
81
72
  config = {
82
73
  ...DEFAULT_CONFIG,
83
74
  outDir: outDir as string,
75
+ clientManagerPath: clientManagerPath as string,
84
76
  }
85
77
 
78
+ // Add initial canister?
79
+ const addCanister = await p.confirm({
80
+ message: "Would you like to configure a canister now?",
81
+ initialValue: true,
82
+ })
83
+ if (p.isCancel(addCanister)) process.exit(0)
84
+
86
85
  if (addCanister) {
87
- const canisterInfo = await promptForCanister(projectRoot)
88
- if (canisterInfo) {
89
- config.canisters[canisterInfo.name] = canisterInfo.config
86
+ const canister = await promptForCanister(projectRoot)
87
+ if (canister) {
88
+ config.canisters[canister.name] = canister
90
89
  }
91
90
  }
92
91
  }
93
92
 
94
- // Save config file
93
+ // Save config
95
94
  const configPath = path.join(projectRoot, CONFIG_FILE_NAME)
96
95
  saveConfig(config, configPath)
97
96
 
98
- // Create output directory
99
- const fullOutDir = path.join(projectRoot, config.outDir)
100
- ensureDir(fullOutDir)
97
+ // Ensure directories exist
98
+ ensureDir(path.join(projectRoot, config.outDir))
99
+
100
+ // Create default Client Manager
101
+ const clientManagerFile = path.join(projectRoot, "src/clients.ts")
102
+ // Simple heuristic: if clientManagerPath is "../../clients", likely file is src/clients.ts
103
+ // Users can move it, but this gives a good start.
101
104
 
102
- // Create a sample client manager if it doesn't exist
103
- const clientManagerPath = path.join(projectRoot, "src/lib/clients.ts")
104
- if (!fs.existsSync(clientManagerPath)) {
105
- const createClient = await p.confirm({
106
- message: "Create a sample client manager at src/lib/clients.ts?",
105
+ if (!fs.existsSync(clientManagerFile)) {
106
+ const createHelpers = await p.confirm({
107
+ message: `Create a default client manager at ${pc.green("src/clients.ts")}?`,
107
108
  initialValue: true,
108
109
  })
109
110
 
110
- if (!p.isCancel(createClient) && createClient) {
111
- ensureDir(path.dirname(clientManagerPath))
112
- fs.writeFileSync(clientManagerPath, getClientManagerTemplate())
113
- p.log.success(`Created ${pc.green("src/lib/clients.ts")}`)
111
+ if (createHelpers === true) {
112
+ ensureDir(path.dirname(clientManagerFile))
113
+ fs.writeFileSync(clientManagerFile, generateClientFile())
114
+ p.log.success(`Created ${pc.green("src/clients.ts")}`)
114
115
  }
115
116
  }
116
117
 
117
118
  p.log.success(`Created ${pc.green(CONFIG_FILE_NAME)}`)
118
- p.log.success(`Created ${pc.green(config.outDir)} directory`)
119
119
 
120
120
  console.log()
121
121
  p.note(
122
- `Next steps:
123
-
124
- 1. ${pc.cyan("Add a canister:")}
125
- ${pc.dim("npx @ic-reactor/cli add")}
126
-
127
- 2. ${pc.cyan("List available methods:")}
128
- ${pc.dim("npx @ic-reactor/cli list -c <canister-name>")}
129
-
130
- 3. ${pc.cyan("Add hooks for specific methods:")}
131
- ${pc.dim("npx @ic-reactor/cli add -c <canister> -m <method>")}`,
132
- "Getting Started"
122
+ `To generate hooks, run:\n${pc.cyan("npx ic-reactor generate")}`,
123
+ "Next Steps"
133
124
  )
134
125
 
135
- p.outro(pc.green("✓ ic-reactor initialized successfully!"))
126
+ p.outro(pc.green("✓ Setup complete!"))
136
127
  }
137
128
 
138
129
  async function promptForCanister(
139
130
  projectRoot: string
140
- ): Promise<{ name: string; config: CanisterConfig } | null> {
131
+ ): Promise<CanisterConfig | null> {
141
132
  const name = await p.text({
142
133
  message: "Canister name",
143
134
  placeholder: "backend",
144
- validate: (value) => {
145
- if (!value) return "Canister name is required"
146
- if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(value)) {
147
- return "Canister name must start with a letter and contain only letters, numbers, hyphens, and underscores"
148
- }
149
- return undefined
135
+ validate: (val) => {
136
+ if (!val) return "Name is required"
137
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(val)) return "Invalid name format"
150
138
  },
151
139
  })
152
-
153
140
  if (p.isCancel(name)) return null
154
141
 
155
142
  const didFile = await p.text({
156
143
  message: "Path to .did file",
157
- placeholder: "./backend.did",
158
- validate: (value) => {
159
- if (!value) return "DID file path is required"
160
- const fullPath = path.resolve(projectRoot, value)
161
- if (!fs.existsSync(fullPath)) {
162
- return `File not found: ${value}`
163
- }
164
- return undefined
144
+ placeholder: "./src/backend/backend.did",
145
+ validate: (val) => {
146
+ if (!val) return "Path is required"
147
+ const fullPath = path.resolve(projectRoot, val)
148
+ if (!fs.existsSync(fullPath)) return `File not found: ${val}`
165
149
  },
166
150
  })
167
-
168
151
  if (p.isCancel(didFile)) return null
169
152
 
170
- const clientManagerPath = await p.text({
171
- message:
172
- "Import path to your client manager (relative from generated hooks)",
173
- placeholder: "../../clients",
174
- defaultValue: "../../clients",
175
- })
176
-
177
- if (p.isCancel(clientManagerPath)) return null
178
-
179
- const useDisplayReactor = await p.confirm({
180
- message:
181
- "Use DisplayReactor? (auto-converts bigint → string, Principal → string)",
182
- initialValue: true,
183
- })
184
-
185
- if (p.isCancel(useDisplayReactor)) return null
186
-
187
153
  return {
188
154
  name: name as string,
189
- config: {
190
- didFile: didFile as string,
191
- clientManagerPath: clientManagerPath as string,
192
- useDisplayReactor: useDisplayReactor as boolean,
193
- },
155
+ didFile: didFile as string,
194
156
  }
195
157
  }
196
-
197
- function getClientManagerTemplate(): string {
198
- return `/**
199
- * IC Client Manager
200
- *
201
- * This file configures the IC agent and client manager for your application.
202
- * Customize the agent options based on your environment.
203
- */
204
-
205
- import { ClientManager } from "@ic-reactor/react"
206
-
207
- /**
208
- * The client manager handles agent lifecycle and authentication.
209
- *
210
- * Configuration options:
211
- * - host: IC network host (defaults to process env or mainnet)
212
- * - identity: Initial identity (optional, can be set later)
213
- * - verifyQuerySignatures: Verify query signatures (recommended for production)
214
- *
215
- * For local development, the agent will automatically detect local replica.
216
- */
217
- export const clientManager = new ClientManager({
218
- // Uncomment for explicit host configuration:
219
- // host: process.env.DFX_NETWORK === "local"
220
- // ? "http://localhost:4943"
221
- // : "https://icp-api.io",
222
- })
223
- `
224
- }
package/src/index.ts CHANGED
@@ -2,42 +2,35 @@
2
2
  /**
3
3
  * @ic-reactor/cli
4
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.
5
+ * CLI tool to generate type-safe React hooks for ICP canisters.
7
6
  */
8
7
 
9
8
  import { Command } from "commander"
10
9
  import { initCommand } from "./commands/init.js"
11
- import { syncCommand } from "./commands/sync.js"
12
- import { listCommand } from "./commands/list.js"
10
+ import { generateCommand } from "./commands/generate.js"
13
11
  import pc from "picocolors"
12
+ import { version } from "../package.json"
14
13
 
15
14
  const program = new Command()
16
15
 
17
16
  program
18
17
  .name("ic-reactor")
19
- .description(
20
- pc.cyan("🔧 Generate shadcn-style React hooks for ICP canisters")
21
- )
22
- .version("3.0.0")
18
+ .description(pc.cyan("🔧 Generate type-safe React hooks for ICP canisters"))
19
+ .version(version)
23
20
 
24
21
  program
25
22
  .command("init")
26
23
  .description("Initialize ic-reactor configuration in your project")
27
24
  .option("-y, --yes", "Skip prompts and use defaults")
28
- .option("-o, --out-dir <path>", "Output directory for generated hooks")
25
+ .option("-o, --out-dir <path>", "Output directory for generated files")
29
26
  .action(initCommand)
30
27
 
31
28
  program
32
- .command("sync")
33
- .description("Sync hooks with .did file changes")
34
- .option("-c, --canister <name>", "Canister to sync")
35
- .action(syncCommand)
36
-
37
- program
38
- .command("list")
39
- .description("List available methods from a canister")
40
- .option("-c, --canister <name>", "Canister to list methods from")
41
- .action(listCommand)
29
+ .command("generate")
30
+ .alias("g")
31
+ .description("Generate hooks from .did files")
32
+ .option("-c, --canister <name>", "Generate for a specific canister only")
33
+ .option("--clean", "Clean output directory before generating")
34
+ .action(generateCommand)
42
35
 
43
36
  program.parse()
package/src/types.ts CHANGED
@@ -1,26 +1,27 @@
1
1
  /**
2
2
  * CLI-specific Types
3
3
  *
4
- * Shared types (MethodInfo, CanisterConfig, HookType, GeneratorOptions)
5
- * are now in @ic-reactor/codegen.
4
+ * Most types are now imported from @ic-reactor/codegen to ensure consistency.
6
5
  */
7
6
 
8
- export type { CanisterConfig, HookType } from "@ic-reactor/codegen"
7
+ import type { CodegenConfig, CanisterConfig } from "@ic-reactor/codegen"
9
8
 
10
- export interface HookConfig {
11
- name: string
12
- type?: HookType
9
+ // Re-export for convenience
10
+ export type { CodegenConfig, CanisterConfig }
11
+
12
+ /**
13
+ * CLI arguments for the `init` command
14
+ */
15
+ export interface InitOptions {
16
+ yes?: boolean
17
+ outDir?: string
18
+ dryRun?: boolean
13
19
  }
14
20
 
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>>
21
+ /**
22
+ * CLI arguments for the `generate` command
23
+ */
24
+ export interface GenerateOptions {
25
+ canister?: string
26
+ clean?: boolean
26
27
  }
@@ -4,16 +4,15 @@
4
4
 
5
5
  import fs from "node:fs"
6
6
  import path from "node:path"
7
- import type { ReactorConfig } from "../types.js"
7
+ import type { CodegenConfig } from "../types.js"
8
8
 
9
9
  export const CONFIG_FILE_NAME = "ic-reactor.json"
10
10
 
11
- export const DEFAULT_CONFIG: ReactorConfig = {
11
+ export const DEFAULT_CONFIG: CodegenConfig = {
12
12
  $schema:
13
13
  "https://raw.githubusercontent.com/B3Pay/ic-reactor/main/packages/cli/schema.json",
14
- outDir: "src/lib/canisters",
14
+ outDir: "src/declarations",
15
15
  canisters: {},
16
- generatedHooks: {},
17
16
  }
18
17
 
19
18
  /**
@@ -38,7 +37,7 @@ export function findConfigFile(
38
37
  /**
39
38
  * Load the reactor config file
40
39
  */
41
- export function loadConfig(configPath?: string): ReactorConfig | null {
40
+ export function loadConfig(configPath?: string): CodegenConfig | null {
42
41
  const filePath = configPath ?? findConfigFile()
43
42
 
44
43
  if (!filePath || !fs.existsSync(filePath)) {
@@ -47,7 +46,7 @@ export function loadConfig(configPath?: string): ReactorConfig | null {
47
46
 
48
47
  try {
49
48
  const content = fs.readFileSync(filePath, "utf-8")
50
- return JSON.parse(content) as ReactorConfig
49
+ return JSON.parse(content) as CodegenConfig
51
50
  } catch {
52
51
  return null
53
52
  }
@@ -57,7 +56,7 @@ export function loadConfig(configPath?: string): ReactorConfig | null {
57
56
  * Save the reactor config file
58
57
  */
59
58
  export function saveConfig(
60
- config: ReactorConfig,
59
+ config: CodegenConfig,
61
60
  configPath: string = path.join(process.cwd(), CONFIG_FILE_NAME)
62
61
  ): void {
63
62
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n")
@@ -65,23 +64,12 @@ export function saveConfig(
65
64
 
66
65
  /**
67
66
  * 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
67
  */
77
68
  export function getProjectRoot(): string {
78
- // First, check if there's a reactor.config.json in the current directory or parents
79
69
  const configPath = findConfigFile()
80
70
  if (configPath) {
81
71
  return path.dirname(configPath)
82
72
  }
83
-
84
- // Default to current working directory for new projects
85
73
  return process.cwd()
86
74
  }
87
75
 
@@ -94,19 +82,8 @@ export function ensureDir(dirPath: string): void {
94
82
  }
95
83
  }
96
84
 
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
85
  export function getRelativePath(from: string, to: string): string {
108
86
  const relativePath = path.relative(path.dirname(from), to)
109
- // Ensure it starts with ./ or ../
110
87
  if (!relativePath.startsWith(".")) {
111
88
  return "./" + relativePath
112
89
  }