iterate 0.2.2 → 0.2.3
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 +317 -437
- 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
|
|
@@ -209,119 +184,42 @@ const getWorkspaceConfig = (configFile, workspacePath) => {
|
|
|
209
184
|
*/
|
|
210
185
|
const getMergedWorkspaceConfig = (configFile, workspacePath) => {
|
|
211
186
|
return {
|
|
212
|
-
...
|
|
187
|
+
...configFile.global,
|
|
213
188
|
...getWorkspaceConfig(configFile, workspacePath),
|
|
214
189
|
};
|
|
215
190
|
};
|
|
216
191
|
|
|
217
|
-
/** @param {string} workspacePath */
|
|
218
|
-
const readLauncherConfig = (workspacePath) => {
|
|
219
|
-
const configFile = readConfigFile();
|
|
220
|
-
return sanitizeLauncherConfig(getMergedWorkspaceConfig(configFile, workspacePath));
|
|
221
|
-
};
|
|
222
|
-
|
|
223
192
|
/**
|
|
224
|
-
* @param {{
|
|
225
|
-
*
|
|
226
|
-
* workspacePatch?: Record<string, unknown>;
|
|
227
|
-
* scope: "workspace" | "global";
|
|
228
|
-
* workspacePath: string;
|
|
229
|
-
* }} options
|
|
193
|
+
* @param {{ patch?: Partial<AuthConfig>; scope: "workspace" | "global"; workspacePath: string; }} options
|
|
194
|
+
* @returns {ConfigFile}
|
|
230
195
|
*/
|
|
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))
|
|
196
|
+
const writeNewConfig = ({ patch, scope, workspacePath }) => {
|
|
197
|
+
patch = Object.fromEntries(
|
|
198
|
+
Object.entries(patch || {}).filter(([_key, value]) => value !== undefined),
|
|
266
199
|
);
|
|
267
|
-
|
|
200
|
+
const configFile = readConfigFile();
|
|
201
|
+
const cloned = structuredClone(configFile);
|
|
268
202
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
let current = resolve(startDir);
|
|
272
|
-
for (;;) {
|
|
273
|
-
if (isIterateRepo(current)) {
|
|
274
|
-
return current;
|
|
275
|
-
}
|
|
276
|
-
const parent = dirname(current);
|
|
277
|
-
if (parent === current) {
|
|
278
|
-
return undefined;
|
|
279
|
-
}
|
|
280
|
-
current = parent;
|
|
203
|
+
if (scope === "global") {
|
|
204
|
+
cloned.global = { ...configFile.global, ...patch };
|
|
281
205
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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";
|
|
206
|
+
if (scope === "workspace" && workspacePath) {
|
|
207
|
+
cloned.workspaces ||= {};
|
|
208
|
+
// @ts-expect-error - we know it's a string
|
|
209
|
+
cloned.workspaces[workspacePath] = {
|
|
210
|
+
...configFile.workspaces?.[workspacePath],
|
|
211
|
+
...patch,
|
|
212
|
+
};
|
|
306
213
|
}
|
|
307
214
|
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
parseBoolean(process.env.ITERATE_AUTO_INSTALL) ??
|
|
313
|
-
launcherConfig.autoInstall ??
|
|
314
|
-
(repoDirSource === "cwd" ? false : true);
|
|
215
|
+
const parsed = ConfigFile.safeParse(cloned);
|
|
216
|
+
if (!parsed.success) {
|
|
217
|
+
throw new Error(`Invalid config file: ${z.prettifyError(parsed.error)}`);
|
|
218
|
+
}
|
|
315
219
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
repoRef,
|
|
320
|
-
repoUrl,
|
|
321
|
-
autoInstall,
|
|
322
|
-
cwdRepoDir,
|
|
323
|
-
launcherConfig,
|
|
324
|
-
};
|
|
220
|
+
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
221
|
+
writeFileSync(CONFIG_PATH, `${JSON.stringify(parsed.data, null, 2)}\n`);
|
|
222
|
+
return cloned;
|
|
325
223
|
};
|
|
326
224
|
|
|
327
225
|
/** @param {string} workspacePath */
|
|
@@ -330,8 +228,8 @@ const readAuthConfig = (workspacePath) => {
|
|
|
330
228
|
const mergedConfig = getMergedWorkspaceConfig(configFile, workspacePath);
|
|
331
229
|
const parsed = AuthConfig.safeParse(mergedConfig);
|
|
332
230
|
if (!parsed.success) {
|
|
333
|
-
|
|
334
|
-
`
|
|
231
|
+
return new Error(
|
|
232
|
+
`Invalid auth config for ${workspacePath} (in config file ${CONFIG_PATH}). Have you run \`iterate setup\`?\n${z.prettifyError(parsed.error)}`,
|
|
335
233
|
);
|
|
336
234
|
}
|
|
337
235
|
return parsed.data;
|
|
@@ -421,11 +319,12 @@ const resolveImpersonationUserId = async ({ superadminAuthClient, userEmail, bas
|
|
|
421
319
|
return resolvedUserId;
|
|
422
320
|
};
|
|
423
321
|
|
|
424
|
-
/** @param {
|
|
425
|
-
const
|
|
322
|
+
/** @param {import('zod').infer<typeof AuthConfig>} authConfig */
|
|
323
|
+
const osAuthDance = async (authConfig) => {
|
|
324
|
+
/** @type {string[] | undefined} */
|
|
426
325
|
let superadminSetCookie;
|
|
427
326
|
const authClient = createAuthClient({
|
|
428
|
-
baseURL: authConfig.
|
|
327
|
+
baseURL: authConfig.osBaseUrl,
|
|
429
328
|
fetchOptions: {
|
|
430
329
|
throw: true,
|
|
431
330
|
},
|
|
@@ -447,11 +346,11 @@ const authDance = async (authConfig) => {
|
|
|
447
346
|
});
|
|
448
347
|
|
|
449
348
|
const superadminAuthClient = createAuthClient({
|
|
450
|
-
baseURL: authConfig.
|
|
349
|
+
baseURL: authConfig.osBaseUrl,
|
|
451
350
|
fetchOptions: {
|
|
452
351
|
throw: true,
|
|
453
352
|
onRequest: (ctx) => {
|
|
454
|
-
ctx.headers.set("origin", authConfig.
|
|
353
|
+
ctx.headers.set("origin", authConfig.osBaseUrl);
|
|
455
354
|
ctx.headers.set("cookie", setCookiesToCookieHeader(superadminSetCookie));
|
|
456
355
|
},
|
|
457
356
|
},
|
|
@@ -461,7 +360,7 @@ const authDance = async (authConfig) => {
|
|
|
461
360
|
const userId = await resolveImpersonationUserId({
|
|
462
361
|
superadminAuthClient,
|
|
463
362
|
userEmail: authConfig.userEmail,
|
|
464
|
-
baseUrl: authConfig.
|
|
363
|
+
baseUrl: authConfig.osBaseUrl,
|
|
465
364
|
});
|
|
466
365
|
|
|
467
366
|
let impersonateSetCookie;
|
|
@@ -478,11 +377,11 @@ const authDance = async (authConfig) => {
|
|
|
478
377
|
const userCookies = setCookiesToCookieHeader(impersonateSetCookie);
|
|
479
378
|
|
|
480
379
|
const userClient = createAuthClient({
|
|
481
|
-
baseURL: authConfig.
|
|
380
|
+
baseURL: authConfig.osBaseUrl,
|
|
482
381
|
fetchOptions: {
|
|
483
382
|
throw: true,
|
|
484
383
|
onRequest: (ctx) => {
|
|
485
|
-
ctx.headers.set("origin", authConfig.
|
|
384
|
+
ctx.headers.set("origin", authConfig.osBaseUrl);
|
|
486
385
|
ctx.headers.set("cookie", userCookies);
|
|
487
386
|
},
|
|
488
387
|
},
|
|
@@ -491,139 +390,38 @@ const authDance = async (authConfig) => {
|
|
|
491
390
|
return { userCookies, userClient };
|
|
492
391
|
};
|
|
493
392
|
|
|
494
|
-
/** @param {string}
|
|
495
|
-
const loadAppRouter = async (
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
393
|
+
/** @param {{baseUrl: string}} params */
|
|
394
|
+
const loadAppRouter = async (params) => {
|
|
395
|
+
const url = `${params.baseUrl}/api/trpc-cli-procedures`;
|
|
396
|
+
const response = await fetch(url);
|
|
397
|
+
if (!response.ok) {
|
|
398
|
+
throw new Error(`${url} got ${response.status}: ${await response.text()}`);
|
|
499
399
|
}
|
|
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
400
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
rejectPromise(new Error(`${command} exited with signal ${signal}`));
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
resolvePromise(code ?? 0);
|
|
531
|
-
});
|
|
401
|
+
const router = await response.json().catch((e) => {
|
|
402
|
+
throw new Error(`${url} returned invalid router: ${e.message}`);
|
|
532
403
|
});
|
|
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;
|
|
576
|
-
}
|
|
577
|
-
};
|
|
578
|
-
|
|
579
|
-
/** @param {string} repoDir */
|
|
580
|
-
const hasInstalledDependencies = (repoDir) => {
|
|
581
|
-
return existsSync(join(repoDir, "node_modules", ".modules.yaml"));
|
|
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;
|
|
404
|
+
if (!Array.isArray(router?.procedures)) {
|
|
405
|
+
throw new Error(`${url} returned invalid router: ${JSON.stringify(router)}`);
|
|
612
406
|
}
|
|
407
|
+
/** @type {{procedures: any[]}} */
|
|
408
|
+
return router;
|
|
613
409
|
};
|
|
614
410
|
|
|
615
|
-
/** @param {string}
|
|
616
|
-
const
|
|
617
|
-
const appRouter = await loadAppRouter(
|
|
618
|
-
|
|
411
|
+
/** @param {{ baseUrl: string }} params */
|
|
412
|
+
const getOsProcedures = async (params) => {
|
|
413
|
+
const appRouter = await loadAppRouter(params);
|
|
414
|
+
/** @type {{}} */
|
|
415
|
+
const proxiedRouter = proxify(appRouter.procedures, async () => {
|
|
619
416
|
return createTRPCClient({
|
|
620
417
|
links: [
|
|
621
418
|
httpLink({
|
|
622
|
-
url: `${
|
|
419
|
+
url: `${params.baseUrl}/api/trpc/`,
|
|
623
420
|
transformer: superjson,
|
|
624
421
|
fetch: async (request, init) => {
|
|
625
422
|
const authConfig = readAuthConfig(process.cwd());
|
|
626
|
-
|
|
423
|
+
if (authConfig instanceof Error) throw authConfig;
|
|
424
|
+
const { userCookies } = await osAuthDance(authConfig);
|
|
627
425
|
const headers = new Headers(init?.headers);
|
|
628
426
|
headers.set("cookie", userCookies);
|
|
629
427
|
return fetch(request, { ...init, headers });
|
|
@@ -633,92 +431,176 @@ const getRuntimeProcedures = async (repoDir) => {
|
|
|
633
431
|
});
|
|
634
432
|
});
|
|
635
433
|
|
|
636
|
-
return
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
434
|
+
return proxiedRouter;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Creates a fetch wrapper that calls /api/trpc-stream/* instead of /api/trpc/*.
|
|
439
|
+
* The streaming endpoint returns SSE: log lines as `event: log` and the final
|
|
440
|
+
* tRPC response as `event: response`, which we reassemble into a normal Response.
|
|
441
|
+
* @param {string} daemonBaseUrl
|
|
442
|
+
* @returns {typeof globalThis.fetch}
|
|
443
|
+
*/
|
|
444
|
+
const streamingFetch = (daemonBaseUrl) => {
|
|
445
|
+
return async (/** @type {any} */ input, /** @type {any} */ init) => {
|
|
446
|
+
// Rewrite URL from /api/trpc/X to /api/trpc-stream/X
|
|
447
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
448
|
+
const rewritten = url.replace(
|
|
449
|
+
`${daemonBaseUrl}/api/trpc/`,
|
|
450
|
+
`${daemonBaseUrl}/api/trpc-stream/`,
|
|
451
|
+
);
|
|
452
|
+
const res = await fetch(rewritten, init);
|
|
453
|
+
if (rewritten === url) return res;
|
|
454
|
+
|
|
455
|
+
const contentType = res.headers.get("content-type") || "";
|
|
456
|
+
// If the daemon didn't respond with SSE, pass through as-is (non-streaming endpoint)
|
|
457
|
+
if (!contentType.includes("text/event-stream")) return res;
|
|
458
|
+
// Parse SSE stream: print log events to stderr, collect the final response
|
|
459
|
+
const reader = res.body?.getReader();
|
|
460
|
+
if (!reader) return res;
|
|
461
|
+
const decoder = new TextDecoder();
|
|
462
|
+
let buffer = "";
|
|
463
|
+
/** @type {string | null} */
|
|
464
|
+
let responseBody = null;
|
|
465
|
+
while (true) {
|
|
466
|
+
const { done, value } = await reader.read();
|
|
467
|
+
if (done) break;
|
|
468
|
+
buffer += decoder.decode(value, { stream: true });
|
|
469
|
+
// Process complete SSE messages (double newline delimited)
|
|
470
|
+
const parts = buffer.split("\n\n");
|
|
471
|
+
buffer = parts.pop() || "";
|
|
472
|
+
for (const part of parts) {
|
|
473
|
+
const lines = part.split("\n");
|
|
474
|
+
const event = lines[0].split(": ")[1];
|
|
475
|
+
const data = lines[1].split(": ").slice(1).join(": ");
|
|
476
|
+
if (event === "log") {
|
|
477
|
+
/** @type {{level: "debug" | "info" | "warn" | "error"; args: unknown[]}} */
|
|
478
|
+
const detail = JSON.parse(data);
|
|
479
|
+
console[detail.level](...detail.args);
|
|
480
|
+
} else if (event === "response") {
|
|
481
|
+
responseBody = data;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Reconstruct a normal Response from the final payload so tRPC client is happy
|
|
486
|
+
return new Response(responseBody, {
|
|
487
|
+
status: res.status,
|
|
488
|
+
headers: { "content-type": "application/json" },
|
|
489
|
+
});
|
|
643
490
|
};
|
|
644
491
|
};
|
|
645
492
|
|
|
493
|
+
/** @param {{ daemonBaseUrl: string }} params */
|
|
494
|
+
const getDaemonProcedures = async (params) => {
|
|
495
|
+
const daemonRouter = await loadAppRouter({ baseUrl: params.daemonBaseUrl });
|
|
496
|
+
const proxiedRouter = proxify(daemonRouter.procedures, async () => {
|
|
497
|
+
return createTRPCClient({
|
|
498
|
+
links: [
|
|
499
|
+
httpLink({
|
|
500
|
+
url: `${params.daemonBaseUrl}/api/trpc/`,
|
|
501
|
+
fetch: streamingFetch(params.daemonBaseUrl),
|
|
502
|
+
}),
|
|
503
|
+
],
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
return proxiedRouter;
|
|
508
|
+
};
|
|
509
|
+
|
|
646
510
|
const launcherProcedures = {
|
|
647
511
|
doctor: t.procedure
|
|
648
512
|
.meta({ description: "Show launcher config and resolved runtime options" })
|
|
649
513
|
.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
|
-
};
|
|
514
|
+
const configFile = readConfigFile();
|
|
515
|
+
const parsed = ConfigFile.safeParse(configFile);
|
|
516
|
+
if (!parsed.success) {
|
|
517
|
+
throw new Error(`Invalid config file ${CONFIG_PATH}: ${z.prettifyError(parsed.error)}`);
|
|
518
|
+
}
|
|
519
|
+
const current = readAuthConfig(process.cwd());
|
|
520
|
+
if (current instanceof Error) throw current;
|
|
521
|
+
return { configPath: CONFIG_PATH, current };
|
|
662
522
|
}),
|
|
663
523
|
setup: t.procedure
|
|
664
|
-
.input(SetupInput)
|
|
665
|
-
.meta({ description: "Configure auth + launcher defaults for current workspace" })
|
|
524
|
+
.input(SetupInput.partial())
|
|
525
|
+
.meta({ prompt: true, description: "Configure auth + launcher defaults for current workspace" })
|
|
666
526
|
.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,
|
|
527
|
+
writeNewConfig({
|
|
528
|
+
scope: input.scope || "workspace",
|
|
529
|
+
patch: {
|
|
530
|
+
osBaseUrl: input.osBaseUrl,
|
|
531
|
+
daemonBaseUrl: input.daemonBaseUrl,
|
|
686
532
|
adminPasswordEnvVarName: input.adminPasswordEnvVarName,
|
|
687
533
|
userEmail: input.userEmail,
|
|
688
534
|
},
|
|
689
|
-
scope: input.scope,
|
|
690
535
|
workspacePath: process.cwd(),
|
|
691
536
|
});
|
|
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 };
|
|
537
|
+
|
|
538
|
+
const current = readAuthConfig(process.cwd());
|
|
539
|
+
if (current instanceof Error) throw current;
|
|
540
|
+
return { configPath: CONFIG_PATH, current };
|
|
705
541
|
}),
|
|
542
|
+
|
|
543
|
+
whoami: t.procedure.mutation(async () => {
|
|
544
|
+
const authConfig = readAuthConfig(process.cwd());
|
|
545
|
+
if (authConfig instanceof Error) throw authConfig;
|
|
546
|
+
const { userClient } = await osAuthDance(authConfig);
|
|
547
|
+
return await userClient.getSession();
|
|
548
|
+
}),
|
|
706
549
|
};
|
|
707
550
|
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
551
|
+
const runCli = async () => {
|
|
552
|
+
const authConfig = readAuthConfig(process.cwd());
|
|
553
|
+
|
|
554
|
+
/** @type {(problem: string) => (e: Error) => {}} */
|
|
555
|
+
const errorProcedure = (problem) => (e) => {
|
|
556
|
+
const message = `${problem}: ${e.message}`;
|
|
557
|
+
return t.procedure.meta({ description: message }).mutation(() => {
|
|
558
|
+
throw new Error(problem, { cause: e });
|
|
559
|
+
});
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
/** @type {import("@trpc/server").AnyRouter[]} */
|
|
563
|
+
const routers = [t.router(launcherProcedures)];
|
|
712
564
|
|
|
713
|
-
if (
|
|
714
|
-
|
|
565
|
+
if (authConfig instanceof Error) {
|
|
566
|
+
routers.push(
|
|
567
|
+
t.router({
|
|
568
|
+
os: errorProcedure(`Invalid auth config`)(authConfig),
|
|
569
|
+
daemon: errorProcedure(`Invalid auth config`)(authConfig),
|
|
570
|
+
}),
|
|
571
|
+
);
|
|
572
|
+
} else {
|
|
573
|
+
const [osProcedures, daemonProcedures] = await Promise.allSettled([
|
|
574
|
+
getOsProcedures({ baseUrl: authConfig.osBaseUrl }),
|
|
575
|
+
getDaemonProcedures({ daemonBaseUrl: authConfig.daemonBaseUrl }),
|
|
576
|
+
]);
|
|
577
|
+
|
|
578
|
+
if (osProcedures.status === "fulfilled") {
|
|
579
|
+
routers.push(t.router({ os: osProcedures.value }));
|
|
580
|
+
} else {
|
|
581
|
+
routers.push(
|
|
582
|
+
t.router({
|
|
583
|
+
os: errorProcedure(`Couldn't connect to os at ${authConfig.osBaseUrl}`)(
|
|
584
|
+
osProcedures.reason,
|
|
585
|
+
),
|
|
586
|
+
}),
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
if (daemonProcedures.status === "fulfilled") {
|
|
590
|
+
// don't nest daemon procedures under "daemon"
|
|
591
|
+
routers.push(daemonProcedures.value);
|
|
592
|
+
} else {
|
|
593
|
+
routers.push(
|
|
594
|
+
t.router({
|
|
595
|
+
daemon: errorProcedure(`Couldn't connect to daemon at ${authConfig.daemonBaseUrl}`)(
|
|
596
|
+
daemonProcedures.reason,
|
|
597
|
+
),
|
|
598
|
+
}),
|
|
599
|
+
);
|
|
600
|
+
}
|
|
715
601
|
}
|
|
716
602
|
|
|
717
|
-
const
|
|
718
|
-
const router = t.router({
|
|
719
|
-
...launcherProcedures,
|
|
720
|
-
...runtimeProcedures,
|
|
721
|
-
});
|
|
603
|
+
const router = t.mergeRouters(...routers);
|
|
722
604
|
|
|
723
605
|
const cli = createCli({
|
|
724
606
|
router,
|
|
@@ -727,15 +609,13 @@ const runCli = async (args) => {
|
|
|
727
609
|
description: "Iterate CLI",
|
|
728
610
|
});
|
|
729
611
|
|
|
730
|
-
process.argv = [process.argv[0], process.argv[1], ...args];
|
|
731
612
|
await cli.run({
|
|
732
613
|
prompts: isAgent ? undefined : prompts,
|
|
733
614
|
});
|
|
734
615
|
};
|
|
735
616
|
|
|
736
617
|
const main = async () => {
|
|
737
|
-
|
|
738
|
-
await runCli(args);
|
|
618
|
+
await runCli();
|
|
739
619
|
};
|
|
740
620
|
|
|
741
621
|
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.3",
|
|
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
|
}
|