iterate 0.1.1 → 0.2.2
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 +744 -0
- package/package.json +38 -20
- package/LICENSE +0 -24
- 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-email dev-yourname@iterate.com \
|
|
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
|
+
"userEmail": "dev-yourname@iterate.com",
|
|
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-email dev-yourname@iterate.com \
|
|
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,744 @@
|
|
|
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
|
+
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
|
+
/**
|
|
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
|
+
const impersonationUserIdCache = new Map();
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* @param {{
|
|
357
|
+
* superadminAuthClient: any;
|
|
358
|
+
* userEmail: string;
|
|
359
|
+
* baseUrl: string;
|
|
360
|
+
* }} options
|
|
361
|
+
*/
|
|
362
|
+
const resolveImpersonationUserId = async ({ superadminAuthClient, userEmail, baseUrl }) => {
|
|
363
|
+
const normalizedEmail = userEmail.trim().toLowerCase();
|
|
364
|
+
const cacheKey = `${baseUrl}::${normalizedEmail}`;
|
|
365
|
+
const cachedUserId = impersonationUserIdCache.get(cacheKey);
|
|
366
|
+
if (cachedUserId) {
|
|
367
|
+
return cachedUserId;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** @type {any[]} */
|
|
371
|
+
let users = [];
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const result = await superadminAuthClient.admin.listUsers({
|
|
375
|
+
query: {
|
|
376
|
+
filterField: "email",
|
|
377
|
+
filterOperator: "eq",
|
|
378
|
+
filterValue: normalizedEmail,
|
|
379
|
+
limit: 10,
|
|
380
|
+
},
|
|
381
|
+
fetchOptions: {
|
|
382
|
+
throw: true,
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
users = Array.isArray(result?.users) ? result.users : [];
|
|
386
|
+
} catch {
|
|
387
|
+
const result = await superadminAuthClient.admin.listUsers({
|
|
388
|
+
query: {
|
|
389
|
+
searchField: "email",
|
|
390
|
+
searchOperator: "contains",
|
|
391
|
+
searchValue: normalizedEmail,
|
|
392
|
+
limit: 100,
|
|
393
|
+
},
|
|
394
|
+
fetchOptions: {
|
|
395
|
+
throw: true,
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
users = Array.isArray(result?.users) ? result.users : [];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const exactMatches = users.filter(
|
|
402
|
+
(user) =>
|
|
403
|
+
user &&
|
|
404
|
+
typeof user === "object" &&
|
|
405
|
+
"email" in user &&
|
|
406
|
+
typeof user.email === "string" &&
|
|
407
|
+
user.email.toLowerCase() === normalizedEmail &&
|
|
408
|
+
"id" in user &&
|
|
409
|
+
typeof user.id === "string",
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
if (exactMatches.length === 0) {
|
|
413
|
+
throw new Error(`No user found with email ${userEmail}`);
|
|
414
|
+
}
|
|
415
|
+
if (exactMatches.length > 1) {
|
|
416
|
+
throw new Error(`Multiple users found with email ${userEmail}`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const resolvedUserId = exactMatches[0].id;
|
|
420
|
+
impersonationUserIdCache.set(cacheKey, resolvedUserId);
|
|
421
|
+
return resolvedUserId;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
/** @param {z.infer<typeof AuthConfig>} authConfig */
|
|
425
|
+
const authDance = async (authConfig) => {
|
|
426
|
+
let superadminSetCookie;
|
|
427
|
+
const authClient = createAuthClient({
|
|
428
|
+
baseURL: authConfig.baseUrl,
|
|
429
|
+
fetchOptions: {
|
|
430
|
+
throw: true,
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
const password = process.env[authConfig.adminPasswordEnvVarName];
|
|
434
|
+
if (!password) {
|
|
435
|
+
throw new Error(`Password not found in env var ${authConfig.adminPasswordEnvVarName}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
await authClient.signIn.email({
|
|
439
|
+
email: "superadmin@nustom.com",
|
|
440
|
+
password,
|
|
441
|
+
fetchOptions: {
|
|
442
|
+
throw: true,
|
|
443
|
+
onResponse: (ctx) => {
|
|
444
|
+
superadminSetCookie = ctx.response.headers.getSetCookie();
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const superadminAuthClient = createAuthClient({
|
|
450
|
+
baseURL: authConfig.baseUrl,
|
|
451
|
+
fetchOptions: {
|
|
452
|
+
throw: true,
|
|
453
|
+
onRequest: (ctx) => {
|
|
454
|
+
ctx.headers.set("origin", authConfig.baseUrl);
|
|
455
|
+
ctx.headers.set("cookie", setCookiesToCookieHeader(superadminSetCookie));
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
plugins: [adminClient()],
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const userId = await resolveImpersonationUserId({
|
|
462
|
+
superadminAuthClient,
|
|
463
|
+
userEmail: authConfig.userEmail,
|
|
464
|
+
baseUrl: authConfig.baseUrl,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
let impersonateSetCookie;
|
|
468
|
+
await superadminAuthClient.admin.impersonateUser({
|
|
469
|
+
userId,
|
|
470
|
+
fetchOptions: {
|
|
471
|
+
throw: true,
|
|
472
|
+
onResponse: (ctx) => {
|
|
473
|
+
impersonateSetCookie = ctx.response.headers.getSetCookie();
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const userCookies = setCookiesToCookieHeader(impersonateSetCookie);
|
|
479
|
+
|
|
480
|
+
const userClient = createAuthClient({
|
|
481
|
+
baseURL: authConfig.baseUrl,
|
|
482
|
+
fetchOptions: {
|
|
483
|
+
throw: true,
|
|
484
|
+
onRequest: (ctx) => {
|
|
485
|
+
ctx.headers.set("origin", authConfig.baseUrl);
|
|
486
|
+
ctx.headers.set("cookie", userCookies);
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
return { userCookies, userClient };
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
/** @param {string} repoDir */
|
|
495
|
+
const loadAppRouter = async (repoDir) => {
|
|
496
|
+
const appRouterPath = join(repoDir, APP_ROUTER_PATH);
|
|
497
|
+
if (!existsSync(appRouterPath)) {
|
|
498
|
+
throw new Error(`Could not find ${APP_ROUTER_PATH} under ${repoDir}.`);
|
|
499
|
+
}
|
|
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
|
+
|
|
525
|
+
child.on("close", (code, signal) => {
|
|
526
|
+
if (signal) {
|
|
527
|
+
rejectPromise(new Error(`${command} exited with signal ${signal}`));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
resolvePromise(code ?? 0);
|
|
531
|
+
});
|
|
532
|
+
});
|
|
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;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
/** @param {string} repoDir */
|
|
616
|
+
const getRuntimeProcedures = async (repoDir) => {
|
|
617
|
+
const appRouter = await loadAppRouter(repoDir);
|
|
618
|
+
const proxiedRouter = proxify(appRouter, async () => {
|
|
619
|
+
return createTRPCClient({
|
|
620
|
+
links: [
|
|
621
|
+
httpLink({
|
|
622
|
+
url: `${readAuthConfig(process.cwd()).baseUrl}/api/trpc/`,
|
|
623
|
+
transformer: superjson,
|
|
624
|
+
fetch: async (request, init) => {
|
|
625
|
+
const authConfig = readAuthConfig(process.cwd());
|
|
626
|
+
const { userCookies } = await authDance(authConfig);
|
|
627
|
+
const headers = new Headers(init?.headers);
|
|
628
|
+
headers.set("cookie", userCookies);
|
|
629
|
+
return fetch(request, { ...init, headers });
|
|
630
|
+
},
|
|
631
|
+
}),
|
|
632
|
+
],
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
whoami: t.procedure.mutation(async () => {
|
|
638
|
+
const authConfig = readAuthConfig(process.cwd());
|
|
639
|
+
const { userClient } = await authDance(authConfig);
|
|
640
|
+
return await userClient.getSession();
|
|
641
|
+
}),
|
|
642
|
+
os: proxiedRouter,
|
|
643
|
+
};
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const launcherProcedures = {
|
|
647
|
+
doctor: t.procedure
|
|
648
|
+
.meta({ description: "Show launcher config and resolved runtime options" })
|
|
649
|
+
.mutation(async () => {
|
|
650
|
+
const runtime = resolveRuntimeOptions();
|
|
651
|
+
return {
|
|
652
|
+
configPath: CONFIG_PATH,
|
|
653
|
+
repoDir: runtime.repoDir,
|
|
654
|
+
repoDirSource: runtime.repoDirSource,
|
|
655
|
+
autoInstall: runtime.autoInstall,
|
|
656
|
+
repoRef: runtime.repoRef ?? null,
|
|
657
|
+
repoUrl: runtime.repoUrl,
|
|
658
|
+
cwdRepoDir: runtime.cwdRepoDir ?? null,
|
|
659
|
+
repoExists: existsSync(runtime.repoDir),
|
|
660
|
+
dependenciesInstalled: hasInstalledDependencies(runtime.repoDir),
|
|
661
|
+
};
|
|
662
|
+
}),
|
|
663
|
+
setup: t.procedure
|
|
664
|
+
.input(SetupInput)
|
|
665
|
+
.meta({ description: "Configure auth + launcher defaults for current workspace" })
|
|
666
|
+
.mutation(async ({ input }) => {
|
|
667
|
+
const runtime = resolveRuntimeOptions();
|
|
668
|
+
|
|
669
|
+
const rawRepoPath = input.repoPath.trim().toLowerCase();
|
|
670
|
+
let repoPath = normalizePath(input.repoPath);
|
|
671
|
+
if (rawRepoPath === "managed") {
|
|
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,
|
|
686
|
+
adminPasswordEnvVarName: input.adminPasswordEnvVarName,
|
|
687
|
+
userEmail: input.userEmail,
|
|
688
|
+
},
|
|
689
|
+
scope: input.scope,
|
|
690
|
+
workspacePath: process.cwd(),
|
|
691
|
+
});
|
|
692
|
+
return {
|
|
693
|
+
configPath: CONFIG_PATH,
|
|
694
|
+
launcher: sanitizeLauncherConfig(getMergedWorkspaceConfig(next, process.cwd())),
|
|
695
|
+
scope: input.scope,
|
|
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 };
|
|
705
|
+
}),
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
/** @param {string[]} args */
|
|
709
|
+
const runCli = async (args) => {
|
|
710
|
+
const runtime = resolveRuntimeOptions();
|
|
711
|
+
await ensureRepoCheckout(runtime);
|
|
712
|
+
|
|
713
|
+
if (runtime.autoInstall && !hasInstalledDependencies(runtime.repoDir)) {
|
|
714
|
+
await installDependencies({ repoDir: runtime.repoDir });
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const runtimeProcedures = await getRuntimeProcedures(runtime.repoDir);
|
|
718
|
+
const router = t.router({
|
|
719
|
+
...launcherProcedures,
|
|
720
|
+
...runtimeProcedures,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
const cli = createCli({
|
|
724
|
+
router,
|
|
725
|
+
name: "iterate",
|
|
726
|
+
version: "0.0.1",
|
|
727
|
+
description: "Iterate CLI",
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
process.argv = [process.argv[0], process.argv[1], ...args];
|
|
731
|
+
await cli.run({
|
|
732
|
+
prompts: isAgent ? undefined : prompts,
|
|
733
|
+
});
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
const main = async () => {
|
|
737
|
+
const args = process.argv.slice(2);
|
|
738
|
+
await runCli(args);
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
main().catch((error) => {
|
|
742
|
+
console.error(error);
|
|
743
|
+
process.exit(1);
|
|
744
|
+
});
|
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.2",
|
|
4
|
+
"description": "CLI for iterate",
|
|
5
|
+
"license": "Apache-2.0",
|
|
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/LICENSE
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
The MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2013 Dominic Tarr
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge,
|
|
6
|
-
to any person obtaining a copy of this software and
|
|
7
|
-
associated documentation files (the "Software"), to
|
|
8
|
-
deal in the Software without restriction, including
|
|
9
|
-
without limitation the rights to use, copy, modify,
|
|
10
|
-
merge, publish, distribute, sublicense, and/or sell
|
|
11
|
-
copies of the Software, and to permit persons to whom
|
|
12
|
-
the Software is furnished to do so,
|
|
13
|
-
subject to the following conditions:
|
|
14
|
-
|
|
15
|
-
The above copyright notice and this permission notice
|
|
16
|
-
shall be included in all copies or substantial portions of the Software.
|
|
17
|
-
|
|
18
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
19
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
20
|
-
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
21
|
-
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
|
22
|
-
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
23
|
-
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
24
|
-
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
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
|
-
}
|