@fishawack/lab-env 5.4.0-beta.1 → 5.5.0-beta.1

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.
@@ -28,9 +28,135 @@ const generateWorkspaceName = (projects, customName) => {
28
28
  return `workspace-${baseName}`;
29
29
  };
30
30
 
31
+ const linkInstructionFiles = (workspaceDir, projects, cwd) => {
32
+ const instructionsDir = path.join(workspaceDir, ".github", "instructions");
33
+ fs.mkdirSync(instructionsDir, { recursive: true });
34
+
35
+ const instructionFiles = projects.flatMap((project) =>
36
+ glob.sync(`${project}/.github/instructions/*.md`, { cwd }),
37
+ );
38
+
39
+ let linked = 0;
40
+ instructionFiles.forEach((filePath) => {
41
+ const fileName = path.basename(filePath);
42
+ const targetPath = path.join(instructionsDir, fileName);
43
+ if (fs.existsSync(targetPath)) return;
44
+ createSymlink(path.join(cwd, filePath), targetPath);
45
+ linked++;
46
+ });
47
+
48
+ return linked;
49
+ };
50
+
51
+ const createWorkspace = (workspaceDir, workspaceName, projects, cwd) => {
52
+ fs.mkdirSync(workspaceDir);
53
+ console.log(`Created workspace directory: ${workspaceName}`);
54
+
55
+ // Create project symlinks
56
+ projects.forEach((project) => {
57
+ createSymlink(
58
+ path.join(cwd, project),
59
+ path.join(workspaceDir, project),
60
+ );
61
+ console.log(`Linked project: ${project}`);
62
+ });
63
+
64
+ // Setup AI instructions
65
+ const linked = linkInstructionFiles(workspaceDir, projects, cwd);
66
+
67
+ console.log(
68
+ linked
69
+ ? `Linked ${linked} instruction file(s) to workspace`
70
+ : "No AI instruction files found in projects",
71
+ );
72
+
73
+ _.copyAIInstructionsFile("workspace-@", workspaceDir);
74
+ _.copyAIInstructionsFile("mcp-@", workspaceDir);
75
+
76
+ // Open in VS Code
77
+ try {
78
+ execSync("code .", { cwd: workspaceDir, stdio: "ignore" });
79
+ console.log(`\nOpened workspace in VS Code: ${workspaceName}`);
80
+ } catch {
81
+ // Silently ignore VS Code errors
82
+ }
83
+
84
+ console.log(`\nWorkspace setup complete: ${workspaceName}`);
85
+ };
86
+
87
+ const addToWorkspace = (workspaceDir, workspaceName, projects, cwd) => {
88
+ let added = 0;
89
+
90
+ projects.forEach((project) => {
91
+ const targetPath = path.join(workspaceDir, project);
92
+ if (fs.existsSync(targetPath)) {
93
+ console.log(`Already linked, skipping: ${project}`);
94
+ return;
95
+ }
96
+ createSymlink(path.join(cwd, project), targetPath);
97
+ console.log(`Linked project: ${project}`);
98
+ added++;
99
+ });
100
+
101
+ const linked = linkInstructionFiles(workspaceDir, projects, cwd);
102
+
103
+ if (linked)
104
+ console.log(`Linked ${linked} instruction file(s) to workspace`);
105
+ if (!added) console.log("\nNo new projects to add");
106
+ else
107
+ console.log(
108
+ `\nAdded ${added} project(s) to workspace: ${workspaceName}`,
109
+ );
110
+ };
111
+
112
+ const removeFromWorkspace = (workspaceDir, workspaceName, projects) => {
113
+ const instructionsDir = path.join(workspaceDir, ".github", "instructions");
114
+ let removed = 0;
115
+
116
+ projects.forEach((project) => {
117
+ const targetPath = path.join(workspaceDir, project);
118
+ if (!fs.existsSync(targetPath)) {
119
+ console.warn(`Not linked, skipping: ${project}`);
120
+ return;
121
+ }
122
+ fs.unlinkSync(targetPath);
123
+ console.log(`Unlinked project: ${project}`);
124
+ removed++;
125
+
126
+ // Remove instruction file symlinks that point into this project
127
+ if (fs.existsSync(instructionsDir)) {
128
+ const projectInstructionsDir = path.join(
129
+ project,
130
+ ".github",
131
+ "instructions",
132
+ );
133
+ fs.readdirSync(instructionsDir).forEach((file) => {
134
+ const filePath = path.join(instructionsDir, file);
135
+ const stat = fs.lstatSync(filePath);
136
+ if (!stat.isSymbolicLink()) return;
137
+
138
+ const linkTarget = path.resolve(
139
+ instructionsDir,
140
+ fs.readlinkSync(filePath),
141
+ );
142
+ if (linkTarget.includes(projectInstructionsDir)) {
143
+ fs.unlinkSync(filePath);
144
+ console.log(`Unlinked instruction file: ${file}`);
145
+ }
146
+ });
147
+ }
148
+ });
149
+
150
+ if (!removed) console.log("\nNo projects to remove");
151
+ else
152
+ console.log(
153
+ `\nRemoved ${removed} project(s) from workspace: ${workspaceName}`,
154
+ );
155
+ };
156
+
31
157
  module.exports = [
32
158
  "workspace <projects..>",
33
- "creates workspace directory with project symlinks and AI instruction files",
159
+ "creates or updates workspace directory with project symlinks and AI instruction files",
34
160
  (yargs) => {
35
161
  yargs
36
162
  .positional("projects", {
@@ -43,82 +169,53 @@ module.exports = [
43
169
  describe:
44
170
  "custom workspace name (will be prefixed with 'workspace-')",
45
171
  type: "string",
172
+ })
173
+ .option("delete", {
174
+ alias: "d",
175
+ describe: "remove the specified projects from the workspace",
176
+ type: "boolean",
177
+ default: false,
178
+ })
179
+ .check((argv) => {
180
+ if (argv.delete && !argv.name)
181
+ throw new Error(
182
+ "--name (-n) is required when using --delete (-d)",
183
+ );
184
+ return true;
46
185
  });
47
186
  },
48
187
  (argv) => {
49
- const { projects, name } = argv;
188
+ const { projects, name, delete: isDelete } = argv;
50
189
  const cwd = process.cwd();
51
190
 
52
191
  // Validation
53
192
  if (!projects?.length)
54
193
  exitWithError("At least one project name is required");
55
194
 
56
- const missingProjects = projects.filter(
57
- (project) => !fs.existsSync(path.join(cwd, project)),
58
- );
59
- if (missingProjects.length)
60
- exitWithError(
61
- `Project directories not found: ${missingProjects.join(", ")}`,
62
- );
63
-
64
- // Setup workspace
65
195
  const workspaceName = generateWorkspaceName(projects, name);
66
196
  const workspaceDir = path.join(cwd, workspaceName);
67
197
 
68
- if (fs.existsSync(workspaceDir))
69
- exitWithError(
70
- `Workspace directory '${workspaceName}' already exists`,
71
- );
72
-
73
- fs.mkdirSync(workspaceDir);
74
- console.log(`Created workspace directory: ${workspaceName}`);
75
-
76
- // Create project symlinks
77
- projects.forEach((project) => {
78
- createSymlink(
79
- path.join(cwd, project),
80
- path.join(workspaceDir, project),
81
- );
82
- console.log(`Linked project: ${project}`);
83
- });
84
-
85
- // Setup AI instructions
86
- const instructionsDir = path.join(
87
- workspaceDir,
88
- ".github",
89
- "instructions",
90
- );
91
- fs.mkdirSync(instructionsDir, { recursive: true });
92
-
93
- const instructionFiles = projects.flatMap((project) =>
94
- glob.sync(`${project}/.github/instructions/*.md`, { cwd }),
95
- );
198
+ if (isDelete) {
199
+ if (!fs.existsSync(workspaceDir))
200
+ exitWithError(
201
+ `Workspace directory '${workspaceName}' does not exist`,
202
+ );
96
203
 
97
- instructionFiles.forEach((filePath) => {
98
- const fileName = path.basename(filePath);
99
- createSymlink(
100
- path.join(cwd, filePath),
101
- path.join(instructionsDir, fileName),
204
+ removeFromWorkspace(workspaceDir, workspaceName, projects);
205
+ } else {
206
+ const missingProjects = projects.filter(
207
+ (project) => !fs.existsSync(path.join(cwd, project)),
102
208
  );
103
- });
104
-
105
- console.log(
106
- instructionFiles.length
107
- ? `Linked ${instructionFiles.length} instruction file(s) to workspace`
108
- : "No AI instruction files found in projects",
109
- );
110
-
111
- _.copyAIInstructionsFile("workspace-@", workspaceDir);
112
- _.copyAIInstructionsFile("mcp-@", workspaceDir);
113
-
114
- // Open in VS Code
115
- try {
116
- execSync("code .", { cwd: workspaceDir, stdio: "ignore" });
117
- console.log(`\nOpened workspace in VS Code: ${workspaceName}`);
118
- } catch {
119
- // Silently ignore VS Code errors
209
+ if (missingProjects.length)
210
+ exitWithError(
211
+ `Project directories not found: ${missingProjects.join(", ")}`,
212
+ );
213
+
214
+ if (fs.existsSync(workspaceDir)) {
215
+ addToWorkspace(workspaceDir, workspaceName, projects, cwd);
216
+ } else {
217
+ createWorkspace(workspaceDir, workspaceName, projects, cwd);
218
+ }
120
219
  }
121
-
122
- console.log(`\nWorkspace setup complete: ${workspaceName}`);
123
220
  },
124
221
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishawack/lab-env",
3
- "version": "5.4.0-beta.1",
3
+ "version": "5.5.0-beta.1",
4
4
  "description": "Docker manager for FW",
5
5
  "main": "cli.js",
6
6
  "scripts": {
@@ -26,6 +26,7 @@
26
26
  "@aws-sdk/client-elastic-beanstalk": "^3.395.0",
27
27
  "@aws-sdk/client-iam": "^3.150.0",
28
28
  "@aws-sdk/client-s3": "^3.141.0",
29
+ "@aws-sdk/credential-providers": "^3.1011.0",
29
30
  "apache-md5": "^1.1.8",
30
31
  "axios": "^0.21.4",
31
32
  "chalk": "4.1.0",
@@ -40,6 +41,7 @@
40
41
  "lodash": "^4.17.21",
41
42
  "nodemailer": "^6.9.15",
42
43
  "ora": "5.4.1",
44
+ "p-limit": "^7.3.0",
43
45
  "semver": "7.3.4",
44
46
  "update-notifier": "^6.0.2",
45
47
  "yargs": "16.2.0"
@@ -27,6 +27,19 @@ services:
27
27
  - $CWD/:/workspace
28
28
  - venv:/workspace/.venv
29
29
  - $FW_DIR/.ssh:/home/py/.ssh
30
+ healthcheck:
31
+ test:
32
+ [
33
+ "CMD",
34
+ "wget",
35
+ "-q",
36
+ "--spider",
37
+ "http://localhost:${PORT_PY:-8000}/v1/health",
38
+ ]
39
+ interval: 10s
40
+ timeout: 5s
41
+ retries: 5
42
+ start_period: 15s
30
43
  networks:
31
44
  default:
32
45
  driver: "bridge"