Project-level guidance for any AI coding assistant (Claude Code, Codex, Gemini CLI, Copilot CLI, etc.) and for humans contributing to this repository.
VS Code extension (pajoma.vscode-journal) for daily markdown journaling. TypeScript, bundled with esbuild, runs in the workspace extension host. Requires Node 20+ and VS Code 1.118+.
npm install
npm run compile # esbuild → dist/extension.js (CJS, external 'vscode')
npm run watch # esbuild watch (used as preLaunchTask for F5)
npm run package # production build (minified, no sourcemap)
npm run compile-tests # tsc -p . --outDir out (separate from extension bundle)
npm run lint # eslint src
npm test # @vscode/test-cli — pretest auto-runs compile-tests + compile + lint
npm run check # lint + compile + testRun a single test by passing a Mocha grep through @vscode/test-cli:
npm test -- --grep "MatchInput"No display available (no xvfb)? Run npm run compile-tests then verify pure logic with node -e "const {fn} = require('./out/path/to/module'); ..." — does not need VS Code.
--grep is the fast TDD loop (~5 s vs 30 s for the full suite). rm -rf out/ between branch switches — npm run compile-tests does not prune stale .test.js files, so deleted/renamed tests keep running and show as phantom failures when comparing test counts across branches.
Tests live in src/test/suite/**/*.test.ts, compile to out/test/suite/**/*.test.js, and run inside a real Extension Host against the workspace test/ws_unittests/. Mocha TDD UI, 20s default timeout (.vscode-test.mjs). src/test/direct/ contains plain-node debug scripts, not part of the suite.
Launch configs (.vscode/launch.json):
- Run Extension — opens Extension Development Host on
test/ws_manual/(default F5) - Run Extension without Workspace — uses
test/ws_empty/ - Extension Tests — runs the suite with
watch-testsas pre-launch
CI (.github/workflows/ci.yml) runs lint → compile → compile-tests → xvfb-run npm test on push to main/master/develop and on PRs.
The source is organized package-by-feature (restructured in #234). Three top-level zones:
src/app/ composition root — wiring only
src/shared/ kernel — no feature dependencies
src/features/ one folder per capability
Entry: src/extension.ts → Startup(config).run(context) (in src/app/startup.ts) which builds the Container composition root and registers commands, code actions, and optional syntax highlighting.
Dependency injection. Container (src/app/container.ts) implements the JournalController interface and constructs the full service graph in one pass: its constructor takes (configSource, loggerFactory) and builds Configuration, Inject, Parser, Dialogues, Writer, Reader, JournalEvents in dependency order (no two-phase initServices, no ! fields). Commands/providers receive the JournalController interface (never the concrete Container); only src/app/ instantiates it. Service interfaces live in src/shared/model/interfaces.ts (IConfiguration, ILogger, IParser, IWriter, IReader, IInject, IDialogues, IFileSystem, IJournalEvents). src/app/register.ts does the command/provider registration.
Named imports only. The old J.* namespace barrel was removed in #234 — src/index.ts is empty (export {}). Import named symbols directly from submodule barrels / files.
Module responsibilities (need multiple files to grasp):
src/shared/— the kernel, no feature dependencies:config/—Configuration(configuration.ts) is a thinIConfigurationfacade composingSettingsReader(scalar settings, base paths, scopes),PathResolver(entry/note/weekly path & file patterns),TemplateProvider(header/inline/time templates).patterns.tsholds the pattern types + defaults.model/— plain data types:Input,FileEntry,HeaderTemplate/InlineTemplate/ScopedTemplate, scope/quickpick types, and all service interfaces (interfaces.ts).fs/—IFileSystemimplVscodeFileSystem+fileExists.logging/—Logger/ConsoleLogger(OutputChannel-backed).dates/,strings/,lang.ts— date (ISO week/locale), string, and primitive helpers.templates/template-engine.ts—resolveDate(template, date, locale?)andtoMomentFormat(template). SingleTEMPLATE_VARIABLE_MAPregistry. Custom${d:fmt}capture group includes thed:prefix — strip with.slice(2).paths.ts— date-from-URI path utilities (getDateFromURIAndConfig,getWeekFromURIAndConfig,resolvePath,inferType).events/—JournalEvents, a typedvscode.EventEmitterbus for cross-feature signals (e.g.entryOpened).
src/features/<feature>/— each owns itscommands/(one file per registered command, each exports a staticcreate(ctrl)returning aDisposable), domain logic, andui/providers:entries/— entry/weeklyReader/Writer/Inject,ScanEntries(QuickPick walker+cache), theshow-entry-for-*commands, and the sharedAbstractLoadEntryForDateCommand.notes/—show-notecommand,LoadNotes,SyncNoteLinks.weekly/—WeeklyEntryWatcher,SyncDailyLinks(subscribes toentryOpened).tasks/— task code actions + migrate/shift CodeLens,copy-task.navigation/— prev/next entry commands +navigation.ts.smart-input/—MatchInput,Parser,Dialogues(QuickPick/InputBox).tools/—print-time/print-duration/print-sum/ open-workspace.
- A feature must not import another feature's internals — cross-feature signals go through
shared/events/. (Three legacy edges remain pending #239.)
Smart-input flow. User triggers journal.day (Ctrl+Shift+J) → Dialogues shows InputBox → MatchInput.parseInput() classifies the text (date expression, weekday, "memo:", "task:", "note ...", week reference) → command dispatches via the JournalController to Reader/Writer/Inject. The default path/file patterns (${base}/${year}/${month}/${day} for notes, ${base}/${year}/${month}/${day}.${ext} for entries) come from journal.patterns in package.json.
Filesystem. Always go through vscode.workspace.fs (the extension declares extensionKind: ["workspace"] so it runs on the remote host for Remote SSH/Codespaces). Avoid raw fs / fs.promises in new code — docs/PLAN.md Phase 1.3 finished migrating the old fs call sites; do not reintroduce them.
Templates. All user-facing inserted content comes from journal.templates (array of {name, template, after?}). Lookup happens via Configuration.getInlineTemplate(name, fallback). Default template names: memo, task, entry, time, note, files, weekly. Issue #167 was a name-mismatch bug (week vs weekly) — when adding a new template type, register the name consistently in package.json defaults and the consumer.
docs/PLAN.mdis the active modernization roadmap. Phases 0 and 1 are complete; Phase 2+ is open. Match the direction in the plan (DI, named imports, nativeasync/awaitinstead ofnew Promise()wrappers,vscode.workspace.fs, replacing moment withIntl/date-fns).- ESLint flat config (
eslint.config.mjs) enforcescurly,eqeqeq,no-throw-literal,semi. Import naming must becamelCaseorPascalCase. tsconfig.jsonrunsstrict,noImplicitReturns,noFallthroughCasesInSwitch. The bundle goes through esbuild, but tests are compiled viatsc— both must succeed fornpm test.- Test files written on a feature branch land on
developafter the PR merges. When a fix touches an enum member or function signature, always grepsrc/test/— tests may reference the old name even if the branch that introduced them is already closed. - When batch-replacing
this.ctrl.X.method(patterns, also grepthis\.ctrl\.X[^.](no trailing dot) to catch argument positions likethis.ctrl.logger,. Runnpm run compile-tests(tsc) not justnpm run compile(esbuild) to catch type errors in the refactor. docs/contains user-facing feature docs (entries, notes, memos, tasks, scopes, settings, codeactions) anddocs/analysis/holds the analysis that producedPLAN.md.- Local
developoften lagsorigin/develop—git fetch origin developand rebase the feature branch before opening a PR. Remote branches created from the issue UI may already exist as empty refs; fetch and rebase rather than force-push. Exception: spec/plan docs are committed directly to localdevelopbefore pushing — when branching a fix, use localdevelop(notorigin/develop) to pick up those uncommitted docs. - Run all
gitcommands from the current working directory using-C <path>or absolute paths if needed — nevercdbefore a git command. - Conventional Commits with scope:
perf(scan-entries):,feat(navigation):,fix(remote):,test(remote):,docs:. Issue ref goes in the commit body (#187), not the subject.
- Test workspace:
test/ws_unittests/.journal.baseis NOT preset — each test doesconfig.update('base', tmpBase, ConfigurationTarget.Workspace)then builds a freshCtrlfromvscode.workspace.getConfiguration('journal'). Seecommands-prev-next.test.tsandissue-51-remote-create.test.tsfor the template. Two-phase init required:const ctrl = new J.Util.Ctrl(config); ctrl.initServices(new TestLogger(false));— thectrl.loggersetter was removed in #208. Usectrl.parser,ctrl.writer, etc. afterinitServices; don't construct action classes directly in tests. TestLogger(src/test/test-logger.ts) has anerrors[]accumulator — assertlogger.errors.length === 0to prove "no error logged on the happy path".- Don't call
Command.create(ctrl)in tests — it re-registers the command and collides with activation. Instantiate directly:new (Cmd as any)(ctrl)then call the instance method. - Monkey-patch seams that work:
(ctrl.ui as any).openDocument = wrapperand(vscode.window as any).showInformationMessage = wrapper. Restore in teardown. vscode.workspace.fsis frozen — to observe FS calls, spy on the class method via prototype ((ScanEntries.prototype as any).walkDir = function(...) { count++; return orig.apply(this, args); }). Restore in teardown. Seescan-entries-cache.test.ts.
fileExists(fs, uri)(src/shared/fs/fs-exists.ts) — stat-first existence check; convertsFileSystemError.FileNotFoundtofalse, re-throws others. Use instead of "open and catch the rejection".AbstractLoadEntryForDateCommand(src/features/entries/commands/show-entry-for-date.ts) — new "open a specific date" commands should extend it and callthis.execute(input)withinput.offsetset. Reuses the local-vs-remote prompt andloadPageForInputplumbing.getDateFromURIAndConfig(src/shared/paths.ts) — parses aDatefrom a journal entry file path. Anchor detection for navigation features.vscode.Uri.joinPathfor composing FS URIs.vscode.workspace.fs.readDirectoryreturns[name, FileType][].
- English-only.
package.nls.jsoncarries command titles and configuration descriptions consumed by the VS Code manifest. Runtime user-facing strings (toasts, prompts) go inl10n/bundle.l10n.jsonand are looked up viavscode.l10n.t(). - Per-locale
package.nls.<loc>.jsonandl10n/bundle.l10n.<loc>.jsonfiles were removed in 1.1.0. If the project ever needs to re-internationalize, restore both sets from git history (git log --diff-filter=D --name-only -- package.nls.*.json l10n/bundle.l10n.*.json).
- Long-running feature/bugfix work is captured under
docs/specs/<YYYY-MM-DD>-<slug>.md(what + why) anddocs/plans/<YYYY-MM-DD>-<slug>.md(how + test scenarios). The corresponding GitHub issue carries short summary comments linking the file. See #51, #144, #170 for examples. - GitHub labels available in this repo:
bug,enhancement,question,wontfix,invalid,duplicate,help wanted,dependencies. Nochore/docs— omit--labelwhen filing other issue types.