@ic-reactor/cli 0.2.0 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "CLI tool to generate shadcn-style React hooks for ICP canisters",
6
6
  "main": "dist/index.js",
@@ -33,10 +33,10 @@
33
33
  "@icp-sdk/core": "^5.0.0",
34
34
  "commander": "^14.0.3",
35
35
  "picocolors": "^1.1.1",
36
- "@ic-reactor/codegen": "0.2.0"
36
+ "@ic-reactor/codegen": "0.3.1"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/node": "^25.2.2",
39
+ "@types/node": "^25.2.3",
40
40
  "tsup": "^8.3.5",
41
41
  "typescript": "~5.9.3"
42
42
  },
@@ -3,7 +3,5 @@
3
3
  */
4
4
 
5
5
  export { initCommand } from "./init.js"
6
- export { addCommand } from "./add.js"
7
6
  export { listCommand } from "./list.js"
8
7
  export { syncCommand } from "./sync.js"
9
- export { fetchCommand } from "./fetch.js"
@@ -54,8 +54,8 @@ export async function initCommand(options: InitOptions) {
54
54
  // Interactive mode
55
55
  const outDir = await p.text({
56
56
  message: "Where should generated hooks be placed?",
57
- placeholder: "src/canisters",
58
- defaultValue: "src/canisters",
57
+ placeholder: "src/lib/canisters",
58
+ defaultValue: "src/lib/canisters",
59
59
  validate: (value) => {
60
60
  if (!value) return "Output directory is required"
61
61
  return undefined
@@ -100,17 +100,17 @@ export async function initCommand(options: InitOptions) {
100
100
  ensureDir(fullOutDir)
101
101
 
102
102
  // Create a sample client manager if it doesn't exist
103
- const clientManagerPath = path.join(projectRoot, "src/lib/client.ts")
103
+ const clientManagerPath = path.join(projectRoot, "src/lib/clients.ts")
104
104
  if (!fs.existsSync(clientManagerPath)) {
105
105
  const createClient = await p.confirm({
106
- message: "Create a sample client manager at src/lib/client.ts?",
106
+ message: "Create a sample client manager at src/lib/clients.ts?",
107
107
  initialValue: true,
108
108
  })
109
109
 
110
110
  if (!p.isCancel(createClient) && createClient) {
111
111
  ensureDir(path.dirname(clientManagerPath))
112
112
  fs.writeFileSync(clientManagerPath, getClientManagerTemplate())
113
- p.log.success(`Created ${pc.green("src/lib/client.ts")}`)
113
+ p.log.success(`Created ${pc.green("src/lib/clients.ts")}`)
114
114
  }
115
115
  }
116
116
 
@@ -170,8 +170,8 @@ async function promptForCanister(
170
170
  const clientManagerPath = await p.text({
171
171
  message:
172
172
  "Import path to your client manager (relative from generated hooks)",
173
- placeholder: "../../lib/client",
174
- defaultValue: "../../lib/client",
173
+ placeholder: "../../clients",
174
+ defaultValue: "../../clients",
175
175
  })
176
176
 
177
177
  if (p.isCancel(clientManagerPath)) return null
@@ -8,21 +8,9 @@ import * as p from "@clack/prompts"
8
8
  import fs from "node:fs"
9
9
  import path from "node:path"
10
10
  import pc from "picocolors"
11
- import {
12
- loadConfig,
13
- getProjectRoot,
14
- findConfigFile,
15
- ensureDir,
16
- } from "../utils/config.js"
17
- import { parseDIDFile } from "@ic-reactor/codegen"
18
- import {
19
- generateReactorFile,
20
- generateQueryHook,
21
- generateMutationHook,
22
- } from "../generators/index.js"
23
- import { getHookFileName } from "@ic-reactor/codegen"
11
+ import { loadConfig, getProjectRoot, findConfigFile } from "../utils/config.js"
12
+ import { generateReactorFile } from "../generators/index.js"
24
13
  import { generateDeclarations } from "@ic-reactor/codegen"
25
- import type { MethodInfo } from "@ic-reactor/codegen"
26
14
 
27
15
  interface SyncOptions {
28
16
  canister?: string
@@ -67,173 +55,52 @@ export async function syncCommand(options: SyncOptions) {
67
55
  }
68
56
  canistersToSync = [options.canister]
69
57
  } else {
70
- // Sync all canisters with generated hooks
71
- canistersToSync = canisterNames.filter(
72
- (name) => (config.generatedHooks[name]?.length ?? 0) > 0
73
- )
74
-
75
- if (canistersToSync.length === 0) {
76
- p.log.warn("No hooks have been generated yet. Run `add` first.")
77
- process.exit(0)
78
- }
58
+ canistersToSync = canisterNames
79
59
  }
80
60
 
81
61
  const spinner = p.spinner()
82
62
  spinner.start("Syncing hooks...")
83
63
 
84
64
  let totalUpdated = 0
85
- let totalSkipped = 0
86
65
  const errors: string[] = []
87
66
 
88
67
  for (const canisterName of canistersToSync) {
89
68
  const canisterConfig = config.canisters[canisterName]
90
- const generatedMethods = config.generatedHooks[canisterName] ?? []
91
-
92
- if (generatedMethods.length === 0) {
93
- continue
94
- }
95
-
96
- // Parse DID file
97
- const didFilePath = path.resolve(projectRoot, canisterConfig.didFile)
98
- let methods: MethodInfo[]
99
-
100
- try {
101
- methods = parseDIDFile(didFilePath)
102
- } catch (error) {
103
- errors.push(
104
- `${canisterName}: Failed to parse DID file - ${(error as Error).message}`
105
- )
106
- continue
107
- }
108
-
109
- // Normalize hooks to objects
110
- const hooks = generatedMethods.map((h) =>
111
- typeof h === "string" ? { name: h } : h
112
- )
113
-
114
- // Check for removed methods
115
- const currentMethodNames = methods.map((m) => m.name)
116
- const removedMethods = hooks
117
- .filter((h) => !currentMethodNames.includes(h.name))
118
- .map((h) => h.name)
119
-
120
- if (removedMethods.length > 0) {
121
- p.log.warn(
122
- `${canisterName}: Methods removed from DID: ${pc.yellow(removedMethods.join(", "))}`
123
- )
124
- }
125
-
126
- // Check for new methods
127
- const generatedNames = hooks.map((h) => h.name)
128
- const newMethods = methods.filter((m) => !generatedNames.includes(m.name))
129
-
130
- if (newMethods.length > 0) {
131
- p.log.info(
132
- `${canisterName}: New methods available: ${pc.cyan(newMethods.map((m) => m.name).join(", "))}`
133
- )
134
- }
135
69
 
136
70
  // Regenerate declarations if missing
137
71
  const canisterOutDir = path.join(projectRoot, config.outDir, canisterName)
138
- const declarationsDir = path.join(canisterOutDir, "declarations")
72
+ const didFilePath = path.resolve(projectRoot, canisterConfig.didFile)
139
73
 
140
- // Check if declarations exist and have files (not just nested empty dir)
141
- const declarationsExist =
142
- fs.existsSync(declarationsDir) &&
143
- fs
144
- .readdirSync(declarationsDir)
145
- .some((f) => f.endsWith(".ts") || f.endsWith(".js"))
74
+ spinner.message(`Regenerating declarations for ${canisterName}...`)
146
75
 
147
- if (!declarationsExist) {
148
- spinner.message(`Regenerating declarations for ${canisterName}...`)
76
+ try {
149
77
  const bindgenResult = await generateDeclarations({
150
78
  didFile: didFilePath,
151
79
  outDir: canisterOutDir,
152
80
  canisterName,
153
81
  })
154
82
 
155
- if (bindgenResult.success) {
156
- totalUpdated++
157
- } else {
83
+ if (!bindgenResult.success) {
84
+ errors.push(`${canisterName}: ${bindgenResult.error}`)
158
85
  p.log.warn(
159
86
  `Could not regenerate declarations for ${canisterName}: ${bindgenResult.error}`
160
87
  )
161
- }
162
- }
163
-
164
- // Regenerate reactor.ts
165
- const reactorPath = path.join(canisterOutDir, "reactor.ts")
166
-
167
- const reactorContent = generateReactorFile({
168
- canisterName,
169
- canisterConfig,
170
- config,
171
- outDir: canisterOutDir,
172
- })
173
- fs.writeFileSync(reactorPath, reactorContent)
174
- totalUpdated++
175
-
176
- // Regenerate existing hooks
177
- const hooksOutDir = path.join(canisterOutDir, "hooks")
178
- ensureDir(hooksOutDir)
179
-
180
- for (const hookConfig of hooks) {
181
- const methodName = hookConfig.name
182
- const method = methods.find((m) => m.name === methodName)
183
-
184
- if (!method) {
185
- // Method was removed, skip but warn
186
- totalSkipped++
187
88
  continue
188
89
  }
189
90
 
190
- // Determine hook type
191
- let hookType: string = hookConfig.type || (method.type as string)
192
-
193
- // If no explicit type in config, try to infer from existing files (backward compat)
194
- if (!hookConfig.type) {
195
- const infiniteQueryFileName = getHookFileName(
196
- methodName,
197
- "infiniteQuery"
198
- )
199
- if (fs.existsSync(path.join(hooksOutDir, infiniteQueryFileName))) {
200
- hookType = "infiniteQuery"
201
- }
202
- }
203
-
204
- const fileName = getHookFileName(methodName, hookType)
205
- let content: string
206
-
207
- if (hookType.includes("Query")) {
208
- content = generateQueryHook({
209
- canisterName,
210
- method,
211
- config,
212
- type: hookType as any,
213
- })
214
- } else {
215
- content = generateMutationHook({
216
- canisterName,
217
- method,
218
- config,
219
- })
220
- }
221
-
222
- const filePath = path.join(hooksOutDir, fileName)
223
-
224
- // Check if file exists and has been customized
225
- if (fs.existsSync(filePath)) {
226
- const existingContent = fs.readFileSync(filePath, "utf-8")
91
+ // Regenerate index.ts
92
+ const reactorPath = path.join(canisterOutDir, "index.ts")
227
93
 
228
- // If content is different from what we'd generate, skip (user customized)
229
- if (existingContent !== content) {
230
- totalSkipped++
231
- continue
232
- }
233
- }
234
-
235
- fs.writeFileSync(filePath, content)
94
+ const reactorContent = generateReactorFile({
95
+ canisterName,
96
+ canisterConfig,
97
+ config,
98
+ })
99
+ fs.writeFileSync(reactorPath, reactorContent)
236
100
  totalUpdated++
101
+ } catch (error) {
102
+ errors.push(`${canisterName}: ${(error as Error).message}`)
103
+ p.log.error(`Failed to sync ${canisterName}: ${(error as Error).message}`)
237
104
  }
238
105
  }
239
106
 
@@ -249,11 +116,7 @@ export async function syncCommand(options: SyncOptions) {
249
116
  }
250
117
 
251
118
  console.log()
252
- p.note(
253
- `Updated: ${pc.green(totalUpdated.toString())} files\n` +
254
- `Skipped: ${pc.dim(totalSkipped.toString())} files (preserved customizations)`,
255
- "Summary"
256
- )
119
+ p.note(`Updated: ${pc.green(totalUpdated.toString())} canisters`, "Summary")
257
120
 
258
121
  p.outro(pc.green("✓ Sync complete!"))
259
122
  }
@@ -1,8 +1 @@
1
- /**
2
- * Generators barrel export
3
- */
4
-
5
1
  export { generateReactorFile } from "./reactor.js"
6
- export { generateQueryHook } from "./query.js"
7
- export { generateMutationHook } from "./mutation.js"
8
- export { generateInfiniteQueryHook } from "./infiniteQuery.js"
@@ -14,28 +14,17 @@ export interface ReactorGeneratorOptions {
14
14
  canisterName: string
15
15
  canisterConfig: CanisterConfig
16
16
  config: ReactorConfig
17
- outDir: string
18
- hasDeclarations?: boolean
19
17
  }
20
18
 
21
19
  /**
22
20
  * Generate the reactor.ts file content
23
21
  */
24
22
  export function generateReactorFile(options: ReactorGeneratorOptions): string {
25
- const {
26
- canisterName,
27
- canisterConfig,
28
- config,
29
- hasDeclarations = true,
30
- } = options
23
+ const { canisterName, canisterConfig, config } = options
31
24
 
32
25
  return generateReactorFileFromCodegen({
33
26
  canisterName,
34
27
  canisterConfig,
35
- hasDeclarations,
36
28
  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
29
  })
41
30
  }
package/src/index.ts CHANGED
@@ -8,10 +8,8 @@
8
8
 
9
9
  import { Command } from "commander"
10
10
  import { initCommand } from "./commands/init.js"
11
- import { addCommand } from "./commands/add.js"
12
11
  import { syncCommand } from "./commands/sync.js"
13
12
  import { listCommand } from "./commands/list.js"
14
- import { fetchCommand } from "./commands/fetch.js"
15
13
  import pc from "picocolors"
16
14
 
17
15
  const program = new Command()
@@ -30,24 +28,6 @@ program
30
28
  .option("-o, --out-dir <path>", "Output directory for generated hooks")
31
29
  .action(initCommand)
32
30
 
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
31
  program
52
32
  .command("sync")
53
33
  .description("Sync hooks with .did file changes")
package/src/types.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * are now in @ic-reactor/codegen.
6
6
  */
7
7
 
8
- import type { CanisterConfig, HookType } from "@ic-reactor/codegen"
8
+ export type { CanisterConfig, HookType } from "@ic-reactor/codegen"
9
9
 
10
10
  export interface HookConfig {
11
11
  name: string
@@ -6,12 +6,12 @@ import fs from "node:fs"
6
6
  import path from "node:path"
7
7
  import type { ReactorConfig } from "../types.js"
8
8
 
9
- export const CONFIG_FILE_NAME = "reactor.config.json"
9
+ export const CONFIG_FILE_NAME = "ic-reactor.json"
10
10
 
11
11
  export const DEFAULT_CONFIG: ReactorConfig = {
12
12
  $schema:
13
13
  "https://raw.githubusercontent.com/B3Pay/ic-reactor/main/packages/cli/schema.json",
14
- outDir: "src/canisters",
14
+ outDir: "src/lib/canisters",
15
15
  canisters: {},
16
16
  generatedHooks: {},
17
17
  }