ima2-gen 1.1.7 → 1.1.9
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 +56 -27
- package/bin/commands/annotate.js +137 -0
- package/bin/commands/annotate.ts +118 -0
- package/bin/commands/cancel.js +37 -33
- package/bin/commands/cancel.ts +45 -0
- package/bin/commands/canvas-versions.js +91 -0
- package/bin/commands/canvas-versions.ts +80 -0
- package/bin/commands/cardnews.js +293 -0
- package/bin/commands/cardnews.ts +248 -0
- package/bin/commands/comfy.js +63 -0
- package/bin/commands/comfy.ts +54 -0
- package/bin/commands/config.js +270 -0
- package/bin/commands/config.ts +265 -0
- package/bin/commands/edit.js +97 -72
- package/bin/commands/edit.ts +116 -0
- package/bin/commands/gen.js +140 -118
- package/bin/commands/gen.ts +176 -0
- package/bin/commands/history.js +164 -0
- package/bin/commands/history.ts +145 -0
- package/bin/commands/ls.js +60 -42
- package/bin/commands/ls.ts +60 -0
- package/bin/commands/metadata.js +45 -0
- package/bin/commands/metadata.ts +36 -0
- package/bin/commands/multimode.js +159 -0
- package/bin/commands/multimode.ts +146 -0
- package/bin/commands/node.js +176 -0
- package/bin/commands/node.ts +157 -0
- package/bin/commands/observability.js +201 -0
- package/bin/commands/observability.ts +176 -0
- package/bin/commands/ping.js +26 -20
- package/bin/commands/ping.ts +29 -0
- package/bin/commands/prompt.js +506 -0
- package/bin/commands/prompt.ts +421 -0
- package/bin/commands/ps.js +78 -71
- package/bin/commands/ps.ts +78 -0
- package/bin/commands/session.js +308 -0
- package/bin/commands/session.ts +265 -0
- package/bin/commands/show.js +75 -40
- package/bin/commands/show.ts +69 -0
- package/bin/ima2.js +324 -310
- package/bin/ima2.ts +444 -0
- package/bin/lib/args.js +75 -66
- package/bin/lib/args.ts +73 -0
- package/bin/lib/browser-id.js +15 -0
- package/bin/lib/browser-id.ts +16 -0
- package/bin/lib/client.js +91 -83
- package/bin/lib/client.ts +109 -0
- package/bin/lib/error-hints.js +14 -17
- package/bin/lib/error-hints.ts +23 -0
- package/bin/lib/files.js +26 -28
- package/bin/lib/files.ts +39 -0
- package/bin/lib/output.js +44 -42
- package/bin/lib/output.ts +58 -0
- package/bin/lib/platform.js +60 -56
- package/bin/lib/platform.ts +97 -0
- package/bin/lib/sse.js +73 -0
- package/bin/lib/sse.ts +73 -0
- package/bin/lib/star-prompt.js +69 -76
- package/bin/lib/star-prompt.ts +97 -0
- package/bin/lib/storage-doctor.js +34 -35
- package/bin/lib/storage-doctor.ts +38 -0
- package/config.js +147 -190
- package/config.ts +331 -0
- package/docs/API.md +48 -8
- package/docs/CLI.md +190 -0
- package/docs/FAQ.ko.md +5 -5
- package/docs/FAQ.md +5 -5
- package/docs/README.ja.md +71 -25
- package/docs/README.ko.md +61 -24
- package/docs/README.zh-CN.md +73 -27
- package/lib/assetLifecycle.js +130 -130
- package/lib/assetLifecycle.ts +142 -0
- package/lib/canvasVersionStore.js +135 -153
- package/lib/canvasVersionStore.ts +181 -0
- package/lib/cardNewsGenerator.js +127 -142
- package/lib/cardNewsGenerator.ts +162 -0
- package/lib/cardNewsJobStore.js +78 -84
- package/lib/cardNewsJobStore.ts +107 -0
- package/lib/cardNewsManifestStore.js +88 -93
- package/lib/cardNewsManifestStore.ts +112 -0
- package/lib/cardNewsPlanner.js +157 -152
- package/lib/cardNewsPlanner.ts +180 -0
- package/lib/cardNewsPlannerClient.js +101 -98
- package/lib/cardNewsPlannerClient.ts +114 -0
- package/lib/cardNewsPlannerPrompt.js +56 -56
- package/lib/cardNewsPlannerPrompt.ts +60 -0
- package/lib/cardNewsPlannerSchema.js +231 -223
- package/lib/cardNewsPlannerSchema.ts +259 -0
- package/lib/cardNewsRoleTemplateStore.js +39 -41
- package/lib/cardNewsRoleTemplateStore.ts +47 -0
- package/lib/cardNewsTemplateStore.js +171 -175
- package/lib/cardNewsTemplateStore.ts +210 -0
- package/lib/codexDetect.js +44 -47
- package/lib/codexDetect.ts +69 -0
- package/lib/comfyBridge.js +164 -184
- package/lib/comfyBridge.ts +214 -0
- package/lib/db.js +41 -51
- package/lib/db.ts +166 -0
- package/lib/errorClassify.js +62 -78
- package/lib/errorClassify.ts +100 -0
- package/lib/generationErrors.js +140 -103
- package/lib/generationErrors.ts +125 -0
- package/lib/historyList.js +149 -147
- package/lib/historyList.ts +164 -0
- package/lib/imageMetadata.js +86 -89
- package/lib/imageMetadata.ts +111 -0
- package/lib/imageMetadataStore.js +46 -51
- package/lib/imageMetadataStore.ts +67 -0
- package/lib/imageModels.js +38 -45
- package/lib/imageModels.ts +52 -0
- package/lib/inflight.js +131 -150
- package/lib/inflight.ts +204 -0
- package/lib/localImportStore.js +105 -0
- package/lib/localImportStore.ts +111 -0
- package/lib/logger.js +105 -112
- package/lib/logger.ts +150 -0
- package/lib/nodeStore.js +65 -64
- package/lib/nodeStore.ts +81 -0
- package/lib/oauthLauncher.js +61 -59
- package/lib/oauthLauncher.ts +64 -0
- package/lib/oauthNormalize.js +15 -19
- package/lib/oauthNormalize.ts +30 -0
- package/lib/oauthProxy.js +834 -832
- package/lib/oauthProxy.ts +995 -0
- package/lib/openDirectory.js +41 -40
- package/lib/openDirectory.ts +45 -0
- package/lib/pngInfo.js +18 -20
- package/lib/pngInfo.ts +26 -0
- package/lib/promptImport/curatedSources.js +135 -0
- package/lib/promptImport/curatedSources.ts +139 -0
- package/lib/promptImport/discoveryRegistry.js +218 -0
- package/lib/promptImport/discoveryRegistry.ts +236 -0
- package/lib/promptImport/errors.js +10 -10
- package/lib/promptImport/errors.ts +18 -0
- package/lib/promptImport/githubDiscovery.js +238 -0
- package/lib/promptImport/githubDiscovery.ts +248 -0
- package/lib/promptImport/githubFolder.js +302 -0
- package/lib/promptImport/githubFolder.ts +308 -0
- package/lib/promptImport/githubSource.js +194 -171
- package/lib/promptImport/githubSource.ts +239 -0
- package/lib/promptImport/gptImageHints.js +61 -0
- package/lib/promptImport/gptImageHints.ts +68 -0
- package/lib/promptImport/parsePromptCandidates.js +110 -112
- package/lib/promptImport/parsePromptCandidates.ts +153 -0
- package/lib/promptImport/promptIndex.js +230 -0
- package/lib/promptImport/promptIndex.ts +248 -0
- package/lib/promptImport/rankPromptCandidates.js +52 -0
- package/lib/promptImport/rankPromptCandidates.ts +49 -0
- package/lib/providerOptions.js +31 -0
- package/lib/providerOptions.ts +41 -0
- package/lib/referenceImageCompress.js +51 -62
- package/lib/referenceImageCompress.ts +75 -0
- package/lib/refs.js +93 -81
- package/lib/refs.ts +117 -0
- package/lib/requestLogger.js +32 -38
- package/lib/requestLogger.ts +48 -0
- package/lib/responsesImageAdapter.js +351 -0
- package/lib/responsesImageAdapter.ts +352 -0
- package/lib/runtimePorts.js +71 -73
- package/lib/runtimePorts.ts +93 -0
- package/lib/sessionStore.js +179 -230
- package/lib/sessionStore.ts +272 -0
- package/lib/storageMigration.js +247 -245
- package/lib/storageMigration.ts +284 -0
- package/lib/styleSheet.js +86 -90
- package/lib/styleSheet.ts +128 -0
- package/lib/systemTrash.js +18 -0
- package/lib/systemTrash.ts +20 -0
- package/package.json +26 -10
- package/routes/annotations.js +76 -79
- package/routes/annotations.ts +95 -0
- package/routes/canvasVersions.js +50 -54
- package/routes/canvasVersions.ts +64 -0
- package/routes/cardNews.js +158 -171
- package/routes/cardNews.ts +183 -0
- package/routes/comfy.js +23 -31
- package/routes/comfy.ts +39 -0
- package/routes/edit.js +183 -214
- package/routes/edit.ts +230 -0
- package/routes/generate.js +269 -291
- package/routes/generate.ts +309 -0
- package/routes/health.js +102 -107
- package/routes/health.ts +114 -0
- package/routes/history.js +136 -144
- package/routes/history.ts +153 -0
- package/routes/imageImport.js +33 -0
- package/routes/imageImport.ts +33 -0
- package/routes/index.js +18 -16
- package/routes/index.ts +35 -0
- package/routes/metadata.js +60 -64
- package/routes/metadata.ts +71 -0
- package/routes/multimode.js +228 -263
- package/routes/multimode.ts +280 -0
- package/routes/nodes.js +378 -424
- package/routes/nodes.ts +455 -0
- package/routes/promptImport.js +291 -152
- package/routes/promptImport.ts +354 -0
- package/routes/prompts.js +333 -360
- package/routes/prompts.ts +379 -0
- package/routes/sessions.js +277 -285
- package/routes/sessions.ts +292 -0
- package/routes/storage.js +29 -31
- package/routes/storage.ts +39 -0
- package/server.js +189 -196
- package/server.ts +235 -0
- package/ui/dist/.vite/manifest.json +101 -0
- package/ui/dist/assets/CardNewsWorkspace-BJOCey7Z.js +2 -0
- package/ui/dist/assets/NodeCanvas-BZV40eAE.css +1 -0
- package/ui/dist/assets/NodeCanvas-C3dzYNsk.js +7 -0
- package/ui/dist/assets/PromptImportDialog-Dqu1VpUh.js +2 -0
- package/ui/dist/assets/PromptImportDiscoverySection-Dg8T9X0L.js +1 -0
- package/ui/dist/assets/PromptImportFolderSection-DBaqsFO4.js +1 -0
- package/ui/dist/assets/PromptLibraryPanel-p5QqR97M.js +2 -0
- package/ui/dist/assets/SettingsWorkspace-B5bSAZ6u.js +1 -0
- package/ui/dist/assets/index-C9cXwiWE.js +25 -0
- package/ui/dist/assets/index-CGMIkZXn.css +1 -0
- package/ui/dist/assets/index-Cvld7dUZ.js +1 -0
- package/ui/dist/index.html +6 -3
- package/assets/screenshot.png +0 -0
- package/assets/screenshots/classic-generate-light.png +0 -0
- package/assets/screenshots/node-graph-branching.png +0 -0
- package/assets/screenshots/settings-oauth-generation.png +0 -0
- package/assets/screenshots/settings-workspace.png +0 -0
- package/assets/screenshots/style-sheet-editor.png +0 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
- package/ui/dist/assets/index-DARPdT4Q.css +0 -1
- package/ui/dist/assets/index-ht80GMq4.js +0 -31
- package/ui/dist/assets/index-ht80GMq4.js.map +0 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
`ima2-gen` is a local image generation studio for people who want the ChatGPT/Codex image workflow in a small desktop-like web app.
|
|
12
12
|
|
|
13
|
-
Run it with `npx`, sign in with Codex OAuth, type a prompt, and keep iterating with history, references,
|
|
13
|
+
Run it with `npx`, sign in with Codex OAuth, type a prompt, and keep iterating with history, references, node branches, multimode batches, and Canvas Mode cleanup. No OpenAI API key is required for the default path, but API-key generation is also supported when configured.
|
|
14
14
|
|
|
15
15
|

