@ic-reactor/cli 0.0.0-beta.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/dist/index.js ADDED
@@ -0,0 +1,1951 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import * as p from "@clack/prompts";
8
+ import fs2 from "fs";
9
+ import path2 from "path";
10
+ import pc from "picocolors";
11
+
12
+ // src/utils/config.ts
13
+ import fs from "fs";
14
+ import path from "path";
15
+ var CONFIG_FILE_NAME = "reactor.config.json";
16
+ var DEFAULT_CONFIG = {
17
+ $schema: "https://raw.githubusercontent.com/B3Pay/ic-reactor/main/packages/cli/schema.json",
18
+ outDir: "src/canisters",
19
+ canisters: {},
20
+ generatedHooks: {}
21
+ };
22
+ function findConfigFile(startDir = process.cwd()) {
23
+ let currentDir = startDir;
24
+ while (currentDir !== path.dirname(currentDir)) {
25
+ const configPath = path.join(currentDir, CONFIG_FILE_NAME);
26
+ if (fs.existsSync(configPath)) {
27
+ return configPath;
28
+ }
29
+ currentDir = path.dirname(currentDir);
30
+ }
31
+ return null;
32
+ }
33
+ function loadConfig(configPath) {
34
+ const filePath = configPath ?? findConfigFile();
35
+ if (!filePath || !fs.existsSync(filePath)) {
36
+ return null;
37
+ }
38
+ try {
39
+ const content = fs.readFileSync(filePath, "utf-8");
40
+ return JSON.parse(content);
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+ function saveConfig(config, configPath = path.join(process.cwd(), CONFIG_FILE_NAME)) {
46
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
47
+ }
48
+ function getProjectRoot() {
49
+ const configPath = findConfigFile();
50
+ if (configPath) {
51
+ return path.dirname(configPath);
52
+ }
53
+ return process.cwd();
54
+ }
55
+ function ensureDir(dirPath) {
56
+ if (!fs.existsSync(dirPath)) {
57
+ fs.mkdirSync(dirPath, { recursive: true });
58
+ }
59
+ }
60
+ function fileExists(filePath) {
61
+ return fs.existsSync(filePath);
62
+ }
63
+
64
+ // src/commands/init.ts
65
+ async function initCommand(options) {
66
+ console.log();
67
+ p.intro(pc.cyan("\u{1F527} ic-reactor CLI Setup"));
68
+ const existingConfig = findConfigFile();
69
+ if (existingConfig) {
70
+ const shouldOverwrite = await p.confirm({
71
+ message: `Config file already exists at ${pc.yellow(existingConfig)}. Overwrite?`,
72
+ initialValue: false
73
+ });
74
+ if (p.isCancel(shouldOverwrite) || !shouldOverwrite) {
75
+ p.cancel("Setup cancelled.");
76
+ process.exit(0);
77
+ }
78
+ }
79
+ const projectRoot = getProjectRoot();
80
+ let config;
81
+ if (options.yes) {
82
+ config = { ...DEFAULT_CONFIG };
83
+ if (options.outDir) {
84
+ config.outDir = options.outDir;
85
+ }
86
+ } else {
87
+ const outDir = await p.text({
88
+ message: "Where should generated hooks be placed?",
89
+ placeholder: "src/canisters",
90
+ defaultValue: "src/canisters",
91
+ validate: (value) => {
92
+ if (!value) return "Output directory is required";
93
+ return void 0;
94
+ }
95
+ });
96
+ if (p.isCancel(outDir)) {
97
+ p.cancel("Setup cancelled.");
98
+ process.exit(0);
99
+ }
100
+ const addCanister = await p.confirm({
101
+ message: "Would you like to add a canister now?",
102
+ initialValue: true
103
+ });
104
+ if (p.isCancel(addCanister)) {
105
+ p.cancel("Setup cancelled.");
106
+ process.exit(0);
107
+ }
108
+ config = {
109
+ ...DEFAULT_CONFIG,
110
+ outDir
111
+ };
112
+ if (addCanister) {
113
+ const canisterInfo = await promptForCanister(projectRoot);
114
+ if (canisterInfo) {
115
+ config.canisters[canisterInfo.name] = canisterInfo.config;
116
+ }
117
+ }
118
+ }
119
+ const configPath = path2.join(projectRoot, CONFIG_FILE_NAME);
120
+ saveConfig(config, configPath);
121
+ const fullOutDir = path2.join(projectRoot, config.outDir);
122
+ ensureDir(fullOutDir);
123
+ const clientManagerPath = path2.join(projectRoot, "src/lib/client.ts");
124
+ if (!fs2.existsSync(clientManagerPath)) {
125
+ const createClient = await p.confirm({
126
+ message: "Create a sample client manager at src/lib/client.ts?",
127
+ initialValue: true
128
+ });
129
+ if (!p.isCancel(createClient) && createClient) {
130
+ ensureDir(path2.dirname(clientManagerPath));
131
+ fs2.writeFileSync(clientManagerPath, getClientManagerTemplate());
132
+ p.log.success(`Created ${pc.green("src/lib/client.ts")}`);
133
+ }
134
+ }
135
+ p.log.success(`Created ${pc.green(CONFIG_FILE_NAME)}`);
136
+ p.log.success(`Created ${pc.green(config.outDir)} directory`);
137
+ console.log();
138
+ p.note(
139
+ `Next steps:
140
+
141
+ 1. ${pc.cyan("Add a canister:")}
142
+ ${pc.dim("npx @ic-reactor/cli add")}
143
+
144
+ 2. ${pc.cyan("List available methods:")}
145
+ ${pc.dim("npx @ic-reactor/cli list -c <canister-name>")}
146
+
147
+ 3. ${pc.cyan("Add hooks for specific methods:")}
148
+ ${pc.dim("npx @ic-reactor/cli add -c <canister> -m <method>")}`,
149
+ "Getting Started"
150
+ );
151
+ p.outro(pc.green("\u2713 ic-reactor initialized successfully!"));
152
+ }
153
+ async function promptForCanister(projectRoot) {
154
+ const name = await p.text({
155
+ message: "Canister name",
156
+ placeholder: "backend",
157
+ validate: (value) => {
158
+ if (!value) return "Canister name is required";
159
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(value)) {
160
+ return "Canister name must start with a letter and contain only letters, numbers, hyphens, and underscores";
161
+ }
162
+ return void 0;
163
+ }
164
+ });
165
+ if (p.isCancel(name)) return null;
166
+ const didFile = await p.text({
167
+ message: "Path to .did file",
168
+ placeholder: "./backend.did",
169
+ validate: (value) => {
170
+ if (!value) return "DID file path is required";
171
+ const fullPath = path2.resolve(projectRoot, value);
172
+ if (!fs2.existsSync(fullPath)) {
173
+ return `File not found: ${value}`;
174
+ }
175
+ return void 0;
176
+ }
177
+ });
178
+ if (p.isCancel(didFile)) return null;
179
+ const clientManagerPath = await p.text({
180
+ message: "Import path to your client manager (relative from generated hooks)",
181
+ placeholder: "../../lib/client",
182
+ defaultValue: "../../lib/client"
183
+ });
184
+ if (p.isCancel(clientManagerPath)) return null;
185
+ const useDisplayReactor = await p.confirm({
186
+ message: "Use DisplayReactor? (auto-converts bigint \u2192 string, Principal \u2192 string)",
187
+ initialValue: true
188
+ });
189
+ if (p.isCancel(useDisplayReactor)) return null;
190
+ return {
191
+ name,
192
+ config: {
193
+ didFile,
194
+ clientManagerPath,
195
+ useDisplayReactor
196
+ }
197
+ };
198
+ }
199
+ function getClientManagerTemplate() {
200
+ return `/**
201
+ * IC Client Manager
202
+ *
203
+ * This file configures the IC agent and client manager for your application.
204
+ * Customize the agent options based on your environment.
205
+ */
206
+
207
+ import { ClientManager } from "@ic-reactor/react"
208
+
209
+ /**
210
+ * The client manager handles agent lifecycle and authentication.
211
+ *
212
+ * Configuration options:
213
+ * - host: IC network host (defaults to process env or mainnet)
214
+ * - identity: Initial identity (optional, can be set later)
215
+ * - verifyQuerySignatures: Verify query signatures (recommended for production)
216
+ *
217
+ * For local development, the agent will automatically detect local replica.
218
+ */
219
+ export const clientManager = new ClientManager({
220
+ // Uncomment for explicit host configuration:
221
+ // host: process.env.DFX_NETWORK === "local"
222
+ // ? "http://localhost:4943"
223
+ // : "https://icp-api.io",
224
+ })
225
+ `;
226
+ }
227
+
228
+ // src/commands/add.ts
229
+ import * as p2 from "@clack/prompts";
230
+ import fs5 from "fs";
231
+ import path5 from "path";
232
+ import pc2 from "picocolors";
233
+
234
+ // src/parsers/did.ts
235
+ import fs3 from "fs";
236
+ function parseDIDFile(didFilePath) {
237
+ if (!fs3.existsSync(didFilePath)) {
238
+ throw new Error(`DID file not found: ${didFilePath}`);
239
+ }
240
+ const content = fs3.readFileSync(didFilePath, "utf-8");
241
+ return extractMethods(content);
242
+ }
243
+ function extractMethods(didContent) {
244
+ const methods = [];
245
+ const cleanContent = didContent.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
246
+ const methodRegex = /([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(?:func\s*)?\(([^)]*)\)\s*->\s*\(([^)]*)\)\s*(query|composite_query)?/g;
247
+ let match;
248
+ while ((match = methodRegex.exec(cleanContent)) !== null) {
249
+ const name = match[1];
250
+ const args = match[2].trim();
251
+ const returnType = match[3].trim();
252
+ const queryAnnotation = match[4];
253
+ const isQuery = queryAnnotation === "query" || queryAnnotation === "composite_query";
254
+ methods.push({
255
+ name,
256
+ type: isQuery ? "query" : "mutation",
257
+ hasArgs: args.length > 0 && args !== "",
258
+ argsDescription: args || void 0,
259
+ returnDescription: returnType || void 0
260
+ });
261
+ }
262
+ return methods;
263
+ }
264
+ function formatMethodForDisplay(method) {
265
+ const typeLabel = method.type === "query" ? "query" : "update";
266
+ const argsLabel = method.hasArgs ? "with args" : "no args";
267
+ return `${method.name} (${typeLabel}, ${argsLabel})`;
268
+ }
269
+
270
+ // src/generators/reactor.ts
271
+ import path3 from "path";
272
+
273
+ // src/utils/naming.ts
274
+ function toPascalCase(str) {
275
+ const parts = str.split(/[-_]/);
276
+ return parts.map((part) => {
277
+ if (part.length === 0) return "";
278
+ return part.charAt(0).toUpperCase() + part.slice(1);
279
+ }).join("");
280
+ }
281
+ function toCamelCase(str) {
282
+ const pascal = toPascalCase(str);
283
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
284
+ }
285
+ function getHookFileName(methodName, hookType) {
286
+ const camelMethod = toCamelCase(methodName);
287
+ const pascalType = toPascalCase(hookType);
288
+ return `${camelMethod}${pascalType}.ts`;
289
+ }
290
+ function getHookExportName(methodName, hookType) {
291
+ const camelMethod = toCamelCase(methodName);
292
+ const pascalType = toPascalCase(hookType);
293
+ return `${camelMethod}${pascalType}`;
294
+ }
295
+ function getReactHookName(methodName, hookType) {
296
+ const pascalMethod = toPascalCase(methodName);
297
+ const pascalType = toPascalCase(hookType);
298
+ return `use${pascalMethod}${pascalType}`;
299
+ }
300
+ function getReactorName(canisterName) {
301
+ return `${toCamelCase(canisterName)}Reactor`;
302
+ }
303
+ function getServiceTypeName(canisterName) {
304
+ return `${toPascalCase(canisterName)}Service`;
305
+ }
306
+
307
+ // src/generators/reactor.ts
308
+ function generateReactorFile(options) {
309
+ const { canisterName, canisterConfig, hasDeclarations = true } = options;
310
+ const pascalName = toPascalCase(canisterName);
311
+ const reactorName = getReactorName(canisterName);
312
+ const serviceName = getServiceTypeName(canisterName);
313
+ const reactorType = canisterConfig.useDisplayReactor !== false ? "DisplayReactor" : "Reactor";
314
+ const clientManagerPath = canisterConfig.clientManagerPath ?? options.config.clientManagerPath ?? "../../lib/client";
315
+ const didFileName = path3.basename(canisterConfig.didFile);
316
+ const declarationsPath = `./declarations/${didFileName}`;
317
+ if (hasDeclarations) {
318
+ return `/**
319
+ * ${pascalName} Reactor
320
+ *
321
+ * Auto-generated by @ic-reactor/cli
322
+ * This file provides the shared reactor instance for the ${canisterName} canister.
323
+ *
324
+ * You can customize this file to add global configuration.
325
+ */
326
+
327
+ import { ${reactorType}, createActorHooks } from "@ic-reactor/react"
328
+ import { clientManager } from "${clientManagerPath}"
329
+ import { idlFactory, type _SERVICE } from "${declarationsPath}"
330
+
331
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
332
+ // SERVICE TYPE
333
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
334
+
335
+ export type ${serviceName} = _SERVICE
336
+
337
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
338
+ // REACTOR INSTANCE
339
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
340
+
341
+ /**
342
+ * ${pascalName} Reactor with ${canisterConfig.useDisplayReactor !== false ? "Display" : "Candid"} type transformations.
343
+ * ${canisterConfig.useDisplayReactor !== false ? "Automatically converts bigint \u2192 string, Principal \u2192 string, etc." : "Uses raw Candid types."}
344
+ */
345
+ export const ${reactorName} = new ${reactorType}<${serviceName}>({
346
+ clientManager,
347
+ idlFactory,
348
+ name: "${canisterName}",
349
+ })
350
+
351
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
352
+ // ACTOR HOOKS
353
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
354
+
355
+ /**
356
+ * Actor hooks for ${canisterName} - use these directly or import method-specific hooks.
357
+ */
358
+ export const {
359
+ useActorQuery,
360
+ useActorMutation,
361
+ useActorSuspenseQuery,
362
+ useActorInfiniteQuery,
363
+ useActorSuspenseInfiniteQuery,
364
+ useActorMethod,
365
+ } = createActorHooks(${reactorName})
366
+ `;
367
+ }
368
+ return `/**
369
+ * ${pascalName} Reactor
370
+ *
371
+ * Auto-generated by @ic-reactor/cli
372
+ * This file provides the shared reactor instance for the ${canisterName} canister.
373
+ *
374
+ * You can customize this file to add global configuration.
375
+ */
376
+
377
+ import { ${reactorType}, createActorHooks } from "@ic-reactor/react"
378
+ import { clientManager } from "${clientManagerPath}"
379
+
380
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
381
+ // DECLARATIONS
382
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
383
+
384
+ // TODO: Generate proper types by running:
385
+ // npx @icp-sdk/bindgen --input <path-to-did> --output ./${canisterName}/declarations
386
+
387
+ // Then uncomment:
388
+ // import { idlFactory, type _SERVICE as ${serviceName} } from "${declarationsPath}"
389
+
390
+ // Fallback generic type - replace with generated types
391
+ type ${serviceName} = Record<string, (...args: unknown[]) => Promise<unknown>>
392
+
393
+ // You'll need to define idlFactory here or import from declarations
394
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
395
+ const idlFactory = ({ IDL }: { IDL: any }) => IDL.Service({})
396
+
397
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
398
+ // REACTOR INSTANCE
399
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
400
+
401
+ /**
402
+ * ${pascalName} Reactor with ${canisterConfig.useDisplayReactor !== false ? "Display" : "Candid"} type transformations.
403
+ * ${canisterConfig.useDisplayReactor !== false ? "Automatically converts bigint \u2192 string, Principal \u2192 string, etc." : "Uses raw Candid types."}
404
+ */
405
+ export const ${reactorName} = new ${reactorType}<${serviceName}>({
406
+ clientManager,
407
+ idlFactory,
408
+ name: "${canisterName}",
409
+ })
410
+
411
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
412
+ // ACTOR HOOKS
413
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
414
+
415
+ /**
416
+ * Actor hooks for ${canisterName} - use these directly or import method-specific hooks.
417
+ */
418
+ export const {
419
+ useActorQuery,
420
+ useActorMutation,
421
+ useActorSuspenseQuery,
422
+ useActorInfiniteQuery,
423
+ useActorSuspenseInfiniteQuery,
424
+ useActorMethod,
425
+ } = createActorHooks(${reactorName})
426
+
427
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
428
+ // RE-EXPORTS
429
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
430
+
431
+ export { idlFactory }
432
+ export type { ${serviceName} }
433
+ `;
434
+ }
435
+
436
+ // src/generators/query.ts
437
+ function generateQueryHook(options) {
438
+ const { canisterName, method, type = "query" } = options;
439
+ const reactorName = getReactorName(canisterName);
440
+ const hookExportName = getHookExportName(method.name, type);
441
+ const isSuspense = type === "suspenseQuery";
442
+ const creatorFn = isSuspense ? "createSuspenseQuery" : "createQuery";
443
+ const factoryFn = isSuspense ? "createSuspenseQueryFactory" : "createQueryFactory";
444
+ const hookName = isSuspense ? "useSuspenseQuery" : "useQuery";
445
+ if (method.hasArgs) {
446
+ return `/**
447
+ * Query Factory: ${method.name}
448
+ *
449
+ * Auto-generated by @ic-reactor/cli
450
+ *
451
+ * @example
452
+ * // Use in components
453
+ * const { data } = ${hookExportName}([arg1, arg2]).${hookName}()
454
+ *
455
+ * // Prefetch in loaders
456
+ * const data = await ${hookExportName}([arg1, arg2]).fetch()
457
+ *
458
+ * // Invalidate cache
459
+ * ${hookExportName}([arg1, arg2]).invalidate()
460
+ *
461
+ * // Get query key for cache manipulation
462
+ * const key = ${hookExportName}([arg1, arg2]).getQueryKey()
463
+ */
464
+
465
+ import { ${factoryFn} } from "@ic-reactor/react"
466
+ import { ${reactorName} } from "../reactor"
467
+
468
+ /**
469
+ * Query factory for ${method.name}
470
+ *
471
+ * Creates a query instance with the provided arguments.
472
+ * Each unique set of args gets its own cached query.
473
+ */
474
+ export const ${hookExportName} = ${factoryFn}(${reactorName}, {
475
+ functionName: "${method.name}",
476
+ // Customize your query options:
477
+ // refetchInterval: 60 * 1000, // Refetch every 60 seconds
478
+ // staleTime: 5 * 60 * 1000, // Cache for 5 minutes
479
+ // select: (data) => data,
480
+ })
481
+ `;
482
+ } else {
483
+ return `/**
484
+ * Query: ${method.name}
485
+ *
486
+ * Auto-generated by @ic-reactor/cli
487
+ *
488
+ * @example
489
+ * // Use in components
490
+ * const { data } = ${hookExportName}.${hookName}()
491
+ *
492
+ * // Prefetch in loaders
493
+ * const data = await ${hookExportName}.fetch()
494
+ *
495
+ * // Invalidate cache
496
+ * ${hookExportName}.invalidate()
497
+ *
498
+ * // Get query key for cache manipulation
499
+ * const key = ${hookExportName}.getQueryKey()
500
+ */
501
+
502
+ import { ${creatorFn} } from "@ic-reactor/react"
503
+ import { ${reactorName} } from "../reactor"
504
+
505
+ /**
506
+ * Query for ${method.name}
507
+ *
508
+ * Provides:
509
+ * - .${hookName}() - React hook
510
+ * - .fetch() - Direct fetch (for loaders)
511
+ * - .invalidate() - Invalidate cache
512
+ * - .getQueryKey() - Get query key
513
+ * - .getCacheData() - Read from cache
514
+ */
515
+ export const ${hookExportName} = ${creatorFn}(${reactorName}, {
516
+ functionName: "${method.name}",
517
+ // Customize your query options:
518
+ // refetchInterval: 60 * 1000, // Refetch every 60 seconds
519
+ // staleTime: 5 * 60 * 1000, // Cache for 5 minutes
520
+ // select: (data) => data,
521
+ })
522
+ `;
523
+ }
524
+ }
525
+
526
+ // src/generators/mutation.ts
527
+ function generateMutationHook(options) {
528
+ const { canisterName, method } = options;
529
+ const pascalMethod = toPascalCase(method.name);
530
+ const reactorName = getReactorName(canisterName);
531
+ const hookExportName = getHookExportName(method.name, "mutation");
532
+ const reactHookName = getReactHookName(method.name, "mutation");
533
+ return `/**
534
+ * Mutation Hook: ${method.name}
535
+ *
536
+ * Auto-generated by @ic-reactor/cli
537
+ * This hook wraps the ${method.name} update method.
538
+ *
539
+ * @example
540
+ * // Use the hook in a component
541
+ * const { mutate, isPending } = ${reactHookName}()
542
+ *
543
+ * // Call the mutation
544
+ * mutate(${method.hasArgs ? "[arg1, arg2]" : "[]"})
545
+ *
546
+ * // Or execute directly
547
+ * const result = await execute${pascalMethod}(${method.hasArgs ? "[arg1, arg2]" : "[]"})
548
+ */
549
+
550
+ import { createMutation } from "@ic-reactor/react"
551
+ import { ${reactorName} } from "../reactor"
552
+
553
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
554
+ // MUTATION INSTANCE
555
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
556
+
557
+ /**
558
+ * Mutation for ${method.name}
559
+ *
560
+ * Provides:
561
+ * - .useMutation() - React hook
562
+ * - .execute() - Direct execution
563
+ */
564
+ export const ${hookExportName} = createMutation(${reactorName}, {
565
+ functionName: "${method.name}",
566
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
567
+ // INVALIDATION
568
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
569
+ // Uncomment and import query keys to auto-invalidate on success:
570
+ // invalidateQueries: [
571
+ // someQuery.getQueryKey(),
572
+ // ],
573
+
574
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
575
+ // SUCCESS HANDLER
576
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
577
+ // onSuccess: (data, variables, context) => {
578
+ // console.log("${method.name} succeeded:", data)
579
+ // },
580
+
581
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
582
+ // ERROR HANDLERS
583
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
584
+ // Handle canister-level errors (from Result { Err })
585
+ // onCanisterError: (error, variables) => {
586
+ // console.error("Canister error:", error)
587
+ // },
588
+
589
+ // Handle all errors (network, canister, etc.)
590
+ // onError: (error, variables, context) => {
591
+ // console.error("Error:", error)
592
+ // },
593
+ })
594
+
595
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
596
+ // CONVENIENCE EXPORTS
597
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
598
+
599
+ /**
600
+ * React hook for ${method.name}
601
+ *
602
+ * @param options - Mutation options (onSuccess, onError, etc.)
603
+ */
604
+ export const ${reactHookName} = ${hookExportName}.useMutation
605
+
606
+ /**
607
+ * Execute ${method.name} directly (outside of React)
608
+ *
609
+ * @param args - Arguments to pass to the canister method
610
+ */
611
+ export const execute${pascalMethod} = ${hookExportName}.execute
612
+ `;
613
+ }
614
+
615
+ // src/generators/infiniteQuery.ts
616
+ function generateInfiniteQueryHook(options) {
617
+ const { canisterName, method, type = "infiniteQuery" } = options;
618
+ const pascalMethod = toPascalCase(method.name);
619
+ const reactorName = getReactorName(canisterName);
620
+ const serviceName = getServiceTypeName(canisterName);
621
+ const hookExportName = getHookExportName(method.name, type);
622
+ return `/**
623
+ * Infinite Query Hook: ${method.name}
624
+ *
625
+ * Auto-generated by @ic-reactor/cli
626
+ * This hook wraps the ${method.name} method for infinite/paginated queries.
627
+ *
628
+ * \u26A0\uFE0F CUSTOMIZATION REQUIRED:
629
+ * You need to configure getArgs and getNextPageParam based on your API.
630
+ *
631
+ * @example
632
+ * const {
633
+ * data,
634
+ * fetchNextPage,
635
+ * hasNextPage,
636
+ * isFetching,
637
+ * } = ${hookExportName}.useInfiniteQuery()
638
+ *
639
+ * // Flatten all pages
640
+ * const allItems = data?.pages.flatMap(page => page.items) ?? []
641
+ */
642
+
643
+ import { createInfiniteQuery } from "@ic-reactor/react"
644
+ import { ${reactorName}, type ${serviceName} } from "../reactor"
645
+
646
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
647
+ // CURSOR TYPE
648
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
649
+
650
+ /**
651
+ * Define your pagination cursor type here.
652
+ * Common patterns:
653
+ * - number (offset-based)
654
+ * - string (cursor-based)
655
+ * - { offset: number; limit: number }
656
+ */
657
+ type PageCursor = number
658
+
659
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
660
+ // INFINITE QUERY INSTANCE
661
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
662
+
663
+ /**
664
+ * Infinite query for ${method.name}
665
+ *
666
+ * Provides:
667
+ * - .useInfiniteQuery() - React hook with pagination
668
+ * - .fetch() - Fetch first page
669
+ * - .invalidate() - Invalidate all pages
670
+ * - .getQueryKey() - Get query key
671
+ * - .getCacheData() - Read from cache
672
+ */
673
+ export const ${hookExportName} = createInfiniteQuery(${reactorName}, {
674
+ functionName: "${method.name}",
675
+
676
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
677
+ // PAGINATION CONFIG (CUSTOMIZE THESE)
678
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
679
+
680
+ /**
681
+ * Initial page parameter (e.g., 0 for offset, null for cursor)
682
+ */
683
+ initialPageParam: 0 as PageCursor,
684
+
685
+ /**
686
+ * Convert page param to method arguments.
687
+ * Customize this based on your canister API.
688
+ */
689
+ getArgs: (pageParam: PageCursor) => {
690
+ // Example: offset-based pagination
691
+ return [{ offset: pageParam, limit: 10 }] as Parameters<${serviceName}["${method.name}"]>
692
+
693
+ // Example: cursor-based pagination
694
+ // return [{ cursor: pageParam, limit: 10 }] as Parameters<${serviceName}["${method.name}"]>
695
+ },
696
+
697
+ /**
698
+ * Extract next page param from the last page.
699
+ * Return undefined/null when there are no more pages.
700
+ */
701
+ getNextPageParam: (lastPage, allPages, lastPageParam) => {
702
+ // Example: offset-based - return next offset or undefined if no more
703
+ // const items = lastPage.items ?? []
704
+ // if (items.length < 10) return undefined
705
+ // return lastPageParam + 10
706
+
707
+ // Example: cursor-based
708
+ // return lastPage.nextCursor ?? undefined
709
+
710
+ // Placeholder - customize for your API
711
+ return undefined
712
+ },
713
+
714
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
715
+ // OPTIONAL CONFIG
716
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
717
+
718
+ // Bi-directional scrolling (optional)
719
+ // getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
720
+ // return firstPageParam > 0 ? firstPageParam - 10 : undefined
721
+ // },
722
+
723
+ // Max pages to keep in cache (for memory management)
724
+ // maxPages: 10,
725
+
726
+ // How long data stays fresh
727
+ // staleTime: 5 * 60 * 1000,
728
+
729
+ // Transform the data
730
+ // select: (data) => ({
731
+ // pages: data.pages,
732
+ // pageParams: data.pageParams,
733
+ // items: data.pages.flatMap(page => page.items),
734
+ // }),
735
+ })
736
+
737
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
738
+ // CONVENIENCE EXPORTS
739
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
740
+
741
+ /**
742
+ * React hook for paginated ${method.name}
743
+ */
744
+ export const use${pascalMethod}InfiniteQuery = ${hookExportName}.useInfiniteQuery
745
+
746
+ /**
747
+ * Fetch first page of ${method.name}
748
+ */
749
+ export const fetch${pascalMethod}FirstPage = ${hookExportName}.fetch
750
+
751
+ /**
752
+ * Invalidate all cached pages
753
+ */
754
+ export const invalidate${pascalMethod}Pages = ${hookExportName}.invalidate
755
+ `;
756
+ }
757
+
758
+ // src/utils/bindgen.ts
759
+ import { generate } from "@icp-sdk/bindgen/core";
760
+ import path4 from "path";
761
+ import fs4 from "fs";
762
+ async function generateDeclarations(options) {
763
+ const { didFile, outDir } = options;
764
+ if (!fs4.existsSync(didFile)) {
765
+ return {
766
+ success: false,
767
+ declarationsDir: "",
768
+ error: `DID file not found: ${didFile}`
769
+ };
770
+ }
771
+ const declarationsDir = path4.join(outDir, "declarations");
772
+ try {
773
+ if (!fs4.existsSync(outDir)) {
774
+ fs4.mkdirSync(outDir, { recursive: true });
775
+ }
776
+ if (fs4.existsSync(declarationsDir)) {
777
+ fs4.rmSync(declarationsDir, { recursive: true, force: true });
778
+ }
779
+ fs4.mkdirSync(declarationsDir, { recursive: true });
780
+ await generate({
781
+ didFile,
782
+ outDir,
783
+ // Pass the parent directory; bindgen appends "declarations"
784
+ output: {
785
+ actor: {
786
+ disabled: true
787
+ // We don't need actor creation, we use Reactor
788
+ },
789
+ force: true
790
+ // Overwrite existing files
791
+ }
792
+ });
793
+ return {
794
+ success: true,
795
+ declarationsDir
796
+ };
797
+ } catch (error) {
798
+ return {
799
+ success: false,
800
+ declarationsDir,
801
+ error: error instanceof Error ? error.message : String(error)
802
+ };
803
+ }
804
+ }
805
+
806
+ // src/commands/add.ts
807
+ async function addCommand(options) {
808
+ console.log();
809
+ p2.intro(pc2.cyan("\u{1F527} Add Canister Hooks"));
810
+ const configPath = findConfigFile();
811
+ if (!configPath) {
812
+ p2.log.error(
813
+ `No ${pc2.yellow("reactor.config.json")} found. Run ${pc2.cyan("npx @ic-reactor/cli init")} first.`
814
+ );
815
+ process.exit(1);
816
+ }
817
+ const config = loadConfig(configPath);
818
+ if (!config) {
819
+ p2.log.error(`Failed to load config from ${pc2.yellow(configPath)}`);
820
+ process.exit(1);
821
+ }
822
+ const projectRoot = getProjectRoot();
823
+ const canisterNames = Object.keys(config.canisters);
824
+ if (canisterNames.length === 0) {
825
+ p2.log.error(
826
+ `No canisters configured. Add a canister to ${pc2.yellow("reactor.config.json")} first.`
827
+ );
828
+ const addNow = await p2.confirm({
829
+ message: "Would you like to add a canister now?",
830
+ initialValue: true
831
+ });
832
+ if (p2.isCancel(addNow) || !addNow) {
833
+ process.exit(1);
834
+ }
835
+ const canisterInfo = await promptForNewCanister(projectRoot);
836
+ if (!canisterInfo) {
837
+ p2.cancel("Cancelled.");
838
+ process.exit(0);
839
+ }
840
+ config.canisters[canisterInfo.name] = canisterInfo.config;
841
+ saveConfig(config, configPath);
842
+ canisterNames.push(canisterInfo.name);
843
+ }
844
+ let selectedCanister = options.canister;
845
+ if (!selectedCanister) {
846
+ if (canisterNames.length === 1) {
847
+ selectedCanister = canisterNames[0];
848
+ } else {
849
+ const result = await p2.select({
850
+ message: "Select a canister",
851
+ options: canisterNames.map((name) => ({
852
+ value: name,
853
+ label: name
854
+ }))
855
+ });
856
+ if (p2.isCancel(result)) {
857
+ p2.cancel("Cancelled.");
858
+ process.exit(0);
859
+ }
860
+ selectedCanister = result;
861
+ }
862
+ }
863
+ const canisterConfig = config.canisters[selectedCanister];
864
+ if (!canisterConfig) {
865
+ p2.log.error(`Canister ${pc2.yellow(selectedCanister)} not found in config.`);
866
+ process.exit(1);
867
+ }
868
+ const didFilePath = path5.resolve(projectRoot, canisterConfig.didFile);
869
+ let methods;
870
+ try {
871
+ methods = parseDIDFile(didFilePath);
872
+ } catch (error) {
873
+ p2.log.error(
874
+ `Failed to parse DID file: ${pc2.yellow(didFilePath)}
875
+ ${error.message}`
876
+ );
877
+ process.exit(1);
878
+ }
879
+ if (methods.length === 0) {
880
+ p2.log.warn(`No methods found in ${pc2.yellow(didFilePath)}`);
881
+ process.exit(0);
882
+ }
883
+ p2.log.info(
884
+ `Found ${pc2.green(methods.length.toString())} methods in ${pc2.dim(selectedCanister)}`
885
+ );
886
+ let selectedMethods;
887
+ if (options.all) {
888
+ selectedMethods = methods;
889
+ } else if (options.methods && options.methods.length > 0) {
890
+ const requestedMethods = options.methods.flatMap((m) => m.split(",")).map((m) => m.trim()).filter((m) => m.length > 0);
891
+ selectedMethods = methods.filter((m) => requestedMethods.includes(m.name));
892
+ const notFound = requestedMethods.filter(
893
+ (name) => !methods.some((m) => m.name === name)
894
+ );
895
+ if (notFound.length > 0) {
896
+ p2.log.warn(`Methods not found: ${pc2.yellow(notFound.join(", "))}`);
897
+ }
898
+ } else {
899
+ const alreadyGenerated = config.generatedHooks[selectedCanister] ?? [];
900
+ const result = await p2.multiselect({
901
+ message: "Select methods to add hooks for",
902
+ options: methods.map((method) => {
903
+ const isGenerated = alreadyGenerated.includes(method.name);
904
+ return {
905
+ value: method.name,
906
+ label: formatMethodForDisplay(method),
907
+ hint: isGenerated ? pc2.dim("(already generated)") : void 0
908
+ };
909
+ }),
910
+ required: true
911
+ });
912
+ if (p2.isCancel(result)) {
913
+ p2.cancel("Cancelled.");
914
+ process.exit(0);
915
+ }
916
+ selectedMethods = methods.filter(
917
+ (m) => result.includes(m.name)
918
+ );
919
+ }
920
+ if (selectedMethods.length === 0) {
921
+ p2.log.warn("No methods selected.");
922
+ process.exit(0);
923
+ }
924
+ const methodsWithHookTypes = [];
925
+ for (const method of selectedMethods) {
926
+ if (method.type === "query") {
927
+ const hookType = await p2.select({
928
+ message: `Hook type for ${pc2.cyan(method.name)}`,
929
+ options: [
930
+ { value: "query", label: "Query", hint: "Standard query hook" },
931
+ {
932
+ value: "suspenseQuery",
933
+ label: "Suspense Query",
934
+ hint: "For React Suspense"
935
+ },
936
+ {
937
+ value: "infiniteQuery",
938
+ label: "Infinite Query",
939
+ hint: "Paginated/infinite scroll"
940
+ },
941
+ {
942
+ value: "suspenseInfiniteQuery",
943
+ label: "Suspense Infinite Query",
944
+ hint: "Paginated with Suspense"
945
+ },
946
+ {
947
+ value: "skip",
948
+ label: "Skip",
949
+ hint: "Don't generate hook for this method"
950
+ }
951
+ ]
952
+ });
953
+ if (p2.isCancel(hookType)) {
954
+ p2.cancel("Cancelled.");
955
+ process.exit(0);
956
+ }
957
+ if (hookType === "skip") {
958
+ p2.log.info(`Skipping ${pc2.dim(method.name)}`);
959
+ continue;
960
+ }
961
+ methodsWithHookTypes.push({ method, hookType });
962
+ } else {
963
+ const hookType = await p2.select({
964
+ message: `Hook type for ${pc2.yellow(method.name)} (mutation)`,
965
+ options: [
966
+ {
967
+ value: "mutation",
968
+ label: "Mutation",
969
+ hint: "Standard mutation hook"
970
+ },
971
+ {
972
+ value: "skip",
973
+ label: "Skip",
974
+ hint: "Don't generate hook for this method"
975
+ }
976
+ ]
977
+ });
978
+ if (p2.isCancel(hookType)) {
979
+ p2.cancel("Cancelled.");
980
+ process.exit(0);
981
+ }
982
+ if (hookType === "skip") {
983
+ p2.log.info(`Skipping ${pc2.dim(method.name)}`);
984
+ continue;
985
+ }
986
+ methodsWithHookTypes.push({ method, hookType: "mutation" });
987
+ }
988
+ }
989
+ if (methodsWithHookTypes.length === 0) {
990
+ p2.log.warn("All methods were skipped. Nothing to generate.");
991
+ process.exit(0);
992
+ }
993
+ const canisterOutDir = path5.join(projectRoot, config.outDir, selectedCanister);
994
+ const hooksOutDir = path5.join(canisterOutDir, "hooks");
995
+ ensureDir(hooksOutDir);
996
+ const spinner4 = p2.spinner();
997
+ spinner4.start("Generating hooks...");
998
+ const generatedFiles = [];
999
+ const reactorPath = path5.join(canisterOutDir, "reactor.ts");
1000
+ if (!fileExists(reactorPath)) {
1001
+ spinner4.message("Generating TypeScript declarations...");
1002
+ const bindgenResult = await generateDeclarations({
1003
+ didFile: didFilePath,
1004
+ outDir: canisterOutDir,
1005
+ canisterName: selectedCanister
1006
+ });
1007
+ if (bindgenResult.success) {
1008
+ generatedFiles.push("declarations/");
1009
+ } else {
1010
+ p2.log.warn(`Could not generate declarations: ${bindgenResult.error}`);
1011
+ p2.log.info(
1012
+ `You can manually run: npx @icp-sdk/bindgen --input ${didFilePath} --output ${canisterOutDir}/declarations`
1013
+ );
1014
+ }
1015
+ spinner4.message("Generating reactor...");
1016
+ const reactorContent = generateReactorFile({
1017
+ canisterName: selectedCanister,
1018
+ canisterConfig,
1019
+ config,
1020
+ outDir: canisterOutDir,
1021
+ hasDeclarations: bindgenResult.success
1022
+ });
1023
+ fs5.writeFileSync(reactorPath, reactorContent);
1024
+ generatedFiles.push("reactor.ts");
1025
+ }
1026
+ for (const { method, hookType } of methodsWithHookTypes) {
1027
+ const fileName = getHookFileName(method.name, hookType);
1028
+ const filePath = path5.join(hooksOutDir, fileName);
1029
+ let content;
1030
+ switch (hookType) {
1031
+ case "query":
1032
+ case "suspenseQuery":
1033
+ content = generateQueryHook({
1034
+ canisterName: selectedCanister,
1035
+ method,
1036
+ config
1037
+ });
1038
+ break;
1039
+ case "infiniteQuery":
1040
+ case "suspenseInfiniteQuery":
1041
+ content = generateInfiniteQueryHook({
1042
+ canisterName: selectedCanister,
1043
+ method,
1044
+ config
1045
+ });
1046
+ break;
1047
+ case "mutation":
1048
+ content = generateMutationHook({
1049
+ canisterName: selectedCanister,
1050
+ method,
1051
+ config
1052
+ });
1053
+ break;
1054
+ }
1055
+ fs5.writeFileSync(filePath, content);
1056
+ generatedFiles.push(path5.join("hooks", fileName));
1057
+ }
1058
+ const indexPath = path5.join(hooksOutDir, "index.ts");
1059
+ let existingExports = [];
1060
+ if (fs5.existsSync(indexPath)) {
1061
+ const content = fs5.readFileSync(indexPath, "utf-8");
1062
+ existingExports = content.split("\n").filter((line) => line.trim().startsWith("export * from")).map((line) => line.trim());
1063
+ }
1064
+ const newExports = methodsWithHookTypes.map(({ method, hookType }) => {
1065
+ const fileName = getHookFileName(method.name, hookType).replace(".ts", "");
1066
+ return `export * from "./${fileName}"`;
1067
+ });
1068
+ const allExports = [.../* @__PURE__ */ new Set([...existingExports, ...newExports])];
1069
+ const indexContent = `/**
1070
+ * Hook barrel exports
1071
+ *
1072
+ * Auto-generated by @ic-reactor/cli
1073
+ */
1074
+
1075
+ ${allExports.join("\n")}
1076
+ `;
1077
+ fs5.writeFileSync(indexPath, indexContent);
1078
+ generatedFiles.push("hooks/index.ts");
1079
+ const existingHooks = config.generatedHooks[selectedCanister] ?? [];
1080
+ const newHookConfigs = methodsWithHookTypes.map(({ method, hookType }) => ({
1081
+ name: method.name,
1082
+ type: hookType
1083
+ }));
1084
+ const filteredExisting = existingHooks.filter((h) => {
1085
+ const name = typeof h === "string" ? h : h.name;
1086
+ return !newHookConfigs.some((n) => n.name === name);
1087
+ });
1088
+ config.generatedHooks[selectedCanister] = [
1089
+ ...filteredExisting,
1090
+ ...newHookConfigs
1091
+ ];
1092
+ saveConfig(config, configPath);
1093
+ spinner4.stop("Hooks generated!");
1094
+ console.log();
1095
+ p2.note(
1096
+ generatedFiles.map((f) => pc2.green(`\u2713 ${f}`)).join("\n"),
1097
+ `Generated in ${pc2.dim(path5.relative(projectRoot, canisterOutDir))}`
1098
+ );
1099
+ p2.outro(pc2.green("\u2713 Done!"));
1100
+ }
1101
+ async function promptForNewCanister(projectRoot) {
1102
+ const name = await p2.text({
1103
+ message: "Canister name",
1104
+ placeholder: "backend",
1105
+ validate: (value) => {
1106
+ if (!value) return "Canister name is required";
1107
+ return void 0;
1108
+ }
1109
+ });
1110
+ if (p2.isCancel(name)) return null;
1111
+ const didFile = await p2.text({
1112
+ message: "Path to .did file",
1113
+ placeholder: "./backend.did",
1114
+ validate: (value) => {
1115
+ if (!value) return "DID file path is required";
1116
+ const fullPath = path5.resolve(projectRoot, value);
1117
+ if (!fs5.existsSync(fullPath)) {
1118
+ return `File not found: ${value}`;
1119
+ }
1120
+ return void 0;
1121
+ }
1122
+ });
1123
+ if (p2.isCancel(didFile)) return null;
1124
+ return {
1125
+ name,
1126
+ config: {
1127
+ didFile,
1128
+ useDisplayReactor: true
1129
+ }
1130
+ };
1131
+ }
1132
+
1133
+ // src/commands/sync.ts
1134
+ import * as p3 from "@clack/prompts";
1135
+ import fs6 from "fs";
1136
+ import path6 from "path";
1137
+ import pc3 from "picocolors";
1138
+ async function syncCommand(options) {
1139
+ console.log();
1140
+ p3.intro(pc3.cyan("\u{1F504} Sync Canister Hooks"));
1141
+ const configPath = findConfigFile();
1142
+ if (!configPath) {
1143
+ p3.log.error(
1144
+ `No ${pc3.yellow("reactor.config.json")} found. Run ${pc3.cyan("npx @ic-reactor/cli init")} first.`
1145
+ );
1146
+ process.exit(1);
1147
+ }
1148
+ const config = loadConfig(configPath);
1149
+ if (!config) {
1150
+ p3.log.error(`Failed to load config from ${pc3.yellow(configPath)}`);
1151
+ process.exit(1);
1152
+ }
1153
+ const projectRoot = getProjectRoot();
1154
+ const canisterNames = Object.keys(config.canisters);
1155
+ if (canisterNames.length === 0) {
1156
+ p3.log.error("No canisters configured.");
1157
+ process.exit(1);
1158
+ }
1159
+ let canistersToSync;
1160
+ if (options.canister) {
1161
+ if (!config.canisters[options.canister]) {
1162
+ p3.log.error(
1163
+ `Canister ${pc3.yellow(options.canister)} not found in config.`
1164
+ );
1165
+ process.exit(1);
1166
+ }
1167
+ canistersToSync = [options.canister];
1168
+ } else {
1169
+ canistersToSync = canisterNames.filter(
1170
+ (name) => (config.generatedHooks[name]?.length ?? 0) > 0
1171
+ );
1172
+ if (canistersToSync.length === 0) {
1173
+ p3.log.warn("No hooks have been generated yet. Run `add` first.");
1174
+ process.exit(0);
1175
+ }
1176
+ }
1177
+ const spinner4 = p3.spinner();
1178
+ spinner4.start("Syncing hooks...");
1179
+ let totalUpdated = 0;
1180
+ let totalSkipped = 0;
1181
+ const errors = [];
1182
+ for (const canisterName of canistersToSync) {
1183
+ const canisterConfig = config.canisters[canisterName];
1184
+ const generatedMethods = config.generatedHooks[canisterName] ?? [];
1185
+ if (generatedMethods.length === 0) {
1186
+ continue;
1187
+ }
1188
+ const didFilePath = path6.resolve(projectRoot, canisterConfig.didFile);
1189
+ let methods;
1190
+ try {
1191
+ methods = parseDIDFile(didFilePath);
1192
+ } catch (error) {
1193
+ errors.push(
1194
+ `${canisterName}: Failed to parse DID file - ${error.message}`
1195
+ );
1196
+ continue;
1197
+ }
1198
+ const hooks = generatedMethods.map(
1199
+ (h) => typeof h === "string" ? { name: h } : h
1200
+ );
1201
+ const currentMethodNames = methods.map((m) => m.name);
1202
+ const removedMethods = hooks.filter((h) => !currentMethodNames.includes(h.name)).map((h) => h.name);
1203
+ if (removedMethods.length > 0) {
1204
+ p3.log.warn(
1205
+ `${canisterName}: Methods removed from DID: ${pc3.yellow(removedMethods.join(", "))}`
1206
+ );
1207
+ }
1208
+ const generatedNames = hooks.map((h) => h.name);
1209
+ const newMethods = methods.filter((m) => !generatedNames.includes(m.name));
1210
+ if (newMethods.length > 0) {
1211
+ p3.log.info(
1212
+ `${canisterName}: New methods available: ${pc3.cyan(newMethods.map((m) => m.name).join(", "))}`
1213
+ );
1214
+ }
1215
+ const canisterOutDir = path6.join(projectRoot, config.outDir, canisterName);
1216
+ const declarationsDir = path6.join(canisterOutDir, "declarations");
1217
+ const declarationsExist = fs6.existsSync(declarationsDir) && fs6.readdirSync(declarationsDir).some((f) => f.endsWith(".ts") || f.endsWith(".js"));
1218
+ if (!declarationsExist) {
1219
+ spinner4.message(`Regenerating declarations for ${canisterName}...`);
1220
+ const bindgenResult = await generateDeclarations({
1221
+ didFile: didFilePath,
1222
+ outDir: canisterOutDir,
1223
+ canisterName
1224
+ });
1225
+ if (bindgenResult.success) {
1226
+ totalUpdated++;
1227
+ } else {
1228
+ p3.log.warn(
1229
+ `Could not regenerate declarations for ${canisterName}: ${bindgenResult.error}`
1230
+ );
1231
+ }
1232
+ }
1233
+ const reactorPath = path6.join(canisterOutDir, "reactor.ts");
1234
+ const reactorContent = generateReactorFile({
1235
+ canisterName,
1236
+ canisterConfig,
1237
+ config,
1238
+ outDir: canisterOutDir
1239
+ });
1240
+ fs6.writeFileSync(reactorPath, reactorContent);
1241
+ totalUpdated++;
1242
+ const hooksOutDir = path6.join(canisterOutDir, "hooks");
1243
+ ensureDir(hooksOutDir);
1244
+ for (const hookConfig of hooks) {
1245
+ const methodName = hookConfig.name;
1246
+ const method = methods.find((m) => m.name === methodName);
1247
+ if (!method) {
1248
+ totalSkipped++;
1249
+ continue;
1250
+ }
1251
+ let hookType = hookConfig.type || method.type;
1252
+ if (!hookConfig.type) {
1253
+ const infiniteQueryFileName = getHookFileName(
1254
+ methodName,
1255
+ "infiniteQuery"
1256
+ );
1257
+ if (fs6.existsSync(path6.join(hooksOutDir, infiniteQueryFileName))) {
1258
+ hookType = "infiniteQuery";
1259
+ }
1260
+ }
1261
+ const fileName = getHookFileName(methodName, hookType);
1262
+ let content;
1263
+ if (hookType.includes("Query")) {
1264
+ content = generateQueryHook({
1265
+ canisterName,
1266
+ method,
1267
+ config,
1268
+ type: hookType
1269
+ });
1270
+ } else {
1271
+ content = generateMutationHook({
1272
+ canisterName,
1273
+ method,
1274
+ config
1275
+ });
1276
+ }
1277
+ const filePath = path6.join(hooksOutDir, fileName);
1278
+ if (fs6.existsSync(filePath)) {
1279
+ const existingContent = fs6.readFileSync(filePath, "utf-8");
1280
+ if (existingContent !== content) {
1281
+ totalSkipped++;
1282
+ continue;
1283
+ }
1284
+ }
1285
+ fs6.writeFileSync(filePath, content);
1286
+ totalUpdated++;
1287
+ }
1288
+ }
1289
+ spinner4.stop("Sync complete!");
1290
+ if (errors.length > 0) {
1291
+ console.log();
1292
+ p3.log.error("Errors encountered:");
1293
+ for (const error of errors) {
1294
+ console.log(` ${pc3.red("\u2022")} ${error}`);
1295
+ }
1296
+ }
1297
+ console.log();
1298
+ p3.note(
1299
+ `Updated: ${pc3.green(totalUpdated.toString())} files
1300
+ Skipped: ${pc3.dim(totalSkipped.toString())} files (preserved customizations)`,
1301
+ "Summary"
1302
+ );
1303
+ p3.outro(pc3.green("\u2713 Sync complete!"));
1304
+ }
1305
+
1306
+ // src/commands/list.ts
1307
+ import * as p4 from "@clack/prompts";
1308
+ import path7 from "path";
1309
+ import pc4 from "picocolors";
1310
+ async function listCommand(options) {
1311
+ console.log();
1312
+ p4.intro(pc4.cyan("\u{1F4CB} List Canister Methods"));
1313
+ const configPath = findConfigFile();
1314
+ if (!configPath) {
1315
+ p4.log.error(
1316
+ `No ${pc4.yellow("reactor.config.json")} found. Run ${pc4.cyan("npx @ic-reactor/cli init")} first.`
1317
+ );
1318
+ process.exit(1);
1319
+ }
1320
+ const config = loadConfig(configPath);
1321
+ if (!config) {
1322
+ p4.log.error(`Failed to load config from ${pc4.yellow(configPath)}`);
1323
+ process.exit(1);
1324
+ }
1325
+ const projectRoot = getProjectRoot();
1326
+ const canisterNames = Object.keys(config.canisters);
1327
+ if (canisterNames.length === 0) {
1328
+ p4.log.error(
1329
+ `No canisters configured. Add a canister to ${pc4.yellow("reactor.config.json")} first.`
1330
+ );
1331
+ process.exit(1);
1332
+ }
1333
+ let selectedCanister = options.canister;
1334
+ if (!selectedCanister) {
1335
+ if (canisterNames.length === 1) {
1336
+ selectedCanister = canisterNames[0];
1337
+ } else {
1338
+ const result = await p4.select({
1339
+ message: "Select a canister",
1340
+ options: canisterNames.map((name) => ({
1341
+ value: name,
1342
+ label: name
1343
+ }))
1344
+ });
1345
+ if (p4.isCancel(result)) {
1346
+ p4.cancel("Cancelled.");
1347
+ process.exit(0);
1348
+ }
1349
+ selectedCanister = result;
1350
+ }
1351
+ }
1352
+ const canisterConfig = config.canisters[selectedCanister];
1353
+ if (!canisterConfig) {
1354
+ p4.log.error(`Canister ${pc4.yellow(selectedCanister)} not found in config.`);
1355
+ process.exit(1);
1356
+ }
1357
+ const didFilePath = path7.resolve(projectRoot, canisterConfig.didFile);
1358
+ try {
1359
+ const methods = parseDIDFile(didFilePath);
1360
+ if (methods.length === 0) {
1361
+ p4.log.warn(`No methods found in ${pc4.yellow(didFilePath)}`);
1362
+ process.exit(0);
1363
+ }
1364
+ const queries = methods.filter((m) => m.type === "query");
1365
+ const mutations = methods.filter((m) => m.type === "mutation");
1366
+ const generatedMethods = config.generatedHooks[selectedCanister] ?? [];
1367
+ if (queries.length > 0) {
1368
+ console.log();
1369
+ console.log(pc4.bold(pc4.cyan(" Queries:")));
1370
+ for (const method of queries) {
1371
+ const isGenerated = generatedMethods.includes(method.name);
1372
+ const status = isGenerated ? pc4.green("\u2713") : pc4.dim("\u25CB");
1373
+ const argsHint = method.hasArgs ? pc4.dim("(args)") : pc4.dim("()");
1374
+ console.log(` ${status} ${method.name} ${argsHint}`);
1375
+ }
1376
+ }
1377
+ if (mutations.length > 0) {
1378
+ console.log();
1379
+ console.log(pc4.bold(pc4.yellow(" Mutations (Updates):")));
1380
+ for (const method of mutations) {
1381
+ const isGenerated = generatedMethods.includes(method.name);
1382
+ const status = isGenerated ? pc4.green("\u2713") : pc4.dim("\u25CB");
1383
+ const argsHint = method.hasArgs ? pc4.dim("(args)") : pc4.dim("()");
1384
+ console.log(` ${status} ${method.name} ${argsHint}`);
1385
+ }
1386
+ }
1387
+ console.log();
1388
+ const generatedCount = generatedMethods.length;
1389
+ const totalCount = methods.length;
1390
+ p4.note(
1391
+ `Total: ${pc4.bold(totalCount.toString())} methods
1392
+ Generated: ${pc4.green(generatedCount.toString())} / ${totalCount}
1393
+
1394
+ ${pc4.green("\u2713")} = hook generated
1395
+ ${pc4.dim("\u25CB")} = not yet generated`,
1396
+ selectedCanister
1397
+ );
1398
+ if (generatedCount < totalCount) {
1399
+ console.log();
1400
+ console.log(
1401
+ pc4.dim(
1402
+ ` Run ${pc4.cyan(`npx @ic-reactor/cli add -c ${selectedCanister}`)} to add hooks`
1403
+ )
1404
+ );
1405
+ }
1406
+ } catch (error) {
1407
+ p4.log.error(
1408
+ `Failed to parse DID file: ${pc4.yellow(didFilePath)}
1409
+ ${error.message}`
1410
+ );
1411
+ process.exit(1);
1412
+ }
1413
+ console.log();
1414
+ }
1415
+
1416
+ // src/commands/fetch.ts
1417
+ import * as p5 from "@clack/prompts";
1418
+ import fs7 from "fs";
1419
+ import path8 from "path";
1420
+ import pc5 from "picocolors";
1421
+
1422
+ // src/utils/network.ts
1423
+ import { HttpAgent, Actor } from "@icp-sdk/core/agent";
1424
+ import { Principal } from "@icp-sdk/core/principal";
1425
+ import { IDL } from "@icp-sdk/core/candid";
1426
+ var IC_HOST = "https://icp-api.io";
1427
+ var LOCAL_HOST = "http://127.0.0.1:4943";
1428
+ async function fetchCandidFromCanister(options) {
1429
+ const { canisterId, network = "ic", host } = options;
1430
+ let principal;
1431
+ try {
1432
+ principal = Principal.fromText(canisterId);
1433
+ } catch {
1434
+ throw new Error(`Invalid canister ID: ${canisterId}`);
1435
+ }
1436
+ const agentHost = host ?? (network === "local" ? LOCAL_HOST : IC_HOST);
1437
+ const agent = await HttpAgent.create({
1438
+ host: agentHost,
1439
+ // Don't verify signatures for CLI queries
1440
+ verifyQuerySignatures: false
1441
+ });
1442
+ if (network === "local") {
1443
+ try {
1444
+ await agent.fetchRootKey();
1445
+ } catch {
1446
+ throw new Error(
1447
+ `Failed to connect to local replica at ${agentHost}. Is it running?`
1448
+ );
1449
+ }
1450
+ }
1451
+ const candidInterface = IDL.Service({
1452
+ __get_candid_interface_tmp_hack: IDL.Func([], [IDL.Text], ["query"])
1453
+ });
1454
+ const actor = Actor.createActor(() => candidInterface, {
1455
+ agent,
1456
+ canisterId: principal
1457
+ });
1458
+ try {
1459
+ const candidSource = await actor.__get_candid_interface_tmp_hack();
1460
+ if (!candidSource || candidSource.trim() === "") {
1461
+ throw new Error("Canister returned empty Candid interface");
1462
+ }
1463
+ const methods = extractMethods(candidSource);
1464
+ return {
1465
+ candidSource,
1466
+ methods,
1467
+ canisterId,
1468
+ network
1469
+ };
1470
+ } catch (error) {
1471
+ const message = error instanceof Error ? error.message : String(error);
1472
+ if (message.includes("Replica Error")) {
1473
+ throw new Error(
1474
+ `Canister ${canisterId} does not expose a Candid interface. The canister may not support the __get_candid_interface_tmp_hack method.`
1475
+ );
1476
+ }
1477
+ if (message.includes("not found") || message.includes("404")) {
1478
+ throw new Error(`Canister ${canisterId} not found on ${network} network.`);
1479
+ }
1480
+ throw new Error(`Failed to fetch Candid from canister: ${message}`);
1481
+ }
1482
+ }
1483
+ function isValidCanisterId(canisterId) {
1484
+ try {
1485
+ Principal.fromText(canisterId);
1486
+ return true;
1487
+ } catch {
1488
+ return false;
1489
+ }
1490
+ }
1491
+ function shortenCanisterId(canisterId) {
1492
+ if (canisterId.length <= 15) return canisterId;
1493
+ return `${canisterId.slice(0, 5)}...${canisterId.slice(-5)}`;
1494
+ }
1495
+
1496
+ // src/commands/fetch.ts
1497
+ async function fetchCommand(options) {
1498
+ console.log();
1499
+ p5.intro(pc5.cyan("\u{1F310} Fetch from Live Canister"));
1500
+ const projectRoot = getProjectRoot();
1501
+ let canisterId = options.canisterId;
1502
+ if (!canisterId) {
1503
+ const input = await p5.text({
1504
+ message: "Enter canister ID",
1505
+ placeholder: "ryjl3-tyaaa-aaaaa-aaaba-cai",
1506
+ validate: (value) => {
1507
+ if (!value) return "Canister ID is required";
1508
+ if (!isValidCanisterId(value)) {
1509
+ return "Invalid canister ID format";
1510
+ }
1511
+ return void 0;
1512
+ }
1513
+ });
1514
+ if (p5.isCancel(input)) {
1515
+ p5.cancel("Cancelled.");
1516
+ process.exit(0);
1517
+ }
1518
+ canisterId = input;
1519
+ }
1520
+ if (!isValidCanisterId(canisterId)) {
1521
+ p5.log.error(`Invalid canister ID: ${pc5.yellow(canisterId)}`);
1522
+ process.exit(1);
1523
+ }
1524
+ let network = options.network;
1525
+ if (!network) {
1526
+ const result = await p5.select({
1527
+ message: "Select network",
1528
+ options: [
1529
+ { value: "ic", label: "IC Mainnet", hint: "icp-api.io" },
1530
+ { value: "local", label: "Local Replica", hint: "localhost:4943" }
1531
+ ]
1532
+ });
1533
+ if (p5.isCancel(result)) {
1534
+ p5.cancel("Cancelled.");
1535
+ process.exit(0);
1536
+ }
1537
+ network = result;
1538
+ }
1539
+ const spinner4 = p5.spinner();
1540
+ spinner4.start(`Fetching Candid from ${shortenCanisterId(canisterId)}...`);
1541
+ let candidResult;
1542
+ try {
1543
+ candidResult = await fetchCandidFromCanister({
1544
+ canisterId,
1545
+ network
1546
+ });
1547
+ spinner4.stop(
1548
+ `Found ${pc5.green(candidResult.methods.length.toString())} methods`
1549
+ );
1550
+ } catch (error) {
1551
+ spinner4.stop("Failed to fetch Candid");
1552
+ p5.log.error(error.message);
1553
+ process.exit(1);
1554
+ }
1555
+ const { methods, candidSource } = candidResult;
1556
+ if (methods.length === 0) {
1557
+ p5.log.warn("No methods found in canister interface");
1558
+ process.exit(0);
1559
+ }
1560
+ let canisterName = options.name;
1561
+ if (!canisterName) {
1562
+ const input = await p5.text({
1563
+ message: "Name for this canister (used in generated code)",
1564
+ placeholder: toCamelCase(canisterId.split("-")[0]),
1565
+ defaultValue: toCamelCase(canisterId.split("-")[0]),
1566
+ validate: (value) => {
1567
+ if (!value) return "Name is required";
1568
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(value)) {
1569
+ return "Name must start with a letter and contain only letters, numbers, hyphens, and underscores";
1570
+ }
1571
+ return void 0;
1572
+ }
1573
+ });
1574
+ if (p5.isCancel(input)) {
1575
+ p5.cancel("Cancelled.");
1576
+ process.exit(0);
1577
+ }
1578
+ canisterName = input;
1579
+ }
1580
+ let selectedMethods;
1581
+ if (options.all) {
1582
+ selectedMethods = methods;
1583
+ } else if (options.methods && options.methods.length > 0) {
1584
+ const requestedMethods = options.methods.flatMap((m) => m.split(",")).map((m) => m.trim()).filter((m) => m.length > 0);
1585
+ selectedMethods = methods.filter((m) => requestedMethods.includes(m.name));
1586
+ const notFound = requestedMethods.filter(
1587
+ (name) => !methods.some((m) => m.name === name)
1588
+ );
1589
+ if (notFound.length > 0) {
1590
+ p5.log.warn(`Methods not found: ${pc5.yellow(notFound.join(", "))}`);
1591
+ }
1592
+ } else {
1593
+ const result = await p5.multiselect({
1594
+ message: "Select methods to generate hooks for",
1595
+ options: methods.map((method) => ({
1596
+ value: method.name,
1597
+ label: formatMethodForDisplay(method)
1598
+ })),
1599
+ required: true
1600
+ });
1601
+ if (p5.isCancel(result)) {
1602
+ p5.cancel("Cancelled.");
1603
+ process.exit(0);
1604
+ }
1605
+ selectedMethods = methods.filter(
1606
+ (m) => result.includes(m.name)
1607
+ );
1608
+ }
1609
+ if (selectedMethods.length === 0) {
1610
+ p5.log.warn("No methods selected.");
1611
+ process.exit(0);
1612
+ }
1613
+ const methodsWithHookTypes = [];
1614
+ for (const method of selectedMethods) {
1615
+ if (method.type === "query") {
1616
+ const hookType = await p5.select({
1617
+ message: `Hook type for ${pc5.cyan(method.name)}`,
1618
+ options: [
1619
+ {
1620
+ value: "skip",
1621
+ label: "Skip",
1622
+ hint: "Don't generate hook for this method"
1623
+ },
1624
+ { value: "query", label: "Query", hint: "Standard query hook" },
1625
+ {
1626
+ value: "suspenseQuery",
1627
+ label: "Suspense Query",
1628
+ hint: "For React Suspense"
1629
+ },
1630
+ {
1631
+ value: "infiniteQuery",
1632
+ label: "Infinite Query",
1633
+ hint: "Paginated/infinite scroll"
1634
+ },
1635
+ {
1636
+ value: "suspenseInfiniteQuery",
1637
+ label: "Suspense Infinite Query",
1638
+ hint: "Paginated with Suspense"
1639
+ }
1640
+ ]
1641
+ });
1642
+ if (p5.isCancel(hookType)) {
1643
+ p5.cancel("Cancelled.");
1644
+ process.exit(0);
1645
+ }
1646
+ if (hookType === "skip") {
1647
+ p5.log.info(`Skipping ${pc5.dim(method.name)}`);
1648
+ continue;
1649
+ }
1650
+ methodsWithHookTypes.push({ method, hookType });
1651
+ } else {
1652
+ const hookType = await p5.select({
1653
+ message: `Hook type for ${pc5.yellow(method.name)} (mutation)`,
1654
+ options: [
1655
+ {
1656
+ value: "mutation",
1657
+ label: "Mutation",
1658
+ hint: "Standard mutation hook"
1659
+ },
1660
+ {
1661
+ value: "skip",
1662
+ label: "Skip",
1663
+ hint: "Don't generate hook for this method"
1664
+ }
1665
+ ]
1666
+ });
1667
+ if (p5.isCancel(hookType)) {
1668
+ p5.cancel("Cancelled.");
1669
+ process.exit(0);
1670
+ }
1671
+ if (hookType === "skip") {
1672
+ p5.log.info(`Skipping ${pc5.dim(method.name)}`);
1673
+ continue;
1674
+ }
1675
+ methodsWithHookTypes.push({ method, hookType: "mutation" });
1676
+ }
1677
+ }
1678
+ if (methodsWithHookTypes.length === 0) {
1679
+ p5.log.warn("All methods were skipped. Nothing to generate.");
1680
+ process.exit(0);
1681
+ }
1682
+ let configPath = findConfigFile();
1683
+ let config;
1684
+ if (!configPath) {
1685
+ configPath = path8.join(projectRoot, CONFIG_FILE_NAME);
1686
+ config = { ...DEFAULT_CONFIG };
1687
+ p5.log.info(`Creating ${pc5.yellow(CONFIG_FILE_NAME)}`);
1688
+ } else {
1689
+ config = loadConfig(configPath) ?? { ...DEFAULT_CONFIG };
1690
+ }
1691
+ const canisterConfig = {
1692
+ didFile: `./candid/${canisterName}.did`,
1693
+ useDisplayReactor: true,
1694
+ canisterId
1695
+ };
1696
+ config.canisters[canisterName] = canisterConfig;
1697
+ const canisterOutDir = path8.join(projectRoot, config.outDir, canisterName);
1698
+ const hooksOutDir = path8.join(canisterOutDir, "hooks");
1699
+ const candidDir = path8.join(projectRoot, "candid");
1700
+ ensureDir(hooksOutDir);
1701
+ ensureDir(candidDir);
1702
+ const genSpinner = p5.spinner();
1703
+ genSpinner.start("Generating hooks...");
1704
+ const generatedFiles = [];
1705
+ const candidPath = path8.join(candidDir, `${canisterName}.did`);
1706
+ fs7.writeFileSync(candidPath, candidSource);
1707
+ generatedFiles.push(`candid/${canisterName}.did`);
1708
+ genSpinner.message("Generating TypeScript declarations...");
1709
+ const bindgenResult = await generateDeclarations({
1710
+ didFile: candidPath,
1711
+ outDir: canisterOutDir,
1712
+ canisterName
1713
+ });
1714
+ if (bindgenResult.success) {
1715
+ generatedFiles.push("declarations/");
1716
+ } else {
1717
+ p5.log.warn(`Could not generate declarations: ${bindgenResult.error}`);
1718
+ p5.log.info(
1719
+ `You can manually run: npx @icp-sdk/bindgen --input ${candidPath} --output ${canisterOutDir}/declarations`
1720
+ );
1721
+ }
1722
+ genSpinner.message("Generating reactor...");
1723
+ const reactorPath = path8.join(canisterOutDir, "reactor.ts");
1724
+ const reactorContent = generateReactorFileForFetch({
1725
+ canisterName,
1726
+ canisterConfig,
1727
+ config,
1728
+ canisterId,
1729
+ hasDeclarations: bindgenResult.success
1730
+ });
1731
+ fs7.writeFileSync(reactorPath, reactorContent);
1732
+ generatedFiles.push("reactor.ts");
1733
+ for (const { method, hookType } of methodsWithHookTypes) {
1734
+ const fileName = getHookFileName(method.name, hookType);
1735
+ const filePath = path8.join(hooksOutDir, fileName);
1736
+ let content;
1737
+ switch (hookType) {
1738
+ case "query":
1739
+ case "suspenseQuery":
1740
+ content = generateQueryHook({
1741
+ canisterName,
1742
+ method,
1743
+ config
1744
+ });
1745
+ break;
1746
+ case "infiniteQuery":
1747
+ case "suspenseInfiniteQuery":
1748
+ content = generateInfiniteQueryHook({
1749
+ canisterName,
1750
+ method,
1751
+ config
1752
+ });
1753
+ break;
1754
+ case "mutation":
1755
+ content = generateMutationHook({
1756
+ canisterName,
1757
+ method,
1758
+ config
1759
+ });
1760
+ break;
1761
+ }
1762
+ fs7.writeFileSync(filePath, content);
1763
+ generatedFiles.push(path8.join("hooks", fileName));
1764
+ }
1765
+ const indexPath = path8.join(hooksOutDir, "index.ts");
1766
+ const indexContent = generateIndexFile(methodsWithHookTypes);
1767
+ fs7.writeFileSync(indexPath, indexContent);
1768
+ generatedFiles.push("hooks/index.ts");
1769
+ config.generatedHooks[canisterName] = [
1770
+ .../* @__PURE__ */ new Set([
1771
+ ...config.generatedHooks[canisterName] ?? [],
1772
+ ...selectedMethods.map((m) => m.name)
1773
+ ])
1774
+ ];
1775
+ saveConfig(config, configPath);
1776
+ genSpinner.stop("Hooks generated!");
1777
+ console.log();
1778
+ p5.note(
1779
+ generatedFiles.map((f) => pc5.green(`\u2713 ${f}`)).join("\n"),
1780
+ `Generated in ${pc5.dim(path8.relative(projectRoot, canisterOutDir))}`
1781
+ );
1782
+ console.log();
1783
+ p5.note(
1784
+ `Canister ID: ${pc5.cyan(canisterId)}
1785
+ Network: ${pc5.yellow(network)}
1786
+ Name: ${pc5.green(canisterName)}
1787
+ Methods: ${pc5.dim(selectedMethods.map((m) => m.name).join(", "))}`,
1788
+ "Canister Info"
1789
+ );
1790
+ p5.outro(pc5.green("\u2713 Done!"));
1791
+ }
1792
+ function generateReactorFileForFetch(options) {
1793
+ const { canisterName, canisterConfig, config, canisterId, hasDeclarations } = options;
1794
+ const pascalName = canisterName.charAt(0).toUpperCase() + canisterName.slice(1);
1795
+ const reactorName = `${canisterName}Reactor`;
1796
+ const serviceName = `${pascalName}Service`;
1797
+ const reactorType = canisterConfig.useDisplayReactor !== false ? "DisplayReactor" : "Reactor";
1798
+ const clientManagerPath = canisterConfig.clientManagerPath ?? config.clientManagerPath ?? "../../lib/client";
1799
+ const didFileName = path8.basename(canisterConfig.didFile);
1800
+ if (hasDeclarations) {
1801
+ return `/**
1802
+ * ${pascalName} Reactor
1803
+ *
1804
+ * Auto-generated by @ic-reactor/cli
1805
+ * Fetched from canister: ${canisterId}
1806
+ *
1807
+ * You can customize this file to add global configuration.
1808
+ */
1809
+
1810
+ import { ${reactorType}, createActorHooks } from "@ic-reactor/react"
1811
+ import { clientManager } from "${clientManagerPath}"
1812
+
1813
+ // Import generated declarations
1814
+ import { idlFactory, type _SERVICE as ${serviceName} } from "./declarations/${didFileName}"
1815
+
1816
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1817
+ // REACTOR INSTANCE
1818
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1819
+
1820
+ /**
1821
+ * ${pascalName} Reactor
1822
+ *
1823
+ * Canister ID: ${canisterId}
1824
+ */
1825
+ export const ${reactorName} = new ${reactorType}<${serviceName}>({
1826
+ clientManager,
1827
+ idlFactory,
1828
+ canisterId: "${canisterId}",
1829
+ name: "${canisterName}",
1830
+ })
1831
+
1832
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1833
+ // ACTOR HOOKS
1834
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1835
+
1836
+ /**
1837
+ * Actor hooks for ${canisterName} - use these directly or import method-specific hooks.
1838
+ */
1839
+ export const {
1840
+ useActorQuery,
1841
+ useActorMutation,
1842
+ useActorSuspenseQuery,
1843
+ useActorInfiniteQuery,
1844
+ useActorSuspenseInfiniteQuery,
1845
+ useActorMethod,
1846
+ } = createActorHooks(${reactorName})
1847
+
1848
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1849
+ // RE-EXPORTS
1850
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1851
+
1852
+ export { idlFactory }
1853
+ export type { ${serviceName} }
1854
+ `;
1855
+ }
1856
+ return `/**
1857
+ * ${pascalName} Reactor
1858
+ *
1859
+ * Auto-generated by @ic-reactor/cli
1860
+ * Fetched from canister: ${canisterId}
1861
+ *
1862
+ * You can customize this file to add global configuration.
1863
+ */
1864
+
1865
+ import { ${reactorType}, createActorHooks } from "@ic-reactor/react"
1866
+ import { clientManager } from "${clientManagerPath}"
1867
+
1868
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1869
+ // DECLARATIONS
1870
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1871
+
1872
+ // TODO: Generate proper types by running:
1873
+ // npx @icp-sdk/bindgen --input ./candid/${canisterName}.did --output ./${canisterName}/declarations
1874
+
1875
+ // For now, import just the IDL factory (you may need to create this manually)
1876
+ // import { idlFactory, type _SERVICE as ${serviceName} } from "./declarations/${didFileName}"
1877
+
1878
+ // Fallback generic type - replace with generated types
1879
+ type ${serviceName} = Record<string, (...args: unknown[]) => Promise<unknown>>
1880
+
1881
+ // You'll need to define idlFactory here or import from declarations
1882
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1883
+ const idlFactory = ({ IDL }: { IDL: any }) => IDL.Service({})
1884
+
1885
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1886
+ // REACTOR INSTANCE
1887
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1888
+
1889
+ /**
1890
+ * ${pascalName} Reactor
1891
+ *
1892
+ * Canister ID: ${canisterId}
1893
+ */
1894
+ export const ${reactorName} = new ${reactorType}<${serviceName}>({
1895
+ clientManager,
1896
+ idlFactory,
1897
+ canisterId: "${canisterId}",
1898
+ name: "${canisterName}",
1899
+ })
1900
+
1901
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1902
+ // ACTOR HOOKS
1903
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1904
+
1905
+ /**
1906
+ * Actor hooks for ${canisterName} - use these directly or import method-specific hooks.
1907
+ */
1908
+ export const {
1909
+ useActorQuery,
1910
+ useActorMutation,
1911
+ useActorSuspenseQuery,
1912
+ useActorInfiniteQuery,
1913
+ useActorSuspenseInfiniteQuery,
1914
+ useActorMethod,
1915
+ } = createActorHooks(${reactorName})
1916
+
1917
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1918
+ // RE-EXPORTS
1919
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1920
+
1921
+ export { idlFactory }
1922
+ export type { ${serviceName} }
1923
+ `;
1924
+ }
1925
+ function generateIndexFile(methods) {
1926
+ const exports = methods.map(({ method, hookType }) => {
1927
+ const fileName = getHookFileName(method.name, hookType).replace(".ts", "");
1928
+ return `export * from "./${fileName}"`;
1929
+ });
1930
+ return `/**
1931
+ * Hook barrel exports
1932
+ *
1933
+ * Auto-generated by @ic-reactor/cli
1934
+ */
1935
+
1936
+ ${exports.join("\n")}
1937
+ `;
1938
+ }
1939
+
1940
+ // src/index.ts
1941
+ import pc6 from "picocolors";
1942
+ var program = new Command();
1943
+ program.name("ic-reactor").description(
1944
+ pc6.cyan("\u{1F527} Generate shadcn-style React hooks for ICP canisters")
1945
+ ).version("3.0.0");
1946
+ program.command("init").description("Initialize ic-reactor configuration in your project").option("-y, --yes", "Skip prompts and use defaults").option("-o, --out-dir <path>", "Output directory for generated hooks").action(initCommand);
1947
+ program.command("add").description("Add hooks for canister methods (from local .did file)").option("-c, --canister <name>", "Canister name to add hooks for").option("-m, --methods <methods...>", "Method names to generate hooks for").option("-a, --all", "Add hooks for all methods").action(addCommand);
1948
+ program.command("fetch").description("Fetch Candid from a live canister and generate hooks").option("-i, --canister-id <id>", "Canister ID to fetch from").option("-n, --network <network>", "Network: 'ic' or 'local'", "ic").option("--name <name>", "Name for the canister in generated code").option("-m, --methods <methods...>", "Method names to generate hooks for").option("-a, --all", "Add hooks for all methods").action(fetchCommand);
1949
+ program.command("sync").description("Sync hooks with .did file changes").option("-c, --canister <name>", "Canister to sync").action(syncCommand);
1950
+ program.command("list").description("List available methods from a canister").option("-c, --canister <name>", "Canister to list methods from").action(listCommand);
1951
+ program.parse();