@duypham93/openkit 0.2.5 → 0.2.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duypham93/openkit",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "files": [
6
6
  ".opencode/",
@@ -1,6 +1,14 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
+ function removePathIfPresent(targetPath) {
5
+ try {
6
+ fs.rmSync(targetPath, { recursive: true, force: true });
7
+ } catch {
8
+ // ignore cleanup failures
9
+ }
10
+ }
11
+
4
12
  function ensureParent(filePath) {
5
13
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
6
14
  }
@@ -13,6 +21,10 @@ function writeFile(filePath, content, mode) {
13
21
  }
14
22
  }
15
23
 
24
+ function writeJson(filePath, value) {
25
+ writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
26
+ }
27
+
16
28
  function relativeTarget(fromPath, toPath) {
17
29
  return path.relative(path.dirname(fromPath), toPath) || '.';
18
30
  }
@@ -22,7 +34,7 @@ function createSymlinkOrCopy({ linkPath, targetPath, type = 'file' }) {
22
34
 
23
35
  try {
24
36
  if (fs.existsSync(linkPath) || fs.lstatSync(linkPath)) {
25
- fs.rmSync(linkPath, { recursive: true, force: true });
37
+ removePathIfPresent(linkPath);
26
38
  }
27
39
  } catch {
28
40
  // ignore cleanup misses
@@ -42,28 +54,40 @@ function createSymlinkOrCopy({ linkPath, targetPath, type = 'file' }) {
42
54
  }
43
55
  }
44
56
 
57
+ function createIfMissing(createdPaths, { linkPath, targetPath, type = 'file' }) {
58
+ if (fs.existsSync(linkPath)) {
59
+ return null;
60
+ }
61
+
62
+ const mode = createSymlinkOrCopy({ linkPath, targetPath, type });
63
+ createdPaths.push(linkPath);
64
+ return mode;
65
+ }
66
+
45
67
  export function ensureWorkspaceShim(paths) {
68
+ const createdPaths = [];
69
+
46
70
  fs.mkdirSync(paths.workspaceShimDir, { recursive: true });
47
71
 
48
- createSymlinkOrCopy({
72
+ createIfMissing(createdPaths, {
49
73
  linkPath: paths.workspaceShimAgentsPath,
50
74
  targetPath: path.join(paths.kitRoot, 'AGENTS.md'),
51
75
  type: 'file',
52
76
  });
53
77
 
54
- createSymlinkOrCopy({
78
+ createIfMissing(createdPaths, {
55
79
  linkPath: paths.workspaceShimContextDir,
56
80
  targetPath: path.join(paths.kitRoot, 'context'),
57
81
  type: 'dir',
58
82
  });
59
83
 
60
- createSymlinkOrCopy({
84
+ createIfMissing(createdPaths, {
61
85
  linkPath: paths.workspaceShimTemplatesDir,
62
86
  targetPath: path.join(paths.kitRoot, 'docs', 'templates'),
63
87
  type: 'dir',
64
88
  });
65
89
 
66
- createSymlinkOrCopy({
90
+ createIfMissing(createdPaths, {
67
91
  linkPath: paths.workspaceShimWorkflowStatePath,
68
92
  targetPath: paths.workflowStatePath,
69
93
  type: 'file',
@@ -85,20 +109,97 @@ if (result.error) {
85
109
  process.exit(typeof result.status === 'number' ? result.status : 1);
86
110
  `;
87
111
 
88
- writeFile(paths.workspaceShimWorkflowCliPath, workflowCli, 0o755);
112
+ if (!fs.existsSync(paths.workspaceShimWorkflowCliPath)) {
113
+ writeFile(paths.workspaceShimWorkflowCliPath, workflowCli, 0o755);
114
+ createdPaths.push(paths.workspaceShimWorkflowCliPath);
115
+ }
89
116
 
90
- const gitDir = path.join(paths.projectRoot, '.git');
91
- if (fs.existsSync(gitDir)) {
92
- const excludePath = path.join(gitDir, 'info', 'exclude');
93
- const entry = '.opencode/openkit/';
94
- let current = '';
95
- if (fs.existsSync(excludePath)) {
96
- current = fs.readFileSync(excludePath, 'utf8');
97
- }
98
- if (!current.split(/\r?\n/).includes(entry)) {
99
- writeFile(excludePath, `${current}${current.endsWith('\n') || current.length === 0 ? '' : '\n'}${entry}\n`);
100
- }
117
+ createIfMissing(createdPaths, {
118
+ linkPath: path.join(paths.projectRoot, 'AGENTS.md'),
119
+ targetPath: paths.workspaceShimAgentsPath,
120
+ type: 'file',
121
+ });
122
+
123
+ createIfMissing(createdPaths, {
124
+ linkPath: path.join(paths.projectRoot, 'context'),
125
+ targetPath: paths.workspaceShimContextDir,
126
+ type: 'dir',
127
+ });
128
+
129
+ createIfMissing(createdPaths, {
130
+ linkPath: path.join(paths.projectRoot, '.opencode', 'workflow-state.json'),
131
+ targetPath: paths.workspaceShimWorkflowStatePath,
132
+ type: 'file',
133
+ });
134
+
135
+ const opencodeRoot = path.join(paths.projectRoot, '.opencode');
136
+ const opencodePackagePath = path.join(opencodeRoot, 'package.json');
137
+ if (!fs.existsSync(opencodePackagePath)) {
138
+ writeJson(opencodePackagePath, { type: 'module' });
139
+ createdPaths.push(opencodePackagePath);
101
140
  }
102
141
 
103
- return paths;
142
+ if (!fs.existsSync(path.join(paths.projectRoot, '.opencode', 'workflow-state.js'))) {
143
+ const rootWorkflowCli = `#!/usr/bin/env node
144
+ import { spawnSync } from 'node:child_process';
145
+
146
+ const rawArgs = process.argv.slice(2);
147
+ const command = rawArgs[0];
148
+ const aliasMap = new Map([
149
+ ['get', 'show'],
150
+ ['--help', 'help'],
151
+ ['-h', 'help'],
152
+ ]);
153
+ const normalizedArgs = rawArgs.length === 0 ? ['help'] : [aliasMap.get(command) ?? command, ...rawArgs.slice(1)];
154
+ const result = spawnSync(process.execPath, [${JSON.stringify(paths.workspaceShimWorkflowCliPath)}, ...normalizedArgs], {
155
+ stdio: 'inherit',
156
+ env: process.env,
157
+ });
158
+
159
+ if (result.error) {
160
+ throw result.error;
161
+ }
162
+
163
+ process.exit(typeof result.status === 'number' ? result.status : 1);
164
+ `;
165
+
166
+ const rootWorkflowCliPath = path.join(paths.projectRoot, '.opencode', 'workflow-state.js');
167
+ writeFile(rootWorkflowCliPath, rootWorkflowCli, 0o755);
168
+ createdPaths.push(rootWorkflowCliPath);
169
+ }
170
+
171
+ if (!fs.existsSync(path.join(paths.projectRoot, '.opencode', 'work-items'))) {
172
+ createIfMissing(createdPaths, {
173
+ linkPath: path.join(paths.projectRoot, '.opencode', 'work-items'),
174
+ targetPath: paths.workItemsDir,
175
+ type: 'dir',
176
+ });
177
+ }
178
+
179
+ return {
180
+ paths,
181
+ createdPaths,
182
+ };
183
+ }
184
+
185
+ export function cleanupWorkspaceShim(shim) {
186
+ if (!shim?.createdPaths) {
187
+ return;
188
+ }
189
+
190
+ for (const createdPath of [...shim.createdPaths].reverse()) {
191
+ removePathIfPresent(createdPath);
192
+ }
193
+
194
+ const maybeShimDir = shim.paths?.workspaceShimDir;
195
+ if (maybeShimDir && fs.existsSync(maybeShimDir)) {
196
+ try {
197
+ const entries = fs.readdirSync(maybeShimDir);
198
+ if (entries.length === 0) {
199
+ removePathIfPresent(maybeShimDir);
200
+ }
201
+ } catch {
202
+ // ignore cleanup misses
203
+ }
204
+ }
104
205
  }
@@ -51,9 +51,12 @@ export function ensureWorkspaceBootstrap(options = {}) {
51
51
  });
52
52
  }
53
53
 
54
- ensureWorkspaceShim(paths);
54
+ const shim = ensureWorkspaceShim(paths);
55
55
 
56
- return paths;
56
+ return {
57
+ ...paths,
58
+ workspaceShim: shim,
59
+ };
57
60
  }
58
61
 
59
62
  export function readWorkspaceMeta(options = {}) {
@@ -33,6 +33,10 @@ function writeExecutable(filePath, content) {
33
33
  fs.chmodSync(filePath, 0o755);
34
34
  }
35
35
 
36
+ function removePathIfPresent(targetPath) {
37
+ fs.rmSync(targetPath, { recursive: true, force: true });
38
+ }
39
+
36
40
  test('openkit --help shows global-install oriented help', () => {
37
41
  const result = runCli(['--help']);
38
42
 
@@ -226,6 +230,10 @@ process.stdout.write('mock opencode launched\\n');
226
230
  assert.equal(fs.existsSync(path.join(projectRoot, '.opencode', 'openkit', 'context', 'core', 'workflow.md')), true);
227
231
  assert.equal(fs.lstatSync(path.join(projectRoot, '.opencode', 'openkit', 'workflow-state.json')).isSymbolicLink() || fs.existsSync(path.join(projectRoot, '.opencode', 'openkit', 'workflow-state.json')), true);
228
232
  assert.equal(fs.existsSync(path.join(projectRoot, '.opencode', 'openkit', 'workflow-state.js')), true);
233
+ assert.equal(fs.existsSync(path.join(projectRoot, 'AGENTS.md')), true);
234
+ assert.equal(fs.existsSync(path.join(projectRoot, 'context', 'core', 'workflow.md')), true);
235
+ assert.equal(fs.lstatSync(path.join(projectRoot, '.opencode', 'workflow-state.json')).isSymbolicLink() || fs.existsSync(path.join(projectRoot, '.opencode', 'workflow-state.json')), true);
236
+ assert.equal(fs.existsSync(path.join(projectRoot, '.opencode', 'workflow-state.js')), true);
229
237
  });
230
238
 
231
239
  test('openkit run does not reinstall when the global install already exists', () => {
@@ -301,6 +309,88 @@ process.stdout.write('mock opencode launched after auto-install\\n');
301
309
  assert.equal(fs.existsSync(path.join(projectRoot, '.opencode', 'openkit', 'AGENTS.md')), true);
302
310
  });
303
311
 
312
+ test('openkit run does not overwrite existing repo-local workflow files when creating shims', () => {
313
+ const tempHome = makeTempDir();
314
+ const projectRoot = makeTempDir();
315
+ const fakeBinDir = path.join(tempHome, 'bin');
316
+
317
+ fs.mkdirSync(path.join(projectRoot, '.opencode'), { recursive: true });
318
+ fs.mkdirSync(path.join(projectRoot, 'context', 'core'), { recursive: true });
319
+ fs.writeFileSync(path.join(projectRoot, 'AGENTS.md'), 'project agents\n', 'utf8');
320
+ fs.writeFileSync(path.join(projectRoot, 'context', 'core', 'workflow.md'), 'project workflow\n', 'utf8');
321
+ fs.writeFileSync(path.join(projectRoot, '.opencode', 'workflow-state.json'), '{"project":true}\n', 'utf8');
322
+ fs.writeFileSync(path.join(projectRoot, '.opencode', 'workflow-state.js'), '#!/usr/bin/env node\n', 'utf8');
323
+
324
+ writeExecutable(path.join(fakeBinDir, 'opencode'), '#!/bin/sh\nexit 0\n');
325
+
326
+ const result = runCli(['run'], {
327
+ cwd: projectRoot,
328
+ env: {
329
+ ...process.env,
330
+ OPENCODE_HOME: tempHome,
331
+ PATH: `${fakeBinDir}${path.delimiter}${process.env.PATH}`,
332
+ },
333
+ });
334
+
335
+ assert.equal(result.status, 0);
336
+ assert.equal(fs.readFileSync(path.join(projectRoot, 'AGENTS.md'), 'utf8'), 'project agents\n');
337
+ assert.equal(fs.readFileSync(path.join(projectRoot, 'context', 'core', 'workflow.md'), 'utf8'), 'project workflow\n');
338
+ assert.equal(fs.readFileSync(path.join(projectRoot, '.opencode', 'workflow-state.json'), 'utf8'), '{"project":true}\n');
339
+ assert.equal(fs.readFileSync(path.join(projectRoot, '.opencode', 'workflow-state.js'), 'utf8'), '#!/usr/bin/env node\n');
340
+ assert.equal(fs.existsSync(path.join(projectRoot, '.opencode', 'openkit', 'AGENTS.md')), true);
341
+ });
342
+
343
+ test('openkit run cleans root compatibility shims when created files are removed', () => {
344
+ const tempHome = makeTempDir();
345
+ const projectRoot = makeTempDir();
346
+ const fakeBinDir = path.join(tempHome, 'bin');
347
+
348
+ writeExecutable(path.join(fakeBinDir, 'opencode'), '#!/bin/sh\nexit 0\n');
349
+
350
+ const result = runCli(['run'], {
351
+ cwd: projectRoot,
352
+ env: {
353
+ ...process.env,
354
+ OPENCODE_HOME: tempHome,
355
+ PATH: `${fakeBinDir}${path.delimiter}${process.env.PATH}`,
356
+ },
357
+ });
358
+
359
+ assert.equal(result.status, 0);
360
+ assert.equal(fs.existsSync(path.join(projectRoot, 'AGENTS.md')), true);
361
+
362
+ removePathIfPresent(path.join(projectRoot, 'AGENTS.md'));
363
+ removePathIfPresent(path.join(projectRoot, 'context'));
364
+ removePathIfPresent(path.join(projectRoot, '.opencode', 'workflow-state.json'));
365
+ removePathIfPresent(path.join(projectRoot, '.opencode', 'workflow-state.js'));
366
+
367
+ assert.equal(fs.existsSync(path.join(projectRoot, '.opencode', 'openkit', 'AGENTS.md')), true);
368
+ });
369
+
370
+ test('openkit run creates a module-aware root workflow wrapper with alias support', () => {
371
+ const tempHome = makeTempDir();
372
+ const projectRoot = makeTempDir();
373
+ const fakeBinDir = path.join(tempHome, 'bin');
374
+
375
+ writeExecutable(path.join(fakeBinDir, 'opencode'), '#!/bin/sh\nexit 0\n');
376
+
377
+ const result = runCli(['run'], {
378
+ cwd: projectRoot,
379
+ env: {
380
+ ...process.env,
381
+ OPENCODE_HOME: tempHome,
382
+ PATH: `${fakeBinDir}${path.delimiter}${process.env.PATH}`,
383
+ },
384
+ });
385
+
386
+ assert.equal(result.status, 0);
387
+ assert.deepEqual(readJson(path.join(projectRoot, '.opencode', 'package.json')), { type: 'module' });
388
+
389
+ const wrapper = fs.readFileSync(path.join(projectRoot, '.opencode', 'workflow-state.js'), 'utf8');
390
+ assert.match(wrapper, /\['get', 'show'\]/);
391
+ assert.match(wrapper, /\['--help', 'help'\]/);
392
+ });
393
+
304
394
  test('openkit run reports missing opencode after first-time setup completes', () => {
305
395
  const tempHome = makeTempDir();
306
396
  const projectRoot = makeTempDir();