@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 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-build` — esbuild → `dist/index.js`
30
- - `foldspace-deploy` — publish to `agent/actions/<env>/<productId>/<agentApiName>`
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
- npm exec --package=@foldspace_npm/harness -- foldspace init my-agent \
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
- install dependencies, initialize Git, or overwrite files. After creation:
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-build --watch",
117
- "build": "foldspace-build",
118
- "inject": "foldspace-inject",
119
- "attach": "foldspace-attach",
120
- "deploy": "foldspace-deploy"
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-build` | ✅ | ✅ |
128
- | `foldspace-deploy` | ✅ | ✅ — it *is* the delivery mechanism |
129
- | `foldspace-inject` | ✅ | — no browser to launch |
130
- | `foldspace-attach` | ✅ | replaced by the extension bridge |
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
- init Create a configured Foldspace actions project
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
- try {
26
- runInit(args);
27
- } catch (error) {
28
- fail(error instanceof Error ? error.message : String(error));
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.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
- "foldspace-build": "bin/build-cli.mjs",
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 !== 1) {
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
- targetDir: path.resolve(positional[0]),
76
- displayName: flags.name || path.basename(path.resolve(positional[0])),
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
- export function runInit(argv, options = {}) {
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
- const config = parseInitArgs(argv);
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
- log(" npm install --ignore-scripts");
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-build --watch",
9
- "build": "foldspace-build",
10
- "inject": "foldspace-inject",
11
- "attach": "foldspace-attach"
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}}",