@assistkick/create 1.6.0 → 1.8.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/package.json +2 -2
- package/templates/assistkick-product-system/.env.example +1 -0
- package/templates/assistkick-product-system/local.db +0 -0
- package/templates/assistkick-product-system/package.json +4 -2
- package/templates/assistkick-product-system/packages/backend/package.json +2 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +165 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +358 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +356 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +96 -1
- package/templates/assistkick-product-system/packages/backend/src/routes/graph.ts +1 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +43 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +200 -84
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +6 -3
- package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +53 -17
- package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +218 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +119 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +154 -0
- package/templates/assistkick-product-system/packages/backend/src/server.ts +81 -9
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +489 -0
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +416 -0
- package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.test.ts +189 -0
- package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.ts +182 -0
- package/templates/assistkick-product-system/packages/backend/src/services/init.ts +28 -78
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +16 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +73 -2
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +4 -4
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +87 -11
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +210 -69
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +210 -215
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +162 -0
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +148 -0
- package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +11 -5
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.test.ts +64 -0
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +134 -0
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.test.ts +256 -0
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +258 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_group_service.ts +106 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.test.ts +275 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.ts +222 -0
- package/templates/assistkick-product-system/packages/frontend/index.html +3 -0
- package/templates/assistkick-product-system/packages/frontend/package-lock.json +800 -11
- package/templates/assistkick-product-system/packages/frontend/package.json +11 -1
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +24 -7
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +456 -16
- package/templates/assistkick-product-system/packages/frontend/src/api/client_files.test.ts +172 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client_video.test.ts +238 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +307 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/CoherenceView.tsx +82 -66
- package/templates/assistkick-product-system/packages/frontend/src/components/CompositionPlaceholder.tsx +97 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +383 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +57 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +313 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeContextMenu.tsx +61 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +73 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +404 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +193 -64
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +71 -73
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphSettings.tsx +8 -8
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphView.tsx +1 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/InviteUserDialog.tsx +15 -11
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +226 -291
- package/templates/assistkick-product-system/packages/frontend/src/components/LoginPage.tsx +14 -14
- package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +54 -33
- package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +40 -66
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +55 -115
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +155 -77
- package/templates/assistkick-product-system/packages/frontend/src/components/UsersView.tsx +52 -52
- package/templates/assistkick-product-system/packages/frontend/src/components/VideoGallery.tsx +313 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/VideographyView.tsx +250 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/WorkflowsView.tsx +474 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/AccentBorderList.tsx +53 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/Button.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonGroup.tsx +29 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonShowcase.tsx +221 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/CardGlass.tsx +141 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/CompletionRing.tsx +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ContentCard.tsx +34 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/IconButton.tsx +74 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +270 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +37 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/Kbd.tsx +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KindBadge.tsx +21 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +207 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/SidePanelShowcase.tsx +370 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/SideSheet.tsx +64 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/StatusDot.tsx +18 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCardPositionNode.tsx +36 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCycleCountNode.tsx +60 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/EndNode.tsx +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +189 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +123 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RunAgentNode.tsx +51 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/SetCardMetadataNode.tsx +53 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/StartNode.tsx +18 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/TransitionCardNode.tsx +59 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +335 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +634 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/autoLayout.ts +103 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/edgeColors.ts +35 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +208 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.test.ts +119 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +107 -0
- package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +13 -11
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useAutoSave.ts +75 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useGraph.ts +6 -21
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +15 -80
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useToast.tsx +16 -3
- package/templates/assistkick-product-system/packages/frontend/src/pages/accept_invitation_page.tsx +30 -27
- package/templates/assistkick-product-system/packages/frontend/src/pages/forgot_password_page.tsx +18 -15
- package/templates/assistkick-product-system/packages/frontend/src/pages/register_page.tsx +21 -18
- package/templates/assistkick-product-system/packages/frontend/src/pages/reset_password_page.tsx +28 -25
- package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +19 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +54 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +93 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +9 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +13 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useGitModalStore.ts +14 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphStore.ts +36 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphUIStore.ts +25 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +90 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useQaSheetStore.ts +27 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +76 -0
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +336 -3632
- package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts +167 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.ts +101 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts +17 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.test.ts +145 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.ts +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.test.ts +4 -10
- package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.ts +19 -1
- package/templates/assistkick-product-system/packages/frontend/vite.config.ts +7 -1
- package/templates/assistkick-product-system/packages/shared/db/local.db +0 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0004_tidy_matthew_murdock.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0005_mysterious_falcon.sql +692 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0006_next_venom.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0007_deep_barracuda.sql +39 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0008_puzzling_hannibal_king.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0009_amused_beast.sql +8 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0010_spotty_moira_mactaggert.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0011_goofy_snowbird.sql +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0011_supreme_doctor_octopus.sql +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0013_reflective_prowler.sql +15 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0004_snapshot.json +921 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0005_snapshot.json +1042 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0006_snapshot.json +1101 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0007_snapshot.json +1336 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0008_snapshot.json +1275 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0009_snapshot.json +1327 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0010_snapshot.json +1393 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0011_snapshot.json +1436 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0013_snapshot.json +1538 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +113 -0
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +32 -7
- package/templates/assistkick-product-system/packages/shared/lib/constants.ts +9 -0
- package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +12 -4
- package/templates/assistkick-product-system/packages/shared/lib/graph.ts +16 -5
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +1753 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1281 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +211 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +43 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +13 -2
- package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +1 -1
- package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.test.ts +226 -0
- package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.ts +251 -0
- package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
- package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.test.ts +10 -0
- package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.ts +6 -0
- package/templates/assistkick-product-system/packages/video/Root.tsx +85 -0
- package/templates/assistkick-product-system/packages/video/components/email_scene.tsx +231 -0
- package/templates/assistkick-product-system/packages/video/components/outro_scene.tsx +153 -0
- package/templates/assistkick-product-system/packages/video/components/part_divider.tsx +90 -0
- package/templates/assistkick-product-system/packages/video/components/scene.tsx +226 -0
- package/templates/assistkick-product-system/packages/video/components/theme.ts +22 -0
- package/templates/assistkick-product-system/packages/video/components/title_scene.tsx +169 -0
- package/templates/assistkick-product-system/packages/video/components/video_split_layout.tsx +84 -0
- package/templates/assistkick-product-system/packages/video/compositions/.gitkeep +0 -0
- package/templates/assistkick-product-system/packages/video/index.ts +4 -0
- package/templates/assistkick-product-system/packages/video/package.json +28 -0
- package/templates/assistkick-product-system/packages/video/remotion.config.ts +11 -0
- package/templates/assistkick-product-system/packages/video/scripts/process_script.test.ts +326 -0
- package/templates/assistkick-product-system/packages/video/scripts/process_script.ts +630 -0
- package/templates/assistkick-product-system/packages/video/style.css +1 -0
- package/templates/assistkick-product-system/packages/video/tsconfig.json +18 -0
- package/templates/assistkick-product-system/tests/graph_legend.test.ts +2 -1
- package/templates/assistkick-product-system/tests/video_render_service.test.ts +179 -0
- package/templates/assistkick-product-system/tests/web_terminal.test.ts +219 -455
- package/templates/assistkick-product-system/tests/workflow_integration.test.ts +341 -0
- package/templates/skills/assistkick-bootstrap/SKILL.md +3 -3
- package/templates/skills/assistkick-code-reviewer/SKILL.md +2 -2
- package/templates/skills/assistkick-debugger/SKILL.md +2 -2
- package/templates/skills/assistkick-developer/SKILL.md +6 -3
- package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
- package/templates/skills/assistkick-interview/SKILL.md +2 -2
- package/templates/skills/product-system/graph.json +1890 -0
- package/templates/skills/product-system/kanban.json +304 -0
- package/templates/skills/product-system/nodes/comp_001.md +56 -0
- package/templates/skills/product-system/nodes/comp_002.md +57 -0
- package/templates/skills/product-system/nodes/data_001.md +51 -0
- package/templates/skills/product-system/nodes/data_002.md +40 -0
- package/templates/skills/product-system/nodes/data_004.md +38 -0
- package/templates/skills/product-system/nodes/dec_001.md +34 -0
- package/templates/skills/product-system/nodes/dec_016.md +32 -0
- package/templates/skills/product-system/nodes/feat_008.md +30 -0
- package/templates/skills/video-composition-agent/SKILL.md +232 -0
- package/templates/skills/video-script-writer/SKILL.md +136 -0
package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { AutoSaveService } from './auto_save_service.ts';
|
|
4
|
+
|
|
5
|
+
describe('AutoSaveService', () => {
|
|
6
|
+
let service: AutoSaveService;
|
|
7
|
+
let saveFn: ReturnType<typeof mock.fn<(path: string, content: string) => Promise<void>>>;
|
|
8
|
+
let dirtyChangeFn: ReturnType<typeof mock.fn<(path: string, dirty: boolean) => void>>;
|
|
9
|
+
let errorFn: ReturnType<typeof mock.fn<(path: string, error: string) => void>>;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
saveFn = mock.fn(async () => {});
|
|
13
|
+
dirtyChangeFn = mock.fn(() => {});
|
|
14
|
+
errorFn = mock.fn(() => {});
|
|
15
|
+
service = new AutoSaveService({
|
|
16
|
+
save: saveFn,
|
|
17
|
+
onDirtyChange: dirtyChangeFn,
|
|
18
|
+
onSaveError: errorFn,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
service.dispose();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('marks path as dirty on change', () => {
|
|
27
|
+
service.handleChange('file.ts', 'content');
|
|
28
|
+
assert.equal(service.isDirty('file.ts'), true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('calls onDirtyChange(path, true) on change', () => {
|
|
32
|
+
service.handleChange('file.ts', 'content');
|
|
33
|
+
assert.equal(dirtyChangeFn.mock.calls.length, 1);
|
|
34
|
+
assert.deepEqual(dirtyChangeFn.mock.calls[0].arguments, ['file.ts', true]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('does not save immediately on change (debounced)', () => {
|
|
38
|
+
service.handleChange('file.ts', 'content');
|
|
39
|
+
assert.equal(saveFn.mock.calls.length, 0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('saveNow cancels pending debounce timer', async () => {
|
|
43
|
+
service.handleChange('file.ts', 'content');
|
|
44
|
+
assert.equal(saveFn.mock.calls.length, 0);
|
|
45
|
+
|
|
46
|
+
// saveNow should cancel the debounce and save immediately
|
|
47
|
+
service.saveNow('file.ts');
|
|
48
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
49
|
+
|
|
50
|
+
assert.equal(saveFn.mock.calls.length, 1);
|
|
51
|
+
assert.deepEqual(saveFn.mock.calls[0].arguments, ['file.ts', 'content']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('saveNow uses latest content when called after multiple changes', async () => {
|
|
55
|
+
service.handleChange('file.ts', 'v1');
|
|
56
|
+
service.handleChange('file.ts', 'v2');
|
|
57
|
+
service.handleChange('file.ts', 'v3');
|
|
58
|
+
|
|
59
|
+
service.saveNow('file.ts');
|
|
60
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
61
|
+
|
|
62
|
+
assert.equal(saveFn.mock.calls.length, 1);
|
|
63
|
+
assert.deepEqual(saveFn.mock.calls[0].arguments, ['file.ts', 'v3']);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('saveNow triggers immediate save', async () => {
|
|
67
|
+
service.handleChange('file.ts', 'content');
|
|
68
|
+
assert.equal(saveFn.mock.calls.length, 0);
|
|
69
|
+
|
|
70
|
+
service.saveNow('file.ts');
|
|
71
|
+
// Allow promise microtask to run
|
|
72
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
73
|
+
|
|
74
|
+
assert.equal(saveFn.mock.calls.length, 1);
|
|
75
|
+
assert.deepEqual(saveFn.mock.calls[0].arguments, ['file.ts', 'content']);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('saveNow does nothing when path has no pending changes', async () => {
|
|
79
|
+
service.saveNow('file.ts');
|
|
80
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
81
|
+
assert.equal(saveFn.mock.calls.length, 0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('clears dirty state after successful save', async () => {
|
|
85
|
+
service.handleChange('file.ts', 'content');
|
|
86
|
+
assert.equal(service.isDirty('file.ts'), true);
|
|
87
|
+
|
|
88
|
+
service.saveNow('file.ts');
|
|
89
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
90
|
+
|
|
91
|
+
assert.equal(service.isDirty('file.ts'), false);
|
|
92
|
+
// Should have called onDirtyChange(path, false)
|
|
93
|
+
const falseCalls = dirtyChangeFn.mock.calls.filter(
|
|
94
|
+
(c) => c.arguments[0] === 'file.ts' && c.arguments[1] === false,
|
|
95
|
+
);
|
|
96
|
+
assert.equal(falseCalls.length, 1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('calls onSaveError on save failure and keeps dirty state', async () => {
|
|
100
|
+
saveFn = mock.fn(async () => { throw new Error('Network error'); });
|
|
101
|
+
service = new AutoSaveService({
|
|
102
|
+
save: saveFn,
|
|
103
|
+
onDirtyChange: dirtyChangeFn,
|
|
104
|
+
onSaveError: errorFn,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
service.handleChange('file.ts', 'content');
|
|
108
|
+
service.saveNow('file.ts');
|
|
109
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
110
|
+
|
|
111
|
+
assert.equal(errorFn.mock.calls.length, 1);
|
|
112
|
+
assert.equal(errorFn.mock.calls[0].arguments[0], 'file.ts');
|
|
113
|
+
assert.equal(errorFn.mock.calls[0].arguments[1], 'Network error');
|
|
114
|
+
// Dirty state should remain
|
|
115
|
+
assert.equal(service.isDirty('file.ts'), true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('tracks multiple files independently', () => {
|
|
119
|
+
service.handleChange('a.ts', 'content-a');
|
|
120
|
+
service.handleChange('b.ts', 'content-b');
|
|
121
|
+
assert.equal(service.isDirty('a.ts'), true);
|
|
122
|
+
assert.equal(service.isDirty('b.ts'), true);
|
|
123
|
+
assert.equal(service.isDirty('c.ts'), false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('saveAndDiscard saves and removes tracking for the path', async () => {
|
|
127
|
+
service.handleChange('file.ts', 'content');
|
|
128
|
+
assert.equal(service.isDirty('file.ts'), true);
|
|
129
|
+
|
|
130
|
+
service.saveAndDiscard('file.ts');
|
|
131
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
132
|
+
|
|
133
|
+
assert.equal(saveFn.mock.calls.length, 1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('dispose clears all timers and state', () => {
|
|
137
|
+
service.handleChange('a.ts', 'content-a');
|
|
138
|
+
service.handleChange('b.ts', 'content-b');
|
|
139
|
+
service.dispose();
|
|
140
|
+
|
|
141
|
+
assert.equal(service.isDirty('a.ts'), false);
|
|
142
|
+
assert.equal(service.isDirty('b.ts'), false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('does not clear dirty if content changed during save', async () => {
|
|
146
|
+
let resolvePromise: () => void;
|
|
147
|
+
const slowSave = mock.fn(() => new Promise<void>((resolve) => { resolvePromise = resolve; }));
|
|
148
|
+
service = new AutoSaveService({
|
|
149
|
+
save: slowSave,
|
|
150
|
+
onDirtyChange: dirtyChangeFn,
|
|
151
|
+
onSaveError: errorFn,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
service.handleChange('file.ts', 'v1');
|
|
155
|
+
service.saveNow('file.ts');
|
|
156
|
+
|
|
157
|
+
// While save is in-flight, user types more
|
|
158
|
+
service.handleChange('file.ts', 'v2');
|
|
159
|
+
|
|
160
|
+
// Now resolve the first save
|
|
161
|
+
resolvePromise!();
|
|
162
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
163
|
+
|
|
164
|
+
// Should still be dirty because content changed to v2 during save
|
|
165
|
+
assert.equal(service.isDirty('file.ts'), true);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const DEBOUNCE_DELAY_MS = 3000;
|
|
2
|
+
|
|
3
|
+
interface SaveFn {
|
|
4
|
+
(path: string, content: string): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface OnDirtyChange {
|
|
8
|
+
(path: string, dirty: boolean): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface OnSaveError {
|
|
12
|
+
(path: string, error: string): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface AutoSaveServiceDeps {
|
|
16
|
+
save: SaveFn;
|
|
17
|
+
onDirtyChange: OnDirtyChange;
|
|
18
|
+
onSaveError: OnSaveError;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class AutoSaveService {
|
|
22
|
+
private save: SaveFn;
|
|
23
|
+
private onDirtyChange: OnDirtyChange;
|
|
24
|
+
private onSaveError: OnSaveError;
|
|
25
|
+
private timers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
|
26
|
+
private contents: Map<string, string> = new Map();
|
|
27
|
+
private saving: Map<string, boolean> = new Map();
|
|
28
|
+
|
|
29
|
+
constructor(deps: AutoSaveServiceDeps) {
|
|
30
|
+
this.save = deps.save;
|
|
31
|
+
this.onDirtyChange = deps.onDirtyChange;
|
|
32
|
+
this.onSaveError = deps.onSaveError;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
handleChange = (path: string, content: string) => {
|
|
36
|
+
this.contents.set(path, content);
|
|
37
|
+
this.onDirtyChange(path, true);
|
|
38
|
+
this.clearTimer(path);
|
|
39
|
+
|
|
40
|
+
const timer = setTimeout(() => {
|
|
41
|
+
this.performSave(path);
|
|
42
|
+
}, DEBOUNCE_DELAY_MS);
|
|
43
|
+
this.timers.set(path, timer);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
saveNow = (path: string) => {
|
|
47
|
+
this.clearTimer(path);
|
|
48
|
+
if (this.contents.has(path)) {
|
|
49
|
+
this.performSave(path);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
saveAndDiscard = (path: string) => {
|
|
54
|
+
this.clearTimer(path);
|
|
55
|
+
if (this.contents.has(path)) {
|
|
56
|
+
this.performSave(path);
|
|
57
|
+
}
|
|
58
|
+
this.contents.delete(path);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
isDirty = (path: string): boolean => {
|
|
62
|
+
return this.contents.has(path);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
dispose = () => {
|
|
66
|
+
for (const timer of this.timers.values()) {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
}
|
|
69
|
+
this.timers.clear();
|
|
70
|
+
this.contents.clear();
|
|
71
|
+
this.saving.clear();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
private performSave = async (path: string) => {
|
|
75
|
+
const content = this.contents.get(path);
|
|
76
|
+
if (content === undefined || this.saving.get(path)) return;
|
|
77
|
+
|
|
78
|
+
this.saving.set(path, true);
|
|
79
|
+
try {
|
|
80
|
+
await this.save(path, content);
|
|
81
|
+
// Only clear dirty if content hasn't changed during save
|
|
82
|
+
if (this.contents.get(path) === content) {
|
|
83
|
+
this.contents.delete(path);
|
|
84
|
+
this.onDirtyChange(path, false);
|
|
85
|
+
}
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
const message = err?.message || 'Failed to save file';
|
|
88
|
+
this.onSaveError(path, message);
|
|
89
|
+
} finally {
|
|
90
|
+
this.saving.set(path, false);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
private clearTimer = (path: string) => {
|
|
95
|
+
const existing = this.timers.get(path);
|
|
96
|
+
if (existing) {
|
|
97
|
+
clearTimeout(existing);
|
|
98
|
+
this.timers.delete(path);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { findCompositionModuleKey } from './composition_matcher.ts';
|
|
4
|
+
|
|
5
|
+
describe('findCompositionModuleKey', () => {
|
|
6
|
+
const moduleKeys = [
|
|
7
|
+
'../../../video/compositions/intro_video/index.tsx',
|
|
8
|
+
'../../../video/compositions/product_demo/index.tsx',
|
|
9
|
+
'../../../video/compositions/outro/index.tsx',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
it('finds matching module by composition ID', () => {
|
|
13
|
+
const result = findCompositionModuleKey(moduleKeys, 'product_demo');
|
|
14
|
+
assert.equal(result, '../../../video/compositions/product_demo/index.tsx');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns undefined for non-existent composition', () => {
|
|
18
|
+
const result = findCompositionModuleKey(moduleKeys, 'nonexistent');
|
|
19
|
+
assert.equal(result, undefined);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns undefined for empty module keys', () => {
|
|
23
|
+
const result = findCompositionModuleKey([], 'intro_video');
|
|
24
|
+
assert.equal(result, undefined);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('matches first found when multiple compositions exist', () => {
|
|
28
|
+
const result = findCompositionModuleKey(moduleKeys, 'intro_video');
|
|
29
|
+
assert.equal(result, '../../../video/compositions/intro_video/index.tsx');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('does not match partial directory names', () => {
|
|
33
|
+
const result = findCompositionModuleKey(moduleKeys, 'intro');
|
|
34
|
+
assert.equal(result, undefined);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('handles paths with different depth', () => {
|
|
38
|
+
const keys = ['./compositions/my_comp/index.tsx'];
|
|
39
|
+
const result = findCompositionModuleKey(keys, 'my_comp');
|
|
40
|
+
assert.equal(result, './compositions/my_comp/index.tsx');
|
|
41
|
+
});
|
|
42
|
+
});
|
package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the module key from an import.meta.glob result that matches
|
|
3
|
+
* a given composition ID by extracting the directory name from the path.
|
|
4
|
+
*
|
|
5
|
+
* Module keys look like: '../../../video/compositions/my_video/index.tsx'
|
|
6
|
+
* The composition ID matches the directory name (second-to-last segment).
|
|
7
|
+
*/
|
|
8
|
+
export const findCompositionModuleKey = (
|
|
9
|
+
moduleKeys: string[],
|
|
10
|
+
compositionId: string,
|
|
11
|
+
): string | undefined => {
|
|
12
|
+
return moduleKeys.find(key => {
|
|
13
|
+
const parts = key.split('/');
|
|
14
|
+
const dirName = parts[parts.length - 2];
|
|
15
|
+
return dirName === compositionId;
|
|
16
|
+
});
|
|
17
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { getLanguageFromPath, getFileName, getParentPath, buildNewPath, updatePathPrefix } from './file_utils.ts';
|
|
4
|
+
|
|
5
|
+
describe('getLanguageFromPath', () => {
|
|
6
|
+
it('returns typescript for .ts files', () => {
|
|
7
|
+
assert.equal(getLanguageFromPath('src/app.ts'), 'typescript');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns typescript for .tsx files', () => {
|
|
11
|
+
assert.equal(getLanguageFromPath('components/Button.tsx'), 'typescript');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('returns javascript for .js files', () => {
|
|
15
|
+
assert.equal(getLanguageFromPath('index.js'), 'javascript');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns javascript for .jsx files', () => {
|
|
19
|
+
assert.equal(getLanguageFromPath('App.jsx'), 'javascript');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns json for .json files', () => {
|
|
23
|
+
assert.equal(getLanguageFromPath('package.json'), 'json');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns css for .css files', () => {
|
|
27
|
+
assert.equal(getLanguageFromPath('styles/index.css'), 'css');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns markdown for .md files', () => {
|
|
31
|
+
assert.equal(getLanguageFromPath('README.md'), 'markdown');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns python for .py files', () => {
|
|
35
|
+
assert.equal(getLanguageFromPath('script.py'), 'python');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns html for .html files', () => {
|
|
39
|
+
assert.equal(getLanguageFromPath('index.html'), 'html');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns yaml for .yml files', () => {
|
|
43
|
+
assert.equal(getLanguageFromPath('config.yml'), 'yaml');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns shell for .sh files', () => {
|
|
47
|
+
assert.equal(getLanguageFromPath('deploy.sh'), 'shell');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns plaintext for unknown extensions', () => {
|
|
51
|
+
assert.equal(getLanguageFromPath('file.xyz'), 'plaintext');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns plaintext for files without extensions', () => {
|
|
55
|
+
assert.equal(getLanguageFromPath('Makefile'), 'plaintext');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('handles nested paths correctly', () => {
|
|
59
|
+
assert.equal(getLanguageFromPath('src/components/deep/file.tsx'), 'typescript');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('is case-insensitive for extensions', () => {
|
|
63
|
+
assert.equal(getLanguageFromPath('FILE.TS'), 'typescript');
|
|
64
|
+
assert.equal(getLanguageFromPath('style.CSS'), 'css');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('getFileName', () => {
|
|
69
|
+
it('returns the filename from a path', () => {
|
|
70
|
+
assert.equal(getFileName('src/components/Button.tsx'), 'Button.tsx');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns the filename when no directory', () => {
|
|
74
|
+
assert.equal(getFileName('index.ts'), 'index.ts');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('handles deeply nested paths', () => {
|
|
78
|
+
assert.equal(getFileName('a/b/c/d/file.js'), 'file.js');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns the input for a single filename', () => {
|
|
82
|
+
assert.equal(getFileName('README.md'), 'README.md');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('getParentPath', () => {
|
|
87
|
+
it('returns parent directory for nested path', () => {
|
|
88
|
+
assert.equal(getParentPath('src/components/Button.tsx'), 'src/components');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns empty string for root-level file', () => {
|
|
92
|
+
assert.equal(getParentPath('index.ts'), '');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('returns parent for deeply nested path', () => {
|
|
96
|
+
assert.equal(getParentPath('a/b/c/d/file.js'), 'a/b/c/d');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('returns parent directory for directory path', () => {
|
|
100
|
+
assert.equal(getParentPath('src/utils'), 'src');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('buildNewPath', () => {
|
|
105
|
+
it('builds new path in same directory', () => {
|
|
106
|
+
assert.equal(buildNewPath('src/old.ts', 'new.ts'), 'src/new.ts');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('builds new path at root level', () => {
|
|
110
|
+
assert.equal(buildNewPath('old.ts', 'new.ts'), 'new.ts');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('builds new path for nested file', () => {
|
|
114
|
+
assert.equal(buildNewPath('src/components/Old.tsx', 'New.tsx'), 'src/components/New.tsx');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('builds new path for directory rename', () => {
|
|
118
|
+
assert.equal(buildNewPath('src/utils', 'helpers'), 'src/helpers');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('updatePathPrefix', () => {
|
|
123
|
+
it('replaces exact matching path', () => {
|
|
124
|
+
assert.equal(updatePathPrefix('src/old.ts', 'src/old.ts', 'src/new.ts'), 'src/new.ts');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('replaces prefix for child paths', () => {
|
|
128
|
+
assert.equal(
|
|
129
|
+
updatePathPrefix('src/utils/helper.ts', 'src/utils', 'src/helpers'),
|
|
130
|
+
'src/helpers/helper.ts',
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('returns unchanged path when no match', () => {
|
|
135
|
+
assert.equal(updatePathPrefix('other/file.ts', 'src/old.ts', 'src/new.ts'), 'other/file.ts');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('does not match partial directory names', () => {
|
|
139
|
+
assert.equal(updatePathPrefix('src/utils2/file.ts', 'src/utils', 'src/helpers'), 'src/utils2/file.ts');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('handles root-level rename', () => {
|
|
143
|
+
assert.equal(updatePathPrefix('old/file.ts', 'old', 'new'), 'new/file.ts');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const EXTENSION_LANGUAGE_MAP: Record<string, string> = {
|
|
2
|
+
'.ts': 'typescript', '.tsx': 'typescript',
|
|
3
|
+
'.js': 'javascript', '.jsx': 'javascript',
|
|
4
|
+
'.json': 'json', '.html': 'html', '.htm': 'html',
|
|
5
|
+
'.css': 'css', '.scss': 'scss', '.less': 'less',
|
|
6
|
+
'.md': 'markdown', '.yaml': 'yaml', '.yml': 'yaml',
|
|
7
|
+
'.xml': 'xml', '.sql': 'sql',
|
|
8
|
+
'.sh': 'shell', '.bash': 'shell', '.zsh': 'shell',
|
|
9
|
+
'.py': 'python', '.rb': 'ruby', '.go': 'go', '.rs': 'rust',
|
|
10
|
+
'.java': 'java', '.c': 'c', '.cpp': 'cpp', '.h': 'c', '.hpp': 'cpp',
|
|
11
|
+
'.cs': 'csharp', '.php': 'php', '.swift': 'swift', '.kt': 'kotlin',
|
|
12
|
+
'.lua': 'lua', '.r': 'r', '.toml': 'toml', '.ini': 'ini',
|
|
13
|
+
'.env': 'plaintext', '.txt': 'plaintext', '.log': 'plaintext',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const getLanguageFromPath = (filePath: string): string => {
|
|
17
|
+
const dotIdx = filePath.lastIndexOf('.');
|
|
18
|
+
if (dotIdx === -1) return 'plaintext';
|
|
19
|
+
const ext = filePath.slice(dotIdx).toLowerCase();
|
|
20
|
+
return EXTENSION_LANGUAGE_MAP[ext] || 'plaintext';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const getFileName = (filePath: string): string => {
|
|
24
|
+
const parts = filePath.split('/');
|
|
25
|
+
return parts[parts.length - 1] || filePath;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const getParentPath = (path: string): string => {
|
|
29
|
+
const lastSlash = path.lastIndexOf('/');
|
|
30
|
+
return lastSlash === -1 ? '' : path.substring(0, lastSlash);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const buildNewPath = (oldPath: string, newName: string): string => {
|
|
34
|
+
const parent = getParentPath(oldPath);
|
|
35
|
+
return parent ? `${parent}/${newName}` : newName;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const updatePathPrefix = (path: string, oldPrefix: string, newPrefix: string): string => {
|
|
39
|
+
if (path === oldPrefix) return newPrefix;
|
|
40
|
+
if (path.startsWith(oldPrefix + '/')) return newPrefix + path.substring(oldPrefix.length);
|
|
41
|
+
return path;
|
|
42
|
+
};
|
|
@@ -21,16 +21,10 @@ describe('getTaskIcon', () => {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
describe('getTaskCssClass', () => {
|
|
24
|
-
it('returns
|
|
25
|
-
assert.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
it('returns correct class for in_progress', () => {
|
|
29
|
-
assert.equal(getTaskCssClass('in_progress'), 'kanban-task-item task-in_progress');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('returns correct class for pending', () => {
|
|
33
|
-
assert.equal(getTaskCssClass('pending'), 'kanban-task-item task-pending');
|
|
24
|
+
it('returns tailwind layout class for any status', () => {
|
|
25
|
+
assert.ok(getTaskCssClass('completed').includes('flex'));
|
|
26
|
+
assert.ok(getTaskCssClass('in_progress').includes('flex'));
|
|
27
|
+
assert.ok(getTaskCssClass('pending').includes('flex'));
|
|
34
28
|
});
|
|
35
29
|
});
|
|
36
30
|
|
|
@@ -8,7 +8,25 @@ export const getTaskIcon = (status: string): string =>
|
|
|
8
8
|
TASK_ICONS[status] ?? TASK_ICONS.pending;
|
|
9
9
|
|
|
10
10
|
export const getTaskCssClass = (status: string): string =>
|
|
11
|
-
|
|
11
|
+
'flex items-center gap-1.5 text-[10px] font-mono py-px';
|
|
12
|
+
|
|
13
|
+
export const getTaskIconClass = (status: string): string => {
|
|
14
|
+
switch (status) {
|
|
15
|
+
case 'completed': return 'text-[#40c057]';
|
|
16
|
+
case 'in_progress': return 'text-accent animate-pulse';
|
|
17
|
+
case 'pending': return 'text-content-secondary opacity-50';
|
|
18
|
+
default: return 'text-content-secondary opacity-50';
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const getTaskNameClass = (status: string): string => {
|
|
23
|
+
switch (status) {
|
|
24
|
+
case 'completed': return 'text-content-secondary line-through opacity-70';
|
|
25
|
+
case 'in_progress': return 'text-accent';
|
|
26
|
+
case 'pending': return 'text-content-secondary';
|
|
27
|
+
default: return 'text-content-secondary';
|
|
28
|
+
}
|
|
29
|
+
};
|
|
12
30
|
|
|
13
31
|
export const shouldShowTaskList = (tasks: { total: number } | undefined | null): boolean =>
|
|
14
32
|
!!tasks && tasks.total > 0;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineConfig, loadEnv } from 'vite';
|
|
2
2
|
import react from '@vitejs/plugin-react';
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { dirname, resolve } from 'node:path';
|
|
5
6
|
|
|
@@ -12,7 +13,7 @@ export default defineConfig(({ mode }) => {
|
|
|
12
13
|
const backendPort = parseInt(env.PORT || '3000', 10);
|
|
13
14
|
|
|
14
15
|
return {
|
|
15
|
-
plugins: [react()],
|
|
16
|
+
plugins: [tailwindcss(), react()],
|
|
16
17
|
build: {
|
|
17
18
|
outDir: 'dist',
|
|
18
19
|
emptyOutDir: true,
|
|
@@ -26,6 +27,11 @@ export default defineConfig(({ mode }) => {
|
|
|
26
27
|
ws: true,
|
|
27
28
|
},
|
|
28
29
|
},
|
|
30
|
+
fs: {
|
|
31
|
+
// Allow serving composition files from the video package for
|
|
32
|
+
// the Remotion Player preview (import.meta.glob traverses here)
|
|
33
|
+
allow: [__dirname, resolve(__dirname, '..', 'video')],
|
|
34
|
+
},
|
|
29
35
|
},
|
|
30
36
|
};
|
|
31
37
|
});
|
|
File without changes
|