|
|
16
16
|
|
|
@@ -47,20 +47,25 @@ persists, reboot and run the update before starting ima2 again.
|
|
|
47
47
|
|
|
48
48
|
- **Classic mode**: generate, edit, reuse the current image, paste references, and continue from history.
|
|
49
49
|
- **Node mode**: branch a good image into multiple directions without losing the original.
|
|
50
|
+
- **Multimode batches**: launch several Classic outputs from one prompt, watch slot-by-slot progress, and continue from the best result.
|
|
51
|
+
- **Canvas Mode**: zoom, pan, annotate, erase, clean backgrounds, keep transparent previews, and export either alpha or matte-backed versions.
|
|
50
52
|
- **Local gallery**: keep generated assets on your machine with session-aware history.
|
|
51
53
|
- **Reference images**: drag, drop, paste, and attach up to 5 references; large images are compressed before upload.
|
|
52
|
-
- **
|
|
54
|
+
- **Prompt library imports**: import local prompt packs, GitHub folders, and curated GPT-image prompt hints into the built-in prompt library.
|
|
55
|
+
- **Mobile shell**: use the app bar, compose sheet, and compact settings toggle on smaller screens.
|
|
53
56
|
- **Observable jobs**: active and recent jobs are tracked with safe logs and request IDs.
|
|
54
57
|
|
|
55
|
-
##
|
|
58
|
+
## Provider Paths
|
|
56
59
|
|
|
57
|
-
Image generation
|
|
60
|
+
Image generation can run through either the local Codex/ChatGPT OAuth path or a configured OpenAI API key.
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
- `provider: "oauth"` uses the local Codex OAuth proxy.
|
|
63
|
+
- `provider: "api"` calls the OpenAI Responses API with the hosted `image_generation` tool.
|
|
64
|
+
- API-key generation supports classic generate, edit, mask-guided edit, multimode, and node generation.
|
|
60
65
|
|
|
61
|
-
If
|
|
66
|
+
If no provider is specified, the app keeps the current OAuth/default behavior. API-key generation defaults to `gpt-5.4-mini`, `low` reasoning, and `1024x1024` unless the request passes validated model, reasoning, size, or web-search options.
|
|
62
67
|
|
|
63
|
-

