@foldspace_npm/harness 0.1.1 → 0.1.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 +22 -23
- package/bin/cli.mjs +56 -6
- package/package.json +2 -6
- package/src/init.mjs +191 -17
- package/templates/agent-starter/package.json +4 -4
package/README.md
CHANGED
|
@@ -26,8 +26,8 @@ constraint this package exists to protect.
|
|
|
26
26
|
|
|
27
27
|
- `agent/actions/*` and `agent/api/*` — a handler is `fetch` plus `runTask`,
|
|
28
28
|
nothing more. The actions built for Figma run unmodified in either track.
|
|
29
|
-
- `foldspace
|
|
30
|
-
- `foldspace
|
|
29
|
+
- `foldspace build` — esbuild → `dist/index.js`
|
|
30
|
+
- `foldspace deploy` — publish to `agent/actions/<env>/<productId>/<agentApiName>`
|
|
31
31
|
- fixtures and tests
|
|
32
32
|
- **the verb interface** below
|
|
33
33
|
|
|
@@ -60,10 +60,16 @@ dead weight, against a contract that cannot drift.
|
|
|
60
60
|
### Create a project
|
|
61
61
|
|
|
62
62
|
Create a configured actions project from the template bundled with the installed
|
|
63
|
-
harness version:
|
|
63
|
+
harness version. Missing values are prompted when stdin is a terminal:
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
|
|
66
|
+
npx --yes @foldspace_npm/harness init
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Non-interactive / CI form:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx --yes @foldspace_npm/harness init my-agent \
|
|
67
73
|
--product-id FR8JUQZAQRZB \
|
|
68
74
|
--agent-api-name my-agent \
|
|
69
75
|
--domain app.example.com \
|
|
@@ -79,24 +85,17 @@ node bin/cli.mjs init ../my-agent \
|
|
|
79
85
|
--domain app.example.com
|
|
80
86
|
```
|
|
81
87
|
|
|
82
|
-
Until `@foldspace_npm/harness` is published, install the local checkout in the
|
|
83
|
-
generated project instead of running the standard install step:
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
cd ../my-agent
|
|
87
|
-
npm install --ignore-scripts --save-dev /absolute/path/to/harness
|
|
88
|
-
```
|
|
89
|
-
|
|
90
88
|
`--name` is optional and defaults to the target directory name. The product ID
|
|
91
89
|
must be the bare ID, not an `EU-…` SDK key. The domain may be a hostname or an
|
|
92
90
|
HTTP(S) URL without a port or path.
|
|
93
91
|
|
|
94
92
|
For safety, `init` requires a target path that does not exist. It does not
|
|
95
|
-
|
|
93
|
+
initialize Git or overwrite files. On a TTY it can offer
|
|
94
|
+
`npm install --ignore-scripts` after scaffolding. After creation:
|
|
96
95
|
|
|
97
96
|
```bash
|
|
98
97
|
cd my-agent
|
|
99
|
-
npm install --ignore-scripts
|
|
98
|
+
npm install --ignore-scripts # if you skipped the install prompt
|
|
100
99
|
npm run build
|
|
101
100
|
npm run inject
|
|
102
101
|
npm run attach
|
|
@@ -113,21 +112,21 @@ npm i -D @foldspace_npm/harness
|
|
|
113
112
|
|
|
114
113
|
```json
|
|
115
114
|
{ "scripts": {
|
|
116
|
-
"dev": "foldspace
|
|
117
|
-
"build": "foldspace
|
|
118
|
-
"inject": "foldspace
|
|
119
|
-
"attach": "foldspace
|
|
120
|
-
"deploy": "foldspace
|
|
115
|
+
"dev": "foldspace build --watch",
|
|
116
|
+
"build": "foldspace build",
|
|
117
|
+
"inject": "foldspace inject",
|
|
118
|
+
"attach": "foldspace attach",
|
|
119
|
+
"deploy": "foldspace deploy"
|
|
121
120
|
} }
|
|
122
121
|
```
|
|
123
122
|
|
|
124
123
|
| Command | A | B |
|
|
125
124
|
|---|:--:|:--:|
|
|
126
125
|
| `foldspace init` | ✅ | ✅ |
|
|
127
|
-
| `foldspace
|
|
128
|
-
| `foldspace
|
|
129
|
-
| `foldspace
|
|
130
|
-
| `foldspace
|
|
126
|
+
| `foldspace build` | ✅ | ✅ |
|
|
127
|
+
| `foldspace deploy` | ✅ | ✅ — it *is* the delivery mechanism |
|
|
128
|
+
| `foldspace inject` | ✅ | — no browser to launch |
|
|
129
|
+
| `foldspace attach` | ✅ | replaced by the extension bridge |
|
|
131
130
|
|
|
132
131
|
Every command resolves the **consuming** repo — `process.cwd()`, or
|
|
133
132
|
`FOLDSPACE_PROJECT_DIR` so a hosted builder can point it at a workspace it
|
package/bin/cli.mjs
CHANGED
|
@@ -1,12 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
3
6
|
import { initUsage, runInit } from "../src/init.mjs";
|
|
4
7
|
|
|
8
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
const commands = {
|
|
11
|
+
init: "Create a configured Foldspace actions project",
|
|
12
|
+
build: "Bundle agent actions to dist/index.js",
|
|
13
|
+
inject: "Launch the dedicated Chrome profile",
|
|
14
|
+
attach: "Load the SDK and local actions over CDP",
|
|
15
|
+
deploy: "Publish the built bundle",
|
|
16
|
+
"package-extension": "Package the browser extension",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const scriptByCommand = {
|
|
20
|
+
build: "build-cli.mjs",
|
|
21
|
+
inject: "inject.mjs",
|
|
22
|
+
attach: "attach.mjs",
|
|
23
|
+
deploy: "deploy.mjs",
|
|
24
|
+
"package-extension": "packageExtension.mjs",
|
|
25
|
+
};
|
|
26
|
+
|
|
5
27
|
const usage = `Usage:
|
|
28
|
+
foldspace <command> [args]
|
|
6
29
|
${initUsage}
|
|
30
|
+
foldspace build [--watch]
|
|
31
|
+
foldspace inject [options]
|
|
32
|
+
foldspace attach [options]
|
|
33
|
+
foldspace deploy [options]
|
|
34
|
+
foldspace package-extension
|
|
7
35
|
|
|
8
36
|
Commands:
|
|
9
|
-
|
|
37
|
+
${Object.entries(commands)
|
|
38
|
+
.map(([name, description]) => ` ${name.padEnd(18)} ${description}`)
|
|
39
|
+
.join("\n")}
|
|
10
40
|
`;
|
|
11
41
|
|
|
12
42
|
function fail(message) {
|
|
@@ -14,6 +44,20 @@ function fail(message) {
|
|
|
14
44
|
process.exitCode = 1;
|
|
15
45
|
}
|
|
16
46
|
|
|
47
|
+
function runScript(scriptName, args) {
|
|
48
|
+
const child = spawn(process.execPath, [path.join(here, scriptName), ...args], {
|
|
49
|
+
stdio: "inherit",
|
|
50
|
+
env: process.env,
|
|
51
|
+
});
|
|
52
|
+
child.on("exit", (code, signal) => {
|
|
53
|
+
if (signal) {
|
|
54
|
+
process.kill(process.pid, signal);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
process.exit(code ?? 0);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
17
61
|
const [command, ...args] = process.argv.slice(2);
|
|
18
62
|
|
|
19
63
|
if (!command || command === "--help" || command === "-h") {
|
|
@@ -22,11 +66,17 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
22
66
|
if (args.includes("--help") || args.includes("-h")) {
|
|
23
67
|
console.log(`Usage: ${initUsage}`);
|
|
24
68
|
} else {
|
|
25
|
-
|
|
26
|
-
runInit(args)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
69
|
+
Promise.resolve()
|
|
70
|
+
.then(() => runInit(args))
|
|
71
|
+
.catch((error) => {
|
|
72
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
} else if (scriptByCommand[command]) {
|
|
76
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
77
|
+
console.log(`Usage: foldspace ${command} [options]`);
|
|
78
|
+
} else {
|
|
79
|
+
runScript(scriptByCommand[command], args);
|
|
30
80
|
}
|
|
31
81
|
} else {
|
|
32
82
|
fail(`unknown command '${command}'\n\n${usage}`);
|
package/package.json
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foldspace_npm/harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Build, inject and verify Foldspace agent experiences against a live app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"foldspace": "bin/cli.mjs",
|
|
8
|
-
"
|
|
9
|
-
"foldspace-inject": "bin/inject.mjs",
|
|
10
|
-
"foldspace-attach": "bin/attach.mjs",
|
|
11
|
-
"foldspace-deploy": "bin/deploy.mjs",
|
|
12
|
-
"foldspace-package-extension": "bin/packageExtension.mjs"
|
|
8
|
+
"harness": "bin/cli.mjs"
|
|
13
9
|
},
|
|
14
10
|
"files": [
|
|
15
11
|
"bin",
|
package/src/init.mjs
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import readline from "node:readline/promises";
|
|
5
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
6
|
import { fileURLToPath } from "node:url";
|
|
4
7
|
|
|
5
8
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
9
|
const defaultTemplateRoot = path.join(packageRoot, "templates", "agent-starter");
|
|
7
10
|
const allowedFlags = new Set(["name", "product-id", "agent-api-name", "domain"]);
|
|
8
11
|
const tokenPattern = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
12
|
+
const defaultDirectory = "my-agent";
|
|
9
13
|
|
|
10
14
|
export const initUsage =
|
|
11
|
-
"foldspace init <directory> --product-id <id> --agent-api-name <name> --domain <host> [--name <display-name>]";
|
|
15
|
+
"foldspace init [<directory>] [--product-id <id>] [--agent-api-name <name>] [--domain <host>] [--name <display-name>]";
|
|
12
16
|
|
|
13
17
|
function assertSupportedNode() {
|
|
14
18
|
const major = Number.parseInt(process.versions.node.split(".", 1)[0], 10);
|
|
@@ -17,6 +21,13 @@ function assertSupportedNode() {
|
|
|
17
21
|
}
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
function isInteractive(options = {}) {
|
|
25
|
+
if (typeof options.interactive === "boolean") {
|
|
26
|
+
return options.interactive;
|
|
27
|
+
}
|
|
28
|
+
return Boolean(options.stdin?.isTTY ?? input.isTTY);
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
function readFlag(argv, index) {
|
|
21
32
|
const argument = argv[index];
|
|
22
33
|
const equalsIndex = argument.indexOf("=");
|
|
@@ -61,25 +72,28 @@ export function parseInitArgs(argv) {
|
|
|
61
72
|
index += 1;
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
if (positional.length
|
|
75
|
+
if (positional.length > 1) {
|
|
65
76
|
throw new Error(`Usage: ${initUsage}`);
|
|
66
77
|
}
|
|
67
78
|
|
|
68
|
-
for (const required of ["product-id", "agent-api-name", "domain"]) {
|
|
69
|
-
if (!flags[required]) {
|
|
70
|
-
throw new Error(`Missing required option: --${required}\nUsage: ${initUsage}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
79
|
return {
|
|
75
|
-
|
|
76
|
-
displayName: flags.name ||
|
|
77
|
-
productId: flags["product-id"],
|
|
78
|
-
agentApiName: flags["agent-api-name"],
|
|
79
|
-
domain: flags.domain,
|
|
80
|
+
directory: positional[0] || null,
|
|
81
|
+
displayName: flags.name || null,
|
|
82
|
+
productId: flags["product-id"] || null,
|
|
83
|
+
agentApiName: flags["agent-api-name"] || null,
|
|
84
|
+
domain: flags.domain || null,
|
|
80
85
|
};
|
|
81
86
|
}
|
|
82
87
|
|
|
88
|
+
function missingInitFields(parsed) {
|
|
89
|
+
const missing = [];
|
|
90
|
+
if (!parsed.directory) missing.push("directory");
|
|
91
|
+
if (!parsed.productId) missing.push("product-id");
|
|
92
|
+
if (!parsed.agentApiName) missing.push("agent-api-name");
|
|
93
|
+
if (!parsed.domain) missing.push("domain");
|
|
94
|
+
return missing;
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
function toPackageName(value) {
|
|
84
98
|
return value
|
|
85
99
|
.trim()
|
|
@@ -142,6 +156,111 @@ function normalizeTarget(value) {
|
|
|
142
156
|
};
|
|
143
157
|
}
|
|
144
158
|
|
|
159
|
+
async function ask(question, options = {}) {
|
|
160
|
+
if (typeof options.ask === "function") {
|
|
161
|
+
return options.ask(question);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const rl = readline.createInterface({
|
|
165
|
+
input: options.stdin || input,
|
|
166
|
+
output: options.stdout || output,
|
|
167
|
+
});
|
|
168
|
+
try {
|
|
169
|
+
return (await rl.question(question)).trim();
|
|
170
|
+
} finally {
|
|
171
|
+
rl.close();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function askRequired(label, options = {}) {
|
|
176
|
+
while (true) {
|
|
177
|
+
const value = await ask(`${label}: `, options);
|
|
178
|
+
if (value) {
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
(options.log || console.log)(`${label} is required.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function askUntilValid(label, validate, options = {}) {
|
|
186
|
+
while (true) {
|
|
187
|
+
const value = await askRequired(label, options);
|
|
188
|
+
try {
|
|
189
|
+
return validate(value);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
(options.log || console.log)(error instanceof Error ? error.message : String(error));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function promptForMissingFields(parsed, options = {}) {
|
|
197
|
+
const next = { ...parsed };
|
|
198
|
+
const log = options.log || console.log;
|
|
199
|
+
|
|
200
|
+
if (!next.directory) {
|
|
201
|
+
const value = await ask(`Project directory [${defaultDirectory}]: `, options);
|
|
202
|
+
next.directory = value || defaultDirectory;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!next.productId) {
|
|
206
|
+
next.productId = await askUntilValid("Product ID", validateProductId, options);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!next.agentApiName) {
|
|
210
|
+
next.agentApiName = await askUntilValid(
|
|
211
|
+
"Agent API name",
|
|
212
|
+
(value) => validateIdentifier(value, "Agent API name"),
|
|
213
|
+
options,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!next.domain) {
|
|
218
|
+
next.domain = await askUntilValid(
|
|
219
|
+
"Domain",
|
|
220
|
+
(value) => {
|
|
221
|
+
normalizeTarget(value);
|
|
222
|
+
return value.trim();
|
|
223
|
+
},
|
|
224
|
+
options,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!next.displayName) {
|
|
229
|
+
const defaultName = path.basename(path.resolve(next.directory));
|
|
230
|
+
const value = await ask(`Display name [${defaultName}]: `, options);
|
|
231
|
+
next.displayName = value || defaultName;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
log("");
|
|
235
|
+
return next;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function finalizeInitConfig(parsed) {
|
|
239
|
+
const directory = parsed.directory;
|
|
240
|
+
if (!directory) {
|
|
241
|
+
throw new Error(`Missing required option: directory\nUsage: ${initUsage}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const [key, label] of [
|
|
245
|
+
["productId", "product-id"],
|
|
246
|
+
["agentApiName", "agent-api-name"],
|
|
247
|
+
["domain", "domain"],
|
|
248
|
+
]) {
|
|
249
|
+
if (!parsed[key]) {
|
|
250
|
+
throw new Error(`Missing required option: --${label}\nUsage: ${initUsage}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const targetDir = path.resolve(directory);
|
|
255
|
+
return {
|
|
256
|
+
targetDir,
|
|
257
|
+
displayName: parsed.displayName || path.basename(targetDir),
|
|
258
|
+
productId: parsed.productId,
|
|
259
|
+
agentApiName: parsed.agentApiName,
|
|
260
|
+
domain: parsed.domain,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
145
264
|
function readHarnessVersion(root) {
|
|
146
265
|
const manifestPath = path.join(root, "package.json");
|
|
147
266
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
@@ -281,21 +400,76 @@ export function scaffoldProject(config, options = {}) {
|
|
|
281
400
|
};
|
|
282
401
|
}
|
|
283
402
|
|
|
284
|
-
|
|
403
|
+
function runInstall(targetDir, options = {}) {
|
|
404
|
+
const install = options.install || ((cwd) => {
|
|
405
|
+
execFileSync("npm", ["install", "--ignore-scripts"], {
|
|
406
|
+
cwd,
|
|
407
|
+
stdio: "inherit",
|
|
408
|
+
env: process.env,
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
install(targetDir);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function isAffirmative(value) {
|
|
415
|
+
const normalized = value.trim().toLowerCase();
|
|
416
|
+
return normalized === "" || normalized === "y" || normalized === "yes";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function maybeInstallDependencies(targetDir, options = {}) {
|
|
420
|
+
if (!isInteractive(options)) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (options.autoInstall === true) {
|
|
425
|
+
runInstall(targetDir, options);
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (options.autoInstall === false) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const answer = await ask("Run npm install --ignore-scripts now? [Y/n] ", options);
|
|
434
|
+
if (!isAffirmative(answer)) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
runInstall(targetDir, options);
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export async function runInit(argv, options = {}) {
|
|
285
443
|
assertSupportedNode();
|
|
286
|
-
|
|
444
|
+
let parsed = parseInitArgs(argv);
|
|
445
|
+
const missing = missingInitFields(parsed);
|
|
446
|
+
|
|
447
|
+
if (missing.length > 0) {
|
|
448
|
+
if (!isInteractive(options)) {
|
|
449
|
+
const label = missing[0] === "directory" ? "directory" : `--${missing[0]}`;
|
|
450
|
+
throw new Error(`Missing required option: ${label}\nUsage: ${initUsage}`);
|
|
451
|
+
}
|
|
452
|
+
parsed = await promptForMissingFields(parsed, options);
|
|
453
|
+
} else if (!parsed.displayName) {
|
|
454
|
+
parsed.displayName = path.basename(path.resolve(parsed.directory));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const config = finalizeInitConfig(parsed);
|
|
287
458
|
const result = scaffoldProject(config, options);
|
|
288
459
|
const log = options.log || console.log;
|
|
460
|
+
const installed = await maybeInstallDependencies(result.targetDir, options);
|
|
289
461
|
|
|
290
462
|
log(`Created Foldspace project at ${result.targetDir}`);
|
|
291
463
|
log(`Using @foldspace_npm/harness ${result.harnessVersion}`);
|
|
292
464
|
log("");
|
|
293
465
|
log("Next steps:");
|
|
294
466
|
log(` cd ${result.targetDir}`);
|
|
295
|
-
|
|
467
|
+
if (!installed) {
|
|
468
|
+
log(" npm install --ignore-scripts");
|
|
469
|
+
}
|
|
296
470
|
log(" npm run build");
|
|
297
471
|
log(" npm run inject");
|
|
298
472
|
log(" npm run attach");
|
|
299
473
|
|
|
300
|
-
return result;
|
|
474
|
+
return { ...result, installed };
|
|
301
475
|
}
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"private": true,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"dev": "foldspace
|
|
9
|
-
"build": "foldspace
|
|
10
|
-
"inject": "foldspace
|
|
11
|
-
"attach": "foldspace
|
|
8
|
+
"dev": "foldspace build --watch",
|
|
9
|
+
"build": "foldspace build",
|
|
10
|
+
"inject": "foldspace inject",
|
|
11
|
+
"attach": "foldspace attach"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@foldspace_npm/harness": "{{HARNESS_VERSION}}",
|