throughline 0.3.23 → 0.3.24
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 +17 -0
- package/package.json +1 -1
- package/src/vscode-task.mjs +65 -0
- package/src/vscode-task.test.mjs +170 -0
package/README.md
CHANGED
|
@@ -509,6 +509,23 @@ Two things to know:
|
|
|
509
509
|
`~/.npm-global/bin` is **before** the Windows npm path in `PATH` (the
|
|
510
510
|
`.bashrc` snippet above prepends, so it does this naturally).
|
|
511
511
|
|
|
512
|
+
**`.vscode/tasks.json` should not be committed to git (recommended)**
|
|
513
|
+
|
|
514
|
+
The `Throughline Monitor` task that gets auto-generated contains absolute
|
|
515
|
+
paths specific to your machine (`process.execPath` and the install location
|
|
516
|
+
of `throughline.mjs`). For shared repositories, add one of these to
|
|
517
|
+
`.gitignore`:
|
|
518
|
+
|
|
519
|
+
```gitignore
|
|
520
|
+
.vscode/tasks.json # only ignore tasks.json (settings.json etc. stay shared)
|
|
521
|
+
.vscode/ # ignore the whole .vscode directory
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Throughline detects this and prints a one-time recommendation when it first
|
|
525
|
+
creates / merges / repairs the file. (You don't have to follow the advice —
|
|
526
|
+
v0.3.23+ auto-repairs stale absolute paths anyway, so committing is not
|
|
527
|
+
catastrophic.)
|
|
528
|
+
|
|
512
529
|
**Cross-environment `.vscode/tasks.json` errors after switching machines**
|
|
513
530
|
|
|
514
531
|
If you commit `.vscode/tasks.json` to git and pull it on a different machine
|
package/package.json
CHANGED
package/src/vscode-task.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { join, dirname, isAbsolute } from 'node:path';
|
|
|
19
19
|
|
|
20
20
|
const MONITOR_LABEL = 'Throughline Monitor';
|
|
21
21
|
const JSONC_MARKER_FILENAME = '.throughline-jsonc-noted';
|
|
22
|
+
const GITIGNORE_MARKER_FILENAME = '.throughline-gitignore-noted';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* VSCode 系エディタが動いているかを env から推定する。
|
|
@@ -243,6 +244,67 @@ function emitSetupNotice(action) {
|
|
|
243
244
|
if (text) process.stdout.write(text);
|
|
244
245
|
}
|
|
245
246
|
|
|
247
|
+
/**
|
|
248
|
+
* `.vscode/tasks.json` が gitignore 推奨対象か判定する。
|
|
249
|
+
*
|
|
250
|
+
* 推奨条件:
|
|
251
|
+
* - cwd が git リポジトリ (`.git/` がある) かつ
|
|
252
|
+
* - `.gitignore` に `.vscode/tasks.json` 相当のエントリが無い
|
|
253
|
+
*
|
|
254
|
+
* 単独プロジェクト (.git なし) は判定対象外 (false を返す)。共有しないので
|
|
255
|
+
* gitignore の必要性そのものが無いため。
|
|
256
|
+
*
|
|
257
|
+
* @param {string} cwd
|
|
258
|
+
* @returns {boolean}
|
|
259
|
+
*/
|
|
260
|
+
export function shouldRecommendGitignore(cwd) {
|
|
261
|
+
const gitDir = join(cwd, '.git');
|
|
262
|
+
if (!existsSync(gitDir)) return false;
|
|
263
|
+
|
|
264
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
265
|
+
if (!existsSync(gitignorePath)) return true;
|
|
266
|
+
|
|
267
|
+
const content = readFileSync(gitignorePath, 'utf8');
|
|
268
|
+
const lines = content.split(/\r?\n/);
|
|
269
|
+
for (const raw of lines) {
|
|
270
|
+
const line = raw.trim();
|
|
271
|
+
if (!line || line.startsWith('#')) continue;
|
|
272
|
+
// 否定 (!) は除外しない: "!.vscode/tasks.json" は明示的に追跡したい意図なので推奨を出すべき
|
|
273
|
+
if (line.startsWith('!')) continue;
|
|
274
|
+
// .vscode 全体除外 / tasks.json 単独除外 / .vscode/* どれかを含むか
|
|
275
|
+
if (line === '.vscode' || line === '.vscode/' || line === '/.vscode' || line === '/.vscode/') return false;
|
|
276
|
+
if (line === '.vscode/*' || line === '.vscode/**') return false;
|
|
277
|
+
if (line === '.vscode/tasks.json' || line === '/.vscode/tasks.json') return false;
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function emitGitignoreRecommendationOnce(vscodeDir) {
|
|
283
|
+
const markerPath = join(vscodeDir, GITIGNORE_MARKER_FILENAME);
|
|
284
|
+
if (existsSync(markerPath)) return;
|
|
285
|
+
process.stdout.write(
|
|
286
|
+
'<system-reminder>\n' +
|
|
287
|
+
'Throughline ヒント: .vscode/tasks.json には現環境の絶対パス (node 実行ファイル / throughline.mjs)\n' +
|
|
288
|
+
'が書き込まれます。共有リポジトリで commit すると別 OS / 別 install 場所のメンバーで起動失敗の\n' +
|
|
289
|
+
'原因になるため、.gitignore に以下のいずれかを追加することを推奨します:\n' +
|
|
290
|
+
'\n' +
|
|
291
|
+
' .vscode/tasks.json # tasks.json だけ除外 (settings.json 等は共有可)\n' +
|
|
292
|
+
' .vscode/ # .vscode 配下を全部除外\n' +
|
|
293
|
+
'\n' +
|
|
294
|
+
'なお Throughline v0.3.23+ は別環境の絶対パスが焼き込まれた tasks.json を\n' +
|
|
295
|
+
'自動修復するため、commit してしまっても致命的ではありません。あくまで推奨です。\n' +
|
|
296
|
+
'\n' +
|
|
297
|
+
'この通知はプロジェクトごとに 1 度だけ流れます。必ずユーザー(あなたの対話相手)に日本語でそのまま伝えてください。\n' +
|
|
298
|
+
'</system-reminder>\n'
|
|
299
|
+
);
|
|
300
|
+
try {
|
|
301
|
+
writeFileSync(markerPath, `${new Date().toISOString()}\n`);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
304
|
+
process.stderr.write(`[throughline] failed to write gitignore marker: ${msg}\n`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
246
308
|
function emitJsoncGuidanceOnce(vscodeDir) {
|
|
247
309
|
const markerPath = join(vscodeDir, JSONC_MARKER_FILENAME);
|
|
248
310
|
if (existsSync(markerPath)) return;
|
|
@@ -302,6 +364,7 @@ export function ensureMonitorTaskFile(opts = {}) {
|
|
|
302
364
|
const obj = { version: '2.0.0', tasks: [buildMonitorTask(bin)] };
|
|
303
365
|
atomicWrite(tasksPath, JSON.stringify(obj, null, 2) + '\n');
|
|
304
366
|
emitSetupNotice('created');
|
|
367
|
+
if (shouldRecommendGitignore(cwd)) emitGitignoreRecommendationOnce(vscodeDir);
|
|
305
368
|
return { action: 'created', path: tasksPath };
|
|
306
369
|
}
|
|
307
370
|
|
|
@@ -336,6 +399,7 @@ export function ensureMonitorTaskFile(opts = {}) {
|
|
|
336
399
|
const nextObj = { ...obj, version: obj.version ?? '2.0.0', tasks: nextTasks };
|
|
337
400
|
atomicWrite(tasksPath, JSON.stringify(nextObj, null, indent) + '\n');
|
|
338
401
|
emitSetupNotice('repaired');
|
|
402
|
+
if (shouldRecommendGitignore(cwd)) emitGitignoreRecommendationOnce(vscodeDir);
|
|
339
403
|
return { action: 'repaired', path: tasksPath };
|
|
340
404
|
}
|
|
341
405
|
return { action: 'already_present', path: tasksPath };
|
|
@@ -349,5 +413,6 @@ export function ensureMonitorTaskFile(opts = {}) {
|
|
|
349
413
|
};
|
|
350
414
|
atomicWrite(tasksPath, JSON.stringify(nextObj, null, indent) + '\n');
|
|
351
415
|
emitSetupNotice('merged');
|
|
416
|
+
if (shouldRecommendGitignore(cwd)) emitGitignoreRecommendationOnce(vscodeDir);
|
|
352
417
|
return { action: 'merged', path: tasksPath };
|
|
353
418
|
}
|
package/src/vscode-task.test.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
isMonitorTaskBroken,
|
|
14
14
|
buildMonitorTask,
|
|
15
15
|
buildSetupNotice,
|
|
16
|
+
shouldRecommendGitignore,
|
|
16
17
|
} from './vscode-task.mjs';
|
|
17
18
|
|
|
18
19
|
const VSCODE_ENV = { TERM_PROGRAM: 'vscode' };
|
|
@@ -481,6 +482,175 @@ test('ensureMonitorTaskFile: second call is idempotent (already_present after cr
|
|
|
481
482
|
}
|
|
482
483
|
});
|
|
483
484
|
|
|
485
|
+
// --- shouldRecommendGitignore ---
|
|
486
|
+
|
|
487
|
+
test('shouldRecommendGitignore: false when not a git repo (.git missing)', () => {
|
|
488
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
489
|
+
try {
|
|
490
|
+
assert.equal(shouldRecommendGitignore(dir), false);
|
|
491
|
+
} finally {
|
|
492
|
+
cleanup();
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test('shouldRecommendGitignore: true when git repo has no .gitignore', () => {
|
|
497
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
498
|
+
try {
|
|
499
|
+
mkdirSync(join(dir, '.git'));
|
|
500
|
+
assert.equal(shouldRecommendGitignore(dir), true);
|
|
501
|
+
} finally {
|
|
502
|
+
cleanup();
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test('shouldRecommendGitignore: true when .gitignore does not list .vscode/tasks.json', () => {
|
|
507
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
508
|
+
try {
|
|
509
|
+
mkdirSync(join(dir, '.git'));
|
|
510
|
+
writeFileSync(join(dir, '.gitignore'), 'node_modules/\n*.log\n');
|
|
511
|
+
assert.equal(shouldRecommendGitignore(dir), true);
|
|
512
|
+
} finally {
|
|
513
|
+
cleanup();
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test('shouldRecommendGitignore: false when .gitignore has .vscode/tasks.json', () => {
|
|
518
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
519
|
+
try {
|
|
520
|
+
mkdirSync(join(dir, '.git'));
|
|
521
|
+
writeFileSync(join(dir, '.gitignore'), 'node_modules/\n.vscode/tasks.json\n');
|
|
522
|
+
assert.equal(shouldRecommendGitignore(dir), false);
|
|
523
|
+
} finally {
|
|
524
|
+
cleanup();
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test('shouldRecommendGitignore: false when .gitignore has .vscode/ (whole dir)', () => {
|
|
529
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
530
|
+
try {
|
|
531
|
+
mkdirSync(join(dir, '.git'));
|
|
532
|
+
writeFileSync(join(dir, '.gitignore'), '.vscode/\n');
|
|
533
|
+
assert.equal(shouldRecommendGitignore(dir), false);
|
|
534
|
+
} finally {
|
|
535
|
+
cleanup();
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test('shouldRecommendGitignore: false when .gitignore has .vscode (no slash)', () => {
|
|
540
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
541
|
+
try {
|
|
542
|
+
mkdirSync(join(dir, '.git'));
|
|
543
|
+
writeFileSync(join(dir, '.gitignore'), '.vscode\n');
|
|
544
|
+
assert.equal(shouldRecommendGitignore(dir), false);
|
|
545
|
+
} finally {
|
|
546
|
+
cleanup();
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test('shouldRecommendGitignore: ignores comments and negation lines', () => {
|
|
551
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
552
|
+
try {
|
|
553
|
+
mkdirSync(join(dir, '.git'));
|
|
554
|
+
// 否定パターンは「除外しない」意図なので、推奨は引き続き出す
|
|
555
|
+
writeFileSync(join(dir, '.gitignore'), '# comment\n!.vscode/tasks.json\n');
|
|
556
|
+
assert.equal(shouldRecommendGitignore(dir), true);
|
|
557
|
+
} finally {
|
|
558
|
+
cleanup();
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// --- ensureMonitorTaskFile: gitignore recommendation notice ---
|
|
563
|
+
|
|
564
|
+
test('ensureMonitorTaskFile: created emits gitignore recommendation when .git exists and no .gitignore entry', () => {
|
|
565
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
566
|
+
const captured = [];
|
|
567
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
568
|
+
process.stdout.write = (chunk) => {
|
|
569
|
+
captured.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
|
|
570
|
+
return true;
|
|
571
|
+
};
|
|
572
|
+
try {
|
|
573
|
+
mkdirSync(join(dir, '.git'));
|
|
574
|
+
const result = ensureMonitorTaskFile({
|
|
575
|
+
cwd: dir,
|
|
576
|
+
env: VSCODE_ENV,
|
|
577
|
+
throughlineBin: FAKE_BIN,
|
|
578
|
+
});
|
|
579
|
+
assert.equal(result.action, 'created');
|
|
580
|
+
} finally {
|
|
581
|
+
process.stdout.write = origWrite;
|
|
582
|
+
cleanup();
|
|
583
|
+
}
|
|
584
|
+
const joined = captured.join('');
|
|
585
|
+
assert.ok(joined.includes('.gitignore'), 'should emit gitignore recommendation');
|
|
586
|
+
assert.ok(joined.includes('Reload Window'), 'should still emit setup notice');
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test('ensureMonitorTaskFile: created does NOT emit gitignore recommendation when not a git repo', () => {
|
|
590
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
591
|
+
const captured = [];
|
|
592
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
593
|
+
process.stdout.write = (chunk) => {
|
|
594
|
+
captured.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
|
|
595
|
+
return true;
|
|
596
|
+
};
|
|
597
|
+
try {
|
|
598
|
+
const result = ensureMonitorTaskFile({
|
|
599
|
+
cwd: dir,
|
|
600
|
+
env: VSCODE_ENV,
|
|
601
|
+
throughlineBin: FAKE_BIN,
|
|
602
|
+
});
|
|
603
|
+
assert.equal(result.action, 'created');
|
|
604
|
+
} finally {
|
|
605
|
+
process.stdout.write = origWrite;
|
|
606
|
+
cleanup();
|
|
607
|
+
}
|
|
608
|
+
const joined = captured.join('');
|
|
609
|
+
assert.ok(!joined.includes('gitignore'), 'should not mention gitignore for non-git dirs');
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test('ensureMonitorTaskFile: gitignore recommendation is emitted only once per project', () => {
|
|
613
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
614
|
+
try {
|
|
615
|
+
mkdirSync(join(dir, '.git'));
|
|
616
|
+
|
|
617
|
+
const captured = [];
|
|
618
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
619
|
+
process.stdout.write = (chunk) => {
|
|
620
|
+
captured.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
|
|
621
|
+
return true;
|
|
622
|
+
};
|
|
623
|
+
try {
|
|
624
|
+
// 1 回目: created → gitignore 推奨が出る
|
|
625
|
+
const r1 = ensureMonitorTaskFile({
|
|
626
|
+
cwd: dir,
|
|
627
|
+
env: VSCODE_ENV,
|
|
628
|
+
throughlineBin: FAKE_BIN,
|
|
629
|
+
});
|
|
630
|
+
assert.equal(r1.action, 'created');
|
|
631
|
+
const firstCount = captured.filter((s) => s.includes('gitignore')).length;
|
|
632
|
+
assert.equal(firstCount, 1);
|
|
633
|
+
|
|
634
|
+
// tasks.json を一度消して再 created 状況を作る
|
|
635
|
+
// (実運用では already_present になるので現実的ではないが、marker の効きを見る)
|
|
636
|
+
const tasksPath = join(dir, '.vscode', 'tasks.json');
|
|
637
|
+
rmSync(tasksPath);
|
|
638
|
+
const r2 = ensureMonitorTaskFile({
|
|
639
|
+
cwd: dir,
|
|
640
|
+
env: VSCODE_ENV,
|
|
641
|
+
throughlineBin: FAKE_BIN,
|
|
642
|
+
});
|
|
643
|
+
assert.equal(r2.action, 'created');
|
|
644
|
+
const secondCount = captured.filter((s) => s.includes('gitignore')).length;
|
|
645
|
+
assert.equal(secondCount, 1, 'marker file should suppress 2nd recommendation');
|
|
646
|
+
} finally {
|
|
647
|
+
process.stdout.write = origWrite;
|
|
648
|
+
}
|
|
649
|
+
} finally {
|
|
650
|
+
cleanup();
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
|
|
484
654
|
// --- ensureMonitorTaskFile: cross-environment repair (地雷 4) ---
|
|
485
655
|
|
|
486
656
|
test('ensureMonitorTaskFile: repaired when existing task points to non-existent absolute paths', () => {
|