@loicngr/kobo 1.7.13 → 1.7.15
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/CHANGELOG.md +25 -0
- package/README.md +81 -399
- package/dist/server/db/migrations.js +33 -0
- package/dist/server/index.js +21 -2
- package/dist/server/routes/changelog.js +47 -0
- package/dist/server/routes/export.js +71 -0
- package/dist/server/routes/fs.js +49 -0
- package/dist/server/routes/git.js +16 -1
- package/dist/server/routes/health.js +2 -1
- package/dist/server/routes/workspaces.js +107 -64
- package/dist/server/services/agent/orchestrator.js +10 -0
- package/dist/server/services/archive-script-service.js +65 -0
- package/dist/server/services/auto-loop-service.js +7 -0
- package/dist/server/services/cleanup-script-service.js +118 -0
- package/dist/server/services/settings-service.js +184 -2
- package/dist/server/services/setup-script-service.js +10 -81
- package/dist/server/utils/git-ops.js +30 -0
- package/dist/server/utils/paths.js +4 -0
- package/dist/server/utils/script-runner.js +96 -0
- package/package.json +2 -1
- package/src/client/dist/spa/assets/ActivityFeed-DU6lDEP0.js +8 -0
- package/src/client/dist/spa/assets/{ActivityFeed-WjiQ9716.css → ActivityFeed-yUMQhnW4.css} +1 -1
- package/src/client/dist/spa/assets/{ClosePopup-C5EF3QrA.js → ClosePopup-CxvZA3ft.js} +1 -1
- package/src/client/dist/spa/assets/CreatePage-CdZr7f3j.js +2 -0
- package/src/client/dist/spa/assets/CreatePage-DgHjL4cZ.css +1 -0
- package/src/client/dist/spa/assets/{DiffViewer-hWxcG22N.js → DiffViewer-m801GPfI.js} +3 -3
- package/src/client/dist/spa/assets/HealthPage-z1uIOpYk.js +1 -0
- package/src/client/dist/spa/assets/{MainLayout-Bp0oVWa-.css → MainLayout-BJmBXwYn.css} +1 -1
- package/src/client/dist/spa/assets/MainLayout-oJdQ-QKM.js +37 -0
- package/src/client/dist/spa/assets/{QBadge-u0mEz_W1.js → QBadge-NEwszYs7.js} +1 -1
- package/src/client/dist/spa/assets/QBanner-Jsq4uJZs.js +1 -0
- package/src/client/dist/spa/assets/QBtn-CoU-UC_j.js +1 -0
- package/src/client/dist/spa/assets/{QCheckbox-skYuqkHX.js → QCheckbox-Cq2STfHp.js} +1 -1
- package/src/client/dist/spa/assets/{QChip-DSMeriN6.js → QChip-DnJyQVs2.js} +2 -2
- package/src/client/dist/spa/assets/QExpansionItem-BTd5m2yV.js +1 -0
- package/src/client/dist/spa/assets/QIcon-BmEX2rXO.js +1 -0
- package/src/client/dist/spa/assets/QInput-D0t39uK_.js +1 -0
- package/src/client/dist/spa/assets/{QItemLabel-B0tYxHQg.js → QItemLabel-Btqw0P7M.js} +1 -1
- package/src/client/dist/spa/assets/QItemSection-DRg-QuAD.js +1 -0
- package/src/client/dist/spa/assets/QList-B3TuWSqL.js +1 -0
- package/src/client/dist/spa/assets/QMenu-C2Wwwf2E.js +1 -0
- package/src/client/dist/spa/assets/QPage-DO_bQyV_.js +1 -0
- package/src/client/dist/spa/assets/QRadio-M9mC5jZy.js +1 -0
- package/src/client/dist/spa/assets/QScrollArea-A1wI0IXU.js +1 -0
- package/src/client/dist/spa/assets/QSpace-DONPiIes.js +1 -0
- package/src/client/dist/spa/assets/{QSpinnerDots-CluOpUgq.js → QSpinnerDots-DspFKwCZ.js} +1 -1
- package/src/client/dist/spa/assets/{QToggle-aBvIHg6j.js → QToggle-DBzTAIbK.js} +1 -1
- package/src/client/dist/spa/assets/QTooltip-Bfdmzm_m.js +1 -0
- package/src/client/dist/spa/assets/SearchPage-ChmKHNKn.js +1 -0
- package/src/client/dist/spa/assets/SettingsPage-B59LoCos.js +9 -0
- package/src/client/dist/spa/assets/{SettingsPage-B_qPRSDH.css → SettingsPage-BJLyYrBN.css} +1 -1
- package/src/client/dist/spa/assets/{TouchPan-DPBZDRzA.js → TouchPan-BIE5rs7U.js} +1 -1
- package/src/client/dist/spa/assets/WorkspacePage-Bj1PJSWT.js +4 -0
- package/src/client/dist/spa/assets/WorkspacePage-tFBswKV9.css +1 -0
- package/src/client/dist/spa/assets/build-path-tree-BGUV3nY1.js +1 -0
- package/src/client/dist/spa/assets/{cssMode-DTibHxcy.js → cssMode-BU4X8R6a.js} +1 -1
- package/src/client/dist/spa/assets/documents-B3nitIYF.js +1 -0
- package/src/client/dist/spa/assets/{editor.api-CdLzoLLv.js → editor.api-B4xBDzmJ.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-DSLdinVQ.js → editor.main-CSZRkloL.js} +3 -3
- package/src/client/dist/spa/assets/engineFeatures-CLOVr5b4.js +1 -0
- package/src/client/dist/spa/assets/expand-template-BxUkuL5g.js +1 -0
- package/src/client/dist/spa/assets/{formatters-CgfY9uSI.js → formatters-9dcj2tyJ.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-wf8G5c3b.js → freemarker2-DRz20wAV.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-BGPZbdOB.js → handlebars-C0dsvPnC.js} +1 -1
- package/src/client/dist/spa/assets/{html-DPEVCP74.js → html-Cqvj1pWs.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-DMWUNRWz.js → htmlMode-BTHNvkm6.js} +1 -1
- package/src/client/dist/spa/assets/i18n-D1I-Us2H.js +1 -0
- package/src/client/dist/spa/assets/{index-5ydpLSpt.css → index-ClabzOPc.css} +1 -1
- package/src/client/dist/spa/assets/index-KABmOIkF.js +2 -0
- package/src/client/dist/spa/assets/{javascript-hOcrXbaP.js → javascript-C8n3U02v.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-EeNIyfJU.js → jsonMode-C3AFxQ6K.js} +1 -1
- package/src/client/dist/spa/assets/{kobo-commands-Bh5k0Smw.js → kobo-commands-BuxgteGZ.js} +1 -1
- package/src/client/dist/spa/assets/{liquid-Cvw1gFPR.js → liquid-C4wtUDrJ.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-CGtsv5px.js → mdx-CaT1p1F2.js} +1 -1
- package/src/client/dist/spa/assets/{monaco.contribution-RY1eW2u5.js → monaco.contribution-CJg5GKVf.js} +2 -2
- package/src/client/dist/spa/assets/notifications-BC6en6Lt.js +1 -0
- package/src/client/dist/spa/assets/permissionModes-BQHBTBwa.js +1 -0
- package/src/client/dist/spa/assets/{python-t0GNay3W.js → python-Cj54W2Tg.js} +1 -1
- package/src/client/dist/spa/assets/{razor-v9eEhqO6.js → razor-D3gJxoX_.js} +1 -1
- package/src/client/dist/spa/assets/render-chat-markdown-DxEHr3lW.js +60 -0
- package/src/client/dist/spa/assets/runtime-core.esm-bundler-D_RRiKBh.js +1 -0
- package/src/client/dist/spa/assets/{tsMode-B376_f_2.js → tsMode-B6S4PLWH.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-WTvQ8gc0.js → typescript-Ca8AEX3t.js} +1 -1
- package/src/client/dist/spa/assets/use-checkbox-DnSuDqo2.js +1 -0
- package/src/client/dist/spa/assets/use-id-BCnfiBjU.js +1 -0
- package/src/client/dist/spa/assets/use-onboarding-B3l7mx48.css +1 -0
- package/src/client/dist/spa/assets/use-onboarding-CNeLPDtv.js +2 -0
- package/src/client/dist/spa/assets/use-quasar-k24tGxE-.js +1 -0
- package/src/client/dist/spa/assets/{vue-i18n-cyuEW5NY.js → vue-i18n-Cq-KgjJC.js} +2 -2
- package/src/client/dist/spa/assets/{xml-BMuHSj3C.js → xml-CsKo4k8C.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-BRigOMm_.js → yaml-X5yKmi6z.js} +1 -1
- package/src/client/dist/spa/index.html +13 -13
- package/src/client/dist/spa/assets/ActivityFeed-CrlJXGsr.js +0 -8
- package/src/client/dist/spa/assets/CreatePage-B7DVVS0u.js +0 -2
- package/src/client/dist/spa/assets/CreatePage-ZyBHUbl0.css +0 -1
- package/src/client/dist/spa/assets/HealthPage-BP_TiWeV.js +0 -1
- package/src/client/dist/spa/assets/MainLayout-BRV8i-6o.js +0 -37
- package/src/client/dist/spa/assets/QBtn-CLU6snCm.js +0 -1
- package/src/client/dist/spa/assets/QExpansionItem-BFqbf9C7.js +0 -1
- package/src/client/dist/spa/assets/QIcon-0rjEivgj.js +0 -1
- package/src/client/dist/spa/assets/QInput-Ciqjq5-e.js +0 -1
- package/src/client/dist/spa/assets/QItemSection-DZnyqPM3.js +0 -1
- package/src/client/dist/spa/assets/QList-MfhZa-uv.js +0 -1
- package/src/client/dist/spa/assets/QMenu-Cx3v9_6z.js +0 -1
- package/src/client/dist/spa/assets/QPage-CGYPttdA.js +0 -1
- package/src/client/dist/spa/assets/QRadio-BEsMlsx1.js +0 -1
- package/src/client/dist/spa/assets/QSpace-BrtkvWzZ.js +0 -1
- package/src/client/dist/spa/assets/QTooltip-DdpeVKGV.js +0 -1
- package/src/client/dist/spa/assets/SearchPage-MfSrrMo1.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-CR3kXpHd.js +0 -9
- package/src/client/dist/spa/assets/WorkspacePage-B4YnZ6re.css +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-DUKKBulA.js +0 -4
- package/src/client/dist/spa/assets/build-path-tree-DOtGuDWX.js +0 -1
- package/src/client/dist/spa/assets/documents-qOarUoMj.js +0 -1
- package/src/client/dist/spa/assets/engineFeatures-DNZNjYIf.js +0 -1
- package/src/client/dist/spa/assets/expand-template-Fy2sU7vj.js +0 -1
- package/src/client/dist/spa/assets/i18n-CUkOJRYM.js +0 -1
- package/src/client/dist/spa/assets/index-D9yyMIdQ.js +0 -2
- package/src/client/dist/spa/assets/notifications-BzyK_kYE.js +0 -1
- package/src/client/dist/spa/assets/permissionModes-BBA8zBcf.js +0 -1
- package/src/client/dist/spa/assets/project-color-d_h7ZYoM.js +0 -1
- package/src/client/dist/spa/assets/purify.es-CxaFgMzS.js +0 -60
- package/src/client/dist/spa/assets/render-chat-markdown-B41YA3kF.js +0 -1
- package/src/client/dist/spa/assets/runtime-core.esm-bundler-DPcTPMmX.js +0 -1
- package/src/client/dist/spa/assets/use-checkbox-DzLCp4E3.js +0 -1
- package/src/client/dist/spa/assets/use-id-BQW6DfJU.js +0 -1
- package/src/client/dist/spa/assets/use-quasar-C5gKpYwL.js +0 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { getDb } from '../db/index.js';
|
|
3
|
+
import { worktreeHasChanges } from '../utils/git-ops.js';
|
|
4
|
+
import { slugifyProjectName } from '../utils/project-slug.js';
|
|
5
|
+
import { runScript, SCRIPT_TIMEOUT_MS } from '../utils/script-runner.js';
|
|
6
|
+
import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
|
|
7
|
+
import { getEffectiveSettings, getGlobalSettings, getProjectSettings, } from './settings-service.js';
|
|
8
|
+
function getRow(workspaceId) {
|
|
9
|
+
const db = getDb();
|
|
10
|
+
const row = db
|
|
11
|
+
.prepare(`SELECT id, name, project_path, working_branch, source_branch, worktree_path, archived_at
|
|
12
|
+
FROM workspaces WHERE id = ?`)
|
|
13
|
+
.get(workspaceId);
|
|
14
|
+
return row ?? null;
|
|
15
|
+
}
|
|
16
|
+
function countPendingTasks(workspaceId) {
|
|
17
|
+
const db = getDb();
|
|
18
|
+
const row = db
|
|
19
|
+
.prepare("SELECT COUNT(*) AS c FROM tasks WHERE workspace_id = ? AND status != 'done'")
|
|
20
|
+
.get(workspaceId);
|
|
21
|
+
return row.c;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Pure decision: should the cleanup script run? Encodes the full matrix so it
|
|
25
|
+
* is unit-testable without a DB.
|
|
26
|
+
*
|
|
27
|
+
* - No script → never.
|
|
28
|
+
* - Auto-loop completion → always (every task is done by definition).
|
|
29
|
+
* - Otherwise the agent must have finished cleanly (`completed`).
|
|
30
|
+
* - A mid-loop `session:ended` (`wasAutoLoop`) never triggers — only the loop's
|
|
31
|
+
* completion does.
|
|
32
|
+
* - `no-tasks` mode additionally requires zero pending tasks; `idle` does not.
|
|
33
|
+
*/
|
|
34
|
+
export function shouldRunCleanup(input) {
|
|
35
|
+
if (!input.script.trim())
|
|
36
|
+
return false;
|
|
37
|
+
if (input.autoLoopCompleted)
|
|
38
|
+
return true;
|
|
39
|
+
if (input.reason !== 'completed')
|
|
40
|
+
return false;
|
|
41
|
+
if (input.wasAutoLoop)
|
|
42
|
+
return false;
|
|
43
|
+
if (input.mode === 'no-tasks' && input.pendingTasks > 0)
|
|
44
|
+
return false;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
/** Execute the cleanup script in a worktree, streaming `cleanup:*` WS events. */
|
|
48
|
+
export function runCleanupScript(workspaceId, worktreePath, script, env) {
|
|
49
|
+
return runScript({
|
|
50
|
+
workspaceId,
|
|
51
|
+
worktreePath,
|
|
52
|
+
script,
|
|
53
|
+
eventPrefix: 'cleanup',
|
|
54
|
+
tmpFileName: '.cleanup-script.tmp',
|
|
55
|
+
env,
|
|
56
|
+
timeoutMs: SCRIPT_TIMEOUT_MS,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function trigger(workspaceId, decision) {
|
|
60
|
+
try {
|
|
61
|
+
const row = getRow(workspaceId);
|
|
62
|
+
if (!row || row.archived_at)
|
|
63
|
+
return;
|
|
64
|
+
const effective = getEffectiveSettings(row.project_path);
|
|
65
|
+
if (!shouldRunCleanup({
|
|
66
|
+
reason: decision.reason,
|
|
67
|
+
wasAutoLoop: decision.wasAutoLoop,
|
|
68
|
+
autoLoopCompleted: decision.autoLoopCompleted,
|
|
69
|
+
script: effective.cleanupScript,
|
|
70
|
+
mode: effective.cleanupScriptMode,
|
|
71
|
+
pendingTasks: countPendingTasks(workspaceId),
|
|
72
|
+
})) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const global = getGlobalSettings();
|
|
76
|
+
const projectSettings = getProjectSettings(row.project_path);
|
|
77
|
+
const projectSlug = global.worktreesPrefixByProject
|
|
78
|
+
? slugifyProjectName(projectSettings?.displayName ?? '', row.project_path)
|
|
79
|
+
: undefined;
|
|
80
|
+
const worktreePath = row.worktree_path ??
|
|
81
|
+
resolveWorkspaceWorktreePath(row.project_path, row.working_branch, global.worktreesPath, projectSlug);
|
|
82
|
+
if (!fs.existsSync(worktreePath)) {
|
|
83
|
+
console.warn(`[cleanup-script-service] worktree missing, skipping cleanup: ${worktreePath}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Optional gate: only run when the worktree has uncommitted changes.
|
|
87
|
+
if (effective.cleanupScriptOnlyOnChanges && !worktreeHasChanges(worktreePath)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Best-effort: never let a cleanup failure break the calling flow.
|
|
91
|
+
void runCleanupScript(workspaceId, worktreePath, effective.cleanupScript, {
|
|
92
|
+
workspaceName: row.name,
|
|
93
|
+
branchName: row.working_branch,
|
|
94
|
+
sourceBranch: row.source_branch,
|
|
95
|
+
projectPath: row.project_path,
|
|
96
|
+
}).catch((err) => {
|
|
97
|
+
console.error('[cleanup-script-service] runCleanupScript failed:', err);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
console.error('[cleanup-script-service] trigger failed:', err);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Hook for orchestrator's `session:ended` handler. `wasAutoLoop` MUST be
|
|
106
|
+
* captured before `auto-loop-service.onSessionEnded` runs, since `disable()`
|
|
107
|
+
* clears the `auto_loop` flag.
|
|
108
|
+
*/
|
|
109
|
+
export function onSessionEnded(workspaceId, reason, opts) {
|
|
110
|
+
trigger(workspaceId, { reason, wasAutoLoop: opts.wasAutoLoop, autoLoopCompleted: false });
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Hook for `auto-loop-service.disable()` with `reason='completed'` — the loop
|
|
114
|
+
* finished every task. Runs the cleanup regardless of trigger mode.
|
|
115
|
+
*/
|
|
116
|
+
export function onAutoLoopCompleted(workspaceId) {
|
|
117
|
+
trigger(workspaceId, { reason: 'completed', wasAutoLoop: false, autoLoopCompleted: true });
|
|
118
|
+
}
|
|
@@ -34,6 +34,10 @@ export const DEFAULT_GIT_CONVENTIONS = `# Git conventions
|
|
|
34
34
|
- Never skip hooks (--no-verify) unless the user explicitly asks
|
|
35
35
|
- Always inspect \`git status\` and \`git diff\` before staging
|
|
36
36
|
`;
|
|
37
|
+
const CLEANUP_SCRIPT_MODES = ['idle', 'no-tasks'];
|
|
38
|
+
export function isValidCleanupScriptMode(value) {
|
|
39
|
+
return typeof value === 'string' && CLEANUP_SCRIPT_MODES.includes(value);
|
|
40
|
+
}
|
|
37
41
|
export const DEFAULT_PR_PROMPT_TEMPLATE = `A pull request has been opened: {{pr_url}} (#{{pr_number}})
|
|
38
42
|
|
|
39
43
|
Context:
|
|
@@ -80,6 +84,39 @@ export const DEFAULT_WORKSPACE_TAGS = [
|
|
|
80
84
|
'notion',
|
|
81
85
|
'sentry',
|
|
82
86
|
];
|
|
87
|
+
/**
|
|
88
|
+
* Default git branch prefixes seeded on fresh install and on settings upgrade.
|
|
89
|
+
* Stored without the trailing `/` — it's a separator added at display time and
|
|
90
|
+
* when composing the working branch (`<prefix>/<slug>`). The first entry is the
|
|
91
|
+
* one pre-selected on the workspace creation page.
|
|
92
|
+
*/
|
|
93
|
+
export const DEFAULT_BRANCH_PREFIXES = ['feature', 'fix', 'hotfix', 'chore', 'refactor', 'docs', 'test'];
|
|
94
|
+
/**
|
|
95
|
+
* Sanitize a raw branch-prefix list: trim, strip surrounding slashes, drop
|
|
96
|
+
* entries that aren't valid git branch segments, dedupe, cap length. Returns
|
|
97
|
+
* an empty array when nothing survives — callers decide the fallback.
|
|
98
|
+
*/
|
|
99
|
+
export function sanitizeBranchPrefixes(input) {
|
|
100
|
+
if (!Array.isArray(input))
|
|
101
|
+
return [];
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
const result = [];
|
|
104
|
+
for (const raw of input) {
|
|
105
|
+
if (typeof raw !== 'string')
|
|
106
|
+
continue;
|
|
107
|
+
const value = raw.trim().replace(/^\/+|\/+$/g, '');
|
|
108
|
+
if (value.length === 0 || value.length > 50)
|
|
109
|
+
continue;
|
|
110
|
+
// Allow only characters safe in a git branch name; reject `..` runs.
|
|
111
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(value) || value.includes('..'))
|
|
112
|
+
continue;
|
|
113
|
+
if (seen.has(value))
|
|
114
|
+
continue;
|
|
115
|
+
seen.add(value);
|
|
116
|
+
result.push(value);
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
83
120
|
const settingsMigrations = [
|
|
84
121
|
{
|
|
85
122
|
version: 1,
|
|
@@ -427,6 +464,72 @@ const settingsMigrations = [
|
|
|
427
464
|
}
|
|
428
465
|
},
|
|
429
466
|
},
|
|
467
|
+
{
|
|
468
|
+
version: 26,
|
|
469
|
+
name: 'add-branch-prefixes',
|
|
470
|
+
migrate: ({ global }) => {
|
|
471
|
+
if (!Array.isArray(global.branchPrefixes)) {
|
|
472
|
+
global.branchPrefixes = [...DEFAULT_BRANCH_PREFIXES];
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
version: 27,
|
|
478
|
+
name: 'add-project-task-prompt',
|
|
479
|
+
migrate: ({ projects }) => {
|
|
480
|
+
for (const p of projects) {
|
|
481
|
+
if (typeof p.taskPromptTemplate !== 'string')
|
|
482
|
+
p.taskPromptTemplate = '';
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
version: 28,
|
|
488
|
+
name: 'add-cleanup-script',
|
|
489
|
+
migrate: ({ global, projects }) => {
|
|
490
|
+
if (typeof global.cleanupScript !== 'string')
|
|
491
|
+
global.cleanupScript = '';
|
|
492
|
+
if (!isValidCleanupScriptMode(global.cleanupScriptMode))
|
|
493
|
+
global.cleanupScriptMode = 'no-tasks';
|
|
494
|
+
for (const p of projects) {
|
|
495
|
+
if (typeof p.cleanupScript !== 'string')
|
|
496
|
+
p.cleanupScript = '';
|
|
497
|
+
// '' = inherit; any other value must be a valid mode.
|
|
498
|
+
if (p.cleanupScriptMode !== '' && !isValidCleanupScriptMode(p.cleanupScriptMode)) {
|
|
499
|
+
p.cleanupScriptMode = '';
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
version: 29,
|
|
506
|
+
name: 'add-archive-script',
|
|
507
|
+
migrate: ({ global, projects }) => {
|
|
508
|
+
if (typeof global.archiveScript !== 'string')
|
|
509
|
+
global.archiveScript = '';
|
|
510
|
+
for (const p of projects) {
|
|
511
|
+
if (typeof p.archiveScript !== 'string')
|
|
512
|
+
p.archiveScript = '';
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
version: 30,
|
|
518
|
+
name: 'add-global-setup-script',
|
|
519
|
+
migrate: ({ global }) => {
|
|
520
|
+
if (typeof global.setupScript !== 'string')
|
|
521
|
+
global.setupScript = '';
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
version: 31,
|
|
526
|
+
name: 'add-cleanup-script-only-on-changes',
|
|
527
|
+
migrate: ({ global }) => {
|
|
528
|
+
if (typeof global.cleanupScriptOnlyOnChanges !== 'boolean') {
|
|
529
|
+
global.cleanupScriptOnlyOnChanges = false;
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
},
|
|
430
533
|
];
|
|
431
534
|
/** Current settings schema version — always equals the highest migration version. */
|
|
432
535
|
export const SETTINGS_SCHEMA_VERSION = settingsMigrations.length > 0 ? settingsMigrations[settingsMigrations.length - 1].version : 0;
|
|
@@ -461,6 +564,11 @@ function defaultSettings() {
|
|
|
461
564
|
notionInitialPromptTemplate: DEFAULT_NOTION_INITIAL_PROMPT,
|
|
462
565
|
sentryInitialPromptTemplate: DEFAULT_SENTRY_INITIAL_PROMPT,
|
|
463
566
|
gitConventions: DEFAULT_GIT_CONVENTIONS,
|
|
567
|
+
setupScript: '',
|
|
568
|
+
cleanupScript: '',
|
|
569
|
+
cleanupScriptMode: 'no-tasks',
|
|
570
|
+
cleanupScriptOnlyOnChanges: false,
|
|
571
|
+
archiveScript: '',
|
|
464
572
|
editorCommand: '',
|
|
465
573
|
browserNotifications: true,
|
|
466
574
|
audioNotifications: true,
|
|
@@ -474,6 +582,7 @@ function defaultSettings() {
|
|
|
474
582
|
notionMcpKey: '',
|
|
475
583
|
sentryMcpKey: '',
|
|
476
584
|
tags: [...DEFAULT_WORKSPACE_TAGS],
|
|
585
|
+
branchPrefixes: [...DEFAULT_BRANCH_PREFIXES],
|
|
477
586
|
worktreesPath: WORKTREES_PATH,
|
|
478
587
|
worktreesPrefixByProject: false,
|
|
479
588
|
voiceEnabled: false,
|
|
@@ -510,6 +619,10 @@ function defaultProjectSettings(projectPath) {
|
|
|
510
619
|
sentryInitialPromptTemplate: '',
|
|
511
620
|
gitConventions: '',
|
|
512
621
|
setupScript: '',
|
|
622
|
+
taskPromptTemplate: '',
|
|
623
|
+
cleanupScript: '',
|
|
624
|
+
cleanupScriptMode: '',
|
|
625
|
+
archiveScript: '',
|
|
513
626
|
devServer: {
|
|
514
627
|
startCommand: '',
|
|
515
628
|
stopCommand: '',
|
|
@@ -586,6 +699,35 @@ function readSettings() {
|
|
|
586
699
|
}
|
|
587
700
|
return migrated;
|
|
588
701
|
}
|
|
702
|
+
/** Number of `settings.json.backup-*` files kept; older ones are rotated out. */
|
|
703
|
+
const SETTINGS_BACKUP_KEEP = 5;
|
|
704
|
+
/**
|
|
705
|
+
* Keep only the `SETTINGS_BACKUP_KEEP` most recent `settings.json.backup-*`
|
|
706
|
+
* files in `dir`, deleting the rest. Best-effort — never throws.
|
|
707
|
+
*/
|
|
708
|
+
function pruneSettingsBackups(dir) {
|
|
709
|
+
try {
|
|
710
|
+
const backups = fs
|
|
711
|
+
.readdirSync(dir)
|
|
712
|
+
.filter((f) => f.startsWith('settings.json.backup-'))
|
|
713
|
+
.map((f) => {
|
|
714
|
+
const full = path.join(dir, f);
|
|
715
|
+
return { path: full, mtimeMs: fs.statSync(full).mtimeMs };
|
|
716
|
+
})
|
|
717
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
718
|
+
for (const entry of backups.slice(SETTINGS_BACKUP_KEEP)) {
|
|
719
|
+
try {
|
|
720
|
+
fs.unlinkSync(entry.path);
|
|
721
|
+
}
|
|
722
|
+
catch (err) {
|
|
723
|
+
console.error(`[settings] Failed to prune backup ${entry.path}:`, err);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
catch (err) {
|
|
728
|
+
console.error('[settings] Failed to prune settings backups:', err);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
589
731
|
function createSettingsBackupIfPresent() {
|
|
590
732
|
if (!fs.existsSync(settingsFilePath))
|
|
591
733
|
return;
|
|
@@ -594,6 +736,7 @@ function createSettingsBackupIfPresent() {
|
|
|
594
736
|
settingsBackupSequence += 1;
|
|
595
737
|
const backupPath = path.join(dir, `settings.json.backup-${stamp}-${settingsBackupSequence}`);
|
|
596
738
|
fs.copyFileSync(settingsFilePath, backupPath);
|
|
739
|
+
pruneSettingsBackups(dir);
|
|
597
740
|
}
|
|
598
741
|
function writeSettings(settings, options) {
|
|
599
742
|
ensureSettingsPathInitialized();
|
|
@@ -689,7 +832,11 @@ export function getEffectiveSettings(projectPath) {
|
|
|
689
832
|
gitConventions: settings.global.gitConventions,
|
|
690
833
|
sourceBranch: '',
|
|
691
834
|
devServer: null,
|
|
692
|
-
setupScript:
|
|
835
|
+
setupScript: settings.global.setupScript,
|
|
836
|
+
cleanupScript: settings.global.cleanupScript,
|
|
837
|
+
cleanupScriptMode: settings.global.cleanupScriptMode,
|
|
838
|
+
cleanupScriptOnlyOnChanges: settings.global.cleanupScriptOnlyOnChanges,
|
|
839
|
+
archiveScript: settings.global.archiveScript,
|
|
693
840
|
notionStatusProperty: settings.global.notionStatusProperty,
|
|
694
841
|
notionInProgressStatus: settings.global.notionInProgressStatus,
|
|
695
842
|
};
|
|
@@ -710,7 +857,11 @@ export function getEffectiveSettings(projectPath) {
|
|
|
710
857
|
gitConventions: project.gitConventions || settings.global.gitConventions,
|
|
711
858
|
sourceBranch: project.defaultSourceBranch,
|
|
712
859
|
devServer: project.devServer,
|
|
713
|
-
setupScript: project.setupScript ||
|
|
860
|
+
setupScript: project.setupScript || settings.global.setupScript,
|
|
861
|
+
cleanupScript: project.cleanupScript || settings.global.cleanupScript,
|
|
862
|
+
cleanupScriptMode: (project.cleanupScriptMode || settings.global.cleanupScriptMode),
|
|
863
|
+
cleanupScriptOnlyOnChanges: settings.global.cleanupScriptOnlyOnChanges,
|
|
864
|
+
archiveScript: project.archiveScript || settings.global.archiveScript,
|
|
714
865
|
notionStatusProperty: settings.global.notionStatusProperty,
|
|
715
866
|
notionInProgressStatus: settings.global.notionInProgressStatus,
|
|
716
867
|
};
|
|
@@ -726,6 +877,13 @@ export function updateGlobalSettings(data) {
|
|
|
726
877
|
delete data.skillSuite;
|
|
727
878
|
}
|
|
728
879
|
}
|
|
880
|
+
// Validate cleanupScriptMode: drop invalid values so the previous one stays.
|
|
881
|
+
if ('cleanupScriptMode' in data) {
|
|
882
|
+
if (!isValidCleanupScriptMode(data.cleanupScriptMode)) {
|
|
883
|
+
console.warn(`[settings] Invalid cleanupScriptMode rejected: ${data.cleanupScriptMode}`);
|
|
884
|
+
delete data.cleanupScriptMode;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
729
887
|
const allowedGlobalKeys = [
|
|
730
888
|
'defaultModelByEngine',
|
|
731
889
|
'dangerouslySkipPermissions',
|
|
@@ -734,6 +892,11 @@ export function updateGlobalSettings(data) {
|
|
|
734
892
|
'notionInitialPromptTemplate',
|
|
735
893
|
'sentryInitialPromptTemplate',
|
|
736
894
|
'gitConventions',
|
|
895
|
+
'setupScript',
|
|
896
|
+
'cleanupScript',
|
|
897
|
+
'cleanupScriptMode',
|
|
898
|
+
'cleanupScriptOnlyOnChanges',
|
|
899
|
+
'archiveScript',
|
|
737
900
|
'editorCommand',
|
|
738
901
|
'browserNotifications',
|
|
739
902
|
'audioNotifications',
|
|
@@ -747,6 +910,7 @@ export function updateGlobalSettings(data) {
|
|
|
747
910
|
'notionMcpKey',
|
|
748
911
|
'sentryMcpKey',
|
|
749
912
|
'tags',
|
|
913
|
+
'branchPrefixes',
|
|
750
914
|
'worktreesPath',
|
|
751
915
|
'worktreesPrefixByProject',
|
|
752
916
|
'voiceEnabled',
|
|
@@ -775,6 +939,12 @@ export function updateGlobalSettings(data) {
|
|
|
775
939
|
.filter((t) => t.length > 0 && t.length <= 50)))
|
|
776
940
|
: settings.global.tags;
|
|
777
941
|
}
|
|
942
|
+
if (filtered.branchPrefixes !== undefined) {
|
|
943
|
+
// Drop invalid entries; never let the list collapse to empty (the creation
|
|
944
|
+
// page needs at least one prefix) — fall back to the previous value.
|
|
945
|
+
const sanitized = sanitizeBranchPrefixes(filtered.branchPrefixes);
|
|
946
|
+
filtered.branchPrefixes = sanitized.length > 0 ? sanitized : settings.global.branchPrefixes;
|
|
947
|
+
}
|
|
778
948
|
if (filtered.audioNotificationVolume !== undefined) {
|
|
779
949
|
const v = Number(filtered.audioNotificationVolume);
|
|
780
950
|
filtered.audioNotificationVolume = Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 1;
|
|
@@ -816,6 +986,14 @@ export function upsertProject(projectPath, data) {
|
|
|
816
986
|
delete data.color;
|
|
817
987
|
}
|
|
818
988
|
}
|
|
989
|
+
// Validate cleanupScriptMode: '' means inherit; any other value must be a
|
|
990
|
+
// valid mode. Drop anything else so the stored value is preserved.
|
|
991
|
+
if ('cleanupScriptMode' in data) {
|
|
992
|
+
if (data.cleanupScriptMode !== '' && !isValidCleanupScriptMode(data.cleanupScriptMode)) {
|
|
993
|
+
console.warn(`[settings] Invalid cleanupScriptMode rejected for project '${projectPath}': ${data.cleanupScriptMode}`);
|
|
994
|
+
delete data.cleanupScriptMode;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
819
997
|
const allowedProjectKeys = [
|
|
820
998
|
'displayName',
|
|
821
999
|
'defaultSourceBranch',
|
|
@@ -827,6 +1005,10 @@ export function upsertProject(projectPath, data) {
|
|
|
827
1005
|
'sentryInitialPromptTemplate',
|
|
828
1006
|
'gitConventions',
|
|
829
1007
|
'setupScript',
|
|
1008
|
+
'taskPromptTemplate',
|
|
1009
|
+
'cleanupScript',
|
|
1010
|
+
'cleanupScriptMode',
|
|
1011
|
+
'archiveScript',
|
|
830
1012
|
'devServer',
|
|
831
1013
|
'e2e',
|
|
832
1014
|
'finalization',
|
|
@@ -1,85 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import * as wsService from './websocket-service.js';
|
|
5
|
-
const SETUP_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
1
|
+
import { runScript, SCRIPT_TIMEOUT_MS } from '../utils/script-runner.js';
|
|
2
|
+
const SETUP_TIMEOUT_MS = SCRIPT_TIMEOUT_MS; // 5 minutes
|
|
6
3
|
/** Execute a setup script in a worktree, streaming output via WebSocket. Resolves with the exit code. */
|
|
7
4
|
export function runSetupScript(workspaceId, worktreePath, script, env, timeoutMs = SETUP_TIMEOUT_MS) {
|
|
8
|
-
return
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
WORKSPACE_ID: workspaceId,
|
|
17
|
-
WORKSPACE_NAME: env?.workspaceName ?? '',
|
|
18
|
-
BRANCH_NAME: env?.branchName ?? '',
|
|
19
|
-
SOURCE_BRANCH: env?.sourceBranch ?? '',
|
|
20
|
-
PROJECT_PATH: env?.projectPath ?? '',
|
|
21
|
-
},
|
|
22
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
23
|
-
});
|
|
24
|
-
const timeout = setTimeout(() => {
|
|
25
|
-
proc.kill('SIGKILL');
|
|
26
|
-
// Destroy pipes so the 'close' event fires immediately even if
|
|
27
|
-
// child processes (e.g. sleep) inherited the file descriptors.
|
|
28
|
-
proc.stdout?.destroy();
|
|
29
|
-
proc.stderr?.destroy();
|
|
30
|
-
wsService.emit(workspaceId, 'setup:output', {
|
|
31
|
-
text: '[kobo] Setup script timed out after 5 minutes',
|
|
32
|
-
});
|
|
33
|
-
}, timeoutMs);
|
|
34
|
-
const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[a-zA-Z]`, 'g');
|
|
35
|
-
const stripAnsi = (s) => s.replace(ansiPattern, '');
|
|
36
|
-
const emitLine = (text) => {
|
|
37
|
-
const trimmed = stripAnsi(text).trim();
|
|
38
|
-
if (trimmed) {
|
|
39
|
-
wsService.emit(workspaceId, 'setup:output', { text: trimmed });
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
proc.stdout.on('data', (data) => {
|
|
43
|
-
for (const line of data.toString().split('\n')) {
|
|
44
|
-
emitLine(line);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
proc.stderr.on('data', (data) => {
|
|
48
|
-
for (const line of data.toString().split('\n')) {
|
|
49
|
-
emitLine(line);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
let settled = false;
|
|
53
|
-
const finish = (exitCode) => {
|
|
54
|
-
if (settled)
|
|
55
|
-
return;
|
|
56
|
-
settled = true;
|
|
57
|
-
clearTimeout(timeout);
|
|
58
|
-
try {
|
|
59
|
-
fs.unlinkSync(scriptPath);
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
/* best-effort */
|
|
63
|
-
}
|
|
64
|
-
if (exitCode === 0) {
|
|
65
|
-
wsService.emitEphemeral(workspaceId, 'setup:complete', {});
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
wsService.emitEphemeral(workspaceId, 'setup:error', {
|
|
69
|
-
exitCode,
|
|
70
|
-
message: `Setup script exited with code ${exitCode}`,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
resolve({ exitCode });
|
|
74
|
-
};
|
|
75
|
-
proc.on('error', (err) => {
|
|
76
|
-
wsService.emit(workspaceId, 'setup:output', {
|
|
77
|
-
text: `[kobo] Setup script failed to start: ${err.message}`,
|
|
78
|
-
});
|
|
79
|
-
finish(1);
|
|
80
|
-
});
|
|
81
|
-
proc.on('close', (code) => {
|
|
82
|
-
finish(code ?? 1);
|
|
83
|
-
});
|
|
5
|
+
return runScript({
|
|
6
|
+
workspaceId,
|
|
7
|
+
worktreePath,
|
|
8
|
+
script,
|
|
9
|
+
eventPrefix: 'setup',
|
|
10
|
+
tmpFileName: '.setup-script.tmp',
|
|
11
|
+
env,
|
|
12
|
+
timeoutMs,
|
|
84
13
|
});
|
|
85
14
|
}
|
|
@@ -538,6 +538,36 @@ export function moveWorktree(projectPath, oldPath, newPath) {
|
|
|
538
538
|
git(projectPath, ['worktree', 'move', oldPath, newPath]);
|
|
539
539
|
}
|
|
540
540
|
/** List files changed between base and HEAD (committed), plus working tree changes. */
|
|
541
|
+
/**
|
|
542
|
+
* List the worktree's files — tracked plus untracked-but-not-git-ignored.
|
|
543
|
+
* Excludes `.git`, `node_modules`, and anything covered by `.gitignore`.
|
|
544
|
+
* Capped at `limit` entries to stay responsive on large monorepos.
|
|
545
|
+
* Returns [] on error (e.g. not a git repo).
|
|
546
|
+
*/
|
|
547
|
+
export function listWorktreeFiles(worktreePath, limit = 5000) {
|
|
548
|
+
try {
|
|
549
|
+
const out = git(worktreePath, ['ls-files', '--cached', '--others', '--exclude-standard']);
|
|
550
|
+
if (!out)
|
|
551
|
+
return [];
|
|
552
|
+
const files = out.split('\n').filter((line) => line.length > 0);
|
|
553
|
+
return files.length > limit ? files.slice(0, limit) : files;
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
return [];
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* True when the worktree has any uncommitted change — modified, added, deleted
|
|
561
|
+
* or untracked files. Returns false on error (e.g. not a git repo).
|
|
562
|
+
*/
|
|
563
|
+
export function worktreeHasChanges(worktreePath) {
|
|
564
|
+
try {
|
|
565
|
+
return git(worktreePath, ['status', '--porcelain']).length > 0;
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
541
571
|
export function getChangedFiles(repoPath, base, includeUntracked = false) {
|
|
542
572
|
const ref = resolveBase(repoPath, base);
|
|
543
573
|
const files = [];
|
|
@@ -132,6 +132,10 @@ export function getPackageVersion() {
|
|
|
132
132
|
_cachedVersion = pkg.version;
|
|
133
133
|
return _cachedVersion;
|
|
134
134
|
}
|
|
135
|
+
/** Absolute path to the root CHANGELOG.md (read by the "What's new" dialog). */
|
|
136
|
+
export function getChangelogPath() {
|
|
137
|
+
return getPackageAssetPath('CHANGELOG.md');
|
|
138
|
+
}
|
|
135
139
|
/**
|
|
136
140
|
* Absolute path to the built Quasar SPA (src/client/dist/spa). Returns null
|
|
137
141
|
* if the SPA has not been built yet.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import * as wsService from '../services/websocket-service.js';
|
|
5
|
+
/** Default wall-clock budget for a user script before it is force-killed. */
|
|
6
|
+
export const SCRIPT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
7
|
+
/**
|
|
8
|
+
* Execute a user-provided bash script inside a worktree, streaming stdout/stderr
|
|
9
|
+
* line-by-line over WebSocket. Resolves with the exit code — never rejects.
|
|
10
|
+
* Shared mechanism behind the setup and cleanup script services.
|
|
11
|
+
*/
|
|
12
|
+
export function runScript(opts) {
|
|
13
|
+
const { workspaceId, worktreePath, script, eventPrefix, tmpFileName, env } = opts;
|
|
14
|
+
const timeoutMs = opts.timeoutMs ?? SCRIPT_TIMEOUT_MS;
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const scriptPath = path.join(worktreePath, '.ai', tmpFileName);
|
|
17
|
+
fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
|
|
18
|
+
fs.writeFileSync(scriptPath, script, { mode: 0o755 });
|
|
19
|
+
const proc = spawn('bash', [scriptPath], {
|
|
20
|
+
cwd: worktreePath,
|
|
21
|
+
env: {
|
|
22
|
+
...process.env,
|
|
23
|
+
WORKSPACE_ID: workspaceId,
|
|
24
|
+
WORKSPACE_NAME: env?.workspaceName ?? '',
|
|
25
|
+
BRANCH_NAME: env?.branchName ?? '',
|
|
26
|
+
SOURCE_BRANCH: env?.sourceBranch ?? '',
|
|
27
|
+
PROJECT_PATH: env?.projectPath ?? '',
|
|
28
|
+
},
|
|
29
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
30
|
+
});
|
|
31
|
+
const timeout = setTimeout(() => {
|
|
32
|
+
proc.kill('SIGKILL');
|
|
33
|
+
// Destroy pipes so the 'close' event fires immediately even if
|
|
34
|
+
// child processes (e.g. sleep) inherited the file descriptors.
|
|
35
|
+
proc.stdout?.destroy();
|
|
36
|
+
proc.stderr?.destroy();
|
|
37
|
+
wsService.emit(workspaceId, `${eventPrefix}:output`, {
|
|
38
|
+
text: `[kobo] Script timed out after ${Math.round(timeoutMs / 60000)} minutes`,
|
|
39
|
+
});
|
|
40
|
+
}, timeoutMs);
|
|
41
|
+
const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[a-zA-Z]`, 'g');
|
|
42
|
+
const stripAnsi = (s) => s.replace(ansiPattern, '');
|
|
43
|
+
// Track whether the script printed anything — lets the UI show a terse
|
|
44
|
+
// "Done" instead of a near-empty card when a script runs silently.
|
|
45
|
+
let outputEmitted = false;
|
|
46
|
+
const emitLine = (text) => {
|
|
47
|
+
const trimmed = stripAnsi(text).trim();
|
|
48
|
+
if (trimmed) {
|
|
49
|
+
outputEmitted = true;
|
|
50
|
+
wsService.emit(workspaceId, `${eventPrefix}:output`, { text: trimmed });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
proc.stdout.on('data', (data) => {
|
|
54
|
+
for (const line of data.toString().split('\n')) {
|
|
55
|
+
emitLine(line);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
proc.stderr.on('data', (data) => {
|
|
59
|
+
for (const line of data.toString().split('\n')) {
|
|
60
|
+
emitLine(line);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
let settled = false;
|
|
64
|
+
const finish = (exitCode) => {
|
|
65
|
+
if (settled)
|
|
66
|
+
return;
|
|
67
|
+
settled = true;
|
|
68
|
+
clearTimeout(timeout);
|
|
69
|
+
try {
|
|
70
|
+
fs.unlinkSync(scriptPath);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* best-effort */
|
|
74
|
+
}
|
|
75
|
+
if (exitCode === 0) {
|
|
76
|
+
wsService.emitEphemeral(workspaceId, `${eventPrefix}:complete`, { hadOutput: outputEmitted });
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
wsService.emitEphemeral(workspaceId, `${eventPrefix}:error`, {
|
|
80
|
+
exitCode,
|
|
81
|
+
message: `Script exited with code ${exitCode}`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
resolve({ exitCode });
|
|
85
|
+
};
|
|
86
|
+
proc.on('error', (err) => {
|
|
87
|
+
wsService.emit(workspaceId, `${eventPrefix}:output`, {
|
|
88
|
+
text: `[kobo] Script failed to start: ${err.message}`,
|
|
89
|
+
});
|
|
90
|
+
finish(1);
|
|
91
|
+
});
|
|
92
|
+
proc.on('close', (code) => {
|
|
93
|
+
finish(code ?? 1);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loicngr/kobo",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.15",
|
|
4
4
|
"description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"src/mcp-server/",
|
|
14
14
|
"AGENTS.md",
|
|
15
15
|
"README.md",
|
|
16
|
+
"CHANGELOG.md",
|
|
16
17
|
"LICENSE"
|
|
17
18
|
],
|
|
18
19
|
"publishConfig": {
|