@tmddev/tmd 0.1.0 → 0.3.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.
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Step executors for automated task and skill execution
3
+ */
4
+ import { exec } from 'child_process';
5
+ import { promisify } from 'util';
6
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, unlinkSync } from 'fs';
7
+ import { resolve } from 'path';
8
+ import { pathToFileURL } from 'url';
9
+ const execAsync = promisify(exec);
10
+ /**
11
+ * Bash step executor - executes shell commands
12
+ */
13
+ export class BashStepExecutor {
14
+ type = 'bash';
15
+ async execute(step, context) {
16
+ const startTime = Date.now();
17
+ const command = step['command'];
18
+ const ignoreErrors = step['ignoreErrors'] ?? false;
19
+ const timeout = step['timeout'] ?? 60000; // Default 60s timeout
20
+ if (!command) {
21
+ return {
22
+ success: false,
23
+ error: 'No command specified for bash step',
24
+ duration: Date.now() - startTime
25
+ };
26
+ }
27
+ if (context.dryRun) {
28
+ return {
29
+ success: true,
30
+ output: `[DRY RUN] Would execute: ${command}`,
31
+ duration: Date.now() - startTime
32
+ };
33
+ }
34
+ try {
35
+ const { stdout, stderr } = await execAsync(command, {
36
+ cwd: context.workingDir,
37
+ env: { ...process.env, ...context.env },
38
+ timeout
39
+ });
40
+ return {
41
+ success: true,
42
+ output: stdout,
43
+ ...(stderr ? { error: stderr } : {}),
44
+ exitCode: 0,
45
+ duration: Date.now() - startTime
46
+ };
47
+ }
48
+ catch (error) {
49
+ const execError = error;
50
+ const duration = Date.now() - startTime;
51
+ const errMsg = execError.stderr ?? execError.message;
52
+ if (ignoreErrors) {
53
+ return {
54
+ success: true,
55
+ output: execError.stdout ?? '',
56
+ ...(errMsg != null ? { error: errMsg } : {}),
57
+ exitCode: execError.code ?? 1,
58
+ duration
59
+ };
60
+ }
61
+ return {
62
+ success: false,
63
+ output: execError.stdout ?? '',
64
+ error: errMsg ?? String(error),
65
+ exitCode: execError.code ?? 1,
66
+ duration
67
+ };
68
+ }
69
+ }
70
+ }
71
+ /**
72
+ * File step executor - performs file operations
73
+ */
74
+ export class FileStepExecutor {
75
+ type = 'file';
76
+ async execute(step, context) {
77
+ await Promise.resolve(); // satisfy require-await; all ops are sync
78
+ const startTime = Date.now();
79
+ const operation = step['operation'];
80
+ const path = step['path'];
81
+ const content = step['content'];
82
+ const destination = step['destination'];
83
+ if (!operation) {
84
+ return {
85
+ success: false,
86
+ error: 'No operation specified for file step',
87
+ duration: Date.now() - startTime
88
+ };
89
+ }
90
+ if (!path) {
91
+ return {
92
+ success: false,
93
+ error: 'No path specified for file step',
94
+ duration: Date.now() - startTime
95
+ };
96
+ }
97
+ if (context.dryRun) {
98
+ return {
99
+ success: true,
100
+ output: `[DRY RUN] Would perform file ${operation} on: ${path}`,
101
+ duration: Date.now() - startTime
102
+ };
103
+ }
104
+ try {
105
+ switch (operation) {
106
+ case 'read': {
107
+ if (!existsSync(path)) {
108
+ return {
109
+ success: false,
110
+ error: `File not found: ${path}`,
111
+ duration: Date.now() - startTime
112
+ };
113
+ }
114
+ const fileContent = readFileSync(path, 'utf-8');
115
+ return {
116
+ success: true,
117
+ output: fileContent,
118
+ data: { content: fileContent, path },
119
+ duration: Date.now() - startTime
120
+ };
121
+ }
122
+ case 'write': {
123
+ if (content === undefined) {
124
+ return {
125
+ success: false,
126
+ error: 'No content specified for write operation',
127
+ duration: Date.now() - startTime
128
+ };
129
+ }
130
+ writeFileSync(path, content);
131
+ return {
132
+ success: true,
133
+ output: `Written ${String(content.length)} bytes to ${path}`,
134
+ data: { path, bytesWritten: content.length },
135
+ duration: Date.now() - startTime
136
+ };
137
+ }
138
+ case 'copy': {
139
+ if (!destination) {
140
+ return {
141
+ success: false,
142
+ error: 'No destination specified for copy operation',
143
+ duration: Date.now() - startTime
144
+ };
145
+ }
146
+ if (!existsSync(path)) {
147
+ return {
148
+ success: false,
149
+ error: `Source file not found: ${path}`,
150
+ duration: Date.now() - startTime
151
+ };
152
+ }
153
+ copyFileSync(path, destination);
154
+ return {
155
+ success: true,
156
+ output: `Copied ${path} to ${destination}`,
157
+ data: { source: path, destination },
158
+ duration: Date.now() - startTime
159
+ };
160
+ }
161
+ case 'delete': {
162
+ if (!existsSync(path)) {
163
+ return {
164
+ success: true,
165
+ output: `File does not exist: ${path}`,
166
+ duration: Date.now() - startTime
167
+ };
168
+ }
169
+ unlinkSync(path);
170
+ return {
171
+ success: true,
172
+ output: `Deleted ${path}`,
173
+ data: { path },
174
+ duration: Date.now() - startTime
175
+ };
176
+ }
177
+ case 'exists': {
178
+ const exists = existsSync(path);
179
+ return {
180
+ success: true,
181
+ output: exists ? `File exists: ${path}` : `File does not exist: ${path}`,
182
+ data: { path, exists },
183
+ duration: Date.now() - startTime
184
+ };
185
+ }
186
+ default:
187
+ return {
188
+ success: false,
189
+ error: `Unknown file operation: ${operation}`,
190
+ duration: Date.now() - startTime
191
+ };
192
+ }
193
+ }
194
+ catch (error) {
195
+ return {
196
+ success: false,
197
+ error: error instanceof Error ? error.message : String(error),
198
+ duration: Date.now() - startTime
199
+ };
200
+ }
201
+ }
202
+ }
203
+ /**
204
+ * API step executor - makes HTTP requests
205
+ */
206
+ export class ApiStepExecutor {
207
+ type = 'api';
208
+ async execute(step, context) {
209
+ const startTime = Date.now();
210
+ const url = step['url'];
211
+ const method = (step['method'] ?? 'GET').toUpperCase();
212
+ const headers = step['headers'];
213
+ const body = step['body'];
214
+ const timeout = step['timeout'] ?? 30000;
215
+ if (!url) {
216
+ return {
217
+ success: false,
218
+ error: 'No URL specified for API step',
219
+ duration: Date.now() - startTime
220
+ };
221
+ }
222
+ if (context.dryRun) {
223
+ return {
224
+ success: true,
225
+ output: `[DRY RUN] Would make ${method} request to: ${url}`,
226
+ duration: Date.now() - startTime
227
+ };
228
+ }
229
+ try {
230
+ const controller = new AbortController();
231
+ const timeoutId = setTimeout(() => { controller.abort(); }, timeout);
232
+ const requestBody = body
233
+ ? typeof body === 'string' ? body : JSON.stringify(body)
234
+ : undefined;
235
+ const response = await fetch(url, {
236
+ method,
237
+ headers: {
238
+ 'Content-Type': 'application/json',
239
+ ...headers
240
+ },
241
+ ...(requestBody !== undefined ? { body: requestBody } : {}),
242
+ signal: controller.signal
243
+ });
244
+ clearTimeout(timeoutId);
245
+ const responseText = await response.text();
246
+ let responseData;
247
+ try {
248
+ responseData = JSON.parse(responseText);
249
+ }
250
+ catch {
251
+ responseData = responseText;
252
+ }
253
+ if (!response.ok) {
254
+ return {
255
+ success: false,
256
+ output: responseText,
257
+ error: `HTTP ${String(response.status)}: ${response.statusText}`,
258
+ data: { status: response.status, response: responseData },
259
+ duration: Date.now() - startTime
260
+ };
261
+ }
262
+ return {
263
+ success: true,
264
+ output: responseText,
265
+ data: { status: response.status, response: responseData },
266
+ duration: Date.now() - startTime
267
+ };
268
+ }
269
+ catch (error) {
270
+ return {
271
+ success: false,
272
+ error: error instanceof Error ? error.message : String(error),
273
+ duration: Date.now() - startTime
274
+ };
275
+ }
276
+ }
277
+ }
278
+ /**
279
+ * Script step executor - executes Node.js scripts
280
+ */
281
+ export class ScriptStepExecutor {
282
+ type = 'script';
283
+ async execute(step, context) {
284
+ const startTime = Date.now();
285
+ const scriptPath = step['script'];
286
+ const args = step['args'];
287
+ const timeout = step['timeout'] ?? 60000;
288
+ if (!scriptPath) {
289
+ return {
290
+ success: false,
291
+ error: 'No script path specified for script step',
292
+ duration: Date.now() - startTime
293
+ };
294
+ }
295
+ if (context.dryRun) {
296
+ return {
297
+ success: true,
298
+ output: `[DRY RUN] Would execute script: ${scriptPath}`,
299
+ duration: Date.now() - startTime
300
+ };
301
+ }
302
+ try {
303
+ // Resolve script path relative to working directory
304
+ const fullScriptPath = resolve(context.workingDir, scriptPath);
305
+ if (!existsSync(fullScriptPath)) {
306
+ return {
307
+ success: false,
308
+ error: `Script not found: ${fullScriptPath}`,
309
+ duration: Date.now() - startTime
310
+ };
311
+ }
312
+ const scriptModule = await import(pathToFileURL(fullScriptPath).href);
313
+ const scriptFn = scriptModule.default ?? scriptModule.main ?? scriptModule.execute;
314
+ if (typeof scriptFn !== 'function') {
315
+ return {
316
+ success: false,
317
+ error: `Script does not export a default function, main, or execute function`,
318
+ duration: Date.now() - startTime
319
+ };
320
+ }
321
+ // Execute script with timeout
322
+ const controller = new AbortController();
323
+ const timeoutId = setTimeout(() => { controller.abort(); }, timeout);
324
+ const result = await Promise.race([
325
+ scriptFn(args ?? {}, context),
326
+ new Promise((_, reject) => {
327
+ controller.signal.addEventListener('abort', () => {
328
+ reject(new Error(`Script execution timeout after ${String(timeout)}ms`));
329
+ });
330
+ })
331
+ ]);
332
+ clearTimeout(timeoutId);
333
+ return {
334
+ success: result.success,
335
+ duration: Date.now() - startTime,
336
+ ...(result.output !== undefined && { output: result.output }),
337
+ ...(result.error !== undefined && { error: result.error }),
338
+ ...(result.data !== undefined && { data: result.data })
339
+ };
340
+ }
341
+ catch (error) {
342
+ return {
343
+ success: false,
344
+ error: error instanceof Error ? error.message : String(error),
345
+ duration: Date.now() - startTime
346
+ };
347
+ }
348
+ }
349
+ }
350
+ // Registry of step executors
351
+ const executors = new Map();
352
+ // Register built-in executors (no optional deps: no image here; see step-executor-image)
353
+ executors.set('bash', new BashStepExecutor());
354
+ executors.set('file', new FileStepExecutor());
355
+ executors.set('api', new ApiStepExecutor());
356
+ executors.set('script', new ScriptStepExecutor());
357
+ export function getStepExecutor(type) {
358
+ return Promise.resolve(executors.get(type));
359
+ }
360
+ export function registerStepExecutor(executor) {
361
+ executors.set(executor.type, executor);
362
+ }
363
+ export async function executeStep(step, context) {
364
+ const executor = await getStepExecutor(step.type);
365
+ if (!executor) {
366
+ return {
367
+ success: false,
368
+ error: `No executor found for step type: ${step.type}`,
369
+ duration: 0
370
+ };
371
+ }
372
+ return executor.execute(step, context);
373
+ }
374
+ //# sourceMappingURL=step-executor.js.map
@@ -9,4 +9,5 @@ export declare const improvementTemplate = "# Improvement Actions: {{taskId}}\n\
9
9
  export declare const resourcesTemplate = "# Resources: {{description}}\n\n## People / Roles\n<!-- Who is needed for this task? -->\n-\n\n## Tools & Systems\n<!-- What tools, systems, or infrastructure is required? -->\n-\n\n## Data Requirements\n<!-- What data or information is needed? -->\n-\n\n## Time Estimate\n<!-- Estimated effort and timeline -->\n- Effort:\n- Timeline:\n";
