iterate 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -0
- package/bin/iterate.js +667 -0
- package/package.json +38 -20
- package/README.markdown +0 -14
- package/index.js +0 -152
- package/test/objects.synct.js +0 -188
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# iterate
|
|
2
|
+
|
|
3
|
+
⚠️⚠️⚠️ Coming soon! `npx iterate` is a work-in-progress CLI for managing [iterate.com](https://iterate.com) agents ⚠️⚠️⚠️
|
|
4
|
+
|
|
5
|
+
CLI for Iterate.
|
|
6
|
+
|
|
7
|
+
Runs as a thin bootstrapper that:
|
|
8
|
+
|
|
9
|
+
1. Resolves an `iterate/iterate` checkout.
|
|
10
|
+
2. Clones/install deps when needed.
|
|
11
|
+
3. Loads `apps/os/backend/trpc/root.ts` from that checkout.
|
|
12
|
+
4. Exposes commands like `iterate os ...` and `iterate whoami`.
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Node `>=22`
|
|
17
|
+
- `git`
|
|
18
|
+
- `pnpm` or `corepack`
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
Run without installing globally:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx iterate --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Initial setup (writes auth + launcher config):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx iterate setup \
|
|
32
|
+
--base-url https://dev-yourname-os.dev.iterate.com \
|
|
33
|
+
--admin-password-env-var-name SERVICE_AUTH_TOKEN \
|
|
34
|
+
--user-id usr_... \
|
|
35
|
+
--repo-path managed \
|
|
36
|
+
--auto-install true \
|
|
37
|
+
--scope global
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then run commands:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx iterate whoami
|
|
44
|
+
npx iterate os project list
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
- `iterate setup` - configure auth + launcher defaults
|
|
50
|
+
- `iterate doctor` - print resolved config/runtime info
|
|
51
|
+
- `iterate install` - force clone/install for resolved checkout
|
|
52
|
+
- `iterate whoami`
|
|
53
|
+
- `iterate os ...`
|
|
54
|
+
|
|
55
|
+
## Config file
|
|
56
|
+
|
|
57
|
+
Config path:
|
|
58
|
+
|
|
59
|
+
`${XDG_CONFIG_HOME:-~/.config}/iterate/config.json`
|
|
60
|
+
|
|
61
|
+
Config shape:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"global": {
|
|
66
|
+
"repoPath": "~/.local/share/iterate/repo",
|
|
67
|
+
"repoRef": "main",
|
|
68
|
+
"repoUrl": "https://github.com/iterate/iterate.git",
|
|
69
|
+
"autoInstall": true
|
|
70
|
+
},
|
|
71
|
+
"workspaces": {
|
|
72
|
+
"/absolute/workspace/path": {
|
|
73
|
+
"baseUrl": "https://dev-yourname-os.dev.iterate.com",
|
|
74
|
+
"adminPasswordEnvVarName": "SERVICE_AUTH_TOKEN",
|
|
75
|
+
"userId": "usr_...",
|
|
76
|
+
"repoPath": "/absolute/path/to/iterate",
|
|
77
|
+
"autoInstall": false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Merge precedence is shallow:
|
|
84
|
+
|
|
85
|
+
`global` -> `workspaces[process.cwd()]`
|
|
86
|
+
|
|
87
|
+
## Repo checkout resolution
|
|
88
|
+
|
|
89
|
+
`repoPath` resolution order:
|
|
90
|
+
|
|
91
|
+
1. `ITERATE_REPO_DIR`
|
|
92
|
+
2. `workspaces[process.cwd()].repoPath`
|
|
93
|
+
3. `global.repoPath`
|
|
94
|
+
4. nearest parent directory containing `.git`, `pnpm-workspace.yaml`, and `apps/os/backend/trpc/root.ts`
|
|
95
|
+
5. default managed checkout path `${XDG_DATA_HOME:-~/.local/share}/iterate/repo`
|
|
96
|
+
|
|
97
|
+
`repoPath` shortcuts in `setup`:
|
|
98
|
+
|
|
99
|
+
- `local` - nearest local iterate checkout
|
|
100
|
+
- `managed` - default managed checkout path
|
|
101
|
+
|
|
102
|
+
Environment overrides:
|
|
103
|
+
|
|
104
|
+
- `ITERATE_REPO_DIR`
|
|
105
|
+
- `ITERATE_REPO_REF`
|
|
106
|
+
- `ITERATE_REPO_URL`
|
|
107
|
+
- `ITERATE_AUTO_INSTALL` (`1/true` or `0/false`)
|
|
108
|
+
|
|
109
|
+
## Local iterate dev
|
|
110
|
+
|
|
111
|
+
If you run inside an `iterate/iterate` clone, the CLI auto-detects it. In that mode, default `autoInstall` is `false`.
|
|
112
|
+
|
|
113
|
+
You can pin explicitly:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npx iterate setup \
|
|
117
|
+
--base-url https://dev-yourname-os.dev.iterate.com \
|
|
118
|
+
--admin-password-env-var-name SERVICE_AUTH_TOKEN \
|
|
119
|
+
--user-id usr_... \
|
|
120
|
+
--repo-path local \
|
|
121
|
+
--auto-install false \
|
|
122
|
+
--scope workspace
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Publishing (maintainers)
|
|
126
|
+
|
|
127
|
+
From repo root:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pnpm --filter ./packages/iterate typecheck
|
|
131
|
+
pnpm eslint packages/iterate/bin/iterate.js
|
|
132
|
+
pnpm prettier --check packages/iterate
|
|
133
|
+
pnpm --filter ./packages/iterate publish --access public
|
|
134
|
+
```
|
package/bin/iterate.js
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
8
|
+
import process from "node:process";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import * as prompts from "@clack/prompts";
|
|
11
|
+
import { createTRPCClient, httpLink } from "@trpc/client";
|
|
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");
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {{
|
|
37
|
+
* repoPath?: string;
|
|
38
|
+
* repoRef?: string;
|
|
39
|
+
* repoUrl?: string;
|
|
40
|
+
* autoInstall?: boolean;
|
|
41
|
+
* }} LauncherConfig
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {{
|
|
46
|
+
* global?: Record<string, unknown>;
|
|
47
|
+
* workspaces?: Record<string, Record<string, unknown>>;
|
|
48
|
+
* } & Record<string, unknown>} ConfigFile
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/** @typedef {"env" | "config" | "cwd" | "default"} RepoDirSource */
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {{
|
|
55
|
+
* repoDir: string;
|
|
56
|
+
* repoDirSource: RepoDirSource;
|
|
57
|
+
* repoRef?: string;
|
|
58
|
+
* repoUrl: string;
|
|
59
|
+
* autoInstall: boolean;
|
|
60
|
+
* cwdRepoDir?: string;
|
|
61
|
+
* launcherConfig: LauncherConfig;
|
|
62
|
+
* }} RuntimeOptions
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @typedef {{
|
|
67
|
+
* command: string;
|
|
68
|
+
* args: string[];
|
|
69
|
+
* cwd?: string;
|
|
70
|
+
* env?: Record<string, string | undefined>;
|
|
71
|
+
* }} SpawnOptions
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {{
|
|
76
|
+
* repoDir: string;
|
|
77
|
+
* repoRef?: string;
|
|
78
|
+
* repoUrl: string;
|
|
79
|
+
* }} CheckoutOptions
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
const isAgent =
|
|
83
|
+
process.env.AGENT === "1" ||
|
|
84
|
+
process.env.OPENCODE === "1" ||
|
|
85
|
+
Boolean(process.env.OPENCODE_SESSION) ||
|
|
86
|
+
Boolean(process.env.CLAUDE_CODE);
|
|
87
|
+
|
|
88
|
+
const t = initTRPC.meta().create();
|
|
89
|
+
|
|
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
|
+
userId: z.string().describe("User ID 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
|
+
userId: z.string(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/** @param {string} message */
|
|
108
|
+
const log = (message) => {
|
|
109
|
+
process.stderr.write(`[iterate] ${message}\n`);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {unknown} value
|
|
114
|
+
* @returns {value is Record<string, unknown>}
|
|
115
|
+
*/
|
|
116
|
+
const isObject = (value) => {
|
|
117
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
118
|
+
};
|
|
119
|
+
|
|
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
|
+
/** @returns {ConfigFile} */
|
|
159
|
+
const readConfigFile = () => {
|
|
160
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
const rawText = readFileSync(CONFIG_PATH, "utf8");
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(rawText);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
169
|
+
throw new Error(`Invalid JSON in ${CONFIG_PATH}: ${detail}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!isObject(parsed)) {
|
|
173
|
+
throw new Error(`${CONFIG_PATH} must contain a JSON object.`);
|
|
174
|
+
}
|
|
175
|
+
return parsed;
|
|
176
|
+
};
|
|
177
|
+
|
|
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
|
+
/**
|
|
197
|
+
* @param {ConfigFile} configFile
|
|
198
|
+
* @param {string} workspacePath
|
|
199
|
+
*/
|
|
200
|
+
const getWorkspaceConfig = (configFile, workspacePath) => {
|
|
201
|
+
const workspaces = isObject(configFile.workspaces) ? configFile.workspaces : {};
|
|
202
|
+
const rawWorkspaceConfig = workspaces[workspacePath];
|
|
203
|
+
return isObject(rawWorkspaceConfig) ? rawWorkspaceConfig : {};
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @param {ConfigFile} configFile
|
|
208
|
+
* @param {string} workspacePath
|
|
209
|
+
*/
|
|
210
|
+
const getMergedWorkspaceConfig = (configFile, workspacePath) => {
|
|
211
|
+
return {
|
|
212
|
+
...getGlobalConfig(configFile),
|
|
213
|
+
...getWorkspaceConfig(configFile, workspacePath),
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/** @param {string} workspacePath */
|
|
218
|
+
const readLauncherConfig = (workspacePath) => {
|
|
219
|
+
const configFile = readConfigFile();
|
|
220
|
+
return sanitizeLauncherConfig(getMergedWorkspaceConfig(configFile, workspacePath));
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {{
|
|
225
|
+
* launcherPatch: Partial<LauncherConfig>;
|
|
226
|
+
* workspacePatch?: Record<string, unknown>;
|
|
227
|
+
* scope: "workspace" | "global";
|
|
228
|
+
* workspacePath: string;
|
|
229
|
+
* }} options
|
|
230
|
+
*/
|
|
231
|
+
const writeLauncherConfig = ({ launcherPatch, workspacePatch, scope, workspacePath }) => {
|
|
232
|
+
const configFile = readConfigFile();
|
|
233
|
+
const existingGlobal = getGlobalConfig(configFile);
|
|
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))
|
|
266
|
+
);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/** @param {string} startDir */
|
|
270
|
+
const findNearestIterateRepo = (startDir) => {
|
|
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;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/** @returns {RuntimeOptions} */
|
|
285
|
+
const resolveRuntimeOptions = () => {
|
|
286
|
+
const launcherConfig = readLauncherConfig(process.cwd());
|
|
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";
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const repoRef = nonEmptyString(process.env.ITERATE_REPO_REF) ?? launcherConfig.repoRef;
|
|
309
|
+
const repoUrl =
|
|
310
|
+
nonEmptyString(process.env.ITERATE_REPO_URL) ?? launcherConfig.repoUrl ?? DEFAULT_REPO_URL;
|
|
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
|
+
};
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/** @param {string} workspacePath */
|
|
328
|
+
const readAuthConfig = (workspacePath) => {
|
|
329
|
+
const configFile = readConfigFile();
|
|
330
|
+
const mergedConfig = getMergedWorkspaceConfig(configFile, workspacePath);
|
|
331
|
+
const parsed = AuthConfig.safeParse(mergedConfig);
|
|
332
|
+
if (!parsed.success) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`Config file ${CONFIG_PATH} is missing auth config for ${workspacePath}. Have you run \`iterate setup\`?\n${z.prettifyError(parsed.error)}`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return parsed.data;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/** @param {string[] | undefined} setCookies */
|
|
341
|
+
const setCookiesToCookieHeader = (setCookies) => {
|
|
342
|
+
const byName = new Map();
|
|
343
|
+
for (const c of setCookies ?? []) {
|
|
344
|
+
const pair = c.split(";")[0]?.trim();
|
|
345
|
+
if (!pair) continue;
|
|
346
|
+
const eq = pair.indexOf("=");
|
|
347
|
+
if (eq === -1) continue;
|
|
348
|
+
byName.set(pair.slice(0, eq), pair.slice(eq + 1));
|
|
349
|
+
}
|
|
350
|
+
return [...byName.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/** @param {z.infer<typeof AuthConfig>} authConfig */
|
|
354
|
+
const authDance = async (authConfig) => {
|
|
355
|
+
let superadminSetCookie;
|
|
356
|
+
const authClient = createAuthClient({
|
|
357
|
+
baseURL: authConfig.baseUrl,
|
|
358
|
+
fetchOptions: {
|
|
359
|
+
throw: true,
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
const password = process.env[authConfig.adminPasswordEnvVarName];
|
|
363
|
+
if (!password) {
|
|
364
|
+
throw new Error(`Password not found in env var ${authConfig.adminPasswordEnvVarName}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
await authClient.signIn.email({
|
|
368
|
+
email: "superadmin@nustom.com",
|
|
369
|
+
password,
|
|
370
|
+
fetchOptions: {
|
|
371
|
+
throw: true,
|
|
372
|
+
onResponse: (ctx) => {
|
|
373
|
+
superadminSetCookie = ctx.response.headers.getSetCookie();
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const superadminAuthClient = createAuthClient({
|
|
379
|
+
baseURL: authConfig.baseUrl,
|
|
380
|
+
fetchOptions: {
|
|
381
|
+
throw: true,
|
|
382
|
+
onRequest: (ctx) => {
|
|
383
|
+
ctx.headers.set("origin", authConfig.baseUrl);
|
|
384
|
+
ctx.headers.set("cookie", setCookiesToCookieHeader(superadminSetCookie));
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
plugins: [adminClient()],
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
let impersonateSetCookie;
|
|
391
|
+
await superadminAuthClient.admin.impersonateUser({
|
|
392
|
+
userId: authConfig.userId,
|
|
393
|
+
fetchOptions: {
|
|
394
|
+
throw: true,
|
|
395
|
+
onResponse: (ctx) => {
|
|
396
|
+
impersonateSetCookie = ctx.response.headers.getSetCookie();
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const userCookies = setCookiesToCookieHeader(impersonateSetCookie);
|
|
402
|
+
|
|
403
|
+
const userClient = createAuthClient({
|
|
404
|
+
baseURL: authConfig.baseUrl,
|
|
405
|
+
fetchOptions: {
|
|
406
|
+
throw: true,
|
|
407
|
+
onRequest: (ctx) => {
|
|
408
|
+
ctx.headers.set("origin", authConfig.baseUrl);
|
|
409
|
+
ctx.headers.set("cookie", userCookies);
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
return { userCookies, userClient };
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/** @param {string} repoDir */
|
|
418
|
+
const loadAppRouter = async (repoDir) => {
|
|
419
|
+
const appRouterPath = join(repoDir, APP_ROUTER_PATH);
|
|
420
|
+
if (!existsSync(appRouterPath)) {
|
|
421
|
+
throw new Error(`Could not find ${APP_ROUTER_PATH} under ${repoDir}.`);
|
|
422
|
+
}
|
|
423
|
+
const rootModule = await import(pathToFileURL(appRouterPath).href);
|
|
424
|
+
if (!rootModule || typeof rootModule !== "object" || !("appRouter" in rootModule)) {
|
|
425
|
+
throw new Error(`Failed to load appRouter from ${appRouterPath}`);
|
|
426
|
+
}
|
|
427
|
+
return rootModule.appRouter;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
/** @param {unknown} error */
|
|
431
|
+
const commandMissing = (error) => {
|
|
432
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/** @param {SpawnOptions} options */
|
|
436
|
+
const run = ({ command, args, cwd, env }) => {
|
|
437
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
438
|
+
const child = spawn(command, args, {
|
|
439
|
+
cwd,
|
|
440
|
+
env,
|
|
441
|
+
stdio: "inherit",
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
child.on("error", (error) => {
|
|
445
|
+
rejectPromise(error);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
child.on("close", (code, signal) => {
|
|
449
|
+
if (signal) {
|
|
450
|
+
rejectPromise(new Error(`${command} exited with signal ${signal}`));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
resolvePromise(code ?? 0);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
/** @param {SpawnOptions} options */
|
|
459
|
+
const runChecked = async ({ command, args, cwd, env }) => {
|
|
460
|
+
const code = await run({ command, args, cwd, env });
|
|
461
|
+
if (code !== 0) {
|
|
462
|
+
throw new Error(`Command failed (${code}): ${command} ${args.join(" ")}`);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
/** @param {CheckoutOptions} options */
|
|
467
|
+
const ensureRepoCheckout = async ({ repoDir, repoRef, repoUrl }) => {
|
|
468
|
+
if (existsSync(repoDir)) {
|
|
469
|
+
if (!existsSync(join(repoDir, APP_ROUTER_PATH))) {
|
|
470
|
+
throw new Error(`Expected ${APP_ROUTER_PATH} in ${repoDir}.`);
|
|
471
|
+
}
|
|
472
|
+
if (!existsSync(join(repoDir, ".git"))) {
|
|
473
|
+
throw new Error(`Expected git checkout at ${repoDir}, but .git is missing.`);
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
mkdirSync(dirname(repoDir), { recursive: true });
|
|
479
|
+
const cloneArgs = ["clone", "--depth", "1"];
|
|
480
|
+
if (repoRef) {
|
|
481
|
+
cloneArgs.push("--branch", repoRef, "--single-branch");
|
|
482
|
+
}
|
|
483
|
+
cloneArgs.push(repoUrl, repoDir);
|
|
484
|
+
|
|
485
|
+
log(`cloning iterate repo into ${repoDir}`);
|
|
486
|
+
try {
|
|
487
|
+
await runChecked({ command: "git", args: cloneArgs });
|
|
488
|
+
const envClientPath = join(repoDir, "apps/os/env-client.ts");
|
|
489
|
+
// todo: remove this as soon as this branch is merged into main
|
|
490
|
+
writeFileSync(
|
|
491
|
+
envClientPath,
|
|
492
|
+
readFileSync(envClientPath, "utf8").replace("import.meta.env.", "import.meta.env?."),
|
|
493
|
+
);
|
|
494
|
+
} catch (error) {
|
|
495
|
+
if (commandMissing(error)) {
|
|
496
|
+
throw new Error("git is required but was not found on PATH.");
|
|
497
|
+
}
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
/** @param {string} repoDir */
|
|
503
|
+
const hasInstalledDependencies = (repoDir) => {
|
|
504
|
+
return existsSync(join(repoDir, "node_modules", ".modules.yaml"));
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/** @param {{ repoDir: string }} options */
|
|
508
|
+
const installDependencies = async ({ repoDir }) => {
|
|
509
|
+
log("installing dependencies with pnpm");
|
|
510
|
+
const installArgs = ["install", "--frozen-lockfile"];
|
|
511
|
+
try {
|
|
512
|
+
await runChecked({
|
|
513
|
+
command: "corepack",
|
|
514
|
+
args: ["pnpm", ...installArgs],
|
|
515
|
+
cwd: repoDir,
|
|
516
|
+
});
|
|
517
|
+
return;
|
|
518
|
+
} catch (error) {
|
|
519
|
+
if (!commandMissing(error)) {
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
await runChecked({
|
|
526
|
+
command: "pnpm",
|
|
527
|
+
args: installArgs,
|
|
528
|
+
cwd: repoDir,
|
|
529
|
+
});
|
|
530
|
+
} catch (error) {
|
|
531
|
+
if (commandMissing(error)) {
|
|
532
|
+
throw new Error("pnpm/corepack is required but was not found on PATH.");
|
|
533
|
+
}
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
/** @param {string} repoDir */
|
|
539
|
+
const getRuntimeProcedures = async (repoDir) => {
|
|
540
|
+
const appRouter = await loadAppRouter(repoDir);
|
|
541
|
+
const proxiedRouter = proxify(appRouter, async () => {
|
|
542
|
+
return createTRPCClient({
|
|
543
|
+
links: [
|
|
544
|
+
httpLink({
|
|
545
|
+
url: `${readAuthConfig(process.cwd()).baseUrl}/api/trpc/`,
|
|
546
|
+
transformer: superjson,
|
|
547
|
+
fetch: async (request, init) => {
|
|
548
|
+
const authConfig = readAuthConfig(process.cwd());
|
|
549
|
+
const { userCookies } = await authDance(authConfig);
|
|
550
|
+
const headers = new Headers(init?.headers);
|
|
551
|
+
headers.set("cookie", userCookies);
|
|
552
|
+
return fetch(request, { ...init, headers });
|
|
553
|
+
},
|
|
554
|
+
}),
|
|
555
|
+
],
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
whoami: t.procedure.mutation(async () => {
|
|
561
|
+
const authConfig = readAuthConfig(process.cwd());
|
|
562
|
+
const { userClient } = await authDance(authConfig);
|
|
563
|
+
return await userClient.getSession();
|
|
564
|
+
}),
|
|
565
|
+
os: proxiedRouter,
|
|
566
|
+
};
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const launcherProcedures = {
|
|
570
|
+
doctor: t.procedure
|
|
571
|
+
.meta({ description: "Show launcher config and resolved runtime options" })
|
|
572
|
+
.mutation(async () => {
|
|
573
|
+
const runtime = resolveRuntimeOptions();
|
|
574
|
+
return {
|
|
575
|
+
configPath: CONFIG_PATH,
|
|
576
|
+
repoDir: runtime.repoDir,
|
|
577
|
+
repoDirSource: runtime.repoDirSource,
|
|
578
|
+
autoInstall: runtime.autoInstall,
|
|
579
|
+
repoRef: runtime.repoRef ?? null,
|
|
580
|
+
repoUrl: runtime.repoUrl,
|
|
581
|
+
cwdRepoDir: runtime.cwdRepoDir ?? null,
|
|
582
|
+
repoExists: existsSync(runtime.repoDir),
|
|
583
|
+
dependenciesInstalled: hasInstalledDependencies(runtime.repoDir),
|
|
584
|
+
};
|
|
585
|
+
}),
|
|
586
|
+
setup: t.procedure
|
|
587
|
+
.input(SetupInput)
|
|
588
|
+
.meta({ description: "Configure auth + launcher defaults for current workspace" })
|
|
589
|
+
.mutation(async ({ input }) => {
|
|
590
|
+
const runtime = resolveRuntimeOptions();
|
|
591
|
+
|
|
592
|
+
const rawRepoPath = input.repoPath.trim().toLowerCase();
|
|
593
|
+
let repoPath = normalizePath(input.repoPath);
|
|
594
|
+
if (rawRepoPath === "managed") {
|
|
595
|
+
repoPath = DEFAULT_REPO_DIR;
|
|
596
|
+
} else if (rawRepoPath === "local") {
|
|
597
|
+
if (!runtime.cwdRepoDir) {
|
|
598
|
+
throw new Error(
|
|
599
|
+
"'local' repoPath was selected but current directory is not inside an iterate repo",
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
repoPath = runtime.cwdRepoDir;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const next = writeLauncherConfig({
|
|
606
|
+
launcherPatch: { repoPath, autoInstall: input.autoInstall },
|
|
607
|
+
workspacePatch: {
|
|
608
|
+
baseUrl: input.baseUrl,
|
|
609
|
+
adminPasswordEnvVarName: input.adminPasswordEnvVarName,
|
|
610
|
+
userId: input.userId,
|
|
611
|
+
},
|
|
612
|
+
scope: input.scope,
|
|
613
|
+
workspacePath: process.cwd(),
|
|
614
|
+
});
|
|
615
|
+
return {
|
|
616
|
+
configPath: CONFIG_PATH,
|
|
617
|
+
launcher: sanitizeLauncherConfig(getMergedWorkspaceConfig(next, process.cwd())),
|
|
618
|
+
scope: input.scope,
|
|
619
|
+
};
|
|
620
|
+
}),
|
|
621
|
+
install: t.procedure
|
|
622
|
+
.meta({ description: "Clone repo if needed, then run pnpm install" })
|
|
623
|
+
.mutation(async () => {
|
|
624
|
+
const runtime = resolveRuntimeOptions();
|
|
625
|
+
await ensureRepoCheckout(runtime);
|
|
626
|
+
await installDependencies({ repoDir: runtime.repoDir });
|
|
627
|
+
return { repoDir: runtime.repoDir };
|
|
628
|
+
}),
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
/** @param {string[]} args */
|
|
632
|
+
const runCli = async (args) => {
|
|
633
|
+
const runtime = resolveRuntimeOptions();
|
|
634
|
+
await ensureRepoCheckout(runtime);
|
|
635
|
+
|
|
636
|
+
if (runtime.autoInstall && !hasInstalledDependencies(runtime.repoDir)) {
|
|
637
|
+
await installDependencies({ repoDir: runtime.repoDir });
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const runtimeProcedures = await getRuntimeProcedures(runtime.repoDir);
|
|
641
|
+
const router = t.router({
|
|
642
|
+
...launcherProcedures,
|
|
643
|
+
...runtimeProcedures,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const cli = createCli({
|
|
647
|
+
router,
|
|
648
|
+
name: "iterate",
|
|
649
|
+
version: "0.0.1",
|
|
650
|
+
description: "Iterate CLI",
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
process.argv = [process.argv[0], process.argv[1], ...args];
|
|
654
|
+
await cli.run({
|
|
655
|
+
prompts: isAgent ? undefined : prompts,
|
|
656
|
+
});
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const main = async () => {
|
|
660
|
+
const args = process.argv.slice(2);
|
|
661
|
+
await runCli(args);
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
main().catch((error) => {
|
|
665
|
+
console.error(error);
|
|
666
|
+
process.exit(1);
|
|
667
|
+
});
|
package/package.json
CHANGED
|
@@ -1,28 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"test": "test"
|
|
8
|
-
},
|
|
9
|
-
"devDependencies": {
|
|
10
|
-
"it-is": "~1.0.2"
|
|
11
|
-
},
|
|
12
|
-
"scripts": {
|
|
13
|
-
"test": "synct test/*.js"
|
|
14
|
-
},
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CLI for iterate",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"type": "module",
|
|
15
7
|
"repository": {
|
|
16
8
|
"type": "git",
|
|
17
|
-
"url": "git://github.com/
|
|
9
|
+
"url": "git+https://github.com/iterate/iterate.git",
|
|
10
|
+
"directory": "packages/iterate"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/iterate/iterate#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/iterate/iterate/issues"
|
|
18
15
|
},
|
|
19
16
|
"keywords": [
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"map",
|
|
24
|
-
"filter"
|
|
17
|
+
"iterate",
|
|
18
|
+
"cli",
|
|
19
|
+
"trpc"
|
|
25
20
|
],
|
|
26
|
-
"
|
|
27
|
-
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"iterate": "./bin/iterate.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"bin",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"typecheck": "node --check ./bin/iterate.js"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@clack/prompts": "^1.0.0",
|
|
39
|
+
"@trpc/client": "^11.7.2",
|
|
40
|
+
"@trpc/server": "^11.7.2",
|
|
41
|
+
"better-auth": "1.4.3",
|
|
42
|
+
"superjson": "^2.2.2",
|
|
43
|
+
"trpc-cli": "^0.12.2",
|
|
44
|
+
"zod": "4.1.12"
|
|
45
|
+
}
|
|
28
46
|
}
|
package/README.markdown
DELETED
package/index.js
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// adds all the fields from obj2 onto obj1
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
var each = exports.each = function (obj,iterator){
|
|
7
|
-
var keys = Object.keys(obj)
|
|
8
|
-
keys.forEach(function (key){
|
|
9
|
-
iterator(obj[key],key,obj)
|
|
10
|
-
})
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
var RX = /sadf/.constructor
|
|
14
|
-
function rx (iterator ){
|
|
15
|
-
return iterator instanceof RX ? function (str) {
|
|
16
|
-
var m = iterator.exec(str)
|
|
17
|
-
return m && (m[1] ? m[1] : m[0])
|
|
18
|
-
} : iterator
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
var times = exports.times = function () {
|
|
22
|
-
var args = [].slice.call(arguments)
|
|
23
|
-
, iterator = rx(args.pop())
|
|
24
|
-
, m = args.pop()
|
|
25
|
-
, i = args.shift()
|
|
26
|
-
, j = args.shift()
|
|
27
|
-
, diff, dir
|
|
28
|
-
, a = []
|
|
29
|
-
|
|
30
|
-
i = 'number' === typeof i ? i : 1
|
|
31
|
-
diff = j ? j - i : 1
|
|
32
|
-
dir = i < m
|
|
33
|
-
if(m == i)
|
|
34
|
-
throw new Error('steps cannot be the same: '+m+', '+i)
|
|
35
|
-
for (; dir ? i <= m : m <= i; i += diff)
|
|
36
|
-
a.push(iterator(i))
|
|
37
|
-
return a
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
var map = exports.map = function (obj, iterator){
|
|
41
|
-
iterator = rx(iterator)
|
|
42
|
-
if(Array.isArray(obj))
|
|
43
|
-
return obj.map(iterator)
|
|
44
|
-
if('number' === typeof obj)
|
|
45
|
-
return times.apply(null, [].slice.call(arguments))
|
|
46
|
-
//return if null ?
|
|
47
|
-
var keys = Object.keys(obj)
|
|
48
|
-
, r = {}
|
|
49
|
-
keys.forEach(function (key){
|
|
50
|
-
r[key] = iterator(obj[key],key,obj)
|
|
51
|
-
})
|
|
52
|
-
return r
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
var findReturn = exports.findReturn = function (obj, iterator) {
|
|
56
|
-
iterator = rx(iterator)
|
|
57
|
-
if(obj == null)
|
|
58
|
-
return
|
|
59
|
-
var keys = Object.keys(obj)
|
|
60
|
-
, l = keys.length
|
|
61
|
-
for (var i = 0; i < l; i ++) {
|
|
62
|
-
var key = keys[i]
|
|
63
|
-
, value = obj[key]
|
|
64
|
-
var r = iterator(value, key)
|
|
65
|
-
if(r) return r
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
var find = exports.find = function (obj, iterator) {
|
|
70
|
-
iterator = rx(iterator)
|
|
71
|
-
return findReturn (obj, function (v, k) {
|
|
72
|
-
var r = iterator(v, k)
|
|
73
|
-
if(r) return v
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
var findKey = exports.findKey = function (obj, iterator) {
|
|
78
|
-
iterator = rx(iterator)
|
|
79
|
-
return findReturn (obj, function (v, k) {
|
|
80
|
-
var r = iterator(v, k)
|
|
81
|
-
if(r) return k
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
var filter = exports.filter = function (obj, iterator){
|
|
86
|
-
iterator = rx (iterator)
|
|
87
|
-
|
|
88
|
-
if(Array.isArray(obj))
|
|
89
|
-
return obj.filter(iterator)
|
|
90
|
-
|
|
91
|
-
var keys = Object.keys(obj)
|
|
92
|
-
, r = {}
|
|
93
|
-
keys.forEach(function (key){
|
|
94
|
-
var v
|
|
95
|
-
if(iterator(v = obj[key],key,obj))
|
|
96
|
-
r[key] = v
|
|
97
|
-
})
|
|
98
|
-
return r
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
var mapKeys = exports.mapKeys = function (ary, iterator){
|
|
102
|
-
var r = {}
|
|
103
|
-
iterator = rx(iterator)
|
|
104
|
-
each(ary, function (v,k){
|
|
105
|
-
r[v] = iterator(v,k)
|
|
106
|
-
})
|
|
107
|
-
return r
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
var mapToArray = exports.mapToArray = function (ary, iterator){
|
|
112
|
-
var r = []
|
|
113
|
-
iterator = rx(iterator)
|
|
114
|
-
each(ary, function (v,k){
|
|
115
|
-
r.push(iterator(v,k))
|
|
116
|
-
})
|
|
117
|
-
return r
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
var path = exports.path = function (object, path) {
|
|
121
|
-
|
|
122
|
-
for (var i in path) {
|
|
123
|
-
if(object == null) return undefined
|
|
124
|
-
var key = path[i]
|
|
125
|
-
object = object[key]
|
|
126
|
-
}
|
|
127
|
-
return object
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/*
|
|
131
|
-
NOTE: naive implementation.
|
|
132
|
-
`match` must not contain circular references.
|
|
133
|
-
*/
|
|
134
|
-
|
|
135
|
-
var setPath = exports.setPath = function (object, path, value) {
|
|
136
|
-
|
|
137
|
-
for (var i in path) {
|
|
138
|
-
var key = path[i]
|
|
139
|
-
if(object[key] == null) object[key] = (
|
|
140
|
-
i + 1 == path.length ? value : {}
|
|
141
|
-
)
|
|
142
|
-
object = object[key]
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
var join = exports.join = function (A, B, it) {
|
|
147
|
-
each(A, function (a, ak) {
|
|
148
|
-
each(B, function (b, bk) {
|
|
149
|
-
it(a, b, ak, bk)
|
|
150
|
-
})
|
|
151
|
-
})
|
|
152
|
-
}
|
package/test/objects.synct.js
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
var objects = require('..')
|
|
2
|
-
, it = require('it-is')
|
|
3
|
-
|
|
4
|
-
exports ['each'] = function (){
|
|
5
|
-
var on = {a: 1,b: 2, c: 3}, count = 0
|
|
6
|
-
objects.each(on, function(v,k){
|
|
7
|
-
it(v).equal(on[k])
|
|
8
|
-
count ++
|
|
9
|
-
})
|
|
10
|
-
it(count).equal(3)
|
|
11
|
-
}
|
|
12
|
-
exports ['map'] = function (){
|
|
13
|
-
|
|
14
|
-
var on = {a: 1,b: 2, c: 3}, count = 0
|
|
15
|
-
var off =
|
|
16
|
-
objects.map(on, function(v,k){
|
|
17
|
-
it(v).equal(on[k])
|
|
18
|
-
count ++
|
|
19
|
-
return v * 2
|
|
20
|
-
})
|
|
21
|
-
it(count).equal(3)
|
|
22
|
-
it(off).deepEqual({
|
|
23
|
-
a: 2, b: 4,c: 6
|
|
24
|
-
})
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
exports ['map -- {}'] = function (){
|
|
28
|
-
|
|
29
|
-
var on = {}, count = 0
|
|
30
|
-
var off =
|
|
31
|
-
objects.map(on, function(v,k){
|
|
32
|
-
count ++
|
|
33
|
-
})
|
|
34
|
-
it(count).equal(0)
|
|
35
|
-
it(off).deepEqual({})
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
exports ['map -- {}'] = function (){
|
|
39
|
-
|
|
40
|
-
var on = {}, count = 0
|
|
41
|
-
var off =
|
|
42
|
-
objects.map(on, function(v,k){
|
|
43
|
-
count ++
|
|
44
|
-
})
|
|
45
|
-
it(count).equal(0)
|
|
46
|
-
it(off).deepEqual({})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
exports ['map on numbers -- start at 1'] = function () {
|
|
50
|
-
|
|
51
|
-
var seven = objects.map(7, function (n) {
|
|
52
|
-
return n
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it(seven).deepEqual([1,2,3,4,5,6,7])
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
exports ['map on numbers: 0 - 8'] = function () {
|
|
60
|
-
|
|
61
|
-
var seven = objects.map(0, 8, function (n) {
|
|
62
|
-
return n
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it(seven).deepEqual([0,1,2,3,4,5,6,7,8])
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
exports ['map even numbers: 2,4...12'] = function () {
|
|
70
|
-
|
|
71
|
-
var seven = objects.map(2, 4, 12, function (n) {
|
|
72
|
-
return n
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it(seven).deepEqual([2,4,6,8,10,12])
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
exports ['map even numbers: 12, 10...2 in reverse'] = function () {
|
|
79
|
-
|
|
80
|
-
var seven = objects.map(12, 10, 2, function (n) {
|
|
81
|
-
return n
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it(seven).deepEqual([2,4,6,8,10,12].reverse())
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
exports ['filter'] = function (){
|
|
90
|
-
|
|
91
|
-
var on = {a: 1,b: 2, c: 3, d:4, e:5, f:6, g:7}, count = 0
|
|
92
|
-
|
|
93
|
-
it(objects.filter(on, function (x){
|
|
94
|
-
return !(x % 2)
|
|
95
|
-
})).deepEqual({b: 2, d:4, f:6})
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
exports ['filter with regexp'] = function (){
|
|
99
|
-
|
|
100
|
-
var on = [ '.git',
|
|
101
|
-
'test',
|
|
102
|
-
'objects.js',
|
|
103
|
-
'index.js',
|
|
104
|
-
'package.json',
|
|
105
|
-
'types.js',
|
|
106
|
-
'readme.markdown',
|
|
107
|
-
'.gitignore',
|
|
108
|
-
'arrays.js' ], count = 0
|
|
109
|
-
|
|
110
|
-
it(objects.filter(on, /^.*\.js$/))
|
|
111
|
-
.deepEqual([
|
|
112
|
-
'objects.js',
|
|
113
|
-
'index.js',
|
|
114
|
-
'types.js',
|
|
115
|
-
'arrays.js' ])
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
exports ['mapKeys -- create a object from a list of keys and a function'] = function (){
|
|
119
|
-
|
|
120
|
-
var keys = ['foo','bar','xux']
|
|
121
|
-
|
|
122
|
-
it(objects.mapKeys(keys, function (k){
|
|
123
|
-
return k.toUpperCase()
|
|
124
|
-
})).deepEqual({foo: 'FOO', bar: 'BAR', xux: 'XUX'})
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
exports ['mapToArray'] = function (){
|
|
129
|
-
|
|
130
|
-
var on = {a: 1,b: 2, c: 3, d:4, e:5, f:6, g:7}, count = 0
|
|
131
|
-
|
|
132
|
-
it(objects.mapToArray(on,function (v){
|
|
133
|
-
return v
|
|
134
|
-
})).deepEqual([1,2,3,4,5,6,7])
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
exports ['find'] = function () {
|
|
139
|
-
|
|
140
|
-
//
|
|
141
|
-
// sometime need to stop before the end, find is like that
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
var numbers = [3, 5, 3, 7, 77, 21, 4]
|
|
145
|
-
|
|
146
|
-
it(objects.find(numbers, function (e) { return !(e % 2)})).equal(4)
|
|
147
|
-
it(objects.findKey(numbers, function (e) { return !(e % 2)})).equal(6)
|
|
148
|
-
it(objects.findReturn(numbers, function (e) { return !(e % 2)})).equal(true)
|
|
149
|
-
|
|
150
|
-
it(objects.find([], function () {})).equal(null)
|
|
151
|
-
it(objects.find(null, function () {})).equal(null)
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
exports ['path'] = function () {
|
|
156
|
-
|
|
157
|
-
var a1 = {A: 1}
|
|
158
|
-
|
|
159
|
-
it(objects.path(a1, ['A'])).equal(1)
|
|
160
|
-
it(objects.path({A: {B: 4}}, ['A', 'B'])).equal(4)
|
|
161
|
-
it(objects.path({A: {B: 4}}, ['A', 'C'])).strictEqual(undefined)
|
|
162
|
-
it(objects.path(null, ['A', 'C'])).strictEqual(undefined)
|
|
163
|
-
it(objects.path({Z: [0, 1, 2]}, ['Z', 2])).strictEqual(2)
|
|
164
|
-
it(objects.path({Z: [0, 1, 2]}, ['Z', 0])).strictEqual(0)
|
|
165
|
-
it(objects.path({Z: [0, 1, 2]}, ['Z', 0, 'T'])).strictEqual(undefined)
|
|
166
|
-
it(objects.path({Z: null}, ['Z', 0, 'T'])).strictEqual(undefined)
|
|
167
|
-
it(objects.path({Z: null}, ['Z'])).strictEqual(null)
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
exports ['setPath'] = function () {
|
|
172
|
-
|
|
173
|
-
var x = {}
|
|
174
|
-
, c = Math.random()
|
|
175
|
-
, z = Math.random()
|
|
176
|
-
objects.setPath(x, ['a','b','c'], c)
|
|
177
|
-
it.equal(x.a.b.c, c)
|
|
178
|
-
objects.setPath(x, ['z'], z)
|
|
179
|
-
it.equal(x.z, z)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
exports['join'] = function () {
|
|
183
|
-
var ary = []
|
|
184
|
-
objects.join('abc'.split(''), 'ijk'.split(''), function (a, i) {
|
|
185
|
-
ary.push([a, i])
|
|
186
|
-
})
|
|
187
|
-
it.equal(ary.length, 9)
|
|
188
|
-
}
|