throughline 0.3.23 → 0.3.25
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/.claude/commands/tl-trim.md +42 -0
- package/.codex-sidecar.yml +62 -0
- package/CHANGELOG.md +583 -0
- package/README.ja.md +42 -5
- package/README.md +400 -23
- package/bin/throughline.mjs +168 -4
- package/codex/skills/throughline/SKILL.md +157 -0
- package/codex/skills/throughline/agents/openai.yaml +7 -0
- package/docs/INHERITANCE_ON_CLEAR_ONLY.md +146 -0
- package/docs/L1_L2_L3_REDESIGN.md +415 -0
- package/docs/PUBLIC_RELEASE_PLAN.md +184 -0
- package/docs/THROUGHLINE_CODEX_DUAL_SUPPORT.md +249 -0
- package/docs/THROUGHLINE_CODEX_FIRST_ROADMAP.md +555 -0
- package/docs/THROUGHLINE_CODEX_MONITOR_IMPLEMENTATION_PLAN.md +220 -0
- package/docs/THROUGHLINE_CODEX_TRIM_IMPLEMENTATION_PLAN.md +528 -0
- package/docs/THROUGHLINE_CODEX_TRIM_ROLLBACK_FIX_PLAN.md +672 -0
- package/docs/archive/CONCEPT.md +476 -0
- package/docs/archive/EXPERIMENT.md +371 -0
- package/docs/archive/README.md +22 -0
- package/docs/archive/SESSION_LINKING_DESIGN.md +231 -0
- package/docs/archive/THROUGHLINE_NEXT_STEPS.md +134 -0
- package/docs/throughline-codex-trim-rollback-incident-report.md +306 -0
- package/docs/throughline-handoff-context.example.json +57 -0
- package/docs/throughline-rollback-context-trim-insight.md +455 -0
- package/package.json +6 -2
- package/src/cli/codex-capture.mjs +95 -0
- package/src/cli/codex-handoff-model-smoke.mjs +292 -0
- package/src/cli/codex-handoff-model-smoke.test.mjs +262 -0
- package/src/cli/codex-handoff-smoke.mjs +163 -0
- package/src/cli/codex-handoff-smoke.test.mjs +149 -0
- package/src/cli/codex-handoff-start.mjs +291 -0
- package/src/cli/codex-handoff-start.test.mjs +194 -0
- package/src/cli/codex-hook.mjs +276 -0
- package/src/cli/codex-hook.test.mjs +293 -0
- package/src/cli/codex-host-primitive-audit.mjs +110 -0
- package/src/cli/codex-host-primitive-audit.test.mjs +75 -0
- package/src/cli/codex-restore-smoke.mjs +357 -0
- package/src/cli/codex-restore-source-audit.mjs +304 -0
- package/src/cli/codex-resume.mjs +138 -0
- package/src/cli/codex-rollback-model-visible-smoke.mjs +373 -0
- package/src/cli/codex-rollback-model-visible-smoke.test.mjs +255 -0
- package/src/cli/codex-sidecar-diagnostics.mjs +48 -0
- package/src/cli/codex-sidecar-dry-run.mjs +85 -0
- package/src/cli/codex-summarize.mjs +224 -0
- package/src/cli/codex-threads.mjs +89 -0
- package/src/cli/codex-visibility-smoke.mjs +196 -0
- package/src/cli/codex-vscode-restore-smoke.mjs +226 -0
- package/src/cli/codex-vscode-rollback-smoke.mjs +114 -0
- package/src/cli/doctor.mjs +503 -1
- package/src/cli/doctor.test.mjs +542 -3
- package/src/cli/handoff-preview.mjs +78 -0
- package/src/cli/help.test.mjs +64 -0
- package/src/cli/install.mjs +227 -4
- package/src/cli/install.test.mjs +207 -4
- package/src/cli/trim.mjs +564 -0
- package/src/codex-app-server.mjs +1816 -0
- package/src/codex-app-server.test.mjs +512 -0
- package/src/codex-auto-refresh.mjs +194 -0
- package/src/codex-auto-refresh.test.mjs +182 -0
- package/src/codex-capture.mjs +235 -0
- package/src/codex-capture.test.mjs +393 -0
- package/src/codex-handoff-model-smoke.mjs +114 -0
- package/src/codex-handoff-model-smoke.test.mjs +89 -0
- package/src/codex-handoff-smoke.mjs +124 -0
- package/src/codex-handoff-smoke.test.mjs +103 -0
- package/src/codex-handoff.mjs +331 -0
- package/src/codex-handoff.test.mjs +220 -0
- package/src/codex-host-primitive-audit.mjs +374 -0
- package/src/codex-host-primitive-audit.test.mjs +208 -0
- package/src/codex-restore-smoke.test.mjs +639 -0
- package/src/codex-restore-source-audit.mjs +1348 -0
- package/src/codex-restore-source-audit.test.mjs +623 -0
- package/src/codex-resume.test.mjs +242 -0
- package/src/codex-rollout-memory.mjs +711 -0
- package/src/codex-rollout-memory.test.mjs +610 -0
- package/src/codex-sidecar-cli.test.mjs +75 -0
- package/src/codex-sidecar.mjs +246 -0
- package/src/codex-sidecar.test.mjs +172 -0
- package/src/codex-summarize.test.mjs +143 -0
- package/src/codex-thread-identity.mjs +23 -0
- package/src/codex-thread-index.mjs +173 -0
- package/src/codex-thread-index.test.mjs +164 -0
- package/src/codex-usage.mjs +110 -0
- package/src/codex-usage.test.mjs +140 -0
- package/src/codex-visibility-smoke.test.mjs +222 -0
- package/src/codex-vscode-restore-smoke.mjs +206 -0
- package/src/codex-vscode-restore-smoke.test.mjs +325 -0
- package/src/codex-vscode-rollback-smoke.mjs +90 -0
- package/src/codex-vscode-rollback-smoke.test.mjs +290 -0
- package/src/db-schema.test.mjs +97 -0
- package/src/haiku-summarizer.mjs +267 -26
- package/src/haiku-summarizer.test.mjs +282 -0
- package/src/handoff-preview.test.mjs +108 -0
- package/src/handoff-record.mjs +294 -0
- package/src/handoff-record.test.mjs +226 -0
- package/src/hook-entrypoints.test.mjs +326 -0
- package/src/package-files.test.mjs +19 -0
- package/src/prompt-submit.mjs +9 -6
- package/src/resume-context.mjs +44 -140
- package/src/resume-context.test.mjs +172 -0
- package/src/session-start.mjs +8 -5
- package/src/state-file.mjs +50 -6
- package/src/state-file.test.mjs +50 -0
- package/src/token-monitor.mjs +14 -10
- package/src/token-monitor.test.mjs +27 -0
- package/src/trim-cli.test.mjs +1584 -0
- package/src/trim-model.mjs +584 -0
- package/src/trim-model.test.mjs +568 -0
- package/src/turn-processor.mjs +17 -10
- package/src/vscode-task.mjs +94 -6
- package/src/vscode-task.test.mjs +186 -6
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 から推定する。
|
|
@@ -238,14 +239,98 @@ export function buildSetupNotice(action) {
|
|
|
238
239
|
return null;
|
|
239
240
|
}
|
|
240
241
|
|
|
241
|
-
function
|
|
242
|
+
function shouldEmitNotices(env) {
|
|
243
|
+
return env.THROUGHLINE_SUPPRESS_VSCODE_NOTICES !== '1';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function emitSetupNotice(action, env) {
|
|
247
|
+
if (!shouldEmitNotices(env)) return;
|
|
242
248
|
const text = buildSetupNotice(action);
|
|
243
249
|
if (text) process.stdout.write(text);
|
|
244
250
|
}
|
|
245
251
|
|
|
246
|
-
|
|
252
|
+
/**
|
|
253
|
+
* `.vscode/tasks.json` が gitignore 推奨対象か判定する。
|
|
254
|
+
*
|
|
255
|
+
* 推奨条件:
|
|
256
|
+
* - cwd が git リポジトリ (`.git/` がある) かつ
|
|
257
|
+
* - `.gitignore` に `.vscode/tasks.json` 相当のエントリが無い
|
|
258
|
+
*
|
|
259
|
+
* 単独プロジェクト (.git なし) は判定対象外 (false を返す)。共有しないので
|
|
260
|
+
* gitignore の必要性そのものが無いため。
|
|
261
|
+
*
|
|
262
|
+
* @param {string} cwd
|
|
263
|
+
* @returns {boolean}
|
|
264
|
+
*/
|
|
265
|
+
export function shouldRecommendGitignore(cwd) {
|
|
266
|
+
const gitDir = join(cwd, '.git');
|
|
267
|
+
if (!existsSync(gitDir)) return false;
|
|
268
|
+
|
|
269
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
270
|
+
if (!existsSync(gitignorePath)) return true;
|
|
271
|
+
|
|
272
|
+
const content = readFileSync(gitignorePath, 'utf8');
|
|
273
|
+
const lines = content.split(/\r?\n/);
|
|
274
|
+
for (const raw of lines) {
|
|
275
|
+
const line = raw.trim();
|
|
276
|
+
if (!line || line.startsWith('#')) continue;
|
|
277
|
+
// 否定 (!) は除外しない: "!.vscode/tasks.json" は明示的に追跡したい意図なので推奨を出すべき
|
|
278
|
+
if (line.startsWith('!')) continue;
|
|
279
|
+
// .vscode 全体除外 / tasks.json 単独除外 / .vscode/* どれかを含むか
|
|
280
|
+
if (line === '.vscode' || line === '.vscode/' || line === '/.vscode' || line === '/.vscode/') return false;
|
|
281
|
+
if (line === '.vscode/*' || line === '.vscode/**') return false;
|
|
282
|
+
if (line === '.vscode/tasks.json' || line === '/.vscode/tasks.json') return false;
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function emitGitignoreRecommendationOnce(vscodeDir, env) {
|
|
288
|
+
const markerPath = join(vscodeDir, GITIGNORE_MARKER_FILENAME);
|
|
289
|
+
if (existsSync(markerPath)) return;
|
|
290
|
+
if (!shouldEmitNotices(env)) {
|
|
291
|
+
try {
|
|
292
|
+
writeFileSync(markerPath, `${new Date().toISOString()}\n`);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
295
|
+
process.stderr.write(`[throughline] failed to write gitignore marker: ${msg}\n`);
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
process.stdout.write(
|
|
300
|
+
'<system-reminder>\n' +
|
|
301
|
+
'Throughline ヒント: .vscode/tasks.json には現環境の絶対パス (node 実行ファイル / throughline.mjs)\n' +
|
|
302
|
+
'が書き込まれます。共有リポジトリで commit すると別 OS / 別 install 場所のメンバーで起動失敗の\n' +
|
|
303
|
+
'原因になるため、.gitignore に以下のいずれかを追加することを推奨します:\n' +
|
|
304
|
+
'\n' +
|
|
305
|
+
' .vscode/tasks.json # tasks.json だけ除外 (settings.json 等は共有可)\n' +
|
|
306
|
+
' .vscode/ # .vscode 配下を全部除外\n' +
|
|
307
|
+
'\n' +
|
|
308
|
+
'なお Throughline v0.3.23+ は別環境の絶対パスが焼き込まれた tasks.json を\n' +
|
|
309
|
+
'自動修復するため、commit してしまっても致命的ではありません。あくまで推奨です。\n' +
|
|
310
|
+
'\n' +
|
|
311
|
+
'この通知はプロジェクトごとに 1 度だけ流れます。必ずユーザー(あなたの対話相手)に日本語でそのまま伝えてください。\n' +
|
|
312
|
+
'</system-reminder>\n'
|
|
313
|
+
);
|
|
314
|
+
try {
|
|
315
|
+
writeFileSync(markerPath, `${new Date().toISOString()}\n`);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
318
|
+
process.stderr.write(`[throughline] failed to write gitignore marker: ${msg}\n`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function emitJsoncGuidanceOnce(vscodeDir, env) {
|
|
247
323
|
const markerPath = join(vscodeDir, JSONC_MARKER_FILENAME);
|
|
248
324
|
if (existsSync(markerPath)) return;
|
|
325
|
+
if (!shouldEmitNotices(env)) {
|
|
326
|
+
try {
|
|
327
|
+
writeFileSync(markerPath, `${new Date().toISOString()}\n`);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
330
|
+
process.stderr.write(`[throughline] failed to write JSONC marker: ${msg}\n`);
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
249
334
|
process.stderr.write(
|
|
250
335
|
`[throughline] .vscode/tasks.json contains JSONC features (comments or trailing commas). ` +
|
|
251
336
|
`Auto-edit is unsafe on this file — add the Throughline Monitor task manually. ` +
|
|
@@ -301,14 +386,15 @@ export function ensureMonitorTaskFile(opts = {}) {
|
|
|
301
386
|
if (!existsSync(vscodeDir)) mkdirSync(vscodeDir, { recursive: true });
|
|
302
387
|
const obj = { version: '2.0.0', tasks: [buildMonitorTask(bin)] };
|
|
303
388
|
atomicWrite(tasksPath, JSON.stringify(obj, null, 2) + '\n');
|
|
304
|
-
emitSetupNotice('created');
|
|
389
|
+
emitSetupNotice('created', env);
|
|
390
|
+
if (shouldRecommendGitignore(cwd)) emitGitignoreRecommendationOnce(vscodeDir, env);
|
|
305
391
|
return { action: 'created', path: tasksPath };
|
|
306
392
|
}
|
|
307
393
|
|
|
308
394
|
const text = readFileSync(tasksPath, 'utf8');
|
|
309
395
|
|
|
310
396
|
if (detectJsoncFeatures(text)) {
|
|
311
|
-
emitJsoncGuidanceOnce(vscodeDir);
|
|
397
|
+
emitJsoncGuidanceOnce(vscodeDir, env);
|
|
312
398
|
return { action: 'skipped', reason: 'jsonc_unsupported', path: tasksPath };
|
|
313
399
|
}
|
|
314
400
|
|
|
@@ -335,7 +421,8 @@ export function ensureMonitorTaskFile(opts = {}) {
|
|
|
335
421
|
nextTasks[existingIdx] = repaired;
|
|
336
422
|
const nextObj = { ...obj, version: obj.version ?? '2.0.0', tasks: nextTasks };
|
|
337
423
|
atomicWrite(tasksPath, JSON.stringify(nextObj, null, indent) + '\n');
|
|
338
|
-
emitSetupNotice('repaired');
|
|
424
|
+
emitSetupNotice('repaired', env);
|
|
425
|
+
if (shouldRecommendGitignore(cwd)) emitGitignoreRecommendationOnce(vscodeDir, env);
|
|
339
426
|
return { action: 'repaired', path: tasksPath };
|
|
340
427
|
}
|
|
341
428
|
return { action: 'already_present', path: tasksPath };
|
|
@@ -348,6 +435,7 @@ export function ensureMonitorTaskFile(opts = {}) {
|
|
|
348
435
|
tasks: [...(Array.isArray(obj.tasks) ? obj.tasks : []), buildMonitorTask(bin)],
|
|
349
436
|
};
|
|
350
437
|
atomicWrite(tasksPath, JSON.stringify(nextObj, null, indent) + '\n');
|
|
351
|
-
emitSetupNotice('merged');
|
|
438
|
+
emitSetupNotice('merged', env);
|
|
439
|
+
if (shouldRecommendGitignore(cwd)) emitGitignoreRecommendationOnce(vscodeDir, env);
|
|
352
440
|
return { action: 'merged', path: tasksPath };
|
|
353
441
|
}
|
package/src/vscode-task.test.mjs
CHANGED
|
@@ -13,9 +13,16 @@ import {
|
|
|
13
13
|
isMonitorTaskBroken,
|
|
14
14
|
buildMonitorTask,
|
|
15
15
|
buildSetupNotice,
|
|
16
|
+
shouldRecommendGitignore,
|
|
16
17
|
} from './vscode-task.mjs';
|
|
17
18
|
|
|
18
|
-
const VSCODE_ENV = {
|
|
19
|
+
const VSCODE_ENV = {
|
|
20
|
+
TERM_PROGRAM: 'vscode',
|
|
21
|
+
THROUGHLINE_SUPPRESS_VSCODE_NOTICES: '1',
|
|
22
|
+
};
|
|
23
|
+
// Production notices are Claude-facing additional context. Tests keep them
|
|
24
|
+
// silent by default and opt in only when asserting notice text.
|
|
25
|
+
const VSCODE_NOTICE_ENV = { TERM_PROGRAM: 'vscode' };
|
|
19
26
|
// 実在する絶対パスを使う。`isMonitorTaskBroken` が「絶対パス + 非存在」で broken 判定するので、
|
|
20
27
|
// 架空パスを使うと意図せず repaired ブランチに落ちてしまう。
|
|
21
28
|
const FAKE_BIN = process.execPath;
|
|
@@ -435,7 +442,11 @@ test('ensureMonitorTaskFile: already_present when command references throughline
|
|
|
435
442
|
{
|
|
436
443
|
label: 'My Custom Monitor',
|
|
437
444
|
type: 'process',
|
|
438
|
-
|
|
445
|
+
// 実在する絶対パスを使う。`/usr/bin/node` は WSL2 では実在するが
|
|
446
|
+
// CI runner (Linux/macOS は /opt/hostedtoolcache, Windows は別) では
|
|
447
|
+
// 存在しないので isMonitorTaskBroken が true になり repaired ブランチに落ちる。
|
|
448
|
+
// process.execPath なら「いま走らせている node 自身の絶対パス」なので必ず実在する。
|
|
449
|
+
command: process.execPath,
|
|
439
450
|
// 相対パスにして broken 判定を避ける(このテストは「label renamed でも検出できるか」だけが論点)
|
|
440
451
|
args: ['./throughline.mjs', 'monitor'],
|
|
441
452
|
},
|
|
@@ -481,6 +492,175 @@ test('ensureMonitorTaskFile: second call is idempotent (already_present after cr
|
|
|
481
492
|
}
|
|
482
493
|
});
|
|
483
494
|
|
|
495
|
+
// --- shouldRecommendGitignore ---
|
|
496
|
+
|
|
497
|
+
test('shouldRecommendGitignore: false when not a git repo (.git missing)', () => {
|
|
498
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
499
|
+
try {
|
|
500
|
+
assert.equal(shouldRecommendGitignore(dir), false);
|
|
501
|
+
} finally {
|
|
502
|
+
cleanup();
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test('shouldRecommendGitignore: true when git repo has no .gitignore', () => {
|
|
507
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
508
|
+
try {
|
|
509
|
+
mkdirSync(join(dir, '.git'));
|
|
510
|
+
assert.equal(shouldRecommendGitignore(dir), true);
|
|
511
|
+
} finally {
|
|
512
|
+
cleanup();
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test('shouldRecommendGitignore: true when .gitignore does not list .vscode/tasks.json', () => {
|
|
517
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
518
|
+
try {
|
|
519
|
+
mkdirSync(join(dir, '.git'));
|
|
520
|
+
writeFileSync(join(dir, '.gitignore'), 'node_modules/\n*.log\n');
|
|
521
|
+
assert.equal(shouldRecommendGitignore(dir), true);
|
|
522
|
+
} finally {
|
|
523
|
+
cleanup();
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test('shouldRecommendGitignore: false when .gitignore has .vscode/tasks.json', () => {
|
|
528
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
529
|
+
try {
|
|
530
|
+
mkdirSync(join(dir, '.git'));
|
|
531
|
+
writeFileSync(join(dir, '.gitignore'), 'node_modules/\n.vscode/tasks.json\n');
|
|
532
|
+
assert.equal(shouldRecommendGitignore(dir), false);
|
|
533
|
+
} finally {
|
|
534
|
+
cleanup();
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test('shouldRecommendGitignore: false when .gitignore has .vscode/ (whole dir)', () => {
|
|
539
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
540
|
+
try {
|
|
541
|
+
mkdirSync(join(dir, '.git'));
|
|
542
|
+
writeFileSync(join(dir, '.gitignore'), '.vscode/\n');
|
|
543
|
+
assert.equal(shouldRecommendGitignore(dir), false);
|
|
544
|
+
} finally {
|
|
545
|
+
cleanup();
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test('shouldRecommendGitignore: false when .gitignore has .vscode (no slash)', () => {
|
|
550
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
551
|
+
try {
|
|
552
|
+
mkdirSync(join(dir, '.git'));
|
|
553
|
+
writeFileSync(join(dir, '.gitignore'), '.vscode\n');
|
|
554
|
+
assert.equal(shouldRecommendGitignore(dir), false);
|
|
555
|
+
} finally {
|
|
556
|
+
cleanup();
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test('shouldRecommendGitignore: ignores comments and negation lines', () => {
|
|
561
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
562
|
+
try {
|
|
563
|
+
mkdirSync(join(dir, '.git'));
|
|
564
|
+
// 否定パターンは「除外しない」意図なので、推奨は引き続き出す
|
|
565
|
+
writeFileSync(join(dir, '.gitignore'), '# comment\n!.vscode/tasks.json\n');
|
|
566
|
+
assert.equal(shouldRecommendGitignore(dir), true);
|
|
567
|
+
} finally {
|
|
568
|
+
cleanup();
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// --- ensureMonitorTaskFile: gitignore recommendation notice ---
|
|
573
|
+
|
|
574
|
+
test('ensureMonitorTaskFile: created emits gitignore recommendation when .git exists and no .gitignore entry', () => {
|
|
575
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
576
|
+
const captured = [];
|
|
577
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
578
|
+
process.stdout.write = (chunk) => {
|
|
579
|
+
captured.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
|
|
580
|
+
return true;
|
|
581
|
+
};
|
|
582
|
+
try {
|
|
583
|
+
mkdirSync(join(dir, '.git'));
|
|
584
|
+
const result = ensureMonitorTaskFile({
|
|
585
|
+
cwd: dir,
|
|
586
|
+
env: VSCODE_NOTICE_ENV,
|
|
587
|
+
throughlineBin: FAKE_BIN,
|
|
588
|
+
});
|
|
589
|
+
assert.equal(result.action, 'created');
|
|
590
|
+
} finally {
|
|
591
|
+
process.stdout.write = origWrite;
|
|
592
|
+
cleanup();
|
|
593
|
+
}
|
|
594
|
+
const joined = captured.join('');
|
|
595
|
+
assert.ok(joined.includes('.gitignore'), 'should emit gitignore recommendation');
|
|
596
|
+
assert.ok(joined.includes('Reload Window'), 'should still emit setup notice');
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test('ensureMonitorTaskFile: created does NOT emit gitignore recommendation when not a git repo', () => {
|
|
600
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
601
|
+
const captured = [];
|
|
602
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
603
|
+
process.stdout.write = (chunk) => {
|
|
604
|
+
captured.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
|
|
605
|
+
return true;
|
|
606
|
+
};
|
|
607
|
+
try {
|
|
608
|
+
const result = ensureMonitorTaskFile({
|
|
609
|
+
cwd: dir,
|
|
610
|
+
env: VSCODE_NOTICE_ENV,
|
|
611
|
+
throughlineBin: FAKE_BIN,
|
|
612
|
+
});
|
|
613
|
+
assert.equal(result.action, 'created');
|
|
614
|
+
} finally {
|
|
615
|
+
process.stdout.write = origWrite;
|
|
616
|
+
cleanup();
|
|
617
|
+
}
|
|
618
|
+
const joined = captured.join('');
|
|
619
|
+
assert.ok(!joined.includes('gitignore'), 'should not mention gitignore for non-git dirs');
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test('ensureMonitorTaskFile: gitignore recommendation is emitted only once per project', () => {
|
|
623
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
624
|
+
try {
|
|
625
|
+
mkdirSync(join(dir, '.git'));
|
|
626
|
+
|
|
627
|
+
const captured = [];
|
|
628
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
629
|
+
process.stdout.write = (chunk) => {
|
|
630
|
+
captured.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
|
|
631
|
+
return true;
|
|
632
|
+
};
|
|
633
|
+
try {
|
|
634
|
+
// 1 回目: created → gitignore 推奨が出る
|
|
635
|
+
const r1 = ensureMonitorTaskFile({
|
|
636
|
+
cwd: dir,
|
|
637
|
+
env: VSCODE_NOTICE_ENV,
|
|
638
|
+
throughlineBin: FAKE_BIN,
|
|
639
|
+
});
|
|
640
|
+
assert.equal(r1.action, 'created');
|
|
641
|
+
const firstCount = captured.filter((s) => s.includes('gitignore')).length;
|
|
642
|
+
assert.equal(firstCount, 1);
|
|
643
|
+
|
|
644
|
+
// tasks.json を一度消して再 created 状況を作る
|
|
645
|
+
// (実運用では already_present になるので現実的ではないが、marker の効きを見る)
|
|
646
|
+
const tasksPath = join(dir, '.vscode', 'tasks.json');
|
|
647
|
+
rmSync(tasksPath);
|
|
648
|
+
const r2 = ensureMonitorTaskFile({
|
|
649
|
+
cwd: dir,
|
|
650
|
+
env: VSCODE_ENV,
|
|
651
|
+
throughlineBin: FAKE_BIN,
|
|
652
|
+
});
|
|
653
|
+
assert.equal(r2.action, 'created');
|
|
654
|
+
const secondCount = captured.filter((s) => s.includes('gitignore')).length;
|
|
655
|
+
assert.equal(secondCount, 1, 'marker file should suppress 2nd recommendation');
|
|
656
|
+
} finally {
|
|
657
|
+
process.stdout.write = origWrite;
|
|
658
|
+
}
|
|
659
|
+
} finally {
|
|
660
|
+
cleanup();
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
484
664
|
// --- ensureMonitorTaskFile: cross-environment repair (地雷 4) ---
|
|
485
665
|
|
|
486
666
|
test('ensureMonitorTaskFile: repaired when existing task points to non-existent absolute paths', () => {
|
|
@@ -621,7 +801,7 @@ test('ensureMonitorTaskFile: repaired emits notice on stdout', () => {
|
|
|
621
801
|
|
|
622
802
|
const result = ensureMonitorTaskFile({
|
|
623
803
|
cwd: dir,
|
|
624
|
-
env:
|
|
804
|
+
env: VSCODE_NOTICE_ENV,
|
|
625
805
|
throughlineBin: FAKE_BIN,
|
|
626
806
|
});
|
|
627
807
|
assert.equal(result.action, 'repaired');
|
|
@@ -697,7 +877,7 @@ test('ensureMonitorTaskFile: jsonc_unsupported marker suppresses stderr on 2nd c
|
|
|
697
877
|
try {
|
|
698
878
|
const r1 = ensureMonitorTaskFile({
|
|
699
879
|
cwd: dir,
|
|
700
|
-
env:
|
|
880
|
+
env: VSCODE_NOTICE_ENV,
|
|
701
881
|
throughlineBin: FAKE_BIN,
|
|
702
882
|
});
|
|
703
883
|
assert.equal(r1.action, 'skipped');
|
|
@@ -786,13 +966,13 @@ test('buildSetupNotice: ensureMonitorTaskFile writes notice to stdout on first c
|
|
|
786
966
|
try {
|
|
787
967
|
const r1 = ensureMonitorTaskFile({
|
|
788
968
|
cwd: dir,
|
|
789
|
-
env:
|
|
969
|
+
env: VSCODE_NOTICE_ENV,
|
|
790
970
|
throughlineBin: FAKE_BIN,
|
|
791
971
|
});
|
|
792
972
|
assert.equal(r1.action, 'created');
|
|
793
973
|
const r2 = ensureMonitorTaskFile({
|
|
794
974
|
cwd: dir,
|
|
795
|
-
env:
|
|
975
|
+
env: VSCODE_NOTICE_ENV,
|
|
796
976
|
throughlineBin: FAKE_BIN,
|
|
797
977
|
});
|
|
798
978
|
assert.equal(r2.action, 'already_present');
|