10
10
  export declare const standardizationTemplate = "# Standardization: {{taskId}}\n\n## Successful Practices\n<!-- Document practices that worked well -->\n\n## Reusable Patterns\n<!-- Patterns that can be reused -->\n\n## Standard Operating Procedures\n<!-- Document SOPs if applicable -->\n";
11
11
  export declare function renderTemplate(template: string, vars: Record<string, string>): string;
12
+ export declare const pipelineConfigTemplate = "# Pipeline Configuration\n# This file controls automated PDCA cycle execution\n\nversion: \"1.0\"\n\nphases:\n # Do phase: Execute tasks and collect data\n do:\n auto: true # Enable automatic execution\n parallel: false # Execute tasks sequentially (set to true for concurrent)\n maxConcurrency: 4 # Max concurrent tasks when parallel is true\n successCriteria:\n minTasksCompleted: \"100%\" # Percentage or absolute number required\n\n # Check phase: Evaluate results and analyze deviations\n check:\n auto: true # Enable automatic comparison\n analyze: true # Perform root cause analysis\n recommend: true # Generate recommendations\n successCriteria:\n allowPartialGoals: true # Continue even if some goals are partial\n allowUnmetGoals: false # Fail if any goals are unmet (set to true to allow)\n\n # Act phase: Process results and take improvement actions\n act:\n auto: true # Enable automatic processing\n standardize: true # Document successful practices\n carryForward: true # Generate next cycle items for unresolved issues\n completeOnSuccess: true # Mark task as completed when pipeline succeeds\n";
12
13
  //# sourceMappingURL=templates.d.ts.map
