@cognite/cli 0.5.1 → 0.6.0-alpha.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -33
- package/_templates/app/new/config/eslint.config.mjs.ejs.t +99 -0
- package/_templates/app/new/config/tsconfig.json.ejs.t +35 -0
- package/_templates/app/new/config/tsconfig.node.json.ejs.t +27 -0
- package/_templates/app/new/config/vite.config.ts.ejs.t +28 -0
- package/_templates/app/new/config/vitest.config.ts.ejs.t +14 -0
- package/_templates/app/new/config/vitest.setup.ts.ejs.t +4 -0
- package/_templates/app/new/github/ci.yml.ejs.t +36 -0
- package/_templates/app/new/prompt.js +49 -0
- package/_templates/app/new/root/.npmrc.ejs.t +4 -0
- package/_templates/app/new/root/AGENTS.md.ejs.t +215 -0
- package/_templates/app/new/root/SPEC.md.ejs.t +77 -0
- package/_templates/app/new/root/app.json.ejs.t +20 -0
- package/_templates/app/new/root/gitignore.ejs.t +21 -0
- package/_templates/app/new/root/index.html.ejs.t +36 -0
- package/_templates/app/new/root/manifest.json.ejs.t +9 -0
- package/_templates/app/new/root/package.json.ejs.t +65 -0
- package/_templates/app/new/src/App.test.tsx.ejs.t +45 -0
- package/_templates/app/new/src/App.tsx.ejs.t +234 -0
- package/_templates/app/new/src/lib/utils.ts.ejs.t +9 -0
- package/_templates/app/new/src/main.tsx.ejs.t +27 -0
- package/_templates/app/new/src/styles.css.ejs.t +12 -0
- package/_vendor/spec-kit/.version +4 -0
- package/_vendor/spec-kit/README.md +39 -0
- package/_vendor/spec-kit/commands/speckit.analyze.md +249 -0
- package/_vendor/spec-kit/commands/speckit.checklist.md +361 -0
- package/_vendor/spec-kit/commands/speckit.clarify.md +247 -0
- package/_vendor/spec-kit/commands/speckit.implement.md +198 -0
- package/_vendor/spec-kit/commands/speckit.plan.md +149 -0
- package/_vendor/spec-kit/commands/speckit.specify.md +327 -0
- package/_vendor/spec-kit/commands/speckit.tasks.md +200 -0
- package/_vendor/spec-kit/scripts/bash/check-prerequisites.sh +190 -0
- package/_vendor/spec-kit/scripts/bash/common.sh +645 -0
- package/_vendor/spec-kit/scripts/bash/setup-plan.sh +75 -0
- package/_vendor/spec-kit/templates/checklist-template.md +40 -0
- package/_vendor/spec-kit/templates/plan-template.md +104 -0
- package/_vendor/spec-kit/templates/spec-template.md +128 -0
- package/_vendor/spec-kit/templates/tasks-template.md +251 -0
- package/dist/chunk-6IFTGM5Y.js +6 -0
- package/dist/chunk-6JBK3X6U.js +2 -0
- package/dist/chunk-7BIIU2MQ.js +8 -0
- package/dist/chunk-CQ5OFVL5.js +2 -0
- package/dist/chunk-F3TJC2SP.js +2 -0
- package/dist/cli/cli.js +350 -0
- package/dist/esm-OFTP7G2W.js +34 -0
- package/dist/getMachineId-bsd-3GB6MPGO.js +2 -0
- package/dist/getMachineId-darwin-4AJ74CH4.js +3 -0
- package/dist/getMachineId-linux-IEUC3AW3.js +2 -0
- package/dist/getMachineId-unsupported-YOCUE26C.js +2 -0
- package/dist/getMachineId-win-DDKCA2D6.js +2 -0
- package/dist/skills-R7PLBJFQ.js +2 -0
- package/package.json +26 -17
- package/index.js +0 -134
- package/operations.js +0 -113
package/README.md
CHANGED
|
@@ -1,61 +1,122 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cognite/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Build and deploy React apps to [Cognite Data Fusion](https://docs.cognite.com/). Apps built with this CLI run inside Cognite Flows — Cognite's app-hosting platform for embedding custom UIs in CDF.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Quick start
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Scaffold a new app — AI skills are pulled automatically:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
```bash
|
|
10
|
+
npx @cognite/cli apps create
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This prompts for your app name, org, project, and cluster, then generates a fully configured React + TypeScript project.
|
|
14
|
+
|
|
15
|
+
## Authentication
|
|
16
|
+
|
|
17
|
+
New apps created with `npx @cognite/cli apps create` depend on [`@cognite/app-sdk`](https://www.npmjs.com/package/@cognite/app-sdk) — **not `@cognite/cli`** — for auth and host integration. `@cognite/cli` is the CLI used to scaffold, develop, and deploy the app; the generated app itself talks to the Flows app host via `@cognite/app-sdk`'s Comlink handshake. The template wires this up for you.
|
|
18
|
+
|
|
19
|
+
## Deployment
|
|
20
|
+
|
|
21
|
+
Deploy interactively via browser OAuth:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @cognite/cli apps deploy --interactive
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For CI, set your client secret as an environment variable and run:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm deploy
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Deployment targets are configured in `app.json` at the project root.
|
|
10
34
|
|
|
11
|
-
|
|
35
|
+
## AI skills
|
|
12
36
|
|
|
13
|
-
|
|
37
|
+
Skills guide your AI agent (GitHub Copilot, Claude Code, Cursor, etc.) through Dune-specific tasks like adding auth, building chat UIs, or reviewing code. They are pulled automatically on `npx @cognite/cli apps create` and can be synced later:
|
|
14
38
|
|
|
15
|
-
|
|
39
|
+
```bash
|
|
40
|
+
npx @cognite/cli apps skills pull
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Browse available skills at [cognitedata/builder-skills](https://github.com/cognitedata/builder-skills).
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
16
46
|
|
|
17
|
-
|
|
18
|
-
|
|
47
|
+
- Node.js ≥ 20
|
|
48
|
+
- React ≥ 18 (optional peer dependency — only needed for auth components)
|
|
19
49
|
|
|
20
|
-
|
|
50
|
+
## Development
|
|
21
51
|
|
|
22
|
-
|
|
52
|
+
### Running tests
|
|
23
53
|
|
|
24
|
-
|
|
54
|
+
```bash
|
|
55
|
+
pnpm test # run once
|
|
56
|
+
pnpm test:watch # watch mode
|
|
57
|
+
```
|
|
25
58
|
|
|
26
|
-
|
|
59
|
+
### Testing the CLI against a mock server
|
|
27
60
|
|
|
28
|
-
|
|
61
|
+
A local mock of the App Hosting API lets you run the real CLI against a controlled server without hitting a real CDF cluster.
|
|
29
62
|
|
|
30
|
-
|
|
63
|
+
**Terminal 1 — start the mock server:**
|
|
31
64
|
|
|
32
|
-
|
|
65
|
+
```bash
|
|
66
|
+
pnpm mock:server
|
|
67
|
+
```
|
|
33
68
|
|
|
34
|
-
|
|
69
|
+
**Terminal 2 — run the deploy command against it:**
|
|
35
70
|
|
|
36
|
-
|
|
71
|
+
```bash
|
|
72
|
+
pnpm mock:deploy
|
|
73
|
+
```
|
|
37
74
|
|
|
38
|
-
`
|
|
75
|
+
`mock:deploy` builds and deploys `apps/mock-app` — a minimal workspace app that serves as the standard CLI test target. Under the hood it sets two env vars that work from any app directory too:
|
|
39
76
|
|
|
40
|
-
|
|
77
|
+
| Env var | Purpose |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `COGNITE_BASE_URL=http://localhost:9090` | Overrides `baseUrl` from `app.json` — all API calls go to the mock |
|
|
80
|
+
| `COGNITE_TOKEN=test-token` | Skips OAuth entirely — no real client secret needed |
|
|
41
81
|
|
|
42
|
-
|
|
82
|
+
To test against your own app instead of the fixture:
|
|
43
83
|
|
|
84
|
+
```bash
|
|
85
|
+
cd apps/my-app
|
|
86
|
+
COGNITE_TOKEN=test-token COGNITE_BASE_URL=http://localhost:9090 pnpm exec dune apps deploy --skip-build
|
|
44
87
|
```
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
88
|
+
|
|
89
|
+
#### Simulating error scenarios
|
|
90
|
+
|
|
91
|
+
Pass `MOCK_SCENARIO` to the server to force a specific HTTP error:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
MOCK_SCENARIO=403 pnpm mock:server
|
|
49
95
|
```
|
|
50
96
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
`
|
|
97
|
+
| `MOCK_SCENARIO` | Step that fails |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `409` | `ensureApp` — app already exists (CLI should recover) |
|
|
100
|
+
| `403` | `ensureApp` — missing `AppHosting:WRITE` capability |
|
|
101
|
+
| `500` | `ensureApp` — internal server error |
|
|
102
|
+
| `upload-409` | `uploadVersion` — version already published, cannot overwrite (hard failure) |
|
|
103
|
+
| `upload-413` | `uploadVersion` — payload too large (50 MB limit) |
|
|
104
|
+
| `upload-403` | `uploadVersion` — missing `AppHosting:WRITE` capability |
|
|
105
|
+
| `upload-500` | `uploadVersion` — internal server error |
|
|
106
|
+
| `publish-403` | `publishAndActivate` — missing `AppHosting:WRITE` capability |
|
|
107
|
+
| `publish-500` | `publishAndActivate` — internal server error |
|
|
108
|
+
| `republish` | `ensureApp` recovers (409), then `uploadVersion` fails (409 — version already published) |
|
|
54
109
|
|
|
55
|
-
|
|
110
|
+
The mock server and MSW handlers live in `cli/testing/msw/`. The same handlers are used by the Vitest integration tests in `src/deploy/apphosting-deployer.msw.test.ts`.
|
|
56
111
|
|
|
57
|
-
##
|
|
112
|
+
## Maintenance
|
|
58
113
|
|
|
59
|
-
|
|
114
|
+
### Updating the spec-kit vendor snapshot
|
|
115
|
+
|
|
116
|
+
The AI skill commands under `_vendor/spec-kit/` are generated by [spec-kit](https://github.com/github/spec-kit) and checked in. To update to a new release, pass the target tag (requires [`uv`](https://docs.astral.sh/uv/), which provides `uvx`):
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pnpm --filter @cognite/cli refresh-spec-kit v0.9.0
|
|
120
|
+
```
|
|
60
121
|
|
|
61
|
-
|
|
122
|
+
The refresh script stages the generated commands, templates, and shell scripts before replacing `_vendor/spec-kit/`. Then review the diff under `_vendor/spec-kit/` and commit it.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: '<%= useCurrentDir ? "" : ((directoryName || name) + "/") %>eslint.config.mjs'
|
|
3
|
+
---
|
|
4
|
+
import { auraEslintPlugin } from '@cognite/aura/eslint';
|
|
5
|
+
import js from '@eslint/js';
|
|
6
|
+
import importPlugin from 'eslint-plugin-import';
|
|
7
|
+
import noOnlyTestsPlugin from 'eslint-plugin-no-only-tests';
|
|
8
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
9
|
+
import reactRefresh from 'eslint-plugin-react-refresh';
|
|
10
|
+
import globals from 'globals';
|
|
11
|
+
import tseslint from 'typescript-eslint';
|
|
12
|
+
|
|
13
|
+
const sharedRules = {
|
|
14
|
+
'import/first': 'error',
|
|
15
|
+
'import/no-duplicates': 'error',
|
|
16
|
+
'import/order': [
|
|
17
|
+
'error',
|
|
18
|
+
{
|
|
19
|
+
groups: ['builtin', 'external', 'internal', 'parent', ['sibling', 'index']],
|
|
20
|
+
'newlines-between': 'always',
|
|
21
|
+
alphabetize: {
|
|
22
|
+
order: 'asc',
|
|
23
|
+
caseInsensitive: true,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
'no-only-tests/no-only-tests': 'error',
|
|
28
|
+
quotes: ['error', 'single', { avoidEscape: true }],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const noUnusedVarsOptions = {
|
|
32
|
+
argsIgnorePattern: '^_',
|
|
33
|
+
caughtErrorsIgnorePattern: '^_',
|
|
34
|
+
varsIgnorePattern: '^_',
|
|
35
|
+
ignoreRestSiblings: true,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default tseslint.config(
|
|
39
|
+
{
|
|
40
|
+
ignores: ['dist', 'build', '.next', 'coverage', '*.min.js', '.agents', '.cursor', '.claude'],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
files: ['**/*.{js,mjs,cjs,ts,tsx}'],
|
|
44
|
+
plugins: {
|
|
45
|
+
import: importPlugin,
|
|
46
|
+
'no-only-tests': noOnlyTestsPlugin,
|
|
47
|
+
},
|
|
48
|
+
rules: sharedRules,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
files: ['**/*.{js,mjs,cjs}'],
|
|
52
|
+
extends: [js.configs.recommended],
|
|
53
|
+
languageOptions: {
|
|
54
|
+
ecmaVersion: 'latest',
|
|
55
|
+
sourceType: 'module',
|
|
56
|
+
globals: {
|
|
57
|
+
...globals.browser,
|
|
58
|
+
...globals.node,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
rules: {
|
|
62
|
+
'no-unused-vars': ['error', noUnusedVarsOptions],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
files: ['**/*.cjs'],
|
|
67
|
+
languageOptions: {
|
|
68
|
+
sourceType: 'commonjs',
|
|
69
|
+
globals: {
|
|
70
|
+
...globals.node,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
files: ['**/*.{ts,tsx}'],
|
|
76
|
+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
|
77
|
+
languageOptions: {
|
|
78
|
+
ecmaVersion: 'latest',
|
|
79
|
+
globals: {
|
|
80
|
+
...globals.browser,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
plugins: {
|
|
84
|
+
aura: auraEslintPlugin,
|
|
85
|
+
'react-hooks': reactHooks,
|
|
86
|
+
'react-refresh': reactRefresh,
|
|
87
|
+
},
|
|
88
|
+
rules: {
|
|
89
|
+
...reactHooks.configs.recommended.rules,
|
|
90
|
+
'@typescript-eslint/consistent-type-imports': 'error',
|
|
91
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
92
|
+
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
93
|
+
'@typescript-eslint/no-unused-vars': ['error', noUnusedVarsOptions],
|
|
94
|
+
'aura/no-overriding-styles': 'warn',
|
|
95
|
+
'no-unused-vars': 'off',
|
|
96
|
+
'react-refresh/only-export-components': ['error', { allowConstantExport: true }],
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: '<%= useCurrentDir ? "" : ((directoryName || name) + "/") %>tsconfig.json'
|
|
3
|
+
---
|
|
4
|
+
{
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"target": "ES2020",
|
|
7
|
+
"useDefineForClassFields": true,
|
|
8
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
9
|
+
"module": "ESNext",
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
|
|
12
|
+
/* Bundler mode */
|
|
13
|
+
"moduleResolution": "bundler",
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"moduleDetection": "force",
|
|
17
|
+
"noEmit": true,
|
|
18
|
+
"jsx": "react-jsx",
|
|
19
|
+
|
|
20
|
+
/* Linting */
|
|
21
|
+
"strict": true,
|
|
22
|
+
"noUnusedLocals": true,
|
|
23
|
+
"noUnusedParameters": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
|
|
26
|
+
/* Testing */
|
|
27
|
+
"types": ["vitest/globals", "@testing-library/jest-dom"],
|
|
28
|
+
"baseUrl": ".",
|
|
29
|
+
"paths": {
|
|
30
|
+
"@/*": ["./src/*"]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"include": ["src"]
|
|
34
|
+
}
|
|
35
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: '<%= useCurrentDir ? "" : ((directoryName || name) + "/") %>tsconfig.node.json'
|
|
3
|
+
---
|
|
4
|
+
{
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"composite": true,
|
|
7
|
+
"target": "ES2022",
|
|
8
|
+
"lib": ["ES2023"],
|
|
9
|
+
"module": "ESNext",
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
|
|
12
|
+
/* Bundler mode */
|
|
13
|
+
"moduleResolution": "bundler",
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"moduleDetection": "force",
|
|
17
|
+
"emitDeclarationOnly": true,
|
|
18
|
+
|
|
19
|
+
/* Linting */
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"noFallthroughCasesInSwitch": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts", "vitest.config.ts"]
|
|
26
|
+
}
|
|
27
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: '<%= useCurrentDir ? "" : ((directoryName || name) + "/") %>vite.config.ts'
|
|
3
|
+
---
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { fusionOpenPlugin, manifestCspPlugin } from '@cognite/app-sdk/vite';
|
|
7
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
8
|
+
import react from '@vitejs/plugin-react';
|
|
9
|
+
import { defineConfig } from 'vite';
|
|
10
|
+
import mkcert from 'vite-plugin-mkcert';
|
|
11
|
+
|
|
12
|
+
export default defineConfig({
|
|
13
|
+
base: './',
|
|
14
|
+
// manifestCspPlugin() must stay first — its middleware sets the
|
|
15
|
+
// Content-Security-Policy header before any HTML response is sent.
|
|
16
|
+
plugins: [manifestCspPlugin(), react(), mkcert(), fusionOpenPlugin(), tailwindcss()],
|
|
17
|
+
resolve: {
|
|
18
|
+
alias: {
|
|
19
|
+
'@': path.resolve(__dirname, './src'),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
server: {
|
|
23
|
+
port: 3001,
|
|
24
|
+
},
|
|
25
|
+
worker: {
|
|
26
|
+
format: 'es',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: '<%= useCurrentDir ? "" : ((directoryName || name) + "/") %>vitest.config.ts'
|
|
3
|
+
---
|
|
4
|
+
import react from '@vitejs/plugin-react';
|
|
5
|
+
import { defineConfig } from 'vitest/config';
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [react()],
|
|
9
|
+
test: {
|
|
10
|
+
globals: true,
|
|
11
|
+
environment: 'happy-dom',
|
|
12
|
+
setupFiles: ['vitest.setup.ts'],
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: '<%= useCurrentDir ? "" : ((directoryName || name) + "/") %>.github/workflows/ci.yml'
|
|
3
|
+
---
|
|
4
|
+
name: CI
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
pull_request:
|
|
8
|
+
push:
|
|
9
|
+
branches:
|
|
10
|
+
- main
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
lint-test-build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout
|
|
18
|
+
uses: actions/checkout@v6
|
|
19
|
+
|
|
20
|
+
- name: Setup Node
|
|
21
|
+
uses: actions/setup-node@v6
|
|
22
|
+
with:
|
|
23
|
+
node-version: 24
|
|
24
|
+
cache: npm
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: npm install
|
|
28
|
+
|
|
29
|
+
- name: Lint
|
|
30
|
+
run: npm run lint
|
|
31
|
+
|
|
32
|
+
- name: Test
|
|
33
|
+
run: npm test
|
|
34
|
+
|
|
35
|
+
- name: Build
|
|
36
|
+
run: npm run build
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export default [
|
|
2
|
+
{
|
|
3
|
+
type: 'input',
|
|
4
|
+
name: 'name',
|
|
5
|
+
message: 'What is the app name? (use kebab-case, e.g., my-awesome-app)',
|
|
6
|
+
initial: 'my-dune-app',
|
|
7
|
+
validate: (input) =>
|
|
8
|
+
/^[a-z][a-z0-9-]*$/.test(input)
|
|
9
|
+
? true
|
|
10
|
+
: 'App name must be kebab-case (lowercase, hyphens only)',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
type: 'input',
|
|
14
|
+
name: 'displayName',
|
|
15
|
+
message: 'What is the display name? (e.g., My Awesome App)',
|
|
16
|
+
initial: 'My Dune app',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
type: 'input',
|
|
20
|
+
name: 'description',
|
|
21
|
+
message: 'What is the app description?',
|
|
22
|
+
initial: 'A Dune application',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: 'input',
|
|
26
|
+
name: 'org',
|
|
27
|
+
message: 'Deployment org? (e.g., cog-demo, cog-atlas)',
|
|
28
|
+
initial: 'cog-demo',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: 'input',
|
|
32
|
+
name: 'project',
|
|
33
|
+
message: 'Deployment project? (e.g., lervik-industries, atlas-greenfield)',
|
|
34
|
+
initial: 'lervik-industries',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: 'input',
|
|
38
|
+
name: 'cluster',
|
|
39
|
+
message: 'Cluster? (e.g., greenfield, westeurope-1, api)',
|
|
40
|
+
initial: 'api',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: 'input',
|
|
44
|
+
name: 'baseUrl',
|
|
45
|
+
// initial is set dynamically in create-prompter based on the cluster answer
|
|
46
|
+
message: 'Base URL? (e.g. https://az-arn-004.cognitedata.com)',
|
|
47
|
+
initial: 'https://api.cognitedata.com',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: '<%= useCurrentDir ? "" : ((directoryName || name) + "/") %>AGENTS.md'
|
|
3
|
+
---
|
|
4
|
+
# Coding Standards
|
|
5
|
+
|
|
6
|
+
<% if (useSpecKit) { -%>
|
|
7
|
+
## 0. Product Spec (spec-driven development)
|
|
8
|
+
|
|
9
|
+
This app uses [github/spec-kit](https://github.com/github/spec-kit) for spec-driven development. Specs live under `specs/<NNN>-<feature-name>/spec.md`, one directory per feature.
|
|
10
|
+
|
|
11
|
+
- To start a new feature, run `/speckit.specify <description>` in Claude Code or Cursor. It generates a properly numbered feature directory and a spec to fill in. Then run `/speckit.clarify` → `/speckit.plan` → `/speckit.tasks` → `/speckit.implement`.
|
|
12
|
+
- When user-visible behavior changes in an existing feature, update its `specs/<NNN>-<feature>/spec.md` before or alongside the code change.
|
|
13
|
+
- When a feature touches Cognite Data Fusion data, the spec must document existing CDF views read from, new views needed, and spaces used.
|
|
14
|
+
<% } else { -%>
|
|
15
|
+
## 0. Product Spec (SPEC.md)
|
|
16
|
+
|
|
17
|
+
`SPEC.md` at the repo root is the living product spec for this app. Read it before making feature decisions, and keep it in sync with user-visible behavior.
|
|
18
|
+
|
|
19
|
+
- If `SPEC.md` is empty or contains only commented `<!-- -->` placeholders, proactively offer to populate it before writing implementation code. Do not silently skip.
|
|
20
|
+
- When user-visible behavior changes, update the relevant `SPEC.md` section before or alongside the code change.
|
|
21
|
+
<% } -%>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 1. Dependency Injection
|
|
26
|
+
|
|
27
|
+
Inject dependencies via React context (hooks/components) or factory-override pattern (plain functions). Never hard-code dependencies.
|
|
28
|
+
|
|
29
|
+
### React context
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
const defaultDeps = { useDataSource, useAnalytics };
|
|
33
|
+
export type MyHookContextType = typeof defaultDeps;
|
|
34
|
+
export const MyHookContext = createContext<MyHookContextType>(defaultDeps);
|
|
35
|
+
|
|
36
|
+
export function useMyHook() {
|
|
37
|
+
const { useDataSource } = useContext(MyHookContext);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Factory overrides
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
type Deps = { serviceFactory: () => SomeService };
|
|
45
|
+
const defaultDeps: Deps = { serviceFactory: () => new SomeServiceImpl() };
|
|
46
|
+
|
|
47
|
+
export const doWork = async (props: Props, overrides?: Partial<Deps>) => {
|
|
48
|
+
const { serviceFactory } = { ...defaultDeps, ...overrides };
|
|
49
|
+
};
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 2. Interface-Based Services
|
|
55
|
+
|
|
56
|
+
Define an interface; implement with a class. Never reference the concrete class outside its own file.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
export interface DataService {
|
|
60
|
+
load(): Promise<Data>;
|
|
61
|
+
save(data: Data): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class ApiDataService implements DataService {
|
|
65
|
+
/* ... */
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 3. ViewModel Pattern
|
|
72
|
+
|
|
73
|
+
Business logic lives in `use<Name>ViewModel`. Components only render.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
export function useTodoViewModel(): TodoViewModel {
|
|
77
|
+
const { useTodoStorage, addTodoCommand } = useContext(TodoViewModelContext);
|
|
78
|
+
const storage = useTodoStorage();
|
|
79
|
+
const addTodo = useCallback(
|
|
80
|
+
(text: string) => addTodoCommand(text, storage),
|
|
81
|
+
[storage, addTodoCommand]
|
|
82
|
+
);
|
|
83
|
+
return { todos: storage.listAllTodos(), addTodo };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const TodoView = () => {
|
|
87
|
+
const { todos, addTodo } = useTodoViewModel();
|
|
88
|
+
return <ul>{todos.map((t) => <TodoItem key={t.id} todo={t} onAdd={addTodo} />)}</ul>;
|
|
89
|
+
};
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 4. Test-First Development
|
|
95
|
+
|
|
96
|
+
Write tests before implementation for all non-trivial behavior changes.
|
|
97
|
+
|
|
98
|
+
### Preferred order
|
|
99
|
+
|
|
100
|
+
Start with behavior-focused tests so requirements are specified before implementation details:
|
|
101
|
+
|
|
102
|
+
1. Integration tests (user-visible behavior)
|
|
103
|
+
2. Unit tests (isolated module logic)
|
|
104
|
+
3. Source files to make tests pass
|
|
105
|
+
|
|
106
|
+
For bug fixes, start by adding a failing regression test that reproduces the issue.
|
|
107
|
+
|
|
108
|
+
Every new module with logic (service, hook, component, utility) must include a corresponding `*.test.ts(x)` file in the same changeset.
|
|
109
|
+
|
|
110
|
+
### Reasonable exceptions
|
|
111
|
+
|
|
112
|
+
- Bootstrapping/entry files (for example `main.tsx`)
|
|
113
|
+
- Generated code
|
|
114
|
+
- Trivial pure-markup components with no logic or state
|
|
115
|
+
|
|
116
|
+
### Test levels in this repo
|
|
117
|
+
|
|
118
|
+
- **Integration test**: validates behavior across boundaries (for example component + view model + service contract), mocking only external systems such as network APIs.
|
|
119
|
+
- **Unit test**: validates one module in isolation (service, hook, utility, or component behavior).
|
|
120
|
+
|
|
121
|
+
### Minimum expected coverage by file type
|
|
122
|
+
|
|
123
|
+
| File type | Required test cases |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| Service (`*Service.ts`) | Correct request construction; response parsing; error thrown on non-OK status |
|
|
126
|
+
| ViewModel hook (`use*ViewModel.ts`) | Loading state; success state with correct derived values; error state |
|
|
127
|
+
| Pure utility / helper | Every exported function and all meaningful branches |
|
|
128
|
+
| View component | Renders expected content from props; loading/error/empty states where applicable |
|
|
129
|
+
|
|
130
|
+
### Conventions
|
|
131
|
+
|
|
132
|
+
- Files: `*.test.ts(x)`; runner: **Vitest** (`npm test` or `vitest run`)
|
|
133
|
+
- Structure: Arrange / Act / Assert (add explicit comments for longer tests)
|
|
134
|
+
- One behavior per test
|
|
135
|
+
- Keep helper functions at the bottom of the file
|
|
136
|
+
- Prefer dependency/context injection over `vi.mock`; add a short reason when `vi.mock` is unavoidable
|
|
137
|
+
|
|
138
|
+
### Type-safe mocks
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// Preferred: vi.fn(() => ...) for consistent behavior
|
|
142
|
+
mockContext = { useUserInfo: vi.fn(() => ({ data: mockUser, isFetched: true })) };
|
|
143
|
+
|
|
144
|
+
// Per-test reconfiguration
|
|
145
|
+
mockContext = { useUserInfo: vi.fn() };
|
|
146
|
+
vi.mocked(mockContext.useUserInfo).mockReturnValue({ data: undefined, isFetched: true });
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
For full interface mocks, use `assert.fail` on methods the unit under test should never call, or preferably define a narrower interface.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
mockStorage = {
|
|
153
|
+
list: vi.fn(),
|
|
154
|
+
retrieve: vi.fn(() => {
|
|
155
|
+
assert.fail('Not implemented');
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### React hook test pattern
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
describe(useMyHook.name, () => {
|
|
164
|
+
let mockContext: MyContextType;
|
|
165
|
+
let wrapper: ComponentType<{ children: ReactNode }>;
|
|
166
|
+
|
|
167
|
+
beforeEach(() => {
|
|
168
|
+
mockContext = { useUserInfo: vi.fn(() => ({ data: mockUser })) };
|
|
169
|
+
wrapper = ({ children }) => (
|
|
170
|
+
<MyHookContext.Provider value={mockContext}>{children}</MyHookContext.Provider>
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should ...', async () => {
|
|
175
|
+
const { result } = renderHook(() => useMyHook(), { wrapper });
|
|
176
|
+
await act(async () => {
|
|
177
|
+
await result.current.someAction();
|
|
178
|
+
});
|
|
179
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Shared mock data
|
|
185
|
+
|
|
186
|
+
Place reusable factories in `src/__mocks__/`. Use `.test` TLD for fake URLs (RFC 2606).
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 5. TypeScript Rules
|
|
191
|
+
|
|
192
|
+
- Never use `any`; prefer `unknown` or explicit strong types
|
|
193
|
+
- Never use `as unknown as T`; for partial test doubles use `{ ...defaults, ...overrides } as T`
|
|
194
|
+
- Use direct React type imports: `import type { ComponentType, ReactNode } from 'react'`
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
function createMockWindow(overrides: Partial<Window> = {}): Window {
|
|
198
|
+
return { postMessage: vi.fn(), ...overrides } as Window;
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 6. Commits and pull requests
|
|
205
|
+
|
|
206
|
+
Use [Conventional Commits v1.0.0](https://www.conventionalcommits.org/en/v1.0.0/).
|
|
207
|
+
|
|
208
|
+
- Commit in **small, buildable steps** while lint and tests remain green for this repo. Split **unrelated** edits into separate commits before opening a pull request.
|
|
209
|
+
- **Subject line:** `type[(scope)][!]: description` — imperative mood, no trailing period, blank line before an optional body. Use `!` before `:` and/or a **`BREAKING CHANGE:`** footer for incompatible changes (full rules in the link above).
|
|
210
|
+
- **Types** (pick the narrowest match): `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`. **Scope:** optional short area (`auth`, `chat`, `deps`); omit if it would be vague.
|
|
211
|
+
- **Body:** only for non-obvious motivation or behaviour; keep it short and do not repeat the diff. **Footers** (for example `Fixes #123`) when this project tracks issues that way.
|
|
212
|
+
- **Pull requests:** title and **Summary** should match the same vocabulary; do not replace conventional commits with only a PR headline.
|
|
213
|
+
- Before committing: review **`git status`** and **`git diff`** (including staged); unstage and commit separately if the index mixes unrelated concerns.
|
|
214
|
+
|
|
215
|
+
---
|