@foldspace_npm/harness 0.1.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 +156 -0
- package/bin/attach.mjs +399 -0
- package/bin/build-cli.mjs +13 -0
- package/bin/build.ts +82 -0
- package/bin/buildExtension.mjs +90 -0
- package/bin/cli.mjs +33 -0
- package/bin/deploy.mjs +220 -0
- package/bin/inject.mjs +524 -0
- package/bin/packageExtension.mjs +29 -0
- package/package.json +23 -0
- package/src/init.mjs +301 -0
- package/src/transports/README.md +24 -0
- package/templates/agent-starter/CLAUDE.md +90 -0
- package/templates/agent-starter/README.md +54 -0
- package/templates/agent-starter/agent/actions/_example.ts +18 -0
- package/templates/agent-starter/agent/actions/index.ts +10 -0
- package/templates/agent-starter/agent/api/.gitkeep +1 -0
- package/templates/agent-starter/agent/constants.ts +3 -0
- package/templates/agent-starter/agent/utils.ts +15 -0
- package/templates/agent-starter/foldspace.dev.json +17 -0
- package/templates/agent-starter/gitignore +6 -0
- package/templates/agent-starter/package.json +17 -0
- package/templates/agent-starter/tsconfig.json +17 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const projectDir = process.env.FOLDSPACE_PROJECT_DIR || process.cwd();
|
|
8
|
+
const indexPath = path.join(projectDir, "extension", "index.js");
|
|
9
|
+
|
|
10
|
+
const LOAD_LOCALLY_TRUE = "const LOAD_LOCALLY = true;";
|
|
11
|
+
const LOAD_LOCALLY_FALSE = "const LOAD_LOCALLY = false;";
|
|
12
|
+
const REMOTE_ENV_DEV = 'const REMOTE_ACTIONS_ENV = "DEV";';
|
|
13
|
+
const REMOTE_ENV_PROD = 'const REMOTE_ACTIONS_ENV = "PROD";';
|
|
14
|
+
|
|
15
|
+
function parseArgs(argv) {
|
|
16
|
+
for (let i = 0; i < argv.length; i++) {
|
|
17
|
+
if (argv[i] === "--env" && argv[i + 1]) return argv[++i];
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const env = parseArgs(process.argv.slice(2));
|
|
23
|
+
if (!env || (env !== "dev" && env !== "prod")) {
|
|
24
|
+
console.error('buildExtension: --env <dev|prod> is required');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function replaceInIndex(from, to, label) {
|
|
29
|
+
const source = fs.readFileSync(indexPath, "utf8");
|
|
30
|
+
|
|
31
|
+
if (!source.includes(from)) {
|
|
32
|
+
if (source.includes(to)) {
|
|
33
|
+
console.log(`buildExtension: ${label} already set`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.error(
|
|
37
|
+
`buildExtension: expected ${label} pattern not found in index.js`,
|
|
38
|
+
);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fs.writeFileSync(indexPath, source.replace(from, to), "utf8");
|
|
43
|
+
console.log(`buildExtension: ${label} → ${to}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setLoadLocally(value) {
|
|
47
|
+
if (!fs.existsSync(indexPath)) {
|
|
48
|
+
console.error("buildExtension: extension/index.js not found");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const from = value ? LOAD_LOCALLY_FALSE : LOAD_LOCALLY_TRUE;
|
|
52
|
+
const to = value ? LOAD_LOCALLY_TRUE : LOAD_LOCALLY_FALSE;
|
|
53
|
+
replaceInIndex(from, to, "LOAD_LOCALLY");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setRemoteActionsEnv(targetEnv) {
|
|
57
|
+
const from = targetEnv === "dev" ? REMOTE_ENV_PROD : REMOTE_ENV_DEV;
|
|
58
|
+
const to = targetEnv === "dev" ? REMOTE_ENV_DEV : REMOTE_ENV_PROD;
|
|
59
|
+
replaceInIndex(from, to, "REMOTE_ACTIONS_ENV");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let failed = false;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
execFileSync("tsx", ["scripts/build.ts"], {
|
|
66
|
+
cwd: projectDir,
|
|
67
|
+
stdio: "inherit",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
setLoadLocally(false);
|
|
71
|
+
setRemoteActionsEnv(env);
|
|
72
|
+
|
|
73
|
+
execFileSync("node", ["scripts/packageExtension.mjs"], {
|
|
74
|
+
cwd: projectDir,
|
|
75
|
+
stdio: "inherit",
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
failed = true;
|
|
79
|
+
} finally {
|
|
80
|
+
try {
|
|
81
|
+
setLoadLocally(true);
|
|
82
|
+
setRemoteActionsEnv("prod");
|
|
83
|
+
} catch {
|
|
84
|
+
failed = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (failed) {
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { initUsage, runInit } from "../src/init.mjs";
|
|
4
|
+
|
|
5
|
+
const usage = `Usage:
|
|
6
|
+
${initUsage}
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
init Create a configured Foldspace actions project
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
function fail(message) {
|
|
13
|
+
console.error(`foldspace: ${message}`);
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const [command, ...args] = process.argv.slice(2);
|
|
18
|
+
|
|
19
|
+
if (!command || command === "--help" || command === "-h") {
|
|
20
|
+
console.log(usage);
|
|
21
|
+
} else if (command === "init") {
|
|
22
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
23
|
+
console.log(`Usage: ${initUsage}`);
|
|
24
|
+
} else {
|
|
25
|
+
try {
|
|
26
|
+
runInit(args);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
fail(`unknown command '${command}'\n\n${usage}`);
|
|
33
|
+
}
|
package/bin/deploy.mjs
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { execFileSync, execSync } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const projectDir = process.env.FOLDSPACE_PROJECT_DIR || process.cwd();
|
|
8
|
+
|
|
9
|
+
const GCS_BUCKET = "prod-us1-eucera-public-scripts";
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const args = {};
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
if (argv[i] === "--env" && argv[i + 1]) args.env = argv[++i];
|
|
15
|
+
}
|
|
16
|
+
return args;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readExtensionConfig() {
|
|
20
|
+
const indexPath = path.join(projectDir, "extension", "index.js");
|
|
21
|
+
if (!fs.existsSync(indexPath)) {
|
|
22
|
+
console.error("deploy: extension/index.js not found");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const src = fs.readFileSync(indexPath, "utf8");
|
|
26
|
+
|
|
27
|
+
const productMatch = src.match(/const\s+PRODUCT_ID\s*=\s*["'](.+?)["']/);
|
|
28
|
+
const agentMatch = src.match(/const\s+AGENT_API_NAME\s*=\s*["'](.+?)["']/);
|
|
29
|
+
|
|
30
|
+
if (!productMatch || !productMatch[1]) {
|
|
31
|
+
console.error("deploy: could not read PRODUCT_ID from extension/index.js");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
if (!agentMatch || !agentMatch[1]) {
|
|
35
|
+
console.error(
|
|
36
|
+
"deploy: could not read AGENT_API_NAME from extension/index.js",
|
|
37
|
+
);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { productId: productMatch[1], agentApiName: agentMatch[1] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validate(args) {
|
|
45
|
+
const errors = [];
|
|
46
|
+
if (args.env !== "dev" && args.env !== "prod")
|
|
47
|
+
errors.push('--env must be "dev" or "prod"');
|
|
48
|
+
if (errors.length) {
|
|
49
|
+
console.error("deploy: validation failed:");
|
|
50
|
+
errors.forEach((e) => console.error(` - ${e}`));
|
|
51
|
+
console.error("\nUsage: node scripts/deploy.mjs [--env <dev|prod>]");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getGitCommitHash() {
|
|
57
|
+
try {
|
|
58
|
+
return execSync("git rev-parse --short HEAD", { cwd: projectDir })
|
|
59
|
+
.toString()
|
|
60
|
+
.trim();
|
|
61
|
+
} catch {
|
|
62
|
+
return "unknown";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readDeployVersion() {
|
|
67
|
+
const sha = getGitCommitHash();
|
|
68
|
+
if (sha === "unknown") {
|
|
69
|
+
console.error("deploy: could not resolve git commit SHA");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
return sha;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function gcsBasePath(env, productId, agentApiName) {
|
|
76
|
+
return `gs://${GCS_BUCKET}/agent/actions/${env}/${productId}/${agentApiName}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function versionExistsInGCS(basePath, version) {
|
|
80
|
+
const target = `${basePath}/index.${version}.js`;
|
|
81
|
+
try {
|
|
82
|
+
execFileSync("gsutil", ["stat", target], { stdio: "pipe" });
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function uploadToGCS(distFile, basePath, version) {
|
|
90
|
+
const versionedTarget = `${basePath}/index.${version}.js`;
|
|
91
|
+
const latestTarget = `${basePath}/index.js`;
|
|
92
|
+
|
|
93
|
+
console.log(`\nUploading versioned: ${versionedTarget}`);
|
|
94
|
+
execFileSync("gsutil", ["cp", "-z", "js", distFile, versionedTarget], {
|
|
95
|
+
stdio: "inherit",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
console.log(`Uploading latest: ${latestTarget}`);
|
|
99
|
+
execFileSync(
|
|
100
|
+
"gsutil",
|
|
101
|
+
[
|
|
102
|
+
"-h",
|
|
103
|
+
"Cache-Control:no-cache, max-age=0",
|
|
104
|
+
"-h",
|
|
105
|
+
"Content-Type:application/javascript",
|
|
106
|
+
"cp",
|
|
107
|
+
"-z",
|
|
108
|
+
"js",
|
|
109
|
+
distFile,
|
|
110
|
+
latestTarget,
|
|
111
|
+
],
|
|
112
|
+
{ stdio: "inherit" },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getGitUserName() {
|
|
117
|
+
try {
|
|
118
|
+
return execSync("git config user.name", { cwd: projectDir })
|
|
119
|
+
.toString()
|
|
120
|
+
.trim();
|
|
121
|
+
} catch {
|
|
122
|
+
return "unknown";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getLastDeployCommitHash(deploymentsPath) {
|
|
127
|
+
if (!fs.existsSync(deploymentsPath)) return null;
|
|
128
|
+
const content = fs.readFileSync(deploymentsPath, "utf8");
|
|
129
|
+
const match = content.match(/> Commit: ([a-f0-9]+)/);
|
|
130
|
+
return match ? match[1] : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getGitLogSince(sinceHash) {
|
|
134
|
+
try {
|
|
135
|
+
const range = sinceHash ? `${sinceHash}..HEAD` : "HEAD~10..HEAD";
|
|
136
|
+
const log = execSync(`git log ${range} --oneline`, { cwd: projectDir })
|
|
137
|
+
.toString()
|
|
138
|
+
.trim();
|
|
139
|
+
if (!log) return ["No changes recorded"];
|
|
140
|
+
return log.split("\n").map((line) => {
|
|
141
|
+
const spaceIdx = line.indexOf(" ");
|
|
142
|
+
return spaceIdx > 0 ? line.substring(spaceIdx + 1) : line;
|
|
143
|
+
});
|
|
144
|
+
} catch {
|
|
145
|
+
return ["Unable to retrieve git log"];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function updateDeployments(env, version) {
|
|
150
|
+
const deploymentsPath = path.join(projectDir, "DEPLOYMENTS.md");
|
|
151
|
+
const commitHash = getGitCommitHash();
|
|
152
|
+
const deployer = getGitUserName();
|
|
153
|
+
const date = new Date().toISOString().split("T")[0];
|
|
154
|
+
|
|
155
|
+
const lastHash = getLastDeployCommitHash(deploymentsPath);
|
|
156
|
+
const changes = getGitLogSince(lastHash);
|
|
157
|
+
|
|
158
|
+
const entry = [
|
|
159
|
+
`## ${version} | ${env} | ${date}`,
|
|
160
|
+
"",
|
|
161
|
+
...changes.map((msg) => `- ${msg}`),
|
|
162
|
+
`> Commit: ${commitHash} | Deployed by: ${deployer}`,
|
|
163
|
+
"",
|
|
164
|
+
].join("\n");
|
|
165
|
+
|
|
166
|
+
if (fs.existsSync(deploymentsPath)) {
|
|
167
|
+
const existing = fs.readFileSync(deploymentsPath, "utf8");
|
|
168
|
+
const headerEnd = existing.indexOf("\n\n");
|
|
169
|
+
if (headerEnd !== -1) {
|
|
170
|
+
const header = existing.substring(0, headerEnd);
|
|
171
|
+
const rest = existing.substring(headerEnd + 2);
|
|
172
|
+
fs.writeFileSync(deploymentsPath, `${header}\n\n${entry}\n${rest}`);
|
|
173
|
+
} else {
|
|
174
|
+
fs.writeFileSync(deploymentsPath, `${existing}\n\n${entry}`);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
fs.writeFileSync(deploymentsPath, `# Deployments\n\n${entry}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(`\nUpdated DEPLOYMENTS.md`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function deploy() {
|
|
184
|
+
const args = parseArgs(process.argv.slice(2));
|
|
185
|
+
if (!args.env) args.env = "dev";
|
|
186
|
+
|
|
187
|
+
const { productId, agentApiName } = readExtensionConfig();
|
|
188
|
+
args.productId = productId;
|
|
189
|
+
args.agentApiName = agentApiName;
|
|
190
|
+
|
|
191
|
+
validate(args);
|
|
192
|
+
|
|
193
|
+
const version = readDeployVersion();
|
|
194
|
+
const distFile = path.join(projectDir, "dist", "index.js");
|
|
195
|
+
|
|
196
|
+
if (!fs.existsSync(distFile)) {
|
|
197
|
+
console.error(`deploy: dist/index.js not found. Run build first.`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const basePath = gcsBasePath(args.env, args.productId, args.agentApiName);
|
|
202
|
+
|
|
203
|
+
console.log(`deploy: ${version} -> ${args.env}`);
|
|
204
|
+
console.log(`deploy: bucket ${GCS_BUCKET}`);
|
|
205
|
+
console.log(`deploy: path ${basePath}`);
|
|
206
|
+
|
|
207
|
+
if (versionExistsInGCS(basePath, version)) {
|
|
208
|
+
console.log(
|
|
209
|
+
`\ndeploy: ${version} already deployed at ${basePath}/index.${version}.js — skipping`,
|
|
210
|
+
);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
uploadToGCS(distFile, basePath, version);
|
|
215
|
+
updateDeployments(args.env, version);
|
|
216
|
+
|
|
217
|
+
console.log(`\ndeploy: ${version} deployed successfully to ${args.env}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
deploy();
|