tuncss-plan-kit 0.1.1 → 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.
- package/.opencode/plugins/tuncss-plan-kit.js +48 -44
- package/README.md +123 -113
- package/bin/cli.js +484 -429
- package/commands/codex/brainstorm.md +1 -1
- package/commands/codex/changelog.md +1 -0
- package/commands/codex/handoff-plan.md +1 -1
- package/commands/codex/plan-universal.md +1 -1
- package/commands/opencode/brainstorm.md +5 -5
- package/commands/opencode/changelog.md +5 -0
- package/commands/opencode/handoff-plan.md +5 -5
- package/commands/opencode/plan-universal.md +5 -5
- package/package.json +36 -36
- package/skills/brainstorm/SKILL.md +132 -132
- package/skills/changelog/SKILL.md +95 -0
- package/skills/handoff-plan/SKILL.md +78 -77
- package/skills/plan-universal/SKILL.md +139 -138
- package/templates/instructions-block.md +3 -2
package/bin/cli.js
CHANGED
|
@@ -1,429 +1,484 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import os from "os";
|
|
6
|
-
import { spawnSync } from "child_process";
|
|
7
|
-
import { fileURLToPath } from "url";
|
|
8
|
-
|
|
9
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
11
|
-
const PKG_NAME = "tuncss-plan-kit";
|
|
12
|
-
|
|
13
|
-
const SKILLS = ["brainstorm", "plan-universal", "handoff-plan"];
|
|
14
|
-
const COMMANDS = ["brainstorm", "plan-universal", "handoff-plan"];
|
|
15
|
-
const MARKER_START = "<!-- tuncss-plan-kit:start -->";
|
|
16
|
-
const MARKER_END = "<!-- tuncss-plan-kit:end -->";
|
|
17
|
-
|
|
18
|
-
const PLATFORMS = {
|
|
19
|
-
claude: {
|
|
20
|
-
label: "claude",
|
|
21
|
-
project: {
|
|
22
|
-
skillsDir: ".claude/skills",
|
|
23
|
-
commandsDir: null,
|
|
24
|
-
instructionsFile: "CLAUDE.md",
|
|
25
|
-
},
|
|
26
|
-
global: (home) => ({
|
|
27
|
-
skillsDir: path.join(home, ".claude", "skills"),
|
|
28
|
-
commandsDir: null,
|
|
29
|
-
instructionsFile: path.join(home, ".claude", "CLAUDE.md"),
|
|
30
|
-
}),
|
|
31
|
-
commandsSrc: null,
|
|
32
|
-
skipCommands: true,
|
|
33
|
-
},
|
|
34
|
-
codex: {
|
|
35
|
-
label: "codex",
|
|
36
|
-
project: {
|
|
37
|
-
skillsDir: ".agents/skills",
|
|
38
|
-
commandsDir: ".codex/prompts",
|
|
39
|
-
instructionsFile: "AGENTS.md",
|
|
40
|
-
},
|
|
41
|
-
global: (home) => ({
|
|
42
|
-
skillsDir: path.join(home, ".agents", "skills"),
|
|
43
|
-
commandsDir: path.join(home, ".codex", "prompts"),
|
|
44
|
-
instructionsFile: path.join(home, ".codex", "AGENTS.md"),
|
|
45
|
-
}),
|
|
46
|
-
commandsSrc: "codex",
|
|
47
|
-
},
|
|
48
|
-
opencode: {
|
|
49
|
-
label: "opencode",
|
|
50
|
-
project: {
|
|
51
|
-
skillsDir: ".opencode/skills",
|
|
52
|
-
commandsDir: ".opencode/commands",
|
|
53
|
-
instructionsFile: "AGENTS.md",
|
|
54
|
-
},
|
|
55
|
-
// Global install for OpenCode uses the npm-plugin route, not file-drop.
|
|
56
|
-
// See installOpenCodeGlobal().
|
|
57
|
-
global: null,
|
|
58
|
-
commandsSrc: "opencode",
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
npx tuncss-plan-kit init --target
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (exists(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
);
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
writeJson(
|
|
345
|
-
lines.push(
|
|
346
|
-
` ${
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import { spawnSync } from "child_process";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
11
|
+
const PKG_NAME = "tuncss-plan-kit";
|
|
12
|
+
|
|
13
|
+
const SKILLS = ["brainstorm", "plan-universal", "handoff-plan", "changelog"];
|
|
14
|
+
const COMMANDS = ["brainstorm", "plan-universal", "handoff-plan", "changelog"];
|
|
15
|
+
const MARKER_START = "<!-- tuncss-plan-kit:start -->";
|
|
16
|
+
const MARKER_END = "<!-- tuncss-plan-kit:end -->";
|
|
17
|
+
|
|
18
|
+
const PLATFORMS = {
|
|
19
|
+
claude: {
|
|
20
|
+
label: "claude",
|
|
21
|
+
project: {
|
|
22
|
+
skillsDir: ".claude/skills",
|
|
23
|
+
commandsDir: null,
|
|
24
|
+
instructionsFile: "CLAUDE.md",
|
|
25
|
+
},
|
|
26
|
+
global: (home) => ({
|
|
27
|
+
skillsDir: path.join(home, ".claude", "skills"),
|
|
28
|
+
commandsDir: null,
|
|
29
|
+
instructionsFile: path.join(home, ".claude", "CLAUDE.md"),
|
|
30
|
+
}),
|
|
31
|
+
commandsSrc: null,
|
|
32
|
+
skipCommands: true,
|
|
33
|
+
},
|
|
34
|
+
codex: {
|
|
35
|
+
label: "codex",
|
|
36
|
+
project: {
|
|
37
|
+
skillsDir: ".agents/skills",
|
|
38
|
+
commandsDir: ".codex/prompts",
|
|
39
|
+
instructionsFile: "AGENTS.md",
|
|
40
|
+
},
|
|
41
|
+
global: (home) => ({
|
|
42
|
+
skillsDir: path.join(home, ".agents", "skills"),
|
|
43
|
+
commandsDir: path.join(home, ".codex", "prompts"),
|
|
44
|
+
instructionsFile: path.join(home, ".codex", "AGENTS.md"),
|
|
45
|
+
}),
|
|
46
|
+
commandsSrc: "codex",
|
|
47
|
+
},
|
|
48
|
+
opencode: {
|
|
49
|
+
label: "opencode",
|
|
50
|
+
project: {
|
|
51
|
+
skillsDir: ".opencode/skills",
|
|
52
|
+
commandsDir: ".opencode/commands",
|
|
53
|
+
instructionsFile: "AGENTS.md",
|
|
54
|
+
},
|
|
55
|
+
// Global install for OpenCode uses the npm-plugin route, not file-drop.
|
|
56
|
+
// See installOpenCodeGlobal().
|
|
57
|
+
global: null,
|
|
58
|
+
commandsSrc: "opencode",
|
|
59
|
+
},
|
|
60
|
+
// Antigravity reads .agents/ in a workspace and ~/.gemini/config/ globally.
|
|
61
|
+
// One global location serves all three variants (desktop app, agy CLI, IDE).
|
|
62
|
+
// Like Claude Code, it expands skills into slash commands on its own, so no
|
|
63
|
+
// wrapper files are written.
|
|
64
|
+
antigravity: {
|
|
65
|
+
label: "antigravity",
|
|
66
|
+
project: {
|
|
67
|
+
skillsDir: ".agents/skills",
|
|
68
|
+
commandsDir: null,
|
|
69
|
+
instructionsFile: "AGENTS.md",
|
|
70
|
+
},
|
|
71
|
+
global: (home) => ({
|
|
72
|
+
skillsDir: path.join(home, ".gemini", "config", "skills"),
|
|
73
|
+
commandsDir: null,
|
|
74
|
+
instructionsFile: path.join(home, ".gemini", "config", "AGENTS.md"),
|
|
75
|
+
}),
|
|
76
|
+
commandsSrc: null,
|
|
77
|
+
skipCommands: true,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const SUPPORTED = Object.keys(PLATFORMS);
|
|
82
|
+
|
|
83
|
+
function parseArgs(argv) {
|
|
84
|
+
const args = { command: null, target: null, global: false, force: false };
|
|
85
|
+
for (const a of argv) {
|
|
86
|
+
if (!args.command && !a.startsWith("--")) {
|
|
87
|
+
args.command = a;
|
|
88
|
+
} else if (a.startsWith("--target=")) {
|
|
89
|
+
args.target = a
|
|
90
|
+
.slice("--target=".length)
|
|
91
|
+
.split(",")
|
|
92
|
+
.map((s) => s.trim())
|
|
93
|
+
.filter(Boolean);
|
|
94
|
+
} else if (a === "--global") {
|
|
95
|
+
args.global = true;
|
|
96
|
+
} else if (a === "--force") {
|
|
97
|
+
args.force = true;
|
|
98
|
+
} else if (a === "--help" || a === "-h") {
|
|
99
|
+
args.command = "help";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return args;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function printHelp() {
|
|
106
|
+
console.log(`tuncss-plan-kit
|
|
107
|
+
|
|
108
|
+
Usage:
|
|
109
|
+
npx tuncss-plan-kit init [--target=<list>] [--global] [--force]
|
|
110
|
+
|
|
111
|
+
Commands:
|
|
112
|
+
init Install the three skills (brainstorm, plan-universal, handoff-plan)
|
|
113
|
+
into the target platform(s).
|
|
114
|
+
|
|
115
|
+
Options:
|
|
116
|
+
--target Comma-separated platforms to install for. Supported:
|
|
117
|
+
claude, codex, opencode, antigravity, all
|
|
118
|
+
If omitted, auto-detects from the current directory.
|
|
119
|
+
--global Install to user-wide locations.
|
|
120
|
+
For OpenCode this uses the npm-plugin route (installs the package
|
|
121
|
+
into ~/.config/opencode/node_modules/ and registers it in
|
|
122
|
+
opencode.json's plugin array).
|
|
123
|
+
--force Overwrite existing skill/command files without warning.
|
|
124
|
+
(Instruction-file marker blocks are always idempotent.)
|
|
125
|
+
|
|
126
|
+
Examples:
|
|
127
|
+
npx tuncss-plan-kit init
|
|
128
|
+
npx tuncss-plan-kit init --target=claude
|
|
129
|
+
npx tuncss-plan-kit init --target=claude,codex
|
|
130
|
+
npx tuncss-plan-kit init --target=all --global
|
|
131
|
+
`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function exists(p) {
|
|
135
|
+
try {
|
|
136
|
+
fs.accessSync(p);
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function detectTargets(cwd) {
|
|
144
|
+
const found = new Set();
|
|
145
|
+
if (exists(path.join(cwd, ".claude")) || exists(path.join(cwd, "CLAUDE.md"))) {
|
|
146
|
+
found.add("claude");
|
|
147
|
+
}
|
|
148
|
+
if (exists(path.join(cwd, ".codex"))) found.add("codex");
|
|
149
|
+
if (exists(path.join(cwd, ".opencode"))) found.add("opencode");
|
|
150
|
+
// Antigravity discovers .agents/ and reads AGENTS.md as rules. Its project
|
|
151
|
+
// paths are a subset of Codex's, so adding it here costs no extra files.
|
|
152
|
+
if (exists(path.join(cwd, ".agents"))) found.add("antigravity");
|
|
153
|
+
if (exists(path.join(cwd, "AGENTS.md"))) {
|
|
154
|
+
found.add("antigravity");
|
|
155
|
+
if (!found.has("codex") && !found.has("opencode")) {
|
|
156
|
+
found.add("codex");
|
|
157
|
+
found.add("opencode");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return [...found];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function ensureDir(dir) {
|
|
164
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function copyFile(src, dest, force) {
|
|
168
|
+
if (exists(dest) && !force) {
|
|
169
|
+
const a = fs.readFileSync(src, "utf8");
|
|
170
|
+
const b = fs.readFileSync(dest, "utf8");
|
|
171
|
+
if (a === b) return { status: "unchanged", dest };
|
|
172
|
+
fs.writeFileSync(dest, a);
|
|
173
|
+
return { status: "updated", dest };
|
|
174
|
+
}
|
|
175
|
+
ensureDir(path.dirname(dest));
|
|
176
|
+
fs.writeFileSync(dest, fs.readFileSync(src, "utf8"));
|
|
177
|
+
return { status: "written", dest };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function escapeRegex(s) {
|
|
181
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function upsertMarkerBlock(filePath, blockBody) {
|
|
185
|
+
const block = `${MARKER_START}\n${blockBody.trim()}\n${MARKER_END}`;
|
|
186
|
+
let existing = "";
|
|
187
|
+
if (exists(filePath)) existing = fs.readFileSync(filePath, "utf8");
|
|
188
|
+
if (existing.includes(MARKER_START) && existing.includes(MARKER_END)) {
|
|
189
|
+
const re = new RegExp(
|
|
190
|
+
`${escapeRegex(MARKER_START)}[\\s\\S]*?${escapeRegex(MARKER_END)}`,
|
|
191
|
+
"m"
|
|
192
|
+
);
|
|
193
|
+
const next = existing.replace(re, block);
|
|
194
|
+
if (next === existing) return { status: "unchanged" };
|
|
195
|
+
ensureDir(path.dirname(filePath));
|
|
196
|
+
fs.writeFileSync(filePath, next);
|
|
197
|
+
return { status: "block-updated" };
|
|
198
|
+
}
|
|
199
|
+
const sep =
|
|
200
|
+
existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
201
|
+
ensureDir(path.dirname(filePath));
|
|
202
|
+
fs.writeFileSync(filePath, existing + sep + block + "\n");
|
|
203
|
+
return { status: existing.length === 0 ? "file-created" : "block-appended" };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const GITATTRIBUTES_LINE = "docs/CHANGELOG.md merge=union";
|
|
207
|
+
|
|
208
|
+
// Three people appending changelog entries on separate branches all touch the
|
|
209
|
+
// same lines of docs/CHANGELOG.md. The union merge driver keeps both sides
|
|
210
|
+
// instead of raising a conflict on every merge.
|
|
211
|
+
function ensureGitattributes(baseRoot) {
|
|
212
|
+
const filePath = path.join(baseRoot, ".gitattributes");
|
|
213
|
+
let existing = "";
|
|
214
|
+
if (exists(filePath)) existing = fs.readFileSync(filePath, "utf8");
|
|
215
|
+
if (existing.split(/\r?\n/).some((l) => l.trim() === GITATTRIBUTES_LINE)) {
|
|
216
|
+
return { status: "unchanged", dest: filePath };
|
|
217
|
+
}
|
|
218
|
+
const sep = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
|
|
219
|
+
ensureDir(path.dirname(filePath));
|
|
220
|
+
fs.writeFileSync(filePath, existing + sep + GITATTRIBUTES_LINE + "\n");
|
|
221
|
+
return { status: existing.length === 0 ? "written" : "updated", dest: filePath };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function loadInstructionsBlock() {
|
|
225
|
+
const tplPath = path.join(PKG_ROOT, "templates", "instructions-block.md");
|
|
226
|
+
const tpl = fs.readFileSync(tplPath, "utf8");
|
|
227
|
+
return tpl.replace(MARKER_START, "").replace(MARKER_END, "").trim();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function relPath(p, base) {
|
|
231
|
+
const r = path.relative(base, p);
|
|
232
|
+
return r.split(path.sep).join("/");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function statusTag(status) {
|
|
236
|
+
if (status === "unchanged") return "·";
|
|
237
|
+
if (status === "updated" || status === "block-updated") return "↻";
|
|
238
|
+
return "✓";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function installPlatform({ platform, isGlobal, force, baseRoot, sharedState }) {
|
|
242
|
+
const cfg = PLATFORMS[platform];
|
|
243
|
+
const paths = isGlobal
|
|
244
|
+
? cfg.global(os.homedir())
|
|
245
|
+
: {
|
|
246
|
+
skillsDir: path.join(baseRoot, cfg.project.skillsDir),
|
|
247
|
+
commandsDir: cfg.project.commandsDir
|
|
248
|
+
? path.join(baseRoot, cfg.project.commandsDir)
|
|
249
|
+
: null,
|
|
250
|
+
instructionsFile: path.join(baseRoot, cfg.project.instructionsFile),
|
|
251
|
+
};
|
|
252
|
+
const displayBase = isGlobal ? os.homedir() : baseRoot;
|
|
253
|
+
|
|
254
|
+
const lines = [];
|
|
255
|
+
|
|
256
|
+
for (const skill of SKILLS) {
|
|
257
|
+
const src = path.join(PKG_ROOT, "skills", skill, "SKILL.md");
|
|
258
|
+
const dest = path.join(paths.skillsDir, skill, "SKILL.md");
|
|
259
|
+
// Codex and Antigravity share .agents/skills; don't copy the same file twice.
|
|
260
|
+
if (sharedState.writtenSkillFiles.has(dest)) {
|
|
261
|
+
lines.push(` · ${relPath(dest, displayBase)} (already written this run)`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const r = copyFile(src, dest, force);
|
|
265
|
+
sharedState.writtenSkillFiles.add(dest);
|
|
266
|
+
lines.push(` ${statusTag(r.status)} ${relPath(r.dest, displayBase)}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!cfg.skipCommands) {
|
|
270
|
+
for (const cmd of COMMANDS) {
|
|
271
|
+
const src = path.join(PKG_ROOT, "commands", cfg.commandsSrc, `${cmd}.md`);
|
|
272
|
+
const dest = path.join(paths.commandsDir, `${cmd}.md`);
|
|
273
|
+
const r = copyFile(src, dest, force);
|
|
274
|
+
lines.push(` ${statusTag(r.status)} ${relPath(r.dest, displayBase)}`);
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
lines.push(
|
|
278
|
+
` · (skill names auto-expose as slash commands; no wrapper files needed)`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (sharedState.writtenInstructionFiles.has(paths.instructionsFile)) {
|
|
283
|
+
lines.push(
|
|
284
|
+
` · ${relPath(paths.instructionsFile, displayBase)} (already updated this run)`
|
|
285
|
+
);
|
|
286
|
+
} else {
|
|
287
|
+
const r = upsertMarkerBlock(
|
|
288
|
+
paths.instructionsFile,
|
|
289
|
+
sharedState.instructionsBlock
|
|
290
|
+
);
|
|
291
|
+
sharedState.writtenInstructionFiles.add(paths.instructionsFile);
|
|
292
|
+
lines.push(
|
|
293
|
+
` ${statusTag(r.status)} ${relPath(paths.instructionsFile, displayBase)} (${r.status})`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return lines;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function readJsonOrDefault(filePath, fallback) {
|
|
301
|
+
if (!exists(filePath)) return fallback;
|
|
302
|
+
try {
|
|
303
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
304
|
+
} catch {
|
|
305
|
+
return fallback;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function writeJson(filePath, obj) {
|
|
310
|
+
ensureDir(path.dirname(filePath));
|
|
311
|
+
fs.writeFileSync(filePath, JSON.stringify(obj, null, 2) + "\n");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function isRunningFromInstalledPackage() {
|
|
315
|
+
// If our package root contains /node_modules/, we were installed as a dep
|
|
316
|
+
// (e.g. via `npm install -g` or `npx`). Otherwise we're being run from
|
|
317
|
+
// source (dev mode).
|
|
318
|
+
return PKG_ROOT.split(path.sep).includes("node_modules");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function dependencySpec() {
|
|
322
|
+
// When running from a published install, use a version range so npm
|
|
323
|
+
// installs from the registry. When running from source (dev), point to
|
|
324
|
+
// our absolute path with file: so changes are picked up without publish.
|
|
325
|
+
if (isRunningFromInstalledPackage()) {
|
|
326
|
+
return `^${readJsonOrDefault(path.join(PKG_ROOT, "package.json"), { version: "0.1.0" }).version || "0.1.0"}`;
|
|
327
|
+
}
|
|
328
|
+
return "file:" + PKG_ROOT.split(path.sep).join("/");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function installOpenCodeGlobal() {
|
|
332
|
+
const home = os.homedir();
|
|
333
|
+
const ocDir = path.join(home, ".config", "opencode");
|
|
334
|
+
ensureDir(ocDir);
|
|
335
|
+
const lines = [];
|
|
336
|
+
|
|
337
|
+
// 1. package.json — add or update tuncss-plan-kit dependency
|
|
338
|
+
const pkgPath = path.join(ocDir, "package.json");
|
|
339
|
+
const pkg = readJsonOrDefault(pkgPath, {});
|
|
340
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
341
|
+
const spec = dependencySpec();
|
|
342
|
+
const prevSpec = pkg.dependencies[PKG_NAME];
|
|
343
|
+
pkg.dependencies[PKG_NAME] = spec;
|
|
344
|
+
writeJson(pkgPath, pkg);
|
|
345
|
+
lines.push(
|
|
346
|
+
` ${prevSpec === spec ? "·" : prevSpec ? "↻" : "✓"} ${relPath(pkgPath, home)} (dep: ${PKG_NAME}@${spec})`
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// 2. npm install in ocDir
|
|
350
|
+
// Pass the full command as a single string with shell: true to avoid the
|
|
351
|
+
// Node DEP0190 warning about unescaped args concatenation. Args are all
|
|
352
|
+
// hardcoded here so there's no injection surface.
|
|
353
|
+
const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
354
|
+
const npmRes = spawnSync(`${npmBin} install --silent --no-audit --no-fund`, {
|
|
355
|
+
cwd: ocDir,
|
|
356
|
+
stdio: "inherit",
|
|
357
|
+
shell: true,
|
|
358
|
+
});
|
|
359
|
+
if (npmRes.status !== 0) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`npm install failed in ${ocDir} (exit code ${npmRes.status}). Fix the error above and re-run.`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
lines.push(` ✓ npm install ran in ${relPath(ocDir, home)}`);
|
|
365
|
+
|
|
366
|
+
// 3. command wrappers — file-drop into ~/.config/opencode/commands/
|
|
367
|
+
// (the plugin handles skills via config injection, but command discovery
|
|
368
|
+
// appears to require file-drop)
|
|
369
|
+
const cmdDir = path.join(ocDir, "commands");
|
|
370
|
+
for (const cmd of COMMANDS) {
|
|
371
|
+
const src = path.join(PKG_ROOT, "commands", "opencode", `${cmd}.md`);
|
|
372
|
+
const dest = path.join(cmdDir, `${cmd}.md`);
|
|
373
|
+
const r = copyFile(src, dest, false);
|
|
374
|
+
lines.push(` ${statusTag(r.status)} ${relPath(r.dest, home)}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 4. opencode.json — add plugin reference (idempotent)
|
|
378
|
+
const ocJsonPath = path.join(ocDir, "opencode.json");
|
|
379
|
+
const ocJson = readJsonOrDefault(ocJsonPath, {
|
|
380
|
+
$schema: "https://opencode.ai/config.json",
|
|
381
|
+
});
|
|
382
|
+
ocJson.plugin = ocJson.plugin || [];
|
|
383
|
+
// Normalize to strings only (we don't use the [string, object] tuple form)
|
|
384
|
+
const pluginPath = `~/.config/opencode/node_modules/${PKG_NAME}`;
|
|
385
|
+
const alreadyHas = ocJson.plugin.some(
|
|
386
|
+
(p) => p === pluginPath || (Array.isArray(p) && p[0] === pluginPath)
|
|
387
|
+
);
|
|
388
|
+
if (!alreadyHas) {
|
|
389
|
+
ocJson.plugin.push(pluginPath);
|
|
390
|
+
}
|
|
391
|
+
writeJson(ocJsonPath, ocJson);
|
|
392
|
+
lines.push(
|
|
393
|
+
` ${alreadyHas ? "·" : "✓"} ${relPath(ocJsonPath, home)} (plugin: ${pluginPath})`
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
return lines;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function init(args) {
|
|
400
|
+
const cwd = process.cwd();
|
|
401
|
+
|
|
402
|
+
let targets = args.target;
|
|
403
|
+
if (targets && targets.includes("all")) {
|
|
404
|
+
targets = SUPPORTED.slice();
|
|
405
|
+
}
|
|
406
|
+
if (!targets) {
|
|
407
|
+
targets = detectTargets(cwd);
|
|
408
|
+
if (targets.length === 0) {
|
|
409
|
+
console.error(
|
|
410
|
+
"No supported platform detected in this directory.\n" +
|
|
411
|
+
"Specify one explicitly, e.g.: npx tuncss-plan-kit init --target=claude\n" +
|
|
412
|
+
"Or install for all three: npx tuncss-plan-kit init --target=all"
|
|
413
|
+
);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const unsupported = targets.filter((t) => !SUPPORTED.includes(t));
|
|
419
|
+
if (unsupported.length > 0) {
|
|
420
|
+
console.error(
|
|
421
|
+
`Unsupported target(s): ${unsupported.join(", ")}.\n` +
|
|
422
|
+
`Supported: ${SUPPORTED.join(", ")}, all.`
|
|
423
|
+
);
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
targets = SUPPORTED.filter((p) => targets.includes(p));
|
|
428
|
+
|
|
429
|
+
console.log(`tuncss-plan-kit init`);
|
|
430
|
+
console.log(` scope: ${args.global ? "global (user-wide)" : "project (./)"}`);
|
|
431
|
+
console.log(` target: ${targets.join(", ")}`);
|
|
432
|
+
console.log("");
|
|
433
|
+
|
|
434
|
+
const sharedState = {
|
|
435
|
+
instructionsBlock: loadInstructionsBlock(),
|
|
436
|
+
writtenInstructionFiles: new Set(),
|
|
437
|
+
writtenSkillFiles: new Set(),
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
for (const platform of targets) {
|
|
441
|
+
console.log(`[${platform}]`);
|
|
442
|
+
if (platform === "opencode" && args.global) {
|
|
443
|
+
const lines = installOpenCodeGlobal();
|
|
444
|
+
for (const l of lines) console.log(l);
|
|
445
|
+
} else {
|
|
446
|
+
const lines = installPlatform({
|
|
447
|
+
platform,
|
|
448
|
+
isGlobal: args.global,
|
|
449
|
+
force: args.force,
|
|
450
|
+
baseRoot: cwd,
|
|
451
|
+
sharedState,
|
|
452
|
+
});
|
|
453
|
+
for (const l of lines) console.log(l);
|
|
454
|
+
}
|
|
455
|
+
console.log("");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (!args.global) {
|
|
459
|
+
const r = ensureGitattributes(cwd);
|
|
460
|
+
console.log(`[git]`);
|
|
461
|
+
console.log(` ${statusTag(r.status)} .gitattributes (${GITATTRIBUTES_LINE})`);
|
|
462
|
+
console.log("");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
console.log(
|
|
466
|
+
"Done. Restart your coding agent(s) to pick up new skills and slash commands."
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function main() {
|
|
471
|
+
const args = parseArgs(process.argv.slice(2));
|
|
472
|
+
if (!args.command || args.command === "help") {
|
|
473
|
+
printHelp();
|
|
474
|
+
process.exit(args.command ? 0 : 1);
|
|
475
|
+
}
|
|
476
|
+
if (args.command === "init") {
|
|
477
|
+
return init(args);
|
|
478
|
+
}
|
|
479
|
+
console.error(`Unknown command: ${args.command}`);
|
|
480
|
+
printHelp();
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
main();
|