nubos-pilot 0.7.2 → 0.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/agents/np-executor.md +1 -0
- package/bin/install.js +2 -0
- package/bin/np-tools/_commands.cjs +98 -64
- package/bin/np-tools/dashboard.cjs +11 -5
- package/bin/np-tools/dashboard.test.cjs +80 -0
- package/bin/np-tools/help.cjs +16 -7
- package/bin/np-tools/help.test.cjs +63 -0
- package/bin/np-tools/stats.cjs +99 -3
- package/bin/np-tools/stats.test.cjs +65 -0
- package/lib/dashboard.cjs +41 -10
- package/lib/dashboard.test.cjs +83 -0
- package/lib/install/runtime-assets.cjs +50 -1
- package/lib/install/runtime-assets.test.cjs +190 -0
- package/lib/install/runtimes-registry.cjs +1 -0
- package/lib/runtime/_readline.cjs +49 -11
- package/lib/runtime/_readline.test.cjs +59 -0
- package/lib/runtime/claude.cjs +8 -1
- package/package.json +2 -1
- package/skills/np-composition-patterns/AGENTS.md +946 -0
- package/skills/np-composition-patterns/README.md +60 -0
- package/skills/np-composition-patterns/SKILL.md +89 -0
- package/skills/np-composition-patterns/metadata.json +11 -0
- package/skills/np-composition-patterns/rules/_sections.md +29 -0
- package/skills/np-composition-patterns/rules/_template.md +24 -0
- package/skills/np-composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
- package/skills/np-composition-patterns/rules/architecture-compound-components.md +112 -0
- package/skills/np-composition-patterns/rules/patterns-children-over-render-props.md +87 -0
- package/skills/np-composition-patterns/rules/patterns-explicit-variants.md +100 -0
- package/skills/np-composition-patterns/rules/react19-no-forwardref.md +42 -0
- package/skills/np-composition-patterns/rules/state-context-interface.md +191 -0
- package/skills/np-composition-patterns/rules/state-decouple-implementation.md +113 -0
- package/skills/np-composition-patterns/rules/state-lift-state.md +125 -0
- package/skills/np-council/SKILL.md +300 -0
- package/skills/np-design/SKILL.md +679 -0
- package/skills/np-frontend-design/LICENSE.txt +177 -0
- package/skills/np-frontend-design/SKILL.md +42 -0
- package/skills/np-high-end-visual-design/SKILL.md +98 -0
- package/skills/np-impeccable/SKILL.md +152 -0
- package/skills/np-impeccable/agents/openai.yaml +4 -0
- package/skills/np-impeccable/reference/adapt.md +190 -0
- package/skills/np-impeccable/reference/animate.md +173 -0
- package/skills/np-impeccable/reference/audit.md +134 -0
- package/skills/np-impeccable/reference/bolder.md +113 -0
- package/skills/np-impeccable/reference/brand.md +104 -0
- package/skills/np-impeccable/reference/clarify.md +174 -0
- package/skills/np-impeccable/reference/cognitive-load.md +106 -0
- package/skills/np-impeccable/reference/color-and-contrast.md +105 -0
- package/skills/np-impeccable/reference/colorize.md +154 -0
- package/skills/np-impeccable/reference/craft.md +138 -0
- package/skills/np-impeccable/reference/critique.md +213 -0
- package/skills/np-impeccable/reference/delight.md +302 -0
- package/skills/np-impeccable/reference/distill.md +111 -0
- package/skills/np-impeccable/reference/document.md +427 -0
- package/skills/np-impeccable/reference/extract.md +70 -0
- package/skills/np-impeccable/reference/harden.md +347 -0
- package/skills/np-impeccable/reference/heuristics-scoring.md +234 -0
- package/skills/np-impeccable/reference/interaction-design.md +195 -0
- package/skills/np-impeccable/reference/layout.md +141 -0
- package/skills/np-impeccable/reference/live.md +513 -0
- package/skills/np-impeccable/reference/motion-design.md +99 -0
- package/skills/np-impeccable/reference/onboard.md +234 -0
- package/skills/np-impeccable/reference/optimize.md +258 -0
- package/skills/np-impeccable/reference/overdrive.md +130 -0
- package/skills/np-impeccable/reference/personas.md +178 -0
- package/skills/np-impeccable/reference/polish.md +232 -0
- package/skills/np-impeccable/reference/product.md +62 -0
- package/skills/np-impeccable/reference/quieter.md +99 -0
- package/skills/np-impeccable/reference/responsive-design.md +114 -0
- package/skills/np-impeccable/reference/shape.md +136 -0
- package/skills/np-impeccable/reference/spatial-design.md +100 -0
- package/skills/np-impeccable/reference/teach.md +137 -0
- package/skills/np-impeccable/reference/typeset.md +124 -0
- package/skills/np-impeccable/reference/typography.md +159 -0
- package/skills/np-impeccable/reference/ux-writing.md +107 -0
- package/skills/np-impeccable/scripts/cleanup-deprecated.mjs +284 -0
- package/skills/np-impeccable/scripts/command-metadata.json +94 -0
- package/skills/np-impeccable/scripts/design-parser.mjs +820 -0
- package/skills/np-impeccable/scripts/detect-csp.mjs +198 -0
- package/skills/np-impeccable/scripts/is-generated.mjs +69 -0
- package/skills/np-impeccable/scripts/live-accept.mjs +465 -0
- package/skills/np-impeccable/scripts/live-browser.js +4684 -0
- package/skills/np-impeccable/scripts/live-inject.mjs +436 -0
- package/skills/np-impeccable/scripts/live-poll.mjs +187 -0
- package/skills/np-impeccable/scripts/live-server.mjs +679 -0
- package/skills/np-impeccable/scripts/live-wrap.mjs +395 -0
- package/skills/np-impeccable/scripts/live.mjs +247 -0
- package/skills/np-impeccable/scripts/load-context.mjs +93 -0
- package/skills/np-impeccable/scripts/modern-screenshot.umd.js +14 -0
- package/skills/np-impeccable/scripts/pin.mjs +214 -0
- package/skills/np-industrial-brutalist-ui/SKILL.md +92 -0
- package/skills/np-minimalist-ui/SKILL.md +85 -0
- package/skills/np-react-best-practices/AGENTS.md +3810 -0
- package/skills/np-react-best-practices/README.md +123 -0
- package/skills/np-react-best-practices/SKILL.md +149 -0
- package/skills/np-react-best-practices/metadata.json +15 -0
- package/skills/np-react-best-practices/rules/_sections.md +46 -0
- package/skills/np-react-best-practices/rules/_template.md +28 -0
- package/skills/np-react-best-practices/rules/advanced-effect-event-deps.md +56 -0
- package/skills/np-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/skills/np-react-best-practices/rules/advanced-init-once.md +42 -0
- package/skills/np-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/skills/np-react-best-practices/rules/async-api-routes.md +38 -0
- package/skills/np-react-best-practices/rules/async-cheap-condition-before-await.md +37 -0
- package/skills/np-react-best-practices/rules/async-defer-await.md +82 -0
- package/skills/np-react-best-practices/rules/async-dependencies.md +51 -0
- package/skills/np-react-best-practices/rules/async-parallel.md +28 -0
- package/skills/np-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/skills/np-react-best-practices/rules/bundle-analyzable-paths.md +63 -0
- package/skills/np-react-best-practices/rules/bundle-barrel-imports.md +60 -0
- package/skills/np-react-best-practices/rules/bundle-conditional.md +31 -0
- package/skills/np-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/skills/np-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/skills/np-react-best-practices/rules/bundle-preload.md +50 -0
- package/skills/np-react-best-practices/rules/client-event-listeners.md +74 -0
- package/skills/np-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/skills/np-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/skills/np-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/skills/np-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/skills/np-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/skills/np-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/skills/np-react-best-practices/rules/js-cache-storage.md +70 -0
- package/skills/np-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/skills/np-react-best-practices/rules/js-early-exit.md +50 -0
- package/skills/np-react-best-practices/rules/js-flatmap-filter.md +60 -0
- package/skills/np-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/skills/np-react-best-practices/rules/js-index-maps.md +37 -0
- package/skills/np-react-best-practices/rules/js-length-check-first.md +49 -0
- package/skills/np-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/skills/np-react-best-practices/rules/js-request-idle-callback.md +105 -0
- package/skills/np-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/skills/np-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/skills/np-react-best-practices/rules/rendering-activity.md +26 -0
- package/skills/np-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/skills/np-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/skills/np-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/skills/np-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/skills/np-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/skills/np-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/skills/np-react-best-practices/rules/rendering-resource-hints.md +85 -0
- package/skills/np-react-best-practices/rules/rendering-script-defer-async.md +68 -0
- package/skills/np-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/skills/np-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/skills/np-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/skills/np-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/skills/np-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/skills/np-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/skills/np-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/skills/np-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/skills/np-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/skills/np-react-best-practices/rules/rerender-memo.md +44 -0
- package/skills/np-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/skills/np-react-best-practices/rules/rerender-no-inline-components.md +82 -0
- package/skills/np-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/skills/np-react-best-practices/rules/rerender-split-combined-hooks.md +64 -0
- package/skills/np-react-best-practices/rules/rerender-transitions.md +40 -0
- package/skills/np-react-best-practices/rules/rerender-use-deferred-value.md +59 -0
- package/skills/np-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/skills/np-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/skills/np-react-best-practices/rules/server-auth-actions.md +96 -0
- package/skills/np-react-best-practices/rules/server-cache-lru.md +41 -0
- package/skills/np-react-best-practices/rules/server-cache-react.md +76 -0
- package/skills/np-react-best-practices/rules/server-dedup-props.md +65 -0
- package/skills/np-react-best-practices/rules/server-hoist-static-io.md +149 -0
- package/skills/np-react-best-practices/rules/server-no-shared-module-state.md +50 -0
- package/skills/np-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/skills/np-react-best-practices/rules/server-parallel-nested-fetching.md +34 -0
- package/skills/np-react-best-practices/rules/server-serialization.md +38 -0
- package/skills/np-react-native-skills/AGENTS.md +2897 -0
- package/skills/np-react-native-skills/README.md +165 -0
- package/skills/np-react-native-skills/SKILL.md +121 -0
- package/skills/np-react-native-skills/metadata.json +16 -0
- package/skills/np-react-native-skills/rules/_sections.md +86 -0
- package/skills/np-react-native-skills/rules/_template.md +28 -0
- package/skills/np-react-native-skills/rules/animation-derived-value.md +53 -0
- package/skills/np-react-native-skills/rules/animation-gesture-detector-press.md +95 -0
- package/skills/np-react-native-skills/rules/animation-gpu-properties.md +65 -0
- package/skills/np-react-native-skills/rules/design-system-compound-components.md +66 -0
- package/skills/np-react-native-skills/rules/fonts-config-plugin.md +71 -0
- package/skills/np-react-native-skills/rules/imports-design-system-folder.md +68 -0
- package/skills/np-react-native-skills/rules/js-hoist-intl.md +61 -0
- package/skills/np-react-native-skills/rules/list-performance-callbacks.md +44 -0
- package/skills/np-react-native-skills/rules/list-performance-function-references.md +132 -0
- package/skills/np-react-native-skills/rules/list-performance-images.md +53 -0
- package/skills/np-react-native-skills/rules/list-performance-inline-objects.md +97 -0
- package/skills/np-react-native-skills/rules/list-performance-item-expensive.md +94 -0
- package/skills/np-react-native-skills/rules/list-performance-item-memo.md +82 -0
- package/skills/np-react-native-skills/rules/list-performance-item-types.md +104 -0
- package/skills/np-react-native-skills/rules/list-performance-virtualize.md +67 -0
- package/skills/np-react-native-skills/rules/monorepo-native-deps-in-app.md +46 -0
- package/skills/np-react-native-skills/rules/monorepo-single-dependency-versions.md +63 -0
- package/skills/np-react-native-skills/rules/navigation-native-navigators.md +188 -0
- package/skills/np-react-native-skills/rules/react-compiler-destructure-functions.md +50 -0
- package/skills/np-react-native-skills/rules/react-compiler-reanimated-shared-values.md +48 -0
- package/skills/np-react-native-skills/rules/react-state-dispatcher.md +91 -0
- package/skills/np-react-native-skills/rules/react-state-fallback.md +56 -0
- package/skills/np-react-native-skills/rules/react-state-minimize.md +65 -0
- package/skills/np-react-native-skills/rules/rendering-no-falsy-and.md +74 -0
- package/skills/np-react-native-skills/rules/rendering-text-in-text-component.md +36 -0
- package/skills/np-react-native-skills/rules/scroll-position-no-state.md +82 -0
- package/skills/np-react-native-skills/rules/state-ground-truth.md +80 -0
- package/skills/np-react-native-skills/rules/ui-expo-image.md +66 -0
- package/skills/np-react-native-skills/rules/ui-image-gallery.md +104 -0
- package/skills/np-react-native-skills/rules/ui-measure-views.md +78 -0
- package/skills/np-react-native-skills/rules/ui-menus.md +174 -0
- package/skills/np-react-native-skills/rules/ui-native-modals.md +77 -0
- package/skills/np-react-native-skills/rules/ui-pressable.md +61 -0
- package/skills/np-react-native-skills/rules/ui-safe-area-scroll.md +65 -0
- package/skills/np-react-native-skills/rules/ui-scrollview-content-inset.md +45 -0
- package/skills/np-react-native-skills/rules/ui-styling.md +87 -0
- package/skills/np-react-view-transitions/AGENTS.md +955 -0
- package/skills/np-react-view-transitions/README.md +42 -0
- package/skills/np-react-view-transitions/SKILL.md +320 -0
- package/skills/np-react-view-transitions/metadata.json +12 -0
- package/skills/np-react-view-transitions/references/css-recipes.md +242 -0
- package/skills/np-react-view-transitions/references/implementation.md +182 -0
- package/skills/np-react-view-transitions/references/nextjs.md +176 -0
- package/skills/np-react-view-transitions/references/patterns.md +262 -0
- package/skills/np-redesign-existing-projects/SKILL.md +178 -0
- package/skills/np-shadcn/SKILL.md +250 -0
- package/skills/np-shadcn/agents/openai.yml +5 -0
- package/skills/np-shadcn/assets/shadcn-small.png +0 -0
- package/skills/np-shadcn/assets/shadcn.png +0 -0
- package/skills/np-shadcn/cli.md +276 -0
- package/skills/np-shadcn/customization.md +209 -0
- package/skills/np-shadcn/evals/evals.json +47 -0
- package/skills/np-shadcn/mcp.md +94 -0
- package/skills/np-shadcn/rules/base-vs-radix.md +306 -0
- package/skills/np-shadcn/rules/composition.md +195 -0
- package/skills/np-shadcn/rules/forms.md +192 -0
- package/skills/np-shadcn/rules/icons.md +101 -0
- package/skills/np-shadcn/rules/styling.md +162 -0
- package/skills/np-stitch-design-taste/DESIGN.md +121 -0
- package/skills/np-stitch-design-taste/SKILL.md +184 -0
- package/skills/np-web-design-guidelines/SKILL.md +39 -0
- package/workflows/add-todo.md +5 -0
- package/workflows/discuss-phase.md +2 -0
- package/workflows/execute-phase.md +27 -0
- package/workflows/note.md +5 -0
- package/workflows/plan-phase.md +12 -0
- package/workflows/stats.md +27 -90
- package/workflows/verify-work.md +12 -0
|
@@ -189,6 +189,71 @@ test('STATS-2: unknown subcommand prints usage', async () => {
|
|
|
189
189
|
assert.match(stderr.toString(), /Usage:/);
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
+
test('STATS-MD-1: stats markdown emits English title + headers when no config', async () => {
|
|
193
|
+
const sb = makeSandbox(DEMO_YAML, DEMO_STATE);
|
|
194
|
+
writeTaskPlan(sb, 1, 1, 1, 'done');
|
|
195
|
+
const stdout = makeSink();
|
|
196
|
+
const stderr = makeSink();
|
|
197
|
+
const code = await statsCli.run(['markdown'], { cwd: sb, stdout, stderr });
|
|
198
|
+
assert.equal(code, 0, 'stderr=' + stderr.toString());
|
|
199
|
+
const out = stdout.toString();
|
|
200
|
+
assert.match(out, /^## Project Stats/m);
|
|
201
|
+
assert.match(out, /^\*\*Milestone:\*\*/m);
|
|
202
|
+
assert.match(out, /^\*\*Progress:\*\*/m);
|
|
203
|
+
assert.match(out, /^### Phases/m);
|
|
204
|
+
assert.match(out, /^### Metrics by Phase/m);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('STATS-MD-2: stats markdown emits German labels when config.response_language=de', async () => {
|
|
208
|
+
const sb = makeSandbox(DEMO_YAML, DEMO_STATE);
|
|
209
|
+
fs.writeFileSync(
|
|
210
|
+
path.join(sb, '.nubos-pilot', 'config.json'),
|
|
211
|
+
JSON.stringify({ response_language: 'de' }),
|
|
212
|
+
);
|
|
213
|
+
writeTaskPlan(sb, 1, 1, 1, 'done');
|
|
214
|
+
const stdout = makeSink();
|
|
215
|
+
const stderr = makeSink();
|
|
216
|
+
const code = await statsCli.run(['markdown'], { cwd: sb, stdout, stderr });
|
|
217
|
+
assert.equal(code, 0, 'stderr=' + stderr.toString());
|
|
218
|
+
const out = stdout.toString();
|
|
219
|
+
assert.match(out, /^## Projekt-Stats/m);
|
|
220
|
+
assert.match(out, /^\*\*Fortschritt:\*\*/m);
|
|
221
|
+
assert.match(out, /^\*\*Letzte Aktivität:\*\*/m);
|
|
222
|
+
assert.match(out, /^\*\*Projekt-Start:\*\*/m);
|
|
223
|
+
assert.match(out, /^### Phasen/m);
|
|
224
|
+
assert.match(out, /^### Metriken pro Phase/m);
|
|
225
|
+
assert.match(out, /Pläne/);
|
|
226
|
+
assert.equal(/^## Project Stats/m.test(out), false, 'no English title');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('STATS-MD-3: --lang flag overrides config language', async () => {
|
|
230
|
+
const sb = makeSandbox(DEMO_YAML, DEMO_STATE);
|
|
231
|
+
fs.writeFileSync(
|
|
232
|
+
path.join(sb, '.nubos-pilot', 'config.json'),
|
|
233
|
+
JSON.stringify({ response_language: 'de' }),
|
|
234
|
+
);
|
|
235
|
+
writeTaskPlan(sb, 1, 1, 1, 'done');
|
|
236
|
+
const stdout = makeSink();
|
|
237
|
+
const stderr = makeSink();
|
|
238
|
+
const code = await statsCli.run(['markdown', '--lang', 'en'], { cwd: sb, stdout, stderr });
|
|
239
|
+
assert.equal(code, 0, 'stderr=' + stderr.toString());
|
|
240
|
+
assert.match(stdout.toString(), /^## Project Stats/m);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('STATS-MD-4: _renderMarkdown is a pure function callable without project', () => {
|
|
244
|
+
const md = statsCli._renderMarkdown({
|
|
245
|
+
schema_version: 2,
|
|
246
|
+
milestone: { version: 'v1', name: 'Auth' },
|
|
247
|
+
phases: [{ number: '1', name: 'F', plans_total: 2, plans_complete: 1, status: 'in-progress' }],
|
|
248
|
+
plans_total: 2, plans_complete: 1, percent: 50,
|
|
249
|
+
git: { commits: 5, first_commit_at: '2026-01-01' },
|
|
250
|
+
last_activity: '2026-04-01T00:00:00Z',
|
|
251
|
+
metrics_by_phase: {},
|
|
252
|
+
}, 'de');
|
|
253
|
+
assert.match(md, /Projekt-Stats/);
|
|
254
|
+
assert.match(md, /Pläne/);
|
|
255
|
+
});
|
|
256
|
+
|
|
192
257
|
test('STATS-3: outside project emits NubosPilotError envelope', async () => {
|
|
193
258
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'np-stats-outside-'));
|
|
194
259
|
_sandboxes.push(tmp);
|
package/lib/dashboard.cjs
CHANGED
|
@@ -5,6 +5,35 @@ const path = require('node:path');
|
|
|
5
5
|
|
|
6
6
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
7
7
|
const { listMilestones, listSlices, listTasks, mId } = require('./layout.cjs');
|
|
8
|
+
const { normalizeLanguage, DEFAULT_LANGUAGE } = require('./language.cjs');
|
|
9
|
+
|
|
10
|
+
const LABELS = Object.freeze({
|
|
11
|
+
en: {
|
|
12
|
+
done: 'done',
|
|
13
|
+
'in-progress': 'in-progress',
|
|
14
|
+
pending: 'pending',
|
|
15
|
+
skipped: 'skipped',
|
|
16
|
+
parked: 'parked',
|
|
17
|
+
no_tasks: 'no tasks',
|
|
18
|
+
no_slices: 'no slices planned',
|
|
19
|
+
no_milestones: 'No milestones yet. Run /np:new-project or /np:new-milestone.',
|
|
20
|
+
},
|
|
21
|
+
de: {
|
|
22
|
+
done: 'erledigt',
|
|
23
|
+
'in-progress': 'in Arbeit',
|
|
24
|
+
pending: 'offen',
|
|
25
|
+
skipped: 'übersprungen',
|
|
26
|
+
parked: 'geparkt',
|
|
27
|
+
no_tasks: 'keine Tasks',
|
|
28
|
+
no_slices: 'keine Slices geplant',
|
|
29
|
+
no_milestones: 'Noch keine Milestones. Führe /np:new-project oder /np:new-milestone aus.',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function _labelsFor(language) {
|
|
34
|
+
const lang = normalizeLanguage(language || DEFAULT_LANGUAGE);
|
|
35
|
+
return LABELS[lang] || LABELS[DEFAULT_LANGUAGE];
|
|
36
|
+
}
|
|
8
37
|
|
|
9
38
|
const ANSI = Object.freeze({
|
|
10
39
|
reset: '\x1b[0m',
|
|
@@ -83,15 +112,15 @@ function collectSnapshot(projectRoot) {
|
|
|
83
112
|
return { milestones: _collectMilestones(cwd) };
|
|
84
113
|
}
|
|
85
114
|
|
|
86
|
-
function _summarizeCounts(c, useColor) {
|
|
115
|
+
function _summarizeCounts(c, useColor, labels) {
|
|
87
116
|
const paint = (code, text) => useColor ? code + text + ANSI.reset : text;
|
|
88
117
|
const bits = [];
|
|
89
|
-
if (c.done) bits.push(paint(ANSI.green, c.done + ' done
|
|
90
|
-
if (c['in-progress']) bits.push(paint(ANSI.yellow, c['in-progress'] + ' in-progress'));
|
|
91
|
-
if (c.pending) bits.push(paint(ANSI.gray, c.pending + ' pending
|
|
92
|
-
if (c.skipped) bits.push(paint(ANSI.dim, c.skipped + ' skipped
|
|
93
|
-
if (c.parked) bits.push(paint(ANSI.red, c.parked + ' parked
|
|
94
|
-
return bits.join(' · ') || paint(ANSI.dim,
|
|
118
|
+
if (c.done) bits.push(paint(ANSI.green, c.done + ' ' + labels.done));
|
|
119
|
+
if (c['in-progress']) bits.push(paint(ANSI.yellow, c['in-progress'] + ' ' + labels['in-progress']));
|
|
120
|
+
if (c.pending) bits.push(paint(ANSI.gray, c.pending + ' ' + labels.pending));
|
|
121
|
+
if (c.skipped) bits.push(paint(ANSI.dim, c.skipped + ' ' + labels.skipped));
|
|
122
|
+
if (c.parked) bits.push(paint(ANSI.red, c.parked + ' ' + labels.parked));
|
|
123
|
+
return bits.join(' · ') || paint(ANSI.dim, labels.no_tasks);
|
|
95
124
|
}
|
|
96
125
|
|
|
97
126
|
function _checkboxRow(statuses, useColor) {
|
|
@@ -106,6 +135,7 @@ function _checkboxRow(statuses, useColor) {
|
|
|
106
135
|
function renderSnapshot(snap, opts) {
|
|
107
136
|
const o = opts || {};
|
|
108
137
|
const useColor = o.color !== false;
|
|
138
|
+
const labels = _labelsFor(o.language);
|
|
109
139
|
const c = (code, text) => useColor ? code + text + ANSI.reset : text;
|
|
110
140
|
const lines = [];
|
|
111
141
|
|
|
@@ -113,7 +143,7 @@ function renderSnapshot(snap, opts) {
|
|
|
113
143
|
lines.push('');
|
|
114
144
|
|
|
115
145
|
if (!snap.milestones || snap.milestones.length === 0) {
|
|
116
|
-
lines.push(c(ANSI.dim,
|
|
146
|
+
lines.push(c(ANSI.dim, labels.no_milestones));
|
|
117
147
|
lines.push('');
|
|
118
148
|
return lines.join('\n');
|
|
119
149
|
}
|
|
@@ -123,10 +153,10 @@ function renderSnapshot(snap, opts) {
|
|
|
123
153
|
const status = m.status ? ' ' + c(ANSI.dim, '[' + m.status + ']') : '';
|
|
124
154
|
lines.push(c(ANSI.bold, m.id) + name + status);
|
|
125
155
|
if (m.slices.length === 0) {
|
|
126
|
-
lines.push(' ' + c(ANSI.dim,
|
|
156
|
+
lines.push(' ' + c(ANSI.dim, labels.no_slices));
|
|
127
157
|
}
|
|
128
158
|
for (const s of m.slices) {
|
|
129
|
-
lines.push(' ' + c(ANSI.bold, s.full_id) + ' ' + _summarizeCounts(s.counts, useColor));
|
|
159
|
+
lines.push(' ' + c(ANSI.bold, s.full_id) + ' ' + _summarizeCounts(s.counts, useColor, labels));
|
|
130
160
|
if (s.task_statuses.length > 0) {
|
|
131
161
|
lines.push(' ' + _checkboxRow(s.task_statuses, useColor));
|
|
132
162
|
}
|
|
@@ -142,4 +172,5 @@ module.exports = {
|
|
|
142
172
|
renderSnapshot,
|
|
143
173
|
ANSI,
|
|
144
174
|
STATUS_GLYPHS,
|
|
175
|
+
LABELS,
|
|
145
176
|
};
|
package/lib/dashboard.test.cjs
CHANGED
|
@@ -162,6 +162,89 @@ test('DB-8: empty slice (no tasks) renders "no tasks" indicator', () => {
|
|
|
162
162
|
}
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
+
test('DB-L1: renderSnapshot uses German labels when language=de', () => {
|
|
166
|
+
const root = _sandbox();
|
|
167
|
+
try {
|
|
168
|
+
_writeMeta(root, 1, { name: 'Auth', status: 'active' });
|
|
169
|
+
_writeTask(root, 1, 1, 1, 'done', 'Login');
|
|
170
|
+
_writeTask(root, 1, 1, 2, 'in-progress', 'Logout');
|
|
171
|
+
_writeTask(root, 1, 1, 3, 'pending', 'Reset');
|
|
172
|
+
const snap = dashboard.collectSnapshot(root);
|
|
173
|
+
const out = dashboard.renderSnapshot(snap, { color: false, language: 'de' });
|
|
174
|
+
assert.match(out, /1 erledigt/);
|
|
175
|
+
assert.match(out, /1 in Arbeit/);
|
|
176
|
+
assert.match(out, /1 offen/);
|
|
177
|
+
assert.equal(/\bdone\b/.test(out), false, 'must not leak English label');
|
|
178
|
+
assert.equal(/in-progress/.test(out), false, 'must not leak English label');
|
|
179
|
+
} finally {
|
|
180
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('DB-L2: renderSnapshot uses German "no milestones" line for de', () => {
|
|
185
|
+
const root = _sandbox();
|
|
186
|
+
try {
|
|
187
|
+
const snap = dashboard.collectSnapshot(root);
|
|
188
|
+
const out = dashboard.renderSnapshot(snap, { color: false, language: 'de' });
|
|
189
|
+
assert.match(out, /Noch keine Milestones/);
|
|
190
|
+
assert.equal(/No milestones yet/.test(out), false);
|
|
191
|
+
} finally {
|
|
192
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('DB-L3: empty slice renders German "keine Tasks" for de', () => {
|
|
197
|
+
const root = _sandbox();
|
|
198
|
+
try {
|
|
199
|
+
_writeMeta(root, 1, { name: 'Empty' });
|
|
200
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S001', 'tasks'), { recursive: true });
|
|
201
|
+
const snap = dashboard.collectSnapshot(root);
|
|
202
|
+
const out = dashboard.renderSnapshot(snap, { color: false, language: 'de' });
|
|
203
|
+
assert.match(out, /keine Tasks/);
|
|
204
|
+
assert.equal(/no tasks/.test(out), false);
|
|
205
|
+
} finally {
|
|
206
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('DB-L4: milestone without slices renders German "keine Slices geplant"', () => {
|
|
211
|
+
const root = _sandbox();
|
|
212
|
+
try {
|
|
213
|
+
_writeMeta(root, 1, { name: 'NoSlices' });
|
|
214
|
+
const snap = dashboard.collectSnapshot(root);
|
|
215
|
+
const out = dashboard.renderSnapshot(snap, { color: false, language: 'de' });
|
|
216
|
+
assert.match(out, /keine Slices geplant/);
|
|
217
|
+
} finally {
|
|
218
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('DB-L5: unknown language falls back to English labels', () => {
|
|
223
|
+
const root = _sandbox();
|
|
224
|
+
try {
|
|
225
|
+
_writeMeta(root, 1, { name: 'Auth' });
|
|
226
|
+
_writeTask(root, 1, 1, 1, 'done', 'X');
|
|
227
|
+
const snap = dashboard.collectSnapshot(root);
|
|
228
|
+
const out = dashboard.renderSnapshot(snap, { color: false, language: 'fr' });
|
|
229
|
+
assert.match(out, /1 done/);
|
|
230
|
+
} finally {
|
|
231
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('DB-L6: omitted language defaults to English (lib stays pure)', () => {
|
|
236
|
+
const root = _sandbox();
|
|
237
|
+
try {
|
|
238
|
+
_writeMeta(root, 1, { name: 'Auth' });
|
|
239
|
+
_writeTask(root, 1, 1, 1, 'done', 'X');
|
|
240
|
+
const snap = dashboard.collectSnapshot(root);
|
|
241
|
+
const out = dashboard.renderSnapshot(snap, { color: false });
|
|
242
|
+
assert.match(out, /1 done/);
|
|
243
|
+
} finally {
|
|
244
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
165
248
|
test('DB-9: multiple milestones render in numeric order', () => {
|
|
166
249
|
const root = _sandbox();
|
|
167
250
|
try {
|
|
@@ -19,6 +19,33 @@ function _listMarkdown(dir) {
|
|
|
19
19
|
.sort();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function _listSkillDirs(skillsRoot) {
|
|
23
|
+
if (!skillsRoot || !fs.existsSync(skillsRoot)) return [];
|
|
24
|
+
return fs.readdirSync(skillsRoot, { withFileTypes: true })
|
|
25
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.'))
|
|
26
|
+
.map((e) => e.name)
|
|
27
|
+
.filter((name) => fs.existsSync(path.join(skillsRoot, name, 'SKILL.md')))
|
|
28
|
+
.sort();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function _walkFiles(dir) {
|
|
32
|
+
const out = [];
|
|
33
|
+
const stack = [''];
|
|
34
|
+
while (stack.length) {
|
|
35
|
+
const rel = stack.pop();
|
|
36
|
+
const abs = rel ? path.join(dir, rel) : dir;
|
|
37
|
+
let entries;
|
|
38
|
+
try { entries = fs.readdirSync(abs, { withFileTypes: true }); }
|
|
39
|
+
catch { continue; }
|
|
40
|
+
for (const e of entries) {
|
|
41
|
+
const childRel = rel ? path.join(rel, e.name) : e.name;
|
|
42
|
+
if (e.isDirectory()) stack.push(childRel);
|
|
43
|
+
else if (e.isFile()) out.push(childRel);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out.sort();
|
|
47
|
+
}
|
|
48
|
+
|
|
22
49
|
function _payloadBase(scope, projectRoot) {
|
|
23
50
|
return scope === 'global' ? os.homedir() : projectRoot;
|
|
24
51
|
}
|
|
@@ -27,10 +54,11 @@ function _toPosix(p) {
|
|
|
27
54
|
return p.split(path.sep).join('/');
|
|
28
55
|
}
|
|
29
56
|
|
|
30
|
-
function planRuntimeAssets({ selectedRuntimes, scope, projectRoot, workflowsDir, agentsDir }) {
|
|
57
|
+
function planRuntimeAssets({ selectedRuntimes, scope, projectRoot, workflowsDir, agentsDir, skillsDir }) {
|
|
31
58
|
const base = _payloadBase(scope, projectRoot);
|
|
32
59
|
const workflows = _listMarkdown(workflowsDir);
|
|
33
60
|
const agents = _listMarkdown(agentsDir);
|
|
61
|
+
const skills = _listSkillDirs(skillsDir);
|
|
34
62
|
const plans = [];
|
|
35
63
|
for (const id of selectedRuntimes || []) {
|
|
36
64
|
const meta = registryMod.getRuntimeMeta(id);
|
|
@@ -60,6 +88,23 @@ function planRuntimeAssets({ selectedRuntimes, scope, projectRoot, workflowsDir,
|
|
|
60
88
|
});
|
|
61
89
|
}
|
|
62
90
|
}
|
|
91
|
+
if (meta.skillsSubdir && skillsDir) {
|
|
92
|
+
for (const skill of skills) {
|
|
93
|
+
const skillSrcDir = path.join(skillsDir, skill);
|
|
94
|
+
for (const rel of _walkFiles(skillSrcDir)) {
|
|
95
|
+
const sourceFile = path.join(skillSrcDir, rel);
|
|
96
|
+
const targetFile = path.join(configDir, meta.skillsSubdir, skill, rel);
|
|
97
|
+
plans.push({
|
|
98
|
+
runtime: id,
|
|
99
|
+
kind: 'skill',
|
|
100
|
+
skillName: skill,
|
|
101
|
+
sourceFile,
|
|
102
|
+
targetFile,
|
|
103
|
+
manifestKey: _toPosix(path.relative(base, targetFile)),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
63
108
|
}
|
|
64
109
|
return plans;
|
|
65
110
|
}
|
|
@@ -105,6 +150,7 @@ function _isAssetKey(key) {
|
|
|
105
150
|
if (key.startsWith('.')) {
|
|
106
151
|
if (key.startsWith('.claude/commands/')) return true;
|
|
107
152
|
if (key.startsWith('.claude/agents/')) return true;
|
|
153
|
+
if (key.startsWith('.claude/skills/')) return true;
|
|
108
154
|
for (const meta of registryMod.RUNTIMES) {
|
|
109
155
|
if (meta.commandsSubdir) {
|
|
110
156
|
if (key.startsWith(meta.localDir + '/' + meta.commandsSubdir + '/')) return true;
|
|
@@ -112,6 +158,9 @@ function _isAssetKey(key) {
|
|
|
112
158
|
if (meta.agentsSubdir) {
|
|
113
159
|
if (key.startsWith(meta.localDir + '/' + meta.agentsSubdir + '/')) return true;
|
|
114
160
|
}
|
|
161
|
+
if (meta.skillsSubdir) {
|
|
162
|
+
if (key.startsWith(meta.localDir + '/' + meta.skillsSubdir + '/')) return true;
|
|
163
|
+
}
|
|
115
164
|
}
|
|
116
165
|
}
|
|
117
166
|
return false;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
const runtimeAssets = require('./runtime-assets.cjs');
|
|
10
|
+
|
|
11
|
+
function _mkTmp(prefix) {
|
|
12
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function _seedSource(root) {
|
|
16
|
+
const workflowsDir = path.join(root, 'workflows');
|
|
17
|
+
const agentsDir = path.join(root, 'agents');
|
|
18
|
+
const skillsDir = path.join(root, 'skills');
|
|
19
|
+
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
20
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
21
|
+
fs.mkdirSync(path.join(skillsDir, 'np-council'), { recursive: true });
|
|
22
|
+
fs.mkdirSync(path.join(skillsDir, 'np-shadcn', 'rules'), { recursive: true });
|
|
23
|
+
fs.mkdirSync(path.join(skillsDir, 'np-shadcn', 'assets'), { recursive: true });
|
|
24
|
+
fs.mkdirSync(path.join(skillsDir, '.draft'), { recursive: true });
|
|
25
|
+
fs.mkdirSync(path.join(skillsDir, 'np-incomplete'), { recursive: true });
|
|
26
|
+
|
|
27
|
+
fs.writeFileSync(path.join(workflowsDir, 'execute-phase.md'), '# wf');
|
|
28
|
+
fs.writeFileSync(path.join(agentsDir, 'np-executor.md'), '# agent');
|
|
29
|
+
fs.writeFileSync(path.join(skillsDir, 'np-council', 'SKILL.md'), '# council');
|
|
30
|
+
fs.writeFileSync(path.join(skillsDir, 'np-shadcn', 'SKILL.md'), '# shadcn');
|
|
31
|
+
fs.writeFileSync(path.join(skillsDir, 'np-shadcn', 'rules', 'react.md'), '# rules');
|
|
32
|
+
fs.writeFileSync(path.join(skillsDir, 'np-shadcn', 'assets', 'preset.json'), '{}');
|
|
33
|
+
fs.writeFileSync(path.join(skillsDir, '.draft', 'SKILL.md'), '# hidden');
|
|
34
|
+
fs.writeFileSync(path.join(skillsDir, 'np-incomplete', 'README.md'), '# no skill md');
|
|
35
|
+
|
|
36
|
+
return { workflowsDir, agentsDir, skillsDir };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test('planRuntimeAssets: discovers skills automatically and skips invalid dirs', () => {
|
|
40
|
+
const root = _mkTmp('np-skills-plan-');
|
|
41
|
+
try {
|
|
42
|
+
const { workflowsDir, agentsDir, skillsDir } = _seedSource(root);
|
|
43
|
+
const projectRoot = path.join(root, 'proj');
|
|
44
|
+
fs.mkdirSync(projectRoot, { recursive: true });
|
|
45
|
+
|
|
46
|
+
const plans = runtimeAssets.planRuntimeAssets({
|
|
47
|
+
selectedRuntimes: ['claude'],
|
|
48
|
+
scope: 'local',
|
|
49
|
+
projectRoot,
|
|
50
|
+
workflowsDir,
|
|
51
|
+
agentsDir,
|
|
52
|
+
skillsDir,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const skills = plans.filter((p) => p.kind === 'skill');
|
|
56
|
+
const skillNames = new Set(skills.map((p) => p.skillName));
|
|
57
|
+
assert.deepStrictEqual([...skillNames].sort(), ['np-council', 'np-shadcn'],
|
|
58
|
+
'only directories with SKILL.md and no leading dot are taken');
|
|
59
|
+
|
|
60
|
+
const shadcnFiles = skills
|
|
61
|
+
.filter((p) => p.skillName === 'np-shadcn')
|
|
62
|
+
.map((p) => p.manifestKey)
|
|
63
|
+
.sort();
|
|
64
|
+
assert.deepStrictEqual(shadcnFiles, [
|
|
65
|
+
'.claude/skills/np-shadcn/SKILL.md',
|
|
66
|
+
'.claude/skills/np-shadcn/assets/preset.json',
|
|
67
|
+
'.claude/skills/np-shadcn/rules/react.md',
|
|
68
|
+
], 'walk yields nested files with posix manifest keys');
|
|
69
|
+
} finally {
|
|
70
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('planRuntimeAssets: skips skills for runtimes without skillsSubdir', () => {
|
|
75
|
+
const root = _mkTmp('np-skills-skip-');
|
|
76
|
+
try {
|
|
77
|
+
const { workflowsDir, agentsDir, skillsDir } = _seedSource(root);
|
|
78
|
+
const projectRoot = path.join(root, 'proj');
|
|
79
|
+
fs.mkdirSync(projectRoot, { recursive: true });
|
|
80
|
+
|
|
81
|
+
const plans = runtimeAssets.planRuntimeAssets({
|
|
82
|
+
selectedRuntimes: ['codex', 'gemini', 'cursor'],
|
|
83
|
+
scope: 'local',
|
|
84
|
+
projectRoot,
|
|
85
|
+
workflowsDir,
|
|
86
|
+
agentsDir,
|
|
87
|
+
skillsDir,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
assert.strictEqual(plans.filter((p) => p.kind === 'skill').length, 0,
|
|
91
|
+
'no skill plans for runtimes without skillsSubdir');
|
|
92
|
+
} finally {
|
|
93
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('writeRuntimeAssets: copies nested skill files into runtime config dir', () => {
|
|
98
|
+
const root = _mkTmp('np-skills-write-');
|
|
99
|
+
try {
|
|
100
|
+
const { workflowsDir, agentsDir, skillsDir } = _seedSource(root);
|
|
101
|
+
const projectRoot = path.join(root, 'proj');
|
|
102
|
+
fs.mkdirSync(projectRoot, { recursive: true });
|
|
103
|
+
|
|
104
|
+
const plans = runtimeAssets.planRuntimeAssets({
|
|
105
|
+
selectedRuntimes: ['claude'],
|
|
106
|
+
scope: 'local',
|
|
107
|
+
projectRoot,
|
|
108
|
+
workflowsDir,
|
|
109
|
+
agentsDir,
|
|
110
|
+
skillsDir,
|
|
111
|
+
});
|
|
112
|
+
runtimeAssets.writeRuntimeAssets(plans);
|
|
113
|
+
|
|
114
|
+
assert.ok(fs.existsSync(path.join(projectRoot, '.claude/skills/np-council/SKILL.md')));
|
|
115
|
+
assert.ok(fs.existsSync(path.join(projectRoot, '.claude/skills/np-shadcn/SKILL.md')));
|
|
116
|
+
assert.ok(fs.existsSync(path.join(projectRoot, '.claude/skills/np-shadcn/rules/react.md')));
|
|
117
|
+
assert.ok(fs.existsSync(path.join(projectRoot, '.claude/skills/np-shadcn/assets/preset.json')));
|
|
118
|
+
} finally {
|
|
119
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('manifestEntriesForPlans: hashes every skill file', () => {
|
|
124
|
+
const root = _mkTmp('np-skills-hash-');
|
|
125
|
+
try {
|
|
126
|
+
const { workflowsDir, agentsDir, skillsDir } = _seedSource(root);
|
|
127
|
+
const projectRoot = path.join(root, 'proj');
|
|
128
|
+
fs.mkdirSync(projectRoot, { recursive: true });
|
|
129
|
+
|
|
130
|
+
const plans = runtimeAssets.planRuntimeAssets({
|
|
131
|
+
selectedRuntimes: ['claude'],
|
|
132
|
+
scope: 'local',
|
|
133
|
+
projectRoot,
|
|
134
|
+
workflowsDir,
|
|
135
|
+
agentsDir,
|
|
136
|
+
skillsDir,
|
|
137
|
+
});
|
|
138
|
+
const entries = runtimeAssets.manifestEntriesForPlans(plans);
|
|
139
|
+
const skillKeys = Object.keys(entries).filter((k) => k.includes('/skills/'));
|
|
140
|
+
|
|
141
|
+
assert.ok(skillKeys.length >= 4, 'each nested skill file gets its own manifest key');
|
|
142
|
+
for (const k of skillKeys) {
|
|
143
|
+
assert.match(entries[k], /^[a-f0-9]{64}$/, k + ' has sha256 hash');
|
|
144
|
+
}
|
|
145
|
+
} finally {
|
|
146
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('isAssetManifestKey: recognises skill paths for all runtimes that opted in', () => {
|
|
151
|
+
assert.strictEqual(runtimeAssets.isAssetManifestKey('.claude/skills/np-council/SKILL.md'), true);
|
|
152
|
+
assert.strictEqual(runtimeAssets.isAssetManifestKey('.claude/skills/np-shadcn/rules/react.md'), true);
|
|
153
|
+
assert.strictEqual(runtimeAssets.isAssetManifestKey('~/.claude/skills/np-council/SKILL.md'), true);
|
|
154
|
+
assert.strictEqual(runtimeAssets.isAssetManifestKey('.codex/skills/np-council/SKILL.md'), false,
|
|
155
|
+
'codex did not opt into skills');
|
|
156
|
+
assert.strictEqual(runtimeAssets.isAssetManifestKey('.claude/nubos-pilot/state.json'), false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('removeStaleAssets: deletes nested skill files and prunes empty dirs', () => {
|
|
160
|
+
const root = _mkTmp('np-skills-stale-');
|
|
161
|
+
try {
|
|
162
|
+
const { workflowsDir, agentsDir, skillsDir } = _seedSource(root);
|
|
163
|
+
const projectRoot = path.join(root, 'proj');
|
|
164
|
+
fs.mkdirSync(projectRoot, { recursive: true });
|
|
165
|
+
|
|
166
|
+
const plans = runtimeAssets.planRuntimeAssets({
|
|
167
|
+
selectedRuntimes: ['claude'],
|
|
168
|
+
scope: 'local',
|
|
169
|
+
projectRoot,
|
|
170
|
+
workflowsDir,
|
|
171
|
+
agentsDir,
|
|
172
|
+
skillsDir,
|
|
173
|
+
});
|
|
174
|
+
runtimeAssets.writeRuntimeAssets(plans);
|
|
175
|
+
|
|
176
|
+
const stale = [
|
|
177
|
+
'.claude/skills/np-shadcn/SKILL.md',
|
|
178
|
+
'.claude/skills/np-shadcn/rules/react.md',
|
|
179
|
+
'.claude/skills/np-shadcn/assets/preset.json',
|
|
180
|
+
];
|
|
181
|
+
runtimeAssets.removeStaleAssets(stale, 'local', projectRoot);
|
|
182
|
+
|
|
183
|
+
assert.strictEqual(fs.existsSync(path.join(projectRoot, '.claude/skills/np-shadcn')), false,
|
|
184
|
+
'empty skill directory tree pruned');
|
|
185
|
+
assert.strictEqual(fs.existsSync(path.join(projectRoot, '.claude/skills/np-council/SKILL.md')), true,
|
|
186
|
+
'untouched skills remain');
|
|
187
|
+
} finally {
|
|
188
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
const readline = require('node:readline');
|
|
2
2
|
const { NubosPilotError } = require('../core.cjs');
|
|
3
|
+
const { resolveLanguage, normalizeLanguage } = require('../language.cjs');
|
|
4
|
+
|
|
5
|
+
const LABELS = Object.freeze({
|
|
6
|
+
en: {
|
|
7
|
+
choice: 'Choice',
|
|
8
|
+
multiselect_hint: 'Select multiple: 1,2,6 or 1 2 6',
|
|
9
|
+
},
|
|
10
|
+
de: {
|
|
11
|
+
choice: 'Auswahl',
|
|
12
|
+
multiselect_hint: 'Mehrfachauswahl: 1,2,6 oder 1 2 6',
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function _labelsFor(language) {
|
|
17
|
+
const lang = normalizeLanguage(language || 'en');
|
|
18
|
+
return LABELS[lang] || LABELS.en;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function _resolveLangForCwd() {
|
|
22
|
+
try { return resolveLanguage(process.cwd()); }
|
|
23
|
+
catch { return 'en'; }
|
|
24
|
+
}
|
|
3
25
|
|
|
4
26
|
let _readlineImpl = null;
|
|
5
27
|
|
|
@@ -39,7 +61,7 @@ function _readOneLine() {
|
|
|
39
61
|
});
|
|
40
62
|
}
|
|
41
63
|
|
|
42
|
-
function _parseAnswer(type, rawLine, options, def) {
|
|
64
|
+
function _parseAnswer(type, rawLine, options, def, language) {
|
|
43
65
|
const line = (rawLine == null ? '' : String(rawLine)).trim();
|
|
44
66
|
if (type === 'select') {
|
|
45
67
|
if (line === '' && def != null) return def;
|
|
@@ -74,6 +96,10 @@ function _parseAnswer(type, rawLine, options, def) {
|
|
|
74
96
|
if (line === '' && def != null) return def;
|
|
75
97
|
if (/^y(es)?$/i.test(line)) return true;
|
|
76
98
|
if (/^n(o)?$/i.test(line)) return false;
|
|
99
|
+
if (normalizeLanguage(language || 'en') === 'de') {
|
|
100
|
+
if (/^j(a)?$/i.test(line)) return true;
|
|
101
|
+
if (/^nein$/i.test(line)) return false;
|
|
102
|
+
}
|
|
77
103
|
if (def != null) return def;
|
|
78
104
|
throw new NubosPilotError(
|
|
79
105
|
'askuser-invalid-response',
|
|
@@ -100,15 +126,25 @@ function _stripAnsi(s) {
|
|
|
100
126
|
return String(s).replace(/\x1b\[[0-9;]*m/g, '');
|
|
101
127
|
}
|
|
102
128
|
|
|
103
|
-
function
|
|
129
|
+
function _confirmGlyphs(language) {
|
|
130
|
+
return normalizeLanguage(language || 'en') === 'de'
|
|
131
|
+
? { yes: 'j', no: 'n' }
|
|
132
|
+
: { yes: 'y', no: 'n' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function _defaultDisplay(type, options, def, language) {
|
|
104
136
|
if (def == null) {
|
|
105
|
-
if (type === 'confirm')
|
|
137
|
+
if (type === 'confirm') {
|
|
138
|
+
const g = _confirmGlyphs(language);
|
|
139
|
+
return '[' + g.yes + '/' + g.no + ']';
|
|
140
|
+
}
|
|
106
141
|
return '';
|
|
107
142
|
}
|
|
108
143
|
if (type === 'confirm') {
|
|
109
|
-
|
|
110
|
-
if (def ===
|
|
111
|
-
return '[
|
|
144
|
+
const g = _confirmGlyphs(language);
|
|
145
|
+
if (def === true) return '[' + g.yes.toUpperCase() + '/' + g.no + ']';
|
|
146
|
+
if (def === false) return '[' + g.yes + '/' + g.no.toUpperCase() + ']';
|
|
147
|
+
return '[' + g.yes + '/' + g.no + ']';
|
|
112
148
|
}
|
|
113
149
|
if (type === 'select') {
|
|
114
150
|
if (options) {
|
|
@@ -127,7 +163,7 @@ function _defaultDisplay(type, options, def) {
|
|
|
127
163
|
return '[' + String(def) + ']';
|
|
128
164
|
}
|
|
129
165
|
|
|
130
|
-
async function askUserReadline({ type, question, options, def }) {
|
|
166
|
+
async function askUserReadline({ type, question, options, def, language }) {
|
|
131
167
|
const hasTTY = !!process.stdin.isTTY;
|
|
132
168
|
if (!hasTTY && !_readlineImpl) {
|
|
133
169
|
if (def != null) return { value: def, source: 'default' };
|
|
@@ -137,6 +173,8 @@ async function askUserReadline({ type, question, options, def }) {
|
|
|
137
173
|
{ question },
|
|
138
174
|
);
|
|
139
175
|
}
|
|
176
|
+
const lang = language || _resolveLangForCwd();
|
|
177
|
+
const labels = _labelsFor(lang);
|
|
140
178
|
process.stderr.write('\n');
|
|
141
179
|
process.stderr.write(' ' + ANSI_YELLOW + _stripAnsi(question) + ANSI_RESET + '\n');
|
|
142
180
|
process.stderr.write('\n');
|
|
@@ -150,14 +188,14 @@ async function askUserReadline({ type, question, options, def }) {
|
|
|
150
188
|
}
|
|
151
189
|
process.stderr.write('\n');
|
|
152
190
|
if (type === 'multiselect') {
|
|
153
|
-
process.stderr.write('
|
|
191
|
+
process.stderr.write(' ' + labels.multiselect_hint + '\n');
|
|
154
192
|
process.stderr.write('\n');
|
|
155
193
|
}
|
|
156
194
|
}
|
|
157
|
-
const marker = _defaultDisplay(type, options, def);
|
|
158
|
-
process.stderr.write('
|
|
195
|
+
const marker = _defaultDisplay(type, options, def, lang);
|
|
196
|
+
process.stderr.write(' ' + labels.choice + (marker ? ' ' + marker : '') + ': ');
|
|
159
197
|
const line = await _readOneLine();
|
|
160
|
-
return { value: _parseAnswer(type, line, options, def), source: 'readline' };
|
|
198
|
+
return { value: _parseAnswer(type, line, options, def, lang), source: 'readline' };
|
|
161
199
|
}
|
|
162
200
|
|
|
163
201
|
module.exports = { askUserReadline, _readOneLine, _parseAnswer, _setReadlineImplForTests, _hasReadlineImplForTests };
|