iterate 0.2.2 → 0.2.4
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 +10 -10
- package/bin/iterate.js +328 -440
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -29,11 +29,10 @@ Initial setup (writes auth + launcher config):
|
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
npx iterate setup \
|
|
32
|
-
--base-url https://dev-yourname-os.dev.iterate.com \
|
|
32
|
+
--os-base-url https://dev-yourname-os.dev.iterate.com \
|
|
33
|
+
--daemon-base-url http://localhost:3001 \
|
|
33
34
|
--admin-password-env-var-name SERVICE_AUTH_TOKEN \
|
|
34
35
|
--user-email dev-yourname@iterate.com \
|
|
35
|
-
--repo-path managed \
|
|
36
|
-
--auto-install true \
|
|
37
36
|
--scope global
|
|
38
37
|
```
|
|
39
38
|
|
|
@@ -51,6 +50,9 @@ npx iterate os project list
|
|
|
51
50
|
- `iterate install` - force clone/install for resolved checkout
|
|
52
51
|
- `iterate whoami`
|
|
53
52
|
- `iterate os ...`
|
|
53
|
+
- `iterate daemon ...`
|
|
54
|
+
|
|
55
|
+
`setup --scope global` writes auth + launcher values into `global`; `setup --scope workspace` writes them into `workspaces[process.cwd()]`.
|
|
54
56
|
|
|
55
57
|
## Config file
|
|
56
58
|
|
|
@@ -70,11 +72,10 @@ Config shape:
|
|
|
70
72
|
},
|
|
71
73
|
"workspaces": {
|
|
72
74
|
"/absolute/workspace/path": {
|
|
73
|
-
"
|
|
75
|
+
"osBaseUrl": "https://dev-yourname-os.dev.iterate.com",
|
|
76
|
+
"daemonBaseUrl": "http://localhost:3001",
|
|
74
77
|
"adminPasswordEnvVarName": "SERVICE_AUTH_TOKEN",
|
|
75
|
-
"userEmail": "dev-yourname@iterate.com"
|
|
76
|
-
"repoPath": "/absolute/path/to/iterate",
|
|
77
|
-
"autoInstall": false
|
|
78
|
+
"userEmail": "dev-yourname@iterate.com"
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
}
|
|
@@ -114,11 +115,10 @@ You can pin explicitly:
|
|
|
114
115
|
|
|
115
116
|
```bash
|
|
116
117
|
npx iterate setup \
|
|
117
|
-
--base-url https://dev-yourname-os.dev.iterate.com \
|
|
118
|
+
--os-base-url https://dev-yourname-os.dev.iterate.com \
|
|
119
|
+
--daemon-base-url http://localhost:3001 \
|
|
118
120
|
--admin-password-env-var-name SERVICE_AUTH_TOKEN \
|
|
119
121
|
--user-email dev-yourname@iterate.com \
|
|
120
|
-
--repo-path local \
|
|
121
|
-
--auto-install false \
|
|
122
122
|
--scope workspace
|
|
123
123
|
```
|
|
124
124
|
|
package/bin/iterate.js
CHANGED
|
@@ -1,66 +1,130 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// @ts-check
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import { dirname,
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
8
|
import process from "node:process";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import { initTRPC } from "@trpc/server";
|
|
13
|
-
import { createAuthClient } from "better-auth/client";
|
|
14
|
-
import { adminClient } from "better-auth/client/plugins";
|
|
15
|
-
import superjson from "superjson";
|
|
16
|
-
import { createCli } from "trpc-cli";
|
|
17
|
-
import { proxify } from "trpc-cli/dist/proxify.js";
|
|
18
|
-
import { z } from "zod/v4";
|
|
19
|
-
|
|
20
|
-
const DEFAULT_REPO_URL = "https://github.com/iterate/iterate.git";
|
|
21
|
-
const XDG_CONFIG_PATH = join(
|
|
22
|
-
process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : join(homedir(), ".config"),
|
|
23
|
-
"iterate",
|
|
24
|
-
"config.json",
|
|
25
|
-
);
|
|
26
|
-
const XDG_REPO_DIR = join(
|
|
27
|
-
process.env.XDG_DATA_HOME ? process.env.XDG_DATA_HOME : join(homedir(), ".local", "share"),
|
|
28
|
-
"iterate",
|
|
29
|
-
"repo",
|
|
30
|
-
);
|
|
31
|
-
const DEFAULT_REPO_DIR = XDG_REPO_DIR;
|
|
32
|
-
const CONFIG_PATH = XDG_CONFIG_PATH;
|
|
33
|
-
const APP_ROUTER_PATH = join("apps", "os", "backend", "trpc", "root.ts");
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
// --- Local delegation (must run before any heavy imports) ---
|
|
34
12
|
|
|
35
13
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* }} LauncherConfig
|
|
14
|
+
* Walk up from `startDir` looking for `relativePath` to exist.
|
|
15
|
+
* Returns the directory where it was found, or null.
|
|
16
|
+
* @param {string} relativePath
|
|
17
|
+
* @param {string} [startDir]
|
|
18
|
+
* @returns {string | null}
|
|
42
19
|
*/
|
|
20
|
+
const findUp = (relativePath, startDir = process.cwd()) => {
|
|
21
|
+
let dir = resolve(startDir);
|
|
22
|
+
while (true) {
|
|
23
|
+
if (existsSync(join(dir, relativePath))) return dir;
|
|
24
|
+
const parent = dirname(dir);
|
|
25
|
+
if (parent === dir) return null;
|
|
26
|
+
dir = parent;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
43
31
|
|
|
44
32
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* workspaces?: Record<string, Record<string, unknown>>;
|
|
48
|
-
* } & Record<string, unknown>} ConfigFile
|
|
33
|
+
* If we're already running from a local version, skip delegation to avoid loops.
|
|
34
|
+
* Otherwise, find the closest local iterate CLI and re-exec into it.
|
|
49
35
|
*/
|
|
36
|
+
const delegateToLocal = () => {
|
|
37
|
+
if (process.env.__ITERATE_CLI_DELEGATED) return;
|
|
38
|
+
|
|
39
|
+
const selfReal = realpathSync(__filename);
|
|
40
|
+
|
|
41
|
+
// 1. Check if we're inside the iterate repo (has pnpm-workspace.yaml at root)
|
|
42
|
+
const repoRoot = findUp("pnpm-workspace.yaml");
|
|
43
|
+
if (repoRoot) {
|
|
44
|
+
const repoScript = join(repoRoot, "packages/iterate/bin/iterate.js");
|
|
45
|
+
if (existsSync(repoScript) && realpathSync(repoScript) !== selfReal) {
|
|
46
|
+
reExec(repoScript);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// 2. Check for a local node_modules install
|
|
52
|
+
const nmRoot = findUp("node_modules/.bin/iterate");
|
|
53
|
+
if (nmRoot) {
|
|
54
|
+
const nmScript = join(nmRoot, "node_modules/.bin/iterate");
|
|
55
|
+
if (existsSync(nmScript) && realpathSync(nmScript) !== selfReal) {
|
|
56
|
+
reExec(nmScript);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
52
61
|
|
|
53
62
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* repoDirSource: RepoDirSource;
|
|
57
|
-
* repoRef?: string;
|
|
58
|
-
* repoUrl: string;
|
|
59
|
-
* autoInstall: boolean;
|
|
60
|
-
* cwdRepoDir?: string;
|
|
61
|
-
* launcherConfig: LauncherConfig;
|
|
62
|
-
* }} RuntimeOptions
|
|
63
|
+
* Re-exec into `scriptPath` with the same argv, never returning.
|
|
64
|
+
* @param {string} scriptPath
|
|
63
65
|
*/
|
|
66
|
+
const reExec = (scriptPath) => {
|
|
67
|
+
try {
|
|
68
|
+
execFileSync(process.execPath, [scriptPath, ...process.argv.slice(2)], {
|
|
69
|
+
stdio: "inherit",
|
|
70
|
+
env: { ...process.env, __ITERATE_CLI_DELEGATED: "1" },
|
|
71
|
+
});
|
|
72
|
+
} catch (e) {
|
|
73
|
+
process.exit(e && typeof e === "object" && "status" in e ? Number(e.status) || 1 : 1);
|
|
74
|
+
}
|
|
75
|
+
process.exit(0);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
delegateToLocal();
|
|
79
|
+
|
|
80
|
+
// --- Normal CLI startup (dynamic imports so delegation can short-circuit first) ---
|
|
81
|
+
|
|
82
|
+
const prompts = await import("@clack/prompts");
|
|
83
|
+
const { createTRPCClient, httpLink } = await import("@trpc/client");
|
|
84
|
+
const { initTRPC } = await import("@trpc/server");
|
|
85
|
+
const { createAuthClient } = await import("better-auth/client");
|
|
86
|
+
const { adminClient } = await import("better-auth/client/plugins");
|
|
87
|
+
const { default: superjson } = await import("superjson");
|
|
88
|
+
const { createCli } = await import("trpc-cli");
|
|
89
|
+
const { proxify } = await import("trpc-cli/dist/proxify.js");
|
|
90
|
+
const { z } = await import("zod/v4");
|
|
91
|
+
|
|
92
|
+
const XDG_CONFIG_PARENT = join(
|
|
93
|
+
process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : join(homedir(), ".config"),
|
|
94
|
+
"iterate",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const XDG_CONFIG_PATH = join(XDG_CONFIG_PARENT, "config.json");
|
|
98
|
+
const CONFIG_PATH = XDG_CONFIG_PATH;
|
|
99
|
+
// todo write json schema to file too - need to make everything zod first
|
|
100
|
+
// const CONFIG_SCHEMA_PATH = join(XDG_CONFIG_PARENT, "config-schema.json");
|
|
101
|
+
|
|
102
|
+
const SetupInput = z.object({
|
|
103
|
+
osBaseUrl: z
|
|
104
|
+
.string()
|
|
105
|
+
.describe(`Base URL for OS API (for example https://dev-yourname-os.dev.iterate.com)`),
|
|
106
|
+
daemonBaseUrl: z.string().describe(`Base URL for daemon API (for example http://localhost:3001)`),
|
|
107
|
+
adminPasswordEnvVarName: z.string().describe("Env var name containing admin password"),
|
|
108
|
+
userEmail: z.string().describe("User email to impersonate for OS calls"),
|
|
109
|
+
scope: z.enum(["workspace", "global"]).describe("Where to store launcher config"),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const AuthConfig = z.object({
|
|
113
|
+
osBaseUrl: z.string(),
|
|
114
|
+
daemonBaseUrl: z.string(),
|
|
115
|
+
adminPasswordEnvVarName: z.string(),
|
|
116
|
+
userEmail: z.string(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const ConfigFile = z.object({
|
|
120
|
+
global: AuthConfig.partial().optional(),
|
|
121
|
+
workspaces: z.record(z.string(), AuthConfig).optional(),
|
|
122
|
+
/** a place where I put old/invalid configs I can't quite let go of */
|
|
123
|
+
rubbish: z.unknown().optional(),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/** @typedef {import('zod').infer<typeof AuthConfig>} AuthConfig */
|
|
127
|
+
/** @typedef {import('zod').infer<typeof ConfigFile>} ConfigFile */
|
|
64
128
|
|
|
65
129
|
/**
|
|
66
130
|
* @typedef {{
|
|
@@ -71,14 +135,6 @@ const APP_ROUTER_PATH = join("apps", "os", "backend", "trpc", "root.ts");
|
|
|
71
135
|
* }} SpawnOptions
|
|
72
136
|
*/
|
|
73
137
|
|
|
74
|
-
/**
|
|
75
|
-
* @typedef {{
|
|
76
|
-
* repoDir: string;
|
|
77
|
-
* repoRef?: string;
|
|
78
|
-
* repoUrl: string;
|
|
79
|
-
* }} CheckoutOptions
|
|
80
|
-
*/
|
|
81
|
-
|
|
82
138
|
const isAgent =
|
|
83
139
|
process.env.AGENT === "1" ||
|
|
84
140
|
process.env.OPENCODE === "1" ||
|
|
@@ -87,28 +143,6 @@ const isAgent =
|
|
|
87
143
|
|
|
88
144
|
const t = initTRPC.meta().create();
|
|
89
145
|
|
|
90
|
-
const SetupInput = z.object({
|
|
91
|
-
baseUrl: z
|
|
92
|
-
.string()
|
|
93
|
-
.describe(`Base URL for os API (for example https://dev-yourname-os.dev.iterate.com)`),
|
|
94
|
-
adminPasswordEnvVarName: z.string().describe("Env var name containing admin password"),
|
|
95
|
-
userEmail: z.string().describe("User email to impersonate for os calls"),
|
|
96
|
-
repoPath: z.string().describe("Path to iterate checkout (or 'local' / 'managed' shortcuts)"),
|
|
97
|
-
autoInstall: z.boolean().describe("Auto install dependencies when missing"),
|
|
98
|
-
scope: z.enum(["workspace", "global"]).describe("Where to store launcher config"),
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const AuthConfig = z.object({
|
|
102
|
-
baseUrl: z.string(),
|
|
103
|
-
adminPasswordEnvVarName: z.string(),
|
|
104
|
-
userEmail: z.string(),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
/** @param {string} message */
|
|
108
|
-
const log = (message) => {
|
|
109
|
-
process.stderr.write(`[iterate] ${message}\n`);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
146
|
/**
|
|
113
147
|
* @param {unknown} value
|
|
114
148
|
* @returns {value is Record<string, unknown>}
|
|
@@ -117,44 +151,6 @@ const isObject = (value) => {
|
|
|
117
151
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
118
152
|
};
|
|
119
153
|
|
|
120
|
-
/** @param {unknown} value */
|
|
121
|
-
const nonEmptyString = (value) => {
|
|
122
|
-
if (typeof value !== "string") {
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
const trimmed = value.trim();
|
|
126
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
/** @param {string} input */
|
|
130
|
-
const normalizePath = (input) => {
|
|
131
|
-
if (input === "~") {
|
|
132
|
-
return homedir();
|
|
133
|
-
}
|
|
134
|
-
if (input.startsWith("~/")) {
|
|
135
|
-
return join(homedir(), input.slice(2));
|
|
136
|
-
}
|
|
137
|
-
if (isAbsolute(input)) {
|
|
138
|
-
return input;
|
|
139
|
-
}
|
|
140
|
-
return resolve(input);
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
/** @param {unknown} value */
|
|
144
|
-
const parseBoolean = (value) => {
|
|
145
|
-
if (typeof value !== "string") {
|
|
146
|
-
return undefined;
|
|
147
|
-
}
|
|
148
|
-
const normalized = value.toLowerCase();
|
|
149
|
-
if (normalized === "1" || normalized === "true") {
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
if (normalized === "0" || normalized === "false") {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
return undefined;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
154
|
/** @returns {ConfigFile} */
|
|
159
155
|
const readConfigFile = () => {
|
|
160
156
|
if (!existsSync(CONFIG_PATH)) {
|
|
@@ -169,30 +165,9 @@ const readConfigFile = () => {
|
|
|
169
165
|
throw new Error(`Invalid JSON in ${CONFIG_PATH}: ${detail}`);
|
|
170
166
|
}
|
|
171
167
|
|
|
172
|
-
if (!isObject(parsed)) {
|
|
173
|
-
throw new Error(`${CONFIG_PATH} must contain a JSON object.`);
|
|
174
|
-
}
|
|
175
168
|
return parsed;
|
|
176
169
|
};
|
|
177
170
|
|
|
178
|
-
/** @param {unknown} launcher */
|
|
179
|
-
const sanitizeLauncherConfig = (launcher) => {
|
|
180
|
-
if (!isObject(launcher)) {
|
|
181
|
-
return {};
|
|
182
|
-
}
|
|
183
|
-
return {
|
|
184
|
-
repoPath: nonEmptyString(launcher.repoPath),
|
|
185
|
-
repoRef: nonEmptyString(launcher.repoRef),
|
|
186
|
-
repoUrl: nonEmptyString(launcher.repoUrl),
|
|
187
|
-
autoInstall: typeof launcher.autoInstall === "boolean" ? launcher.autoInstall : undefined,
|
|
188
|
-
};
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
/** @param {ConfigFile} configFile */
|
|
192
|
-
const getGlobalConfig = (configFile) => {
|
|
193
|
-
return isObject(configFile.global) ? configFile.global : {};
|
|
194
|
-
};
|
|
195
|
-
|
|
196
171
|
/**
|
|
197
172
|
* @param {ConfigFile} configFile
|
|
198
173
|
* @param {string} workspacePath
|
|
@@ -208,120 +183,51 @@ const getWorkspaceConfig = (configFile, workspacePath) => {
|
|
|
208
183
|
* @param {string} workspacePath
|
|
209
184
|
*/
|
|
210
185
|
const getMergedWorkspaceConfig = (configFile, workspacePath) => {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return
|
|
186
|
+
const configs = [];
|
|
187
|
+
while (workspacePath && workspacePath !== "/") {
|
|
188
|
+
if (workspacePath in (configFile.workspaces || {})) {
|
|
189
|
+
configs.push(configFile.workspaces?.[workspacePath]);
|
|
190
|
+
}
|
|
191
|
+
workspacePath = dirname(workspacePath);
|
|
192
|
+
}
|
|
193
|
+
configs.push(configFile.global);
|
|
194
|
+
/** @type {AuthConfig} */
|
|
195
|
+
return configs.reverse().reduce((acc, config) => {
|
|
196
|
+
return { ...acc, ...config };
|
|
197
|
+
}, {});
|
|
221
198
|
};
|
|
222
199
|
|
|
223
200
|
/**
|
|
224
|
-
* @param {{
|
|
225
|
-
*
|
|
226
|
-
* workspacePatch?: Record<string, unknown>;
|
|
227
|
-
* scope: "workspace" | "global";
|
|
228
|
-
* workspacePath: string;
|
|
229
|
-
* }} options
|
|
201
|
+
* @param {{ patch?: Partial<AuthConfig>; scope: "workspace" | "global"; workspacePath: string; }} options
|
|
202
|
+
* @returns {ConfigFile}
|
|
230
203
|
*/
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const existingWorkspaces = isObject(configFile.workspaces) ? configFile.workspaces : {};
|
|
235
|
-
|
|
236
|
-
const nextGlobal = scope === "global" ? { ...existingGlobal, ...launcherPatch } : existingGlobal;
|
|
237
|
-
const nextWorkspaces =
|
|
238
|
-
scope === "workspace" || workspacePatch
|
|
239
|
-
? {
|
|
240
|
-
...existingWorkspaces,
|
|
241
|
-
[workspacePath]: {
|
|
242
|
-
...getWorkspaceConfig(configFile, workspacePath),
|
|
243
|
-
...(scope === "workspace" ? launcherPatch : {}),
|
|
244
|
-
...(workspacePatch ?? {}),
|
|
245
|
-
},
|
|
246
|
-
}
|
|
247
|
-
: existingWorkspaces;
|
|
248
|
-
|
|
249
|
-
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
250
|
-
const { launcher: _unusedLauncher, ...rest } = configFile;
|
|
251
|
-
const next = {
|
|
252
|
-
...rest,
|
|
253
|
-
global: nextGlobal,
|
|
254
|
-
workspaces: nextWorkspaces,
|
|
255
|
-
};
|
|
256
|
-
writeFileSync(CONFIG_PATH, `${JSON.stringify(next, null, 2)}\n`);
|
|
257
|
-
return next;
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
/** @param {string} dir */
|
|
261
|
-
const isIterateRepo = (dir) => {
|
|
262
|
-
return (
|
|
263
|
-
existsSync(join(dir, ".git")) &&
|
|
264
|
-
existsSync(join(dir, "pnpm-workspace.yaml")) &&
|
|
265
|
-
existsSync(join(dir, APP_ROUTER_PATH))
|
|
204
|
+
const writeNewConfig = ({ patch, scope, workspacePath }) => {
|
|
205
|
+
patch = Object.fromEntries(
|
|
206
|
+
Object.entries(patch || {}).filter(([_key, value]) => value !== undefined),
|
|
266
207
|
);
|
|
267
|
-
|
|
208
|
+
const configFile = readConfigFile();
|
|
209
|
+
const cloned = structuredClone(configFile);
|
|
268
210
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
current = parent;
|
|
211
|
+
if (scope === "global") {
|
|
212
|
+
cloned.global = { ...configFile.global, ...patch };
|
|
213
|
+
}
|
|
214
|
+
if (scope === "workspace" && workspacePath) {
|
|
215
|
+
cloned.workspaces ||= {};
|
|
216
|
+
// @ts-expect-error - we know it's a string
|
|
217
|
+
cloned.workspaces[workspacePath] = {
|
|
218
|
+
...configFile.workspaces?.[workspacePath],
|
|
219
|
+
...patch,
|
|
220
|
+
};
|
|
281
221
|
}
|
|
282
|
-
};
|
|
283
222
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const cwdRepoDir = findNearestIterateRepo(process.cwd());
|
|
288
|
-
const envRepoDir = nonEmptyString(process.env.ITERATE_REPO_DIR);
|
|
289
|
-
|
|
290
|
-
let repoDir;
|
|
291
|
-
/** @type {RepoDirSource} */
|
|
292
|
-
let repoDirSource;
|
|
293
|
-
|
|
294
|
-
if (envRepoDir) {
|
|
295
|
-
repoDir = normalizePath(envRepoDir);
|
|
296
|
-
repoDirSource = "env";
|
|
297
|
-
} else if (launcherConfig.repoPath) {
|
|
298
|
-
repoDir = normalizePath(launcherConfig.repoPath);
|
|
299
|
-
repoDirSource = "config";
|
|
300
|
-
} else if (cwdRepoDir) {
|
|
301
|
-
repoDir = cwdRepoDir;
|
|
302
|
-
repoDirSource = "cwd";
|
|
303
|
-
} else {
|
|
304
|
-
repoDir = DEFAULT_REPO_DIR;
|
|
305
|
-
repoDirSource = "default";
|
|
223
|
+
const parsed = ConfigFile.safeParse(cloned);
|
|
224
|
+
if (!parsed.success) {
|
|
225
|
+
throw new Error(`Invalid config file: ${z.prettifyError(parsed.error)}`);
|
|
306
226
|
}
|
|
307
227
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const autoInstall =
|
|
312
|
-
parseBoolean(process.env.ITERATE_AUTO_INSTALL) ??
|
|
313
|
-
launcherConfig.autoInstall ??
|
|
314
|
-
(repoDirSource === "cwd" ? false : true);
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
repoDir,
|
|
318
|
-
repoDirSource,
|
|
319
|
-
repoRef,
|
|
320
|
-
repoUrl,
|
|
321
|
-
autoInstall,
|
|
322
|
-
cwdRepoDir,
|
|
323
|
-
launcherConfig,
|
|
324
|
-
};
|
|
228
|
+
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
229
|
+
writeFileSync(CONFIG_PATH, `${JSON.stringify(parsed.data, null, 2)}\n`);
|
|
230
|
+
return cloned;
|
|
325
231
|
};
|
|
326
232
|
|
|
327
233
|
/** @param {string} workspacePath */
|
|
@@ -330,8 +236,8 @@ const readAuthConfig = (workspacePath) => {
|
|
|
330
236
|
const mergedConfig = getMergedWorkspaceConfig(configFile, workspacePath);
|
|
331
237
|
const parsed = AuthConfig.safeParse(mergedConfig);
|
|
332
238
|
if (!parsed.success) {
|
|
333
|
-
|
|
334
|
-
`
|
|
239
|
+
return new Error(
|
|
240
|
+
`Invalid auth config for ${workspacePath} (in config file ${CONFIG_PATH}). Have you run \`iterate setup\`?\n${z.prettifyError(parsed.error)}`,
|
|
335
241
|
);
|
|
336
242
|
}
|
|
337
243
|
return parsed.data;
|
|
@@ -421,11 +327,12 @@ const resolveImpersonationUserId = async ({ superadminAuthClient, userEmail, bas
|
|
|
421
327
|
return resolvedUserId;
|
|
422
328
|
};
|
|
423
329
|
|
|
424
|
-
/** @param {
|
|
425
|
-
const
|
|
330
|
+
/** @param {import('zod').infer<typeof AuthConfig>} authConfig */
|
|
331
|
+
const osAuthDance = async (authConfig) => {
|
|
332
|
+
/** @type {string[] | undefined} */
|
|
426
333
|
let superadminSetCookie;
|
|
427
334
|
const authClient = createAuthClient({
|
|
428
|
-
baseURL: authConfig.
|
|
335
|
+
baseURL: authConfig.osBaseUrl,
|
|
429
336
|
fetchOptions: {
|
|
430
337
|
throw: true,
|
|
431
338
|
},
|
|
@@ -447,11 +354,11 @@ const authDance = async (authConfig) => {
|
|
|
447
354
|
});
|
|
448
355
|
|
|
449
356
|
const superadminAuthClient = createAuthClient({
|
|
450
|
-
baseURL: authConfig.
|
|
357
|
+
baseURL: authConfig.osBaseUrl,
|
|
451
358
|
fetchOptions: {
|
|
452
359
|
throw: true,
|
|
453
360
|
onRequest: (ctx) => {
|
|
454
|
-
ctx.headers.set("origin", authConfig.
|
|
361
|
+
ctx.headers.set("origin", authConfig.osBaseUrl);
|
|
455
362
|
ctx.headers.set("cookie", setCookiesToCookieHeader(superadminSetCookie));
|
|
456
363
|
},
|
|
457
364
|
},
|
|
@@ -461,7 +368,7 @@ const authDance = async (authConfig) => {
|
|
|
461
368
|
const userId = await resolveImpersonationUserId({
|
|
462
369
|
superadminAuthClient,
|
|
463
370
|
userEmail: authConfig.userEmail,
|
|
464
|
-
baseUrl: authConfig.
|
|
371
|
+
baseUrl: authConfig.osBaseUrl,
|
|
465
372
|
});
|
|
466
373
|
|
|
467
374
|
let impersonateSetCookie;
|
|
@@ -478,11 +385,11 @@ const authDance = async (authConfig) => {
|
|
|
478
385
|
const userCookies = setCookiesToCookieHeader(impersonateSetCookie);
|
|
479
386
|
|
|
480
387
|
const userClient = createAuthClient({
|
|
481
|
-
baseURL: authConfig.
|
|
388
|
+
baseURL: authConfig.osBaseUrl,
|
|
482
389
|
fetchOptions: {
|
|
483
390
|
throw: true,
|
|
484
391
|
onRequest: (ctx) => {
|
|
485
|
-
ctx.headers.set("origin", authConfig.
|
|
392
|
+
ctx.headers.set("origin", authConfig.osBaseUrl);
|
|
486
393
|
ctx.headers.set("cookie", userCookies);
|
|
487
394
|
},
|
|
488
395
|
},
|
|
@@ -491,139 +398,38 @@ const authDance = async (authConfig) => {
|
|
|
491
398
|
return { userCookies, userClient };
|
|
492
399
|
};
|
|
493
400
|
|
|
494
|
-
/** @param {string}
|
|
495
|
-
const loadAppRouter = async (
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
401
|
+
/** @param {{baseUrl: string}} params */
|
|
402
|
+
const loadAppRouter = async (params) => {
|
|
403
|
+
const url = `${params.baseUrl}/api/trpc-cli-procedures`;
|
|
404
|
+
const response = await fetch(url);
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
throw new Error(`${url} got ${response.status}: ${await response.text()}`);
|
|
499
407
|
}
|
|
500
|
-
const rootModule = await import(pathToFileURL(appRouterPath).href);
|
|
501
|
-
if (!rootModule || typeof rootModule !== "object" || !("appRouter" in rootModule)) {
|
|
502
|
-
throw new Error(`Failed to load appRouter from ${appRouterPath}`);
|
|
503
|
-
}
|
|
504
|
-
return rootModule.appRouter;
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
/** @param {unknown} error */
|
|
508
|
-
const commandMissing = (error) => {
|
|
509
|
-
return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
/** @param {SpawnOptions} options */
|
|
513
|
-
const run = ({ command, args, cwd, env }) => {
|
|
514
|
-
return new Promise((resolvePromise, rejectPromise) => {
|
|
515
|
-
const child = spawn(command, args, {
|
|
516
|
-
cwd,
|
|
517
|
-
env,
|
|
518
|
-
stdio: "inherit",
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
child.on("error", (error) => {
|
|
522
|
-
rejectPromise(error);
|
|
523
|
-
});
|
|
524
408
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
rejectPromise(new Error(`${command} exited with signal ${signal}`));
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
resolvePromise(code ?? 0);
|
|
531
|
-
});
|
|
409
|
+
const router = await response.json().catch((e) => {
|
|
410
|
+
throw new Error(`${url} returned invalid router: ${e.message}`);
|
|
532
411
|
});
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
/** @param {SpawnOptions} options */
|
|
536
|
-
const runChecked = async ({ command, args, cwd, env }) => {
|
|
537
|
-
const code = await run({ command, args, cwd, env });
|
|
538
|
-
if (code !== 0) {
|
|
539
|
-
throw new Error(`Command failed (${code}): ${command} ${args.join(" ")}`);
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
|
|
543
|
-
/** @param {CheckoutOptions} options */
|
|
544
|
-
const ensureRepoCheckout = async ({ repoDir, repoRef, repoUrl }) => {
|
|
545
|
-
if (existsSync(repoDir)) {
|
|
546
|
-
if (!existsSync(join(repoDir, APP_ROUTER_PATH))) {
|
|
547
|
-
throw new Error(`Expected ${APP_ROUTER_PATH} in ${repoDir}.`);
|
|
548
|
-
}
|
|
549
|
-
if (!existsSync(join(repoDir, ".git"))) {
|
|
550
|
-
throw new Error(`Expected git checkout at ${repoDir}, but .git is missing.`);
|
|
551
|
-
}
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
mkdirSync(dirname(repoDir), { recursive: true });
|
|
556
|
-
const cloneArgs = ["clone", "--depth", "1"];
|
|
557
|
-
if (repoRef) {
|
|
558
|
-
cloneArgs.push("--branch", repoRef, "--single-branch");
|
|
559
|
-
}
|
|
560
|
-
cloneArgs.push(repoUrl, repoDir);
|
|
561
|
-
|
|
562
|
-
log(`cloning iterate repo into ${repoDir}`);
|
|
563
|
-
try {
|
|
564
|
-
await runChecked({ command: "git", args: cloneArgs });
|
|
565
|
-
const envClientPath = join(repoDir, "apps/os/env-client.ts");
|
|
566
|
-
// todo: remove this as soon as this branch is merged into main
|
|
567
|
-
writeFileSync(
|
|
568
|
-
envClientPath,
|
|
569
|
-
readFileSync(envClientPath, "utf8").replace("import.meta.env.", "import.meta.env?."),
|
|
570
|
-
);
|
|
571
|
-
} catch (error) {
|
|
572
|
-
if (commandMissing(error)) {
|
|
573
|
-
throw new Error("git is required but was not found on PATH.");
|
|
574
|
-
}
|
|
575
|
-
throw error;
|
|
412
|
+
if (!Array.isArray(router?.procedures)) {
|
|
413
|
+
throw new Error(`${url} returned invalid router: ${JSON.stringify(router)}`);
|
|
576
414
|
}
|
|
415
|
+
/** @type {{procedures: any[]}} */
|
|
416
|
+
return router;
|
|
577
417
|
};
|
|
578
418
|
|
|
579
|
-
/** @param {string}
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/** @param {{ repoDir: string }} options */
|
|
585
|
-
const installDependencies = async ({ repoDir }) => {
|
|
586
|
-
log("installing dependencies with pnpm");
|
|
587
|
-
const installArgs = ["install", "--frozen-lockfile"];
|
|
588
|
-
try {
|
|
589
|
-
await runChecked({
|
|
590
|
-
command: "corepack",
|
|
591
|
-
args: ["pnpm", ...installArgs],
|
|
592
|
-
cwd: repoDir,
|
|
593
|
-
});
|
|
594
|
-
return;
|
|
595
|
-
} catch (error) {
|
|
596
|
-
if (!commandMissing(error)) {
|
|
597
|
-
throw error;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
try {
|
|
602
|
-
await runChecked({
|
|
603
|
-
command: "pnpm",
|
|
604
|
-
args: installArgs,
|
|
605
|
-
cwd: repoDir,
|
|
606
|
-
});
|
|
607
|
-
} catch (error) {
|
|
608
|
-
if (commandMissing(error)) {
|
|
609
|
-
throw new Error("pnpm/corepack is required but was not found on PATH.");
|
|
610
|
-
}
|
|
611
|
-
throw error;
|
|
612
|
-
}
|
|
613
|
-
};
|
|
614
|
-
|
|
615
|
-
/** @param {string} repoDir */
|
|
616
|
-
const getRuntimeProcedures = async (repoDir) => {
|
|
617
|
-
const appRouter = await loadAppRouter(repoDir);
|
|
618
|
-
const proxiedRouter = proxify(appRouter, async () => {
|
|
419
|
+
/** @param {{ baseUrl: string }} params */
|
|
420
|
+
const getOsProcedures = async (params) => {
|
|
421
|
+
const appRouter = await loadAppRouter(params);
|
|
422
|
+
/** @type {{}} */
|
|
423
|
+
const proxiedRouter = proxify(appRouter.procedures, async () => {
|
|
619
424
|
return createTRPCClient({
|
|
620
425
|
links: [
|
|
621
426
|
httpLink({
|
|
622
|
-
url: `${
|
|
427
|
+
url: `${params.baseUrl}/api/trpc/`,
|
|
623
428
|
transformer: superjson,
|
|
624
429
|
fetch: async (request, init) => {
|
|
625
430
|
const authConfig = readAuthConfig(process.cwd());
|
|
626
|
-
|
|
431
|
+
if (authConfig instanceof Error) throw authConfig;
|
|
432
|
+
const { userCookies } = await osAuthDance(authConfig);
|
|
627
433
|
const headers = new Headers(init?.headers);
|
|
628
434
|
headers.set("cookie", userCookies);
|
|
629
435
|
return fetch(request, { ...init, headers });
|
|
@@ -633,92 +439,176 @@ const getRuntimeProcedures = async (repoDir) => {
|
|
|
633
439
|
});
|
|
634
440
|
});
|
|
635
441
|
|
|
636
|
-
return
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
442
|
+
return proxiedRouter;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Creates a fetch wrapper that calls /api/trpc-stream/* instead of /api/trpc/*.
|
|
447
|
+
* The streaming endpoint returns SSE: log lines as `event: log` and the final
|
|
448
|
+
* tRPC response as `event: response`, which we reassemble into a normal Response.
|
|
449
|
+
* @param {string} daemonBaseUrl
|
|
450
|
+
* @returns {typeof globalThis.fetch}
|
|
451
|
+
*/
|
|
452
|
+
const streamingFetch = (daemonBaseUrl) => {
|
|
453
|
+
return async (/** @type {any} */ input, /** @type {any} */ init) => {
|
|
454
|
+
// Rewrite URL from /api/trpc/X to /api/trpc-stream/X
|
|
455
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
456
|
+
const rewritten = url.replace(
|
|
457
|
+
`${daemonBaseUrl}/api/trpc/`,
|
|
458
|
+
`${daemonBaseUrl}/api/trpc-stream/`,
|
|
459
|
+
);
|
|
460
|
+
const res = await fetch(rewritten, init);
|
|
461
|
+
if (rewritten === url) return res;
|
|
462
|
+
|
|
463
|
+
const contentType = res.headers.get("content-type") || "";
|
|
464
|
+
// If the daemon didn't respond with SSE, pass through as-is (non-streaming endpoint)
|
|
465
|
+
if (!contentType.includes("text/event-stream")) return res;
|
|
466
|
+
// Parse SSE stream: print log events to stderr, collect the final response
|
|
467
|
+
const reader = res.body?.getReader();
|
|
468
|
+
if (!reader) return res;
|
|
469
|
+
const decoder = new TextDecoder();
|
|
470
|
+
let buffer = "";
|
|
471
|
+
/** @type {string | null} */
|
|
472
|
+
let responseBody = null;
|
|
473
|
+
while (true) {
|
|
474
|
+
const { done, value } = await reader.read();
|
|
475
|
+
if (done) break;
|
|
476
|
+
buffer += decoder.decode(value, { stream: true });
|
|
477
|
+
// Process complete SSE messages (double newline delimited)
|
|
478
|
+
const parts = buffer.split("\n\n");
|
|
479
|
+
buffer = parts.pop() || "";
|
|
480
|
+
for (const part of parts) {
|
|
481
|
+
const lines = part.split("\n");
|
|
482
|
+
const event = lines[0].split(": ")[1];
|
|
483
|
+
const data = lines[1].split(": ").slice(1).join(": ");
|
|
484
|
+
if (event === "log") {
|
|
485
|
+
/** @type {{level: "debug" | "info" | "warn" | "error"; args: unknown[]}} */
|
|
486
|
+
const detail = JSON.parse(data);
|
|
487
|
+
console[detail.level](...detail.args);
|
|
488
|
+
} else if (event === "response") {
|
|
489
|
+
responseBody = data;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// Reconstruct a normal Response from the final payload so tRPC client is happy
|
|
494
|
+
return new Response(responseBody, {
|
|
495
|
+
status: res.status,
|
|
496
|
+
headers: { "content-type": "application/json" },
|
|
497
|
+
});
|
|
643
498
|
};
|
|
644
499
|
};
|
|
645
500
|
|
|
501
|
+
/** @param {{ daemonBaseUrl: string }} params */
|
|
502
|
+
const getDaemonProcedures = async (params) => {
|
|
503
|
+
const daemonRouter = await loadAppRouter({ baseUrl: params.daemonBaseUrl });
|
|
504
|
+
const proxiedRouter = proxify(daemonRouter.procedures, async () => {
|
|
505
|
+
return createTRPCClient({
|
|
506
|
+
links: [
|
|
507
|
+
httpLink({
|
|
508
|
+
url: `${params.daemonBaseUrl}/api/trpc/`,
|
|
509
|
+
fetch: streamingFetch(params.daemonBaseUrl),
|
|
510
|
+
}),
|
|
511
|
+
],
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
return proxiedRouter;
|
|
516
|
+
};
|
|
517
|
+
|
|
646
518
|
const launcherProcedures = {
|
|
647
519
|
doctor: t.procedure
|
|
648
520
|
.meta({ description: "Show launcher config and resolved runtime options" })
|
|
649
521
|
.mutation(async () => {
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
cwdRepoDir: runtime.cwdRepoDir ?? null,
|
|
659
|
-
repoExists: existsSync(runtime.repoDir),
|
|
660
|
-
dependenciesInstalled: hasInstalledDependencies(runtime.repoDir),
|
|
661
|
-
};
|
|
522
|
+
const configFile = readConfigFile();
|
|
523
|
+
const parsed = ConfigFile.safeParse(configFile);
|
|
524
|
+
if (!parsed.success) {
|
|
525
|
+
throw new Error(`Invalid config file ${CONFIG_PATH}: ${z.prettifyError(parsed.error)}`);
|
|
526
|
+
}
|
|
527
|
+
const current = readAuthConfig(process.cwd());
|
|
528
|
+
if (current instanceof Error) throw current;
|
|
529
|
+
return { configPath: CONFIG_PATH, current };
|
|
662
530
|
}),
|
|
663
531
|
setup: t.procedure
|
|
664
|
-
.input(SetupInput)
|
|
665
|
-
.meta({ description: "Configure auth + launcher defaults for current workspace" })
|
|
532
|
+
.input(SetupInput.partial())
|
|
533
|
+
.meta({ prompt: true, description: "Configure auth + launcher defaults for current workspace" })
|
|
666
534
|
.mutation(async ({ input }) => {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
repoPath = DEFAULT_REPO_DIR;
|
|
673
|
-
} else if (rawRepoPath === "local") {
|
|
674
|
-
if (!runtime.cwdRepoDir) {
|
|
675
|
-
throw new Error(
|
|
676
|
-
"'local' repoPath was selected but current directory is not inside an iterate repo",
|
|
677
|
-
);
|
|
678
|
-
}
|
|
679
|
-
repoPath = runtime.cwdRepoDir;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
const next = writeLauncherConfig({
|
|
683
|
-
launcherPatch: { repoPath, autoInstall: input.autoInstall },
|
|
684
|
-
workspacePatch: {
|
|
685
|
-
baseUrl: input.baseUrl,
|
|
535
|
+
writeNewConfig({
|
|
536
|
+
scope: input.scope || "workspace",
|
|
537
|
+
patch: {
|
|
538
|
+
osBaseUrl: input.osBaseUrl,
|
|
539
|
+
daemonBaseUrl: input.daemonBaseUrl,
|
|
686
540
|
adminPasswordEnvVarName: input.adminPasswordEnvVarName,
|
|
687
541
|
userEmail: input.userEmail,
|
|
688
542
|
},
|
|
689
|
-
scope: input.scope,
|
|
690
543
|
workspacePath: process.cwd(),
|
|
691
544
|
});
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
};
|
|
697
|
-
}),
|
|
698
|
-
install: t.procedure
|
|
699
|
-
.meta({ description: "Clone repo if needed, then run pnpm install" })
|
|
700
|
-
.mutation(async () => {
|
|
701
|
-
const runtime = resolveRuntimeOptions();
|
|
702
|
-
await ensureRepoCheckout(runtime);
|
|
703
|
-
await installDependencies({ repoDir: runtime.repoDir });
|
|
704
|
-
return { repoDir: runtime.repoDir };
|
|
545
|
+
|
|
546
|
+
const current = readAuthConfig(process.cwd());
|
|
547
|
+
if (current instanceof Error) throw current;
|
|
548
|
+
return { configPath: CONFIG_PATH, current };
|
|
705
549
|
}),
|
|
550
|
+
|
|
551
|
+
whoami: t.procedure.mutation(async () => {
|
|
552
|
+
const authConfig = readAuthConfig(process.cwd());
|
|
553
|
+
if (authConfig instanceof Error) throw authConfig;
|
|
554
|
+
const { userClient } = await osAuthDance(authConfig);
|
|
555
|
+
return await userClient.getSession();
|
|
556
|
+
}),
|
|
706
557
|
};
|
|
707
558
|
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
const runtime = resolveRuntimeOptions();
|
|
711
|
-
await ensureRepoCheckout(runtime);
|
|
559
|
+
const runCli = async () => {
|
|
560
|
+
const authConfig = readAuthConfig(process.cwd());
|
|
712
561
|
|
|
713
|
-
|
|
714
|
-
|
|
562
|
+
/** @type {(problem: string) => (e: Error) => {}} */
|
|
563
|
+
const errorProcedure = (problem) => (e) => {
|
|
564
|
+
const message = `${problem}: ${e.message}`;
|
|
565
|
+
return t.procedure.meta({ description: message }).mutation(() => {
|
|
566
|
+
throw new Error(problem, { cause: e });
|
|
567
|
+
});
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
/** @type {import("@trpc/server").AnyRouter[]} */
|
|
571
|
+
const routers = [t.router(launcherProcedures)];
|
|
572
|
+
|
|
573
|
+
if (authConfig instanceof Error) {
|
|
574
|
+
routers.push(
|
|
575
|
+
t.router({
|
|
576
|
+
os: errorProcedure(`Invalid auth config`)(authConfig),
|
|
577
|
+
daemon: errorProcedure(`Invalid auth config`)(authConfig),
|
|
578
|
+
}),
|
|
579
|
+
);
|
|
580
|
+
} else {
|
|
581
|
+
const [osProcedures, daemonProcedures] = await Promise.allSettled([
|
|
582
|
+
getOsProcedures({ baseUrl: authConfig.osBaseUrl }),
|
|
583
|
+
getDaemonProcedures({ daemonBaseUrl: authConfig.daemonBaseUrl }),
|
|
584
|
+
]);
|
|
585
|
+
|
|
586
|
+
if (osProcedures.status === "fulfilled") {
|
|
587
|
+
routers.push(t.router({ os: osProcedures.value }));
|
|
588
|
+
} else {
|
|
589
|
+
routers.push(
|
|
590
|
+
t.router({
|
|
591
|
+
os: errorProcedure(`Couldn't connect to os at ${authConfig.osBaseUrl}`)(
|
|
592
|
+
osProcedures.reason,
|
|
593
|
+
),
|
|
594
|
+
}),
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
if (daemonProcedures.status === "fulfilled") {
|
|
598
|
+
// don't nest daemon procedures under "daemon"
|
|
599
|
+
routers.push(daemonProcedures.value);
|
|
600
|
+
} else {
|
|
601
|
+
routers.push(
|
|
602
|
+
t.router({
|
|
603
|
+
daemon: errorProcedure(`Couldn't connect to daemon at ${authConfig.daemonBaseUrl}`)(
|
|
604
|
+
daemonProcedures.reason,
|
|
605
|
+
),
|
|
606
|
+
}),
|
|
607
|
+
);
|
|
608
|
+
}
|
|
715
609
|
}
|
|
716
610
|
|
|
717
|
-
const
|
|
718
|
-
const router = t.router({
|
|
719
|
-
...launcherProcedures,
|
|
720
|
-
...runtimeProcedures,
|
|
721
|
-
});
|
|
611
|
+
const router = t.mergeRouters(...routers);
|
|
722
612
|
|
|
723
613
|
const cli = createCli({
|
|
724
614
|
router,
|
|
@@ -727,15 +617,13 @@ const runCli = async (args) => {
|
|
|
727
617
|
description: "Iterate CLI",
|
|
728
618
|
});
|
|
729
619
|
|
|
730
|
-
process.argv = [process.argv[0], process.argv[1], ...args];
|
|
731
620
|
await cli.run({
|
|
732
621
|
prompts: isAgent ? undefined : prompts,
|
|
733
622
|
});
|
|
734
623
|
};
|
|
735
624
|
|
|
736
625
|
const main = async () => {
|
|
737
|
-
|
|
738
|
-
await runCli(args);
|
|
626
|
+
await runCli();
|
|
739
627
|
};
|
|
740
628
|
|
|
741
629
|
main().catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "CLI for iterate",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@trpc/server": "^11.7.2",
|
|
41
41
|
"better-auth": "1.4.3",
|
|
42
42
|
"superjson": "^2.2.2",
|
|
43
|
-
"trpc-cli": "
|
|
43
|
+
"trpc-cli": "0.12.4",
|
|
44
44
|
"zod": "4.1.12"
|
|
45
45
|
}
|
|
46
46
|
}
|