|
|
64
69
|
|
|
65
70
|
## Model Guidance
|
|
66
71
|
|
|
@@ -81,7 +86,10 @@ Use Classic when you want one strong result quickly.
|
|
|
81
86
|
1. Write a prompt.
|
|
82
87
|
2. Attach or paste references if needed.
|
|
83
88
|
3. Pick model, quality, size, format, and moderation.
|
|
84
|
-
4. Generate
|
|
89
|
+
4. Generate one image, or enable multimode to fan out several candidate slots from the same prompt.
|
|
90
|
+
5. Copy, download, continue from the result, or send it into Canvas Mode.
|
|
91
|
+
|
|
92
|
+

|
|
85
93
|
|
|
86
94
|
### Node Mode
|
|
87
95
|
|
|
@@ -91,22 +99,36 @@ Use Node mode when you want to explore branches.
|
|
|
91
99
|
|
|
92
100
|
Each node keeps its own prompt and result. Root nodes can attach local references; child nodes use the parent image as their source. Completed jobs are matched back to nodes by request ID, so reloads and graph version conflicts can recover finished results.
|
|
93
101
|
|
|
102
|
+
### Canvas Mode
|
|
103
|
+
|
|
104
|
+
Use Canvas Mode when a generated image is close but needs targeted cleanup before the next prompt.
|
|
105
|
+
|
|
106
|
+
- Separate viewport panning from selection so you can move around a zoomed image without accidentally changing annotations.
|
|
107
|
+
- Use annotation, eraser, multiselect, grouping, undo/redo, and sticky notes while keeping the original gallery image available.
|
|
108
|
+
- Pick background-cleanup seeds, preview the mask, and save the cleanup as a canvas version.
|
|
109
|
+
- Detect transparent images and show a checkerboard preview; export with preserved alpha or with a chosen matte color.
|
|
110
|
+
- Saved canvas versions stay hidden from Gallery and HistoryStrip, but Canvas Mode can reuse them and attach a canvas version as the next reference.
|
|
111
|
+
|
|
112
|
+

