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