@@ -140,4 +140,34 @@ export function renderTemplate(template, vars) {
140
140
  }
141
141
  return result;
142
142
  }
143
+ export const pipelineConfigTemplate = `# Pipeline Configuration
144
+ # This file controls automated PDCA cycle execution
145
+
146
+ version: "1.0"
147
+
148
+ phases:
149
+ # Do phase: Execute tasks and collect data
150
+ do:
151
+ auto: true # Enable automatic execution
152
+ parallel: false # Execute tasks sequentially (set to true for concurrent)
153
+ maxConcurrency: 4 # Max concurrent tasks when parallel is true
154
+ successCriteria:
155
+ minTasksCompleted: "100%" # Percentage or absolute number required
156
+
157
+ # Check phase: Evaluate results and analyze deviations
158
+ check:
159
+ auto: true # Enable automatic comparison
160
+ analyze: true # Perform root cause analysis
161
+ recommend: true # Generate recommendations
162
+ successCriteria:
163
+ allowPartialGoals: true # Continue even if some goals are partial
164
+ allowUnmetGoals: false # Fail if any goals are unmet (set to true to allow)
165
+
166
+ # Act phase: Process results and take improvement actions
167
+ act:
168
+ auto: true # Enable automatic processing
169
+ standardize: true # Document successful practices
170
+ carryForward: true # Generate next cycle items for unresolved issues
171
+ completeOnSuccess: true # Mark task as completed when pipeline succeeds
172
+ `;
143
173
  //# sourceMappingURL=templates.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmddev/tmd",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Task Markdown Driven - A lightweight PDCA cycle management framework integrated with OpenSpec",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -27,23 +27,6 @@
