@ic-reactor/cli 0.0.0-dev

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