|
|
113
|
+
|
|
114
|
+
### Prompt Library And Imports
|
|
115
|
+
|
|
116
|
+
The prompt library can now be filled from local files, GitHub folders, curated sources, and GPT-image hint packs. Imported prompts are indexed locally so search and ranking work without re-importing the same source every session.
|
|
117
|
+
|
|
118
|
+

|
|
119
|
+
|
|
94
120
|
### Experimental Card News Mode
|
|
95
121
|
|
|
96
122
|
Card News is still dev-only and experimental. It is hidden in the default
|
|
97
123
|
published runtime unless explicitly enabled for development, and it should not
|
|
98
124
|
be treated as a stable public feature yet.
|
|
99
125
|
|
|
100
|
-
### Settings
|
|
126
|
+
### Settings
|
|
101
127
|
|
|
102
128
|
The settings workspace keeps account, model, appearance, and language controls away from the generation sidebar.
|
|
103
129
|
|
|
104
130
|

|
|
105
131
|
|
|
106
|
-
Style sheets let you capture a reusable visual direction.
|
|
107
|
-
|
|
108
|
-

|
|
109
|
-
|
|
110
132
|
## CLI Commands
|
|
111
133
|
|
|
112
134
|
### Server
|
|
@@ -122,27 +144,32 @@ Style sheets let you capture a reusable visual direction.
|
|
|
122
144
|
|
|
123
145
|
### Client
|
|
124
146
|
|
|
125
|
-
These require a running `ima2 serve`.
|
|
147
|
+
These require a running `ima2 serve`. The CLI covers every server route. The most common ones are below — the [full CLI reference](docs/CLI.md) lists everything (generation, history, sessions, prompt library, annotations, Card News, observability, config).
|
|
126
148
|
|
|
127
149
|
| Command | Description |
|
|
128
150
|
|---|---|
|
|
129
151
|
| `ima2 gen <prompt>` | Generate from the CLI |
|
|
130
152
|
| `ima2 edit <file> --prompt <text>` | Edit an existing image |
|
|
131
|
-
| `ima2
|
|
132
|
-
| `ima2
|
|
133
|
-
| `ima2
|
|
134
|
-
| `ima2
|
|
153
|
+
| `ima2 multimode <prompt>` | Multi-image SSE generation |
|
|
154
|
+
| `ima2 ls [--session <id>] [--favorites]` | List recent history |
|
|
155
|
+
| `ima2 show <name> [--metadata]` | Reveal a generated asset |
|
|
156
|
+
| `ima2 prompt ls -q <search>` | Search the prompt library |
|
|
157
|
+
| `ima2 inflight ls [--terminal]` | List active and recent jobs (alias of `ps`) |
|
|
158
|
+
| `ima2 config set <key> <value>` | Write to `~/.ima2/config.json` |
|
|
135
159
|
| `ima2 ping` | Health-check the running server |
|
|
136
160
|
|
|
137
|
-
The server advertises its actual port at `~/.ima2/server.json`. If `3333` is busy, the backend
|
|
161
|
+
The server advertises its actual port at `~/.ima2/server.json`. If `3333` is busy, the backend falls back to `3334+` and CLI commands follow the advertised URL. Override discovery with `--server <url>` or `IMA2_SERVER=http://localhost:3333`.
|
|
138
162
|
|
|
139
163
|
```bash
|
|
140
|
-
ima2 gen "poster" --model gpt-5.4 --
|
|
141
|
-
ima2 edit input.png --prompt "make it rainy" --
|
|
142
|
-
ima2
|
|
143
|
-
ima2
|
|
164
|
+
ima2 gen "poster" --model gpt-5.4 --reasoning-effort high
|
|
165
|
+
ima2 edit input.png --prompt "make it rainy" --web-search
|
|
166
|
+
ima2 multimode "two cats playing" -n 2
|
|
167
|
+
ima2 inflight ls --terminal
|
|
168
|
+
ima2 config set imageModels.reasoningEffort high
|
|
144
169
|
```
|
|
145
170
|
|
|
171
|
+
Full reference: [docs/CLI.md](docs/CLI.md).
|
|
172
|
+
|
|
146
173
|
## Configuration
|
|
147
174
|
|
|
148
175
|
Config priority:
|
|
@@ -164,7 +191,7 @@ environment variables > ~/.ima2/config.json > built-in defaults
|
|
|
164
191
|
| `IMA2_NO_OAUTH_PROXY` | — | Set `1` to disable the auto-started OAuth proxy |
|
|
165
192
|
| `IMA2_LOG_LEVEL` | `warn` | Normal serve defaults to `warn`; dev mode defaults to `debug`; supports `debug`, `info`, `warn`, `error`, or `silent` |
|
|
166
193
|
| `IMA2_INFLIGHT_TERMINAL_TTL_MS` | `30000` | Recent terminal job retention for debug views |
|
|
167
|
-
| `OPENAI_API_KEY` | — | API key for
|
|
194
|
+
| `OPENAI_API_KEY` | — | API key for the `provider: "api"` Responses API image path and auxiliary API-key features |
|
|
168
195
|
|
|
169
196
|
### Logging modes
|
|
170
197
|
|
|
@@ -178,6 +205,7 @@ The endpoint list moved to [docs/API.md](docs/API.md) so this README can stay fo
|
|
|
178
205
|
|
|
179
206
|
Useful references:
|
|
180
207
|
|
|
208
|
+
- [CLI Reference](docs/CLI.md)
|
|
181
209
|
- [API Reference](docs/API.md)
|
|
182
210
|
- [FAQ](docs/FAQ.md)
|
|
183
211
|
- [Recover old images](docs/RECOVER_OLD_IMAGES.md)
|
|
@@ -196,8 +224,8 @@ Run `npx @openai/codex login`, confirm `ima2 status`, then restart `ima2 serve`.
|
|
|
196
224
|
**`fetch failed` repeats on a proxy/VPN network**
|
|
197
225
|
Check that the local OAuth proxy is reachable. On networks that require a proxy, enable your proxy client's TUN/TURN-style mode, then retry `npx openai-oauth --port 10531`. If it still fails, set `HTTP_PROXY` and `HTTPS_PROXY` in the same terminal that runs `ima2 serve` or `openai-oauth`.
|
|
198
226
|
|
|
199
|
-
**Images fail with `
|
|
200
|
-
|
|
227
|
+
**Images fail with `API_KEY_REQUIRED`**
|
|
228
|
+
Set `OPENAI_API_KEY` or configure an API key before using `provider: "api"`. The default OAuth path still works without an API key.
|
|
201
229
|
|
|
202
230
|
**A large reference image fails**
|
|
203
231
|
The app compresses large JPEG/PNG references before upload. If a file still fails, convert it to JPEG or PNG at a lower resolution and try again. HEIC/HEIF files are not supported by the browser path.
|
|
@@ -223,11 +251,12 @@ git clone https://github.com/lidge-jun/ima2-gen.git
|
|
|
223
251
|
cd ima2-gen
|
|
224
252
|
npm install
|
|
225
253
|
npm run dev
|
|
254
|
+
npm run typecheck
|
|
226
255
|
npm test
|
|
227
256
|
npm run build
|
|
228
257
|
```
|
|
229
258
|
|
|
230
|
-
`npm run dev` builds the UI and starts
|
|
259
|
+
`npm run dev` builds the UI and starts the TypeScript server entry with `--watch` and verbose server diagnostics. `npm run typecheck`, `npm run build:server`, and `npm run build:cli` verify the TypeScript migration and package emit path. Node mode and Canvas Mode are part of the packaged UI by default.
|
|
231
260
|
|
|
232
261
|
## License
|
|
233
262
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import { parseArgs } from "../lib/args.js";
|
|
3
|
+
import { resolveServer, request } from "../lib/client.js";
|
|
4
|
+
import { readStdin } from "../lib/files.js";
|
|
5
|
+
import { out, die, color, json, exitCodeForError } from "../lib/output.js";
|
|
6
|
+
import { getCliBrowserId } from "../lib/browser-id.js";
|
|
7
|
+
const HELP = `
|
|
8
|
+
ima2 annotate <subcommand> <filename> [options]
|
|
9
|
+
|
|
10
|
+
Subcommands:
|
|
11
|
+
get <filename> [--json]
|
|
12
|
+
set <filename> --body <json|@file|->
|
|
13
|
+
rm <filename> [--yes]
|
|
14
|
+
`;
|
|
15
|
+
const FLAGS = {
|
|
16
|
+
json: { type: "boolean" },
|
|
17
|
+
server: { type: "string" },
|
|
18
|
+
yes: { type: "boolean" },
|
|
19
|
+
body: { type: "string" },
|
|
20
|
+
help: { short: "h", type: "boolean" },
|
|
21
|
+
};
|
|
22
|
+
async function getServer(args) {
|
|
23
|
+
try {
|
|
24
|
+
return await resolveServer({ serverFlag: args.server });
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
die(exitCodeForError(e), e.message);
|
|
28
|
+
throw e;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function resolveBody(value) {
|
|
32
|
+
if (!value)
|
|
33
|
+
return null;
|
|
34
|
+
let text;
|
|
35
|
+
if (value === "-")
|
|
36
|
+
text = await readStdin();
|
|
37
|
+
else if (value.startsWith("@"))
|
|
38
|
+
text = await readFile(value.slice(1), "utf-8");
|
|
39
|
+
else
|
|
40
|
+
text = value;
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(text);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
die(2, "--body must be valid JSON");
|
|
46
|
+
throw 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function readLine() {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
let buf = "";
|
|
52
|
+
process.stdin.setEncoding("utf-8");
|
|
53
|
+
const onData = (chunk) => {
|
|
54
|
+
buf += chunk;
|
|
55
|
+
const nl = buf.indexOf("\n");
|
|
56
|
+
if (nl !== -1) {
|
|
57
|
+
process.stdin.removeListener("data", onData);
|
|
58
|
+
process.stdin.pause();
|
|
59
|
+
resolve(buf.slice(0, nl));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
process.stdin.resume();
|
|
63
|
+
process.stdin.on("data", onData);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function getSub(argv) {
|
|
67
|
+
const args = parseArgs(argv, { flags: FLAGS });
|
|
68
|
+
const filename = args.positional[0];
|
|
69
|
+
if (!filename)
|
|
70
|
+
die(2, "filename required");
|
|
71
|
+
const server = await getServer(args);
|
|
72
|
+
const browserId = getCliBrowserId();
|
|
73
|
+
const resp = await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
|
|
74
|
+
headers: { "X-Ima2-Browser-Id": browserId },
|
|
75
|
+
}).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
|
|
76
|
+
json(resp);
|
|
77
|
+
}
|
|
78
|
+
async function setSub(argv) {
|
|
79
|
+
const args = parseArgs(argv, { flags: FLAGS });
|
|
80
|
+
const filename = args.positional[0];
|
|
81
|
+
if (!filename)
|
|
82
|
+
die(2, "filename required");
|
|
83
|
+
if (!args.body)
|
|
84
|
+
die(2, "--body <json|@file|-> required");
|
|
85
|
+
const body = await resolveBody(args.body);
|
|
86
|
+
const server = await getServer(args);
|
|
87
|
+
const browserId = getCliBrowserId();
|
|
88
|
+
const resp = await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
|
|
89
|
+
method: "PUT",
|
|
90
|
+
body,
|
|
91
|
+
headers: { "X-Ima2-Browser-Id": browserId },
|
|
92
|
+
}).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
|
|
93
|
+
if (args.json) {
|
|
94
|
+
json(resp);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
out(color.green("✓ annotation saved"));
|
|
98
|
+
}
|
|
99
|
+
async function rmSub(argv) {
|
|
100
|
+
const args = parseArgs(argv, { flags: FLAGS });
|
|
101
|
+
const filename = args.positional[0];
|
|
102
|
+
if (!filename)
|
|
103
|
+
die(2, "filename required");
|
|
104
|
+
if (!args.yes && !process.stdin.isTTY)
|
|
105
|
+
die(2, "destructive: pass --yes for non-TTY");
|
|
106
|
+
if (!args.yes) {
|
|
107
|
+
process.stdout.write(`Delete annotation for ${filename}? [y/N] `);
|
|
108
|
+
const ans = await readLine();
|
|
109
|
+
if (!/^y(es)?$/i.test(ans.trim())) {
|
|
110
|
+
out("(canceled)");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const server = await getServer(args);
|
|
115
|
+
const browserId = getCliBrowserId();
|
|
116
|
+
await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
|
|
117
|
+
method: "DELETE",
|
|
118
|
+
headers: { "X-Ima2-Browser-Id": browserId },
|
|
119
|
+
}).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
|
|
120
|
+
out(color.green("✓ deleted"));
|
|
121
|
+
}
|
|
122
|
+
const SUB = {
|
|
123
|
+
get: getSub,
|
|
124
|
+
set: setSub,
|
|
125
|
+
rm: rmSub,
|
|
126
|
+
};
|
|
127
|
+
export default async function annotateCmd(argv) {
|
|
128
|
+
const sub = argv[0];
|
|
129
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
130
|
+
out(HELP);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const handler = SUB[sub];
|
|
134
|
+
if (!handler)
|
|
135
|
+
die(2, `unknown subcommand: ${sub}\n${HELP}`);
|
|
136
|
+
return handler(argv.slice(1));
|
|
137
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import { parseArgs } from "../lib/args.js";
|
|
3
|
+
import { resolveServer, request } from "../lib/client.js";
|
|
4
|
+
import { readStdin } from "../lib/files.js";
|
|
5
|
+
import { out, die, color, json, exitCodeForError } from "../lib/output.js";
|
|
6
|
+
import { getCliBrowserId } from "../lib/browser-id.js";
|
|
7
|
+
|
|
8
|
+
const HELP = `
|
|
9
|
+
ima2 annotate <subcommand> <filename> [options]
|
|
10
|
+
|
|
11
|
+
Subcommands:
|
|
12
|
+
get <filename> [--json]
|
|
13
|
+
set <filename> --body <json|@file|->
|
|
14
|
+
rm <filename> [--yes]
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const FLAGS = {
|
|
18
|
+
json: { type: "boolean" },
|
|
19
|
+
server: { type: "string" },
|
|
20
|
+
yes: { type: "boolean" },
|
|
21
|
+
body: { type: "string" },
|
|
22
|
+
help: { short: "h", type: "boolean" },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
async function getServer(args) {
|
|
26
|
+
try { return await resolveServer({ serverFlag: args.server }); }
|
|
27
|
+
catch (e: any) { die(exitCodeForError(e), e.message); throw e; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function resolveBody(value): Promise<any> {
|
|
31
|
+
if (!value) return null;
|
|
32
|
+
let text;
|
|
33
|
+
if (value === "-") text = await readStdin();
|
|
34
|
+
else if (value.startsWith("@")) text = await readFile(value.slice(1), "utf-8");
|
|
35
|
+
else text = value;
|
|
36
|
+
try { return JSON.parse(text); }
|
|
37
|
+
catch { die(2, "--body must be valid JSON"); throw 0; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function readLine(): Promise<string> {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
let buf = "";
|
|
43
|
+
process.stdin.setEncoding("utf-8");
|
|
44
|
+
const onData = (chunk) => {
|
|
45
|
+
buf += chunk;
|
|
46
|
+
const nl = buf.indexOf("\n");
|
|
47
|
+
if (nl !== -1) {
|
|
48
|
+
process.stdin.removeListener("data", onData);
|
|
49
|
+
process.stdin.pause();
|
|
50
|
+
resolve(buf.slice(0, nl));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
process.stdin.resume();
|
|
54
|
+
process.stdin.on("data", onData);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function getSub(argv) {
|
|
59
|
+
const args = parseArgs(argv, { flags: FLAGS });
|
|
60
|
+
const filename = args.positional[0];
|
|
61
|
+
if (!filename) die(2, "filename required");
|
|
62
|
+
const server = await getServer(args);
|
|
63
|
+
const browserId = getCliBrowserId();
|
|
64
|
+
const resp = await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
|
|
65
|
+
headers: { "X-Ima2-Browser-Id": browserId },
|
|
66
|
+
}).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
|
|
67
|
+
json(resp);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function setSub(argv) {
|
|
71
|
+
const args = parseArgs(argv, { flags: FLAGS });
|
|
72
|
+
const filename = args.positional[0];
|
|
73
|
+
if (!filename) die(2, "filename required");
|
|
74
|
+
if (!args.body) die(2, "--body <json|@file|-> required");
|
|
75
|
+
const body = await resolveBody(args.body);
|
|
76
|
+
const server = await getServer(args);
|
|
77
|
+
const browserId = getCliBrowserId();
|
|
78
|
+
const resp = await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
|
|
79
|
+
method: "PUT",
|
|
80
|
+
body,
|
|
81
|
+
headers: { "X-Ima2-Browser-Id": browserId },
|
|
82
|
+
}).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
|
|
83
|
+
if (args.json) { json(resp); return; }
|
|
84
|
+
out(color.green("✓ annotation saved"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function rmSub(argv) {
|
|
88
|
+
const args = parseArgs(argv, { flags: FLAGS });
|
|
89
|
+
const filename = args.positional[0];
|
|
90
|
+
if (!filename) die(2, "filename required");
|
|
91
|
+
if (!args.yes && !process.stdin.isTTY) die(2, "destructive: pass --yes for non-TTY");
|
|
92
|
+
if (!args.yes) {
|
|
93
|
+
process.stdout.write(`Delete annotation for ${filename}? [y/N] `);
|
|
94
|
+
const ans = await readLine();
|
|
95
|
+
if (!/^y(es)?$/i.test(ans.trim())) { out("(canceled)"); return; }
|
|
96
|
+
}
|
|
97
|
+
const server = await getServer(args);
|
|
98
|
+
const browserId = getCliBrowserId();
|
|
99
|
+
await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
|
|
100
|
+
method: "DELETE",
|
|
101
|
+
headers: { "X-Ima2-Browser-Id": browserId },
|
|
102
|
+
}).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
|
|
103
|
+
out(color.green("✓ deleted"));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const SUB: Record<string, (argv: any[]) => Promise<void>> = {
|
|
107
|
+
get: getSub,
|
|
108
|
+
set: setSub,
|
|
109
|
+
rm: rmSub,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default async function annotateCmd(argv) {
|
|
113
|
+
const sub = argv[0];
|
|
114
|
+
if (!sub || sub === "--help" || sub === "-h") { out(HELP); return; }
|
|
115
|
+
const handler = SUB[sub];
|
|
116
|
+
if (!handler) die(2, `unknown subcommand: ${sub}\n${HELP}`);
|
|
117
|
+
return handler(argv.slice(1));
|
|
118
|
+
}
|
package/bin/commands/cancel.js
CHANGED
|
@@ -1,45 +1,49 @@
|
|
|
1
1
|
import { parseArgs } from "../lib/args.js";
|
|
2
2
|
import { resolveServer, request } from "../lib/client.js";
|
|
3
3
|
import { out, die, dieWithError, color, json } from "../lib/output.js";
|
|
4
|
-
|
|
5
4
|
const SPEC = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
flags: {
|
|
6
|
+
json: { type: "boolean" },
|
|
7
|
+
server: { type: "string" },
|
|
8
|
+
help: { short: "h", type: "boolean" },
|
|
9
|
+
},
|
|
11
10
|
};
|
|
12
|
-
|
|
13
11
|
const HELP = `
|
|
14
12
|
ima2 cancel <requestId> [--json]
|
|
15
13
|
|
|
16
14
|
Mark an in-flight job as canceled in the local ima2 server registry.
|
|
17
15
|
`;
|
|
18
|
-
|
|
19
16
|
export default async function cancelCmd(argv) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
17
|
+
const args = parseArgs(argv, SPEC);
|
|
18
|
+
if (args.help) {
|
|
19
|
+
out(HELP);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const requestId = args.positional[0];
|
|
23
|
+
if (!requestId)
|
|
24
|
+
die(2, "requestId required");
|
|
25
|
+
let server;
|
|
26
|
+
try {
|
|
27
|
+
server = await resolveServer({ serverFlag: args.server });
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
if (args.json)
|
|
31
|
+
json({ ok: false, requestId, error: e.message, code: e.code, status: e.status });
|
|
32
|
+
dieWithError(e);
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
await request(server.base, `/api/inflight/${encodeURIComponent(requestId)}`, {
|
|
36
|
+
method: "DELETE",
|
|
37
|
+
timeoutMs: 30_000,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
if (args.json)
|
|
42
|
+
json({ ok: false, requestId, error: e.message, code: e.code, status: e.status });
|
|
43
|
+
dieWithError(e);
|
|
44
|
+
}
|
|
45
|
+
if (args.json)
|
|
46
|
+
json({ ok: true, requestId });
|
|
47
|
+
else
|
|
48
|
+
out(color.green("✓ ") + `canceled ${requestId}`);
|
|
45
49
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { parseArgs } from "../lib/args.js";
|
|
2
|
+
import { resolveServer, request } from "../lib/client.js";
|
|
3
|
+
import { out, die, dieWithError, color, json } from "../lib/output.js";
|
|
4
|
+
|
|
5
|
+
const SPEC = {
|
|
6
|
+
flags: {
|
|
7
|
+
json: { type: "boolean" },
|
|
8
|
+
server: { type: "string" },
|
|
9
|
+
help: { short: "h", type: "boolean" },
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const HELP = `
|
|
14
|
+
ima2 cancel <requestId> [--json]
|
|
15
|
+
|
|
16
|
+
Mark an in-flight job as canceled in the local ima2 server registry.
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
export default async function cancelCmd(argv) {
|
|
20
|
+
const args = parseArgs(argv, SPEC);
|
|
21
|
+
if (args.help) { out(HELP); return; }
|
|
22
|
+
|
|
23
|
+
const requestId = args.positional[0];
|
|
24
|
+
if (!requestId) die(2, "requestId required");
|
|
25
|
+
|
|
26
|
+
let server;
|
|
27
|
+
try { server = await resolveServer({ serverFlag: args.server }); }
|
|
28
|
+
catch (e) {
|
|
29
|
+
if (args.json) json({ ok: false, requestId, error: e.message, code: e.code, status: e.status });
|
|
30
|
+
dieWithError(e);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await request(server.base, `/api/inflight/${encodeURIComponent(requestId)}`, {
|
|
35
|
+
method: "DELETE",
|
|
36
|
+
timeoutMs: 30_000,
|
|
37
|
+
});
|
|
38
|
+
} catch (e) {
|
|
39
|
+
if (args.json) json({ ok: false, requestId, error: e.message, code: e.code, status: e.status });
|
|
40
|
+
dieWithError(e);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (args.json) json({ ok: true, requestId });
|
|
44
|
+
else out(color.green("✓ ") + `canceled ${requestId}`);
|
|
45
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import { parseArgs } from "../lib/args.js";
|
|
3
|
+
import { resolveServer, request } from "../lib/client.js";
|
|
4
|
+
import { out, die, color, json, exitCodeForError } from "../lib/output.js";
|
|
5
|
+
const HELP = `
|
|
6
|
+
ima2 canvas-versions <subcommand> [options]
|
|
7
|
+
|
|
8
|
+
Subcommands:
|
|
9
|
+
save <imagefile> [--source <filename>] [--prompt <text>]
|
|
10
|
+
update <filename> <imagefile> [--source <filename>] [--prompt <text>]
|
|
11
|
+
|
|
12
|
+
Notes: server only exposes POST /api/canvas-versions (collection) and
|
|
13
|
+
PUT /api/canvas-versions/:filename. No GET, no DELETE.
|
|
14
|
+
`;
|
|
15
|
+
const FLAGS = {
|
|
16
|
+
json: { type: "boolean" },
|
|
17
|
+
server: { type: "string" },
|
|
18
|
+
source: { type: "string" },
|
|
19
|
+
prompt: { type: "string" },
|
|
20
|
+
help: { short: "h", type: "boolean" },
|
|
21
|
+
};
|
|
22
|
+
async function getServer(args) {
|
|
23
|
+
try {
|
|
24
|
+
return await resolveServer({ serverFlag: args.server });
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
die(exitCodeForError(e), e.message);
|
|
28
|
+
throw e;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function buildHeaders(args) {
|
|
32
|
+
const h = { "Content-Type": "image/png" };
|
|
33
|
+
if (args.source)
|
|
34
|
+
h["X-Ima2-Canvas-Source-Filename"] = args.source;
|
|
35
|
+
if (args.prompt)
|
|
36
|
+
h["X-Ima2-Canvas-Prompt"] = args.prompt;
|
|
37
|
+
return h;
|
|
38
|
+
}
|
|
39
|
+
async function saveSub(argv) {
|
|
40
|
+
const args = parseArgs(argv, { flags: FLAGS });
|
|
41
|
+
const file = args.positional[0];
|
|
42
|
+
if (!file)
|
|
43
|
+
die(2, "image file required");
|
|
44
|
+
const buf = await readFile(file);
|
|
45
|
+
const server = await getServer(args);
|
|
46
|
+
const resp = await request(server.base, "/api/canvas-versions", {
|
|
47
|
+
method: "POST",
|
|
48
|
+
body: buf,
|
|
49
|
+
raw: true,
|
|
50
|
+
headers: buildHeaders(args),
|
|
51
|
+
}).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
|
|
52
|
+
if (args.json) {
|
|
53
|
+
json(resp);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
out(color.green("✓ saved canvas version"));
|
|
57
|
+
}
|
|
58
|
+
async function updateSub(argv) {
|
|
59
|
+
const args = parseArgs(argv, { flags: FLAGS });
|
|
60
|
+
const [filename, file] = args.positional;
|
|
61
|
+
if (!filename || !file)
|
|
62
|
+
die(2, "usage: canvas-versions update <filename> <imagefile>");
|
|
63
|
+
const buf = await readFile(file);
|
|
64
|
+
const server = await getServer(args);
|
|
65
|
+
const resp = await request(server.base, `/api/canvas-versions/${encodeURIComponent(filename)}`, {
|
|
66
|
+
method: "PUT",
|
|
67
|
+
body: buf,
|
|
68
|
+
raw: true,
|
|
69
|
+
headers: buildHeaders(args),
|
|
70
|
+
}).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
|
|
71
|
+
if (args.json) {
|
|
72
|
+
json(resp);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
out(color.green("✓ updated"));
|
|
76
|
+
}
|
|
77
|
+
const SUB = {
|
|
78
|
+
save: saveSub,
|
|
79
|
+
update: updateSub,
|
|
80
|
+
};
|
|
81
|
+
export default async function canvasVersionsCmd(argv) {
|
|
82
|
+
const sub = argv[0];
|
|
83
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
84
|
+
out(HELP);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const handler = SUB[sub];
|
|
88
|
+
if (!handler)
|
|
89
|
+
die(2, `unknown subcommand: ${sub}\n${HELP}`);
|
|
90
|
+
return handler(argv.slice(1));
|
|
91
|
+
}
|