27
27
  "!dist/**/__tests__",
28
28
  "!dist/**/*.map"
29
29
  ],
30
- "scripts": {
31
- "lint": "eslint src/",
32
- "build": "tsx build.ts",
33
- "dev": "tsc --watch",
34
- "dev:cli": "pnpm build && node bin/tmd.js",
35
- "test": "vitest run",
36
- "test:watch": "vitest",
37
- "test:ui": "vitest --ui",
38
- "test:coverage": "vitest --coverage",
39
- "prepare": "pnpm run build",
40
- "prepublishOnly": "pnpm run build",
41
- "postinstall": "node scripts/postinstall.js",
42
- "check:pack-version": "node scripts/pack-version-check.mjs",
43
- "release": "pnpm run release:ci",
44
- "release:ci": "pnpm run check:pack-version",
45
- "changeset": "changeset"
46
- },
47
30
  "keywords": [
48
31
  "pdca",
49
32
  "openspec",
@@ -60,7 +43,7 @@
60
43
  "url": "https://github.com/sdd330/tmd.git"
61
44
  },
62
45
  "author": "TMD Contributors",
63
- "license": "MIT",
46
+ "license": "Apache-2.0",
64
47
  "dependencies": {
65
48
  "commander": "^13.1.0",
66
49
  "js-yaml": "^4.1.0",
@@ -79,6 +62,24 @@
79
62
  "vitest": "^3.2.4"
80
63
  },
81
64
  "engines": {
82
- "node": ">=20.19.0"
65
+ "node": ">=20.19.0",
66
+ "bun": ">=1.0"
67
+ },
68
+ "scripts": {
69
+ "lint": "eslint src/",
70
+ "build": "tsx build.ts",
71
+ "build:native": "node scripts/build-native.mjs",
72
+ "dev": "tsc --watch",
73
+ "dev:cli": "pnpm build && node bin/tmd.js",
74
+ "test": "vitest run",
75
+ "test:watch": "vitest",
76
+ "test:ui": "vitest --ui",
77
+ "test:coverage": "vitest --coverage",
78
+ "postinstall": "node scripts/postinstall.js || bun scripts/postinstall.js",
79
+ "check:pack-version": "node scripts/pack-version-check.mjs",
80
+ "check:docs-english": "node scripts/check-docs-english.mjs",
81
+ "release": "pnpm run release:ci",
82
+ "release:ci": "pnpm run check:pack-version",
83
+ "changeset": "changeset"
83
84
  }
84
- }
85
+ }
@@ -1,22 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Postinstall script for TMD
4
+ * Postinstall script for TMD. Supports Node and Bun (fs, path, process.env are compatible).
5
5
  *
6
- * This script runs automatically after npm install unless:
7
- * - CI=true environment variable is set
8
- * - TMD_NO_POSTINSTALL=1 environment variable is set
9
- * - dist/ directory doesn't exist (dev setup scenario)
6
+ * Runs after npm/pnpm/bun install unless:
7
+ * - CI=true
8
+ * - TMD_NO_POSTINSTALL=1
9
+ * - dist/ does not exist (dev setup)
10
10
  *
11
- * The script never fails npm install - all errors are caught and handled gracefully.
11
+ * Never fails the install; errors are caught and handled.
12
12
  */
13
13
 
14
14
  import { promises as fs } from 'fs';
15
15
  import path from 'path';
16
16
  import { fileURLToPath } from 'url';
17
17
 
18
- const __filename = fileURLToPath(import.meta.url);
19
- const __dirname = path.dirname(__filename);
18
+ // Bun: import.meta.dirname; Node: derive from import.meta.url
19
+ const __dirname =
20
+ typeof import.meta.dirname === 'string'
21
+ ? import.meta.dirname
22
+ : path.dirname(fileURLToPath(import.meta.url));
20
23
 
21
24
  /**
22
25
  * Check if